33import random
44import signal
55import string
6+ import sys
67import threading
78import uuid
89from collections import Counter , OrderedDict
1314from math import ceil
1415from multiprocessing import Manager , Process
1516from multiprocessing .managers import SyncManager
17+ from pathlib import Path
1618from queue import Empty , Queue
1719from threading import Event
1820from types import FrameType
2830from psycopg .errors import DeadlockDetected , InFailedSqlTransaction
2931from psycopg_pool .abc import ACT
3032
33+ from .exception_utils import (
34+ SerializedExceptionChain ,
35+ rebuild_exception_chain ,
36+ serialize_exception_chain ,
37+ )
3138from .load_partition import LoadPartition
3239from .model .base_model import DbType , IdrBaseModel
3340from .model .load_progress import LoadProgress
@@ -183,7 +190,7 @@ class _LoadingBatchWorker(Process):
183190 def __init__ (
184191 self ,
185192 task_queue : Queue [_TaskSequence ],
186- errors_queue : Queue [BaseException ],
193+ errors_queue : Queue [SerializedExceptionChain ],
187194 started_signal : Event ,
188195 cancel_signal : Event ,
189196 root_logger : Logger ,
@@ -200,24 +207,34 @@ def __init__(
200207 self ._running_tasks : set [_Task ] = set ()
201208
202209 def run (self ) -> None :
203- self ._logger .reinstall ()
210+ # The next four lines suppress unhandled/unraised Exception output to prevent noise when
211+ # the ExternallyCanceled signal is used to stop this worker. Without this, Python's default
212+ # behavior prints the full trace and Exception context to stderr, cluttering the logs.
213+ sys .unraisablehook = lambda _ : None
214+ sys .excepthook = lambda _ , __ , ___ : None
215+ with Path (os .devnull ).open ("w" ) as devnull :
216+ sys .stderr = devnull
204217
205- def _watch_for_parent_cancel (stop_signal : Event ) -> None :
206- stop_signal .wait ()
207- os .kill (os .getpid (), signal .SIGUSR1 )
218+ self ._logger .reinstall ()
208219
209- def sigusr1_handler (signum : int , frame : FrameType | None ) -> Never : # noqa: ARG001
210- raise ExternallyCanceled ("Externally canceled, interrupting" )
220+ def _watch_for_parent_cancel (stop_signal : Event ) -> None :
221+ stop_signal .wait ()
222+ os .kill (os .getpid (), signal .SIGUSR1 )
211223
212- signal .signal (signal .SIGUSR1 , sigusr1_handler )
213- threading .Thread (
214- target = lambda : _watch_for_parent_cancel (self ._cancel_signal ), daemon = True
215- ).start ()
224+ def sigusr1_handler (signum : int , frame : FrameType | None ) -> Never : # noqa: ARG001
225+ raise ExternallyCanceled ("Externally canceled, interrupting" )
216226
217- try :
218- anyio .run (self ._worker_main )
219- except BaseException as ex :
220- self .errors_queue .put (ex )
227+ signal .signal (signal .SIGUSR1 , sigusr1_handler )
228+ threading .Thread (
229+ target = lambda : _watch_for_parent_cancel (self ._cancel_signal ), daemon = True
230+ ).start ()
231+
232+ try :
233+ anyio .run (self ._worker_main )
234+ except ExternallyCanceled :
235+ pass
236+ except BaseException as ex :
237+ self .errors_queue .put (serialize_exception_chain (ex ))
221238
222239 async def _worker_main (self ) -> None :
223240 task_send , task_receive = anyio .create_memory_object_stream [_TaskSequence ](
@@ -508,7 +525,7 @@ async def start(self, stop: anyio.Event, task_status: TaskStatus | None = None)
508525 return
509526
510527 self ._started_signal .clear ()
511- errors_queue : Queue [BaseException ] = self ._manager .Queue ()
528+ errors_queue : Queue [SerializedExceptionChain ] = self ._manager .Queue ()
512529 cancel_signal = self ._manager .Event ()
513530
514531 self ._worker = _LoadingBatchWorker (
@@ -521,28 +538,29 @@ async def start(self, stop: anyio.Event, task_status: TaskStatus | None = None)
521538 )
522539 self ._worker .start ()
523540
524- # Block until the worker signals it has started
525- self ._started_signal .wait ()
526-
527- logger .info ("LoadingBatchWorker signaled startup" )
541+ # Block until the worker signals it has started, or 10 seconds have passed
542+ self ._started_signal .wait (10 )
543+ if self ._started_signal .is_set ():
544+ logger .info ("LoadingBatchWorker signaled startup" )
545+ else :
546+ logger .error ("LoadingBatchWorker start signal never set. See exception for detail" )
528547
529548 if task_status :
530549 task_status .started ()
531550
532551 async def watch_queue () -> None :
533552 while not stop .is_set ():
534553 with contextlib .suppress (Empty ):
535- errors = errors_queue .get_nowait ()
536- raise errors
554+ raise rebuild_exception_chain (errors_queue .get_nowait ())
537555
538556 await anyio .sleep (0.01 )
539557
540- async with anyio . create_task_group () as tg :
541- try :
558+ try :
559+ async with anyio . create_task_group () as tg :
542560 tg .start_soon (watch_queue )
543- except BaseException :
544- cancel_signal .set ()
545- raise
561+ except BaseException :
562+ cancel_signal .set ()
563+ raise
546564
547565 def cleanup (self , timeout : float = 5.0 ) -> None :
548566 if self ._worker is None :
0 commit comments