2222
2323logger = logging .getLogger (__name__ .split ("." )[0 ])
2424
25+ # Parallel populate hands the live table and its open connection to workers by
26+ # process inheritance, so it requires the `fork` start method. Python 3.14
27+ # changed the default from `fork` to `forkserver`, which pickles the payload
28+ # instead — that neither carries a live DB connection nor the table's internal
29+ # state, and it deadlocks. Pin to `fork` where available (all POSIX platforms);
30+ # fall back to the platform default elsewhere.
31+ _MP_START_METHOD = "fork" if "fork" in mp .get_all_start_methods () else None
32+
2533
2634# --- helper functions for multiprocessing --
2735
@@ -30,7 +38,8 @@ def _initialize_populate(table: Table, jobs: Job | None, populate_kwargs: dict[s
3038 """
3139 Initialize a worker process for multiprocessing.
3240
33- Saves the unpickled table to the current process and reconnects to database.
41+ Stores the inherited table on the worker process and reconnects to database
42+ (the parent closes its connection before forking; each worker reopens one).
3443
3544 Parameters
3645 ----------
@@ -477,7 +486,9 @@ def _populate_direct(
477486 if hasattr (self .connection ._conn , "ctx" ):
478487 del self .connection ._conn .ctx
479488 with (
480- mp .Pool (processes , _initialize_populate , (self , None , populate_kwargs )) as pool ,
489+ mp .get_context (_MP_START_METHOD ).Pool (
490+ processes , _initialize_populate , (self , None , populate_kwargs )
491+ ) as pool ,
481492 tqdm (desc = "Processes: " , total = nkeys ) if display_progress else contextlib .nullcontext () as progress_bar ,
482493 ):
483494 for status in pool .imap (_call_populate1 , keys , chunksize = 1 ):
@@ -573,7 +584,9 @@ def handler(signum, frame):
573584 if hasattr (self .connection ._conn , "ctx" ):
574585 del self .connection ._conn .ctx # SSLContext is not pickleable
575586 with (
576- mp .Pool (processes , _initialize_populate , (self , self .jobs , populate_kwargs )) as pool ,
587+ mp .get_context (_MP_START_METHOD ).Pool (
588+ processes , _initialize_populate , (self , self .jobs , populate_kwargs )
589+ ) as pool ,
577590 tqdm (desc = "Processes: " , total = nkeys )
578591 if display_progress
579592 else contextlib .nullcontext () as progress_bar ,
@@ -834,8 +847,6 @@ def _update_job_metadata(self, key, start_time, duration, version):
834847
835848 pk_condition = make_condition (self , key , set ())
836849 self .connection .query (
837- f"UPDATE { self .full_table_name } SET "
838- "_job_start_time=%s, _job_duration=%s, _job_version=%s "
839- f"WHERE { pk_condition } " ,
850+ f"UPDATE { self .full_table_name } SET _job_start_time=%s, _job_duration=%s, _job_version=%s WHERE { pk_condition } " ,
840851 args = (start_time , duration , version [:64 ] if version else "" ),
841852 )
0 commit comments