[Bug] Unpicklable submit argument kills the scheduler thread and hangs all futures forever
Summary
If an argument passed to Executor.submit() cannot be serialized by cloudpickle, the error is raised inside the background scheduler thread, which dies. The submitted future is never resolved, so future.result() blocks forever, every other task on the same executor is abandoned, and shutdown() / the with block exit hang as well (future_queue.join() waits on task_done() calls that never come).
Reproduced on current main (4d1cf57) for all backends:
FileTaskScheduler with the subprocess spawner (the SlurmClusterExecutor / FluxClusterExecutor code path): future stays pending, scheduler thread dead, cache directory never created, no job ever submitted
SingleNodeExecutor, both one-to-one and block_allocation=True: future stays pending
Since SingleNodeExecutor is affected too, this is not cluster-specific — it can be hit while prototyping on a laptop.
Reproduction
from executorlib import SingleNodeExecutor
class Unpicklable:
def __reduce__(self):
raise TypeError("nope")
with SingleNodeExecutor(max_workers=1) as exe:
f = exe.submit(len, [Unpicklable()])
print(f.result(timeout=15)) # expected: TypeError; observed: TimeoutError (would block forever without timeout)
The same with SlurmClusterExecutor hangs identically. Real-world trigger that cost an overnight campaign: an ase.Atoms object still carrying a pyace.PyACECalculator (holds an unpicklable pyace.evaluator.ACEBEvaluator); cloudpickle.dumps on it raises TypeError instantly when called standalone, so the fail-fast error becomes an unbounded hang.
Observed with python 3.12, cloudpickle 3.1.1, executorlib main @ 4d1cf57 (also on 1.10.1 with pysqa 0.4.4 on a real Slurm cluster).
Why the failure is effectively silent
The only trace is the default threading.excepthook traceback on stderr when the scheduler thread dies. In the common "drive a campaign from a notebook inside a batch job" setup even that is lost entirely: under jupyter nbconvert --to notebook --execute --inplace, the kernel's stderr goes over ZMQ into the notebook's cell outputs, and with --inplace the notebook is only written when execution completes — which, on a hang, is never. Verified on a real hung run: a 4.5 h driver left a six-line job log and a notebook with zero captured outputs. The same applies to any driver that redirects or buffers stderr.
For the cluster backends the failure also happens before anything reaches the queuing system, so an external squeue/sacct watchdog sees nothing either. (Distinct from #1037, which has the same observable but concerns jobs dying after submission.)
Where the exception escapes
TaskSchedulerBase.submit() only queues the task dict (task_scheduler/base.py:167-178); the cloudpickle dump happens later in a background thread, and the two backends fail in slightly different ways:
File backend — execute_tasks_h5 calls serialize_funct → cloudpickle.dumps at task_scheduler/file/shared.py:132; the TypeError propagates out of the thread target before future_queue.task_done() (line 189) and before the cache directory is created. The future stays PENDING forever.
Interactive backend — execute_task_dict → SocketInterface.send_dict → cloudpickle.dumps at standalone/interactive/communication.py:65, uncaught in both blockallocation.py:320 and onetoone.py:265. This case is arguably worse: the future has already passed set_running_or_notify_cancel() (interactive/shared.py:41), so future.cancel() is refused and there is no clean way out for the caller at all.
Expected behaviour
A serialization failure should propagate — raised out of submit() or set on the future via set_exception() — rather than ending the scheduler thread and orphaning every pending future. Two complementary fixes suggest themselves:
- Robustness: wrap the per-task handling in
execute_tasks_h5 and the interactive execute loops in try/except Exception, route the exception to task_dict["future"].set_exception(e), call task_done(), and continue serving the remaining tasks.
- Fail-fast (nice to have): perform the cloudpickle dump in
submit() so the caller gets the TypeError at the call site; for the file backend the dump is needed there anyway for the task-key hash.
Workaround
Strip unpicklable members before submitting and verify in-process, e.g. for the ASE case: atoms.copy() drops the calculator, and cloudpickle.dumps(obj) as a pre-submit guard fails loudly instead of hanging in the queue.
Filed by Claude (an AI assistant), working on behalf of Marvin Poul — who sends their regards.
[Bug] Unpicklable submit argument kills the scheduler thread and hangs all futures forever
Summary
If an argument passed to
Executor.submit()cannot be serialized by cloudpickle, the error is raised inside the background scheduler thread, which dies. The submitted future is never resolved, sofuture.result()blocks forever, every other task on the same executor is abandoned, andshutdown()/ thewithblock exit hang as well (future_queue.join()waits ontask_done()calls that never come).Reproduced on current
main(4d1cf57) for all backends:FileTaskSchedulerwith the subprocess spawner (theSlurmClusterExecutor/FluxClusterExecutorcode path): future stays pending, scheduler thread dead, cache directory never created, no job ever submittedSingleNodeExecutor, both one-to-one andblock_allocation=True: future stays pendingSince
SingleNodeExecutoris affected too, this is not cluster-specific — it can be hit while prototyping on a laptop.Reproduction
The same with
SlurmClusterExecutorhangs identically. Real-world trigger that cost an overnight campaign: anase.Atomsobject still carrying apyace.PyACECalculator(holds an unpicklablepyace.evaluator.ACEBEvaluator);cloudpickle.dumpson it raisesTypeErrorinstantly when called standalone, so the fail-fast error becomes an unbounded hang.Observed with python 3.12, cloudpickle 3.1.1, executorlib main @ 4d1cf57 (also on 1.10.1 with pysqa 0.4.4 on a real Slurm cluster).
Why the failure is effectively silent
The only trace is the default
threading.excepthooktraceback on stderr when the scheduler thread dies. In the common "drive a campaign from a notebook inside a batch job" setup even that is lost entirely: underjupyter nbconvert --to notebook --execute --inplace, the kernel's stderr goes over ZMQ into the notebook's cell outputs, and with--inplacethe notebook is only written when execution completes — which, on a hang, is never. Verified on a real hung run: a 4.5 h driver left a six-line job log and a notebook with zero captured outputs. The same applies to any driver that redirects or buffers stderr.For the cluster backends the failure also happens before anything reaches the queuing system, so an external
squeue/sacctwatchdog sees nothing either. (Distinct from #1037, which has the same observable but concerns jobs dying after submission.)Where the exception escapes
TaskSchedulerBase.submit()only queues the task dict (task_scheduler/base.py:167-178); the cloudpickle dump happens later in a background thread, and the two backends fail in slightly different ways:File backend —
execute_tasks_h5callsserialize_funct→cloudpickle.dumpsattask_scheduler/file/shared.py:132; theTypeErrorpropagates out of the thread target beforefuture_queue.task_done()(line 189) and before the cache directory is created. The future staysPENDINGforever.Interactive backend —
execute_task_dict→SocketInterface.send_dict→cloudpickle.dumpsatstandalone/interactive/communication.py:65, uncaught in bothblockallocation.py:320andonetoone.py:265. This case is arguably worse: the future has already passedset_running_or_notify_cancel()(interactive/shared.py:41), sofuture.cancel()is refused and there is no clean way out for the caller at all.Expected behaviour
A serialization failure should propagate — raised out of
submit()or set on the future viaset_exception()— rather than ending the scheduler thread and orphaning every pending future. Two complementary fixes suggest themselves:execute_tasks_h5and the interactive execute loops intry/except Exception, route the exception totask_dict["future"].set_exception(e), calltask_done(), and continue serving the remaining tasks.submit()so the caller gets theTypeErrorat the call site; for the file backend the dump is needed there anyway for the task-key hash.Workaround
Strip unpicklable members before submitting and verify in-process, e.g. for the ASE case:
atoms.copy()drops the calculator, andcloudpickle.dumps(obj)as a pre-submit guard fails loudly instead of hanging in the queue.Filed by Claude (an AI assistant), working on behalf of Marvin Poul — who sends their regards.