deadpool-executor 2026.3.2__tar.gz → 2026.4.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {deadpool_executor-2026.3.2 → deadpool_executor-2026.4.1}/PKG-INFO +1 -1
- {deadpool_executor-2026.3.2 → deadpool_executor-2026.4.1}/deadpool.py +50 -26
- {deadpool_executor-2026.3.2 → deadpool_executor-2026.4.1}/noxfile.py +2 -0
- {deadpool_executor-2026.3.2 → deadpool_executor-2026.4.1}/tests/test_deadpool.py +54 -11
- {deadpool_executor-2026.3.2 → deadpool_executor-2026.4.1}/.coveragerc +0 -0
- {deadpool_executor-2026.3.2 → deadpool_executor-2026.4.1}/.pre-commit-config.yaml +0 -0
- {deadpool_executor-2026.3.2 → deadpool_executor-2026.4.1}/LICENSE-AGPL +0 -0
- {deadpool_executor-2026.3.2 → deadpool_executor-2026.4.1}/LICENSE-Apache +0 -0
- {deadpool_executor-2026.3.2 → deadpool_executor-2026.4.1}/README.rst +0 -0
- {deadpool_executor-2026.3.2 → deadpool_executor-2026.4.1}/covstart.pth +0 -0
- {deadpool_executor-2026.3.2 → deadpool_executor-2026.4.1}/examples/callbacks.py +0 -0
- {deadpool_executor-2026.3.2 → deadpool_executor-2026.4.1}/examples/entrypoint.py +0 -0
- {deadpool_executor-2026.3.2 → deadpool_executor-2026.4.1}/examples/leftover.py +0 -0
- {deadpool_executor-2026.3.2 → deadpool_executor-2026.4.1}/examples/priorities.py +0 -0
- {deadpool_executor-2026.3.2 → deadpool_executor-2026.4.1}/img1.jpg +0 -0
- {deadpool_executor-2026.3.2 → deadpool_executor-2026.4.1}/pyproject.toml +0 -0
- {deadpool_executor-2026.3.2 → deadpool_executor-2026.4.1}/tests/conftest.py +0 -0
- {deadpool_executor-2026.3.2 → deadpool_executor-2026.4.1}/tests/test_oom.py +0 -0
|
@@ -44,7 +44,7 @@ from functools import partial
|
|
|
44
44
|
import psutil
|
|
45
45
|
from setproctitle import setproctitle
|
|
46
46
|
|
|
47
|
-
__version__ = "2026.
|
|
47
|
+
__version__ = "2026.4.1"
|
|
48
48
|
__all__ = [
|
|
49
49
|
"Deadpool",
|
|
50
50
|
"Future",
|
|
@@ -320,6 +320,9 @@ class Deadpool(Executor):
|
|
|
320
320
|
self.running_jobs = Queue(maxsize=self.pool_size)
|
|
321
321
|
self.running_futs = weakref.WeakSet()
|
|
322
322
|
self.existing_workers = weakref.WeakSet()
|
|
323
|
+
# Lock protecting busy_workers, existing_workers, and
|
|
324
|
+
# running_futs for thread-safety without the GIL.
|
|
325
|
+
self._workers_lock = threading.Lock()
|
|
323
326
|
self.closed = False
|
|
324
327
|
self.shutdown_wait = shutdown_wait
|
|
325
328
|
self.shutdown_cancel_futures = shutdown_cancel_futures
|
|
@@ -352,9 +355,10 @@ class Deadpool(Executor):
|
|
|
352
355
|
|
|
353
356
|
# These are not counters; they are determined at the time of the
|
|
354
357
|
# call based on the state of the worker processes.
|
|
355
|
-
|
|
358
|
+
with self._workers_lock:
|
|
359
|
+
stats["worker_processes_still_alive"] = len(self.existing_workers)
|
|
360
|
+
stats["worker_processes_busy"] = len(self.busy_workers)
|
|
356
361
|
stats["worker_processes_idle"] = self.workers.qsize()
|
|
357
|
-
stats["worker_processes_busy"] = len(self.busy_workers)
|
|
358
362
|
|
|
359
363
|
return stats
|
|
360
364
|
|
|
@@ -388,7 +392,8 @@ class Deadpool(Executor):
|
|
|
388
392
|
)
|
|
389
393
|
self.workers.put(worker)
|
|
390
394
|
self._statistics.worker_processes_created.increment()
|
|
391
|
-
self.
|
|
395
|
+
with self._workers_lock:
|
|
396
|
+
self.existing_workers.add(worker)
|
|
392
397
|
|
|
393
398
|
def clear_workers(self):
|
|
394
399
|
"""Clear all workers from the pool.
|
|
@@ -434,7 +439,8 @@ class Deadpool(Executor):
|
|
|
434
439
|
t.start()
|
|
435
440
|
|
|
436
441
|
def get_process(self) -> WorkerProcess:
|
|
437
|
-
|
|
442
|
+
with self._workers_lock:
|
|
443
|
+
bw = len(self.busy_workers)
|
|
438
444
|
mw = self.pool_size
|
|
439
445
|
qs = self.workers.qsize()
|
|
440
446
|
|
|
@@ -443,22 +449,20 @@ class Deadpool(Executor):
|
|
|
443
449
|
self.add_worker_to_pool()
|
|
444
450
|
|
|
445
451
|
wp = self.workers.get()
|
|
446
|
-
self.
|
|
447
|
-
|
|
448
|
-
len(self.busy_workers)
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
self.busy_workers
|
|
453
|
-
)
|
|
452
|
+
with self._workers_lock:
|
|
453
|
+
self.busy_workers.add(wp)
|
|
454
|
+
busy_count = len(self.busy_workers)
|
|
455
|
+
with self._statistics.max_workers_busy_concurrently.lock:
|
|
456
|
+
if busy_count > self._statistics.max_workers_busy_concurrently.value:
|
|
457
|
+
self._statistics.max_workers_busy_concurrently.value = busy_count
|
|
454
458
|
|
|
455
459
|
return wp
|
|
456
460
|
|
|
457
461
|
def done_with_process(self, wp: WorkerProcess):
|
|
458
462
|
# This worker is done with its job and is no longer busy.
|
|
459
|
-
self.
|
|
460
|
-
|
|
461
|
-
|
|
463
|
+
with self._workers_lock:
|
|
464
|
+
self.busy_workers.remove(wp)
|
|
465
|
+
count_workers_busy = len(self.busy_workers)
|
|
462
466
|
count_workers_idle = self.workers.qsize()
|
|
463
467
|
backlog_size = self.submitted_jobs.qsize()
|
|
464
468
|
|
|
@@ -544,7 +548,8 @@ class Deadpool(Executor):
|
|
|
544
548
|
return
|
|
545
549
|
|
|
546
550
|
fut.pid = worker.pid
|
|
547
|
-
self.
|
|
551
|
+
with self._workers_lock:
|
|
552
|
+
self.running_futs.add(fut)
|
|
548
553
|
|
|
549
554
|
while True:
|
|
550
555
|
if worker.results_are_available():
|
|
@@ -552,19 +557,35 @@ class Deadpool(Executor):
|
|
|
552
557
|
results = worker.get_results()
|
|
553
558
|
except EOFError:
|
|
554
559
|
self._statistics.tasks_failed.increment()
|
|
555
|
-
fut.
|
|
556
|
-
|
|
557
|
-
|
|
560
|
+
if not fut.done():
|
|
561
|
+
try:
|
|
562
|
+
fut.set_exception(
|
|
563
|
+
ProcessError("Worker process died unexpectedly")
|
|
564
|
+
)
|
|
565
|
+
except InvalidStateError:
|
|
566
|
+
pass
|
|
558
567
|
except BaseException as e:
|
|
559
568
|
self._statistics.tasks_failed.increment()
|
|
560
569
|
logger.debug(f"Unexpected exception from worker: {e}")
|
|
561
|
-
fut.
|
|
570
|
+
if not fut.done():
|
|
571
|
+
try:
|
|
572
|
+
fut.set_exception(e)
|
|
573
|
+
except InvalidStateError:
|
|
574
|
+
pass
|
|
562
575
|
else:
|
|
563
576
|
if isinstance(results, BaseException):
|
|
564
577
|
self._statistics.tasks_failed.increment()
|
|
565
|
-
fut.
|
|
578
|
+
if not fut.done():
|
|
579
|
+
try:
|
|
580
|
+
fut.set_exception(results)
|
|
581
|
+
except InvalidStateError:
|
|
582
|
+
pass
|
|
566
583
|
else:
|
|
567
|
-
fut.
|
|
584
|
+
if not fut.done():
|
|
585
|
+
try:
|
|
586
|
+
fut.set_result(results)
|
|
587
|
+
except InvalidStateError:
|
|
588
|
+
pass
|
|
568
589
|
|
|
569
590
|
if isinstance(results, TimeoutError):
|
|
570
591
|
self._statistics.tasks_failed.increment()
|
|
@@ -680,7 +701,8 @@ class Deadpool(Executor):
|
|
|
680
701
|
# want to wait, that she probably wants us to also stop
|
|
681
702
|
# running processes.
|
|
682
703
|
if (not wait) and cancel_futures:
|
|
683
|
-
|
|
704
|
+
with self._workers_lock:
|
|
705
|
+
running_futs = list(self.running_futs)
|
|
684
706
|
for fut in running_futs:
|
|
685
707
|
fut.cancel_and_kill_if_running()
|
|
686
708
|
|
|
@@ -700,8 +722,10 @@ class Deadpool(Executor):
|
|
|
700
722
|
|
|
701
723
|
# There may be a few processes left in the
|
|
702
724
|
# `busy_workers` queue. Shut them down too.
|
|
703
|
-
|
|
704
|
-
|
|
725
|
+
with self._workers_lock:
|
|
726
|
+
remaining = list(self.busy_workers)
|
|
727
|
+
self.busy_workers.clear()
|
|
728
|
+
for worker in remaining:
|
|
705
729
|
worker.shutdown()
|
|
706
730
|
|
|
707
731
|
def __enter__(self):
|
|
@@ -7,7 +7,8 @@ import signal
|
|
|
7
7
|
import sys
|
|
8
8
|
import time
|
|
9
9
|
import multiprocessing as mp
|
|
10
|
-
|
|
10
|
+
import threading
|
|
11
|
+
from concurrent.futures import CancelledError, InvalidStateError, as_completed
|
|
11
12
|
from contextlib import contextmanager
|
|
12
13
|
from functools import partial
|
|
13
14
|
|
|
@@ -634,6 +635,47 @@ def test_cancel_and_kill():
|
|
|
634
635
|
fut.result()
|
|
635
636
|
|
|
636
637
|
|
|
638
|
+
def delayed_error(delay=0.5):
|
|
639
|
+
time.sleep(delay)
|
|
640
|
+
raise ValueError("delayed error from worker")
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def test_set_exception_on_cancelled_future():
|
|
644
|
+
"""Cancelling a future whose worker raises an exception must not
|
|
645
|
+
cause an InvalidStateError in the run_task thread.
|
|
646
|
+
|
|
647
|
+
Reproduces the bug: deadpool futures stay PENDING (never set to
|
|
648
|
+
RUNNING), so fut.cancel() succeeds even while a worker is
|
|
649
|
+
processing. When the worker finishes with an exception, run_task
|
|
650
|
+
calls fut.set_exception() on the already-cancelled future, raising
|
|
651
|
+
InvalidStateError.
|
|
652
|
+
"""
|
|
653
|
+
thread_exceptions = []
|
|
654
|
+
original_hook = threading.excepthook
|
|
655
|
+
|
|
656
|
+
def capture_hook(args):
|
|
657
|
+
thread_exceptions.append(args)
|
|
658
|
+
|
|
659
|
+
threading.excepthook = capture_hook
|
|
660
|
+
try:
|
|
661
|
+
exe = deadpool.Deadpool(max_workers=1)
|
|
662
|
+
fut = exe.submit(delayed_error, 0.5)
|
|
663
|
+
time.sleep(0.2) # let worker start
|
|
664
|
+
fut.cancel() # cancel while worker is still running
|
|
665
|
+
time.sleep(1.0) # let worker finish and run_task process results
|
|
666
|
+
exe.shutdown(wait=True)
|
|
667
|
+
|
|
668
|
+
assert fut.cancelled()
|
|
669
|
+
invalid_state_errors = [
|
|
670
|
+
e for e in thread_exceptions if isinstance(e.exc_value, InvalidStateError)
|
|
671
|
+
]
|
|
672
|
+
assert (
|
|
673
|
+
not invalid_state_errors
|
|
674
|
+
), f"InvalidStateError raised in background thread: {invalid_state_errors}"
|
|
675
|
+
finally:
|
|
676
|
+
threading.excepthook = original_hook
|
|
677
|
+
|
|
678
|
+
|
|
637
679
|
def test_trim_memory():
|
|
638
680
|
"""Just testing it doesn't fail."""
|
|
639
681
|
deadpool.trim_memory()
|
|
@@ -654,13 +696,15 @@ def leaker(n):
|
|
|
654
696
|
def test_max_memory(logging_initializer):
|
|
655
697
|
# Verify that the memory threshold feature in deadpool
|
|
656
698
|
# works as expected. This test will run 20 functions, 10
|
|
657
|
-
# of which will consume
|
|
699
|
+
# of which will consume 150MB of memory. The other 10 will
|
|
658
700
|
# consume 0.1MB of memory. The memory threshold is set to
|
|
659
|
-
#
|
|
660
|
-
# to be replaced by new workers, while the
|
|
701
|
+
# 100MB, so the large functions should cause their workers
|
|
702
|
+
# to be replaced by new workers, while the small functions
|
|
661
703
|
# should be able to run without requiring their workers to be
|
|
662
|
-
# replaced.
|
|
663
|
-
#
|
|
704
|
+
# replaced. We verify that more unique PIDs were seen than
|
|
705
|
+
# the pool size, proving that worker replacement occurred.
|
|
706
|
+
# We use >= 10 rather than == 11 because PID recycling can
|
|
707
|
+
# cause a replaced worker's PID to be reused by its successor.
|
|
664
708
|
|
|
665
709
|
leak_test_accumulator.clear()
|
|
666
710
|
with deadpool.Deadpool(
|
|
@@ -675,11 +719,10 @@ def test_max_memory(logging_initializer):
|
|
|
675
719
|
|
|
676
720
|
pids = set(f.result() for f in deadpool.as_completed(futs))
|
|
677
721
|
|
|
678
|
-
# We
|
|
679
|
-
#
|
|
680
|
-
#
|
|
681
|
-
|
|
682
|
-
assert len(pids) == 11
|
|
722
|
+
# We expect ~11 unique PIDs (1 initial + 10 replacements), but
|
|
723
|
+
# PID recycling may reduce this slightly. At minimum we should
|
|
724
|
+
# see substantially more than max_workers (1).
|
|
725
|
+
assert len(pids) >= 10
|
|
683
726
|
|
|
684
727
|
|
|
685
728
|
def test_can_pickle_nested_function():
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|