flexexecutor 0.0.11__tar.gz → 0.0.13__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.
- {flexexecutor-0.0.11 → flexexecutor-0.0.13}/PKG-INFO +11 -8
- {flexexecutor-0.0.11 → flexexecutor-0.0.13}/README.md +4 -0
- flexexecutor-0.0.13/flexexecutor.py +811 -0
- {flexexecutor-0.0.11 → flexexecutor-0.0.13}/pyproject.toml +21 -20
- flexexecutor-0.0.11/flexexecutor.py +0 -386
- {flexexecutor-0.0.11 → flexexecutor-0.0.13}/LICENSE +0 -0
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: flexexecutor
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.13
|
|
4
4
|
Summary: Executors for both async and sync functions and automatically scales the number of workers
|
|
5
|
-
Home-page: https://github.com/leavers/flexexecutor
|
|
6
5
|
Author: leavers
|
|
7
6
|
Author-Email: Leavers <leavers930@gmail.com>
|
|
8
7
|
License: MIT
|
|
@@ -11,17 +10,17 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
11
10
|
Classifier: Operating System :: OS Independent
|
|
12
11
|
Classifier: Programming Language :: Python
|
|
13
12
|
Classifier: Programming Language :: Python :: 3
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
17
13
|
Classifier: Programming Language :: Python :: 3.11
|
|
18
14
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
19
18
|
Classifier: Topic :: Software Development
|
|
20
19
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
20
|
Classifier: Topic :: Utilities
|
|
22
|
-
Project-URL:
|
|
23
|
-
Project-URL:
|
|
24
|
-
Requires-Python: >=3.
|
|
21
|
+
Project-URL: homepage, https://github.com/leavers/flexexecutor
|
|
22
|
+
Project-URL: documentation, https://github.com/leavers/flexexecutor/blob/main/README.md
|
|
23
|
+
Requires-Python: >=3.11
|
|
25
24
|
Description-Content-Type: text/markdown
|
|
26
25
|
|
|
27
26
|
# Flexexecutor
|
|
@@ -96,6 +95,10 @@ if __name__ == "__main__":
|
|
|
96
95
|
|
|
97
96
|
### AsyncPoolExecutor
|
|
98
97
|
|
|
98
|
+
`AsyncPoolExecutor` runs all coroutines in a dedicated worker thread with its own
|
|
99
|
+
event loop. Note that **tasks are not executed in the thread where they are
|
|
100
|
+
submitted** — they are sent to the internal worker thread for execution.
|
|
101
|
+
|
|
99
102
|
```python
|
|
100
103
|
from flexexecutor import AsyncPoolExecutor
|
|
101
104
|
|
|
@@ -70,6 +70,10 @@ if __name__ == "__main__":
|
|
|
70
70
|
|
|
71
71
|
### AsyncPoolExecutor
|
|
72
72
|
|
|
73
|
+
`AsyncPoolExecutor` runs all coroutines in a dedicated worker thread with its own
|
|
74
|
+
event loop. Note that **tasks are not executed in the thread where they are
|
|
75
|
+
submitted** — they are sent to the internal worker thread for execution.
|
|
76
|
+
|
|
73
77
|
```python
|
|
74
78
|
from flexexecutor import AsyncPoolExecutor
|
|
75
79
|
|
|
@@ -0,0 +1,811 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Flexexecutor provides executors that can automatically scale the number of
|
|
3
|
+
workers up and down.
|
|
4
|
+
|
|
5
|
+
Copyright (c) 2020-2026, Leavers.
|
|
6
|
+
License: MIT
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import atexit
|
|
11
|
+
import itertools
|
|
12
|
+
import queue
|
|
13
|
+
import types
|
|
14
|
+
from asyncio import AbstractEventLoop
|
|
15
|
+
from asyncio import Queue as AsyncQueue
|
|
16
|
+
from asyncio.timeouts import timeout
|
|
17
|
+
from concurrent.futures import Future, ProcessPoolExecutor, _base
|
|
18
|
+
from concurrent.futures.thread import BrokenThreadPool
|
|
19
|
+
from inspect import iscoroutinefunction
|
|
20
|
+
from queue import Empty
|
|
21
|
+
from threading import Event, Lock, Semaphore, Thread
|
|
22
|
+
from time import monotonic
|
|
23
|
+
from typing import TYPE_CHECKING
|
|
24
|
+
from weakref import WeakKeyDictionary, ref, WeakSet
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from typing_extensions import Callable, ParamSpec, TypeVar
|
|
28
|
+
|
|
29
|
+
P = ParamSpec("P")
|
|
30
|
+
T = TypeVar("T")
|
|
31
|
+
|
|
32
|
+
__all__ = (
|
|
33
|
+
"__version__",
|
|
34
|
+
"AsyncPoolExecutor",
|
|
35
|
+
"AsyncWorkerContext",
|
|
36
|
+
"BrokenThreadPool",
|
|
37
|
+
"Future",
|
|
38
|
+
"ProcessPoolExecutor",
|
|
39
|
+
"ThreadPoolExecutor",
|
|
40
|
+
"WorkerContext",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
__version__ = "0.0.13"
|
|
44
|
+
|
|
45
|
+
# Type alias for async component (loop, queue) tuple
|
|
46
|
+
_AsyncComponent = tuple[AbstractEventLoop, "AsyncQueue[_AsyncWorkItem | None]"]
|
|
47
|
+
|
|
48
|
+
_threads_queues: WeakKeyDictionary[Thread, queue.SimpleQueue] = WeakKeyDictionary()
|
|
49
|
+
_async_executors: WeakSet["AsyncPoolExecutor"] = WeakSet()
|
|
50
|
+
_shutdown = False
|
|
51
|
+
_global_shutdown_lock = Lock()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _python_exit():
|
|
55
|
+
global _shutdown
|
|
56
|
+
with _global_shutdown_lock:
|
|
57
|
+
_shutdown = True
|
|
58
|
+
|
|
59
|
+
threads_items = list(_threads_queues.items())
|
|
60
|
+
for t, q in threads_items:
|
|
61
|
+
q.put(None)
|
|
62
|
+
for t, q in threads_items:
|
|
63
|
+
t.join()
|
|
64
|
+
|
|
65
|
+
for e in _async_executors:
|
|
66
|
+
e.shutdown()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
atexit.register(_python_exit)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class WorkerContext:
|
|
73
|
+
"""Worker context for thread pool executor.
|
|
74
|
+
|
|
75
|
+
This class provides a way to customize worker thread initialization,
|
|
76
|
+
task execution, and cleanup. It is designed to be compatible with
|
|
77
|
+
Python 3.11+ and follows the design of Python 3.14's ThreadPoolExecutor.
|
|
78
|
+
|
|
79
|
+
Example:
|
|
80
|
+
class DatabaseContext(WorkerContext):
|
|
81
|
+
def initialize(self):
|
|
82
|
+
self.db = create_db_connection()
|
|
83
|
+
|
|
84
|
+
def finalize(self):
|
|
85
|
+
self.db.close()
|
|
86
|
+
|
|
87
|
+
def run(self, task):
|
|
88
|
+
fn, args, kwargs = task
|
|
89
|
+
return fn(self.db, *args, **kwargs)
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
def prepare(
|
|
94
|
+
cls, initializer: "Callable | None" = None, initargs: tuple = ()
|
|
95
|
+
) -> tuple["Callable[[], WorkerContext]", "Callable[..., tuple]"]:
|
|
96
|
+
"""Prepare context factory and task resolver.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
initializer: A callable used to initialize worker threads.
|
|
100
|
+
initargs: A tuple of arguments to pass to the initializer.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
A tuple of (create_context, resolve_task) functions.
|
|
104
|
+
"""
|
|
105
|
+
if initializer is not None:
|
|
106
|
+
if not callable(initializer):
|
|
107
|
+
raise TypeError("initializer must be a callable")
|
|
108
|
+
|
|
109
|
+
def create_context() -> "WorkerContext":
|
|
110
|
+
return cls(initializer, initargs)
|
|
111
|
+
|
|
112
|
+
def resolve_task(fn, args, kwargs) -> tuple:
|
|
113
|
+
return (fn, args, kwargs)
|
|
114
|
+
|
|
115
|
+
return create_context, resolve_task
|
|
116
|
+
|
|
117
|
+
def __init__(self, initializer: "Callable | None" = None, initargs: tuple = ()):
|
|
118
|
+
self.initializer = initializer
|
|
119
|
+
self.initargs = initargs
|
|
120
|
+
|
|
121
|
+
def initialize(self) -> None:
|
|
122
|
+
"""Initialize the worker context.
|
|
123
|
+
|
|
124
|
+
Called once when the worker thread starts.
|
|
125
|
+
"""
|
|
126
|
+
if self.initializer is not None:
|
|
127
|
+
self.initializer(*self.initargs)
|
|
128
|
+
|
|
129
|
+
def finalize(self) -> None:
|
|
130
|
+
"""Finalize the worker context.
|
|
131
|
+
|
|
132
|
+
Called once when the worker thread exits.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
def run(self, task: tuple) -> "T":
|
|
136
|
+
"""Execute a task.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
task: A tuple of (fn, args, kwargs).
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
The result of the task execution.
|
|
143
|
+
"""
|
|
144
|
+
fn, args, kwargs = task
|
|
145
|
+
return fn(*args, **kwargs)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class _WorkItem:
|
|
149
|
+
"""A work item for the thread pool executor."""
|
|
150
|
+
|
|
151
|
+
def __init__(self, future: Future, task: tuple):
|
|
152
|
+
self.future = future
|
|
153
|
+
self.task = task
|
|
154
|
+
|
|
155
|
+
def run(self, ctx: WorkerContext) -> None:
|
|
156
|
+
if not self.future.set_running_or_notify_cancel():
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
result = ctx.run(self.task)
|
|
161
|
+
except BaseException as exc:
|
|
162
|
+
self.future.set_exception(exc)
|
|
163
|
+
# Break a reference cycle with the exception 'exc'
|
|
164
|
+
self = None # noqa: F841
|
|
165
|
+
else:
|
|
166
|
+
self.future.set_result(result)
|
|
167
|
+
|
|
168
|
+
__class_getitem__ = classmethod(types.GenericAlias)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _worker(
|
|
172
|
+
executor_ref,
|
|
173
|
+
work_queue: queue.SimpleQueue,
|
|
174
|
+
create_context: "Callable[[], WorkerContext]",
|
|
175
|
+
idle_timeout: float,
|
|
176
|
+
):
|
|
177
|
+
"""Worker function for the thread pool executor with idle timeout support."""
|
|
178
|
+
ctx = create_context()
|
|
179
|
+
try:
|
|
180
|
+
ctx.initialize()
|
|
181
|
+
except BaseException:
|
|
182
|
+
_base.LOGGER.critical("Exception in initializer:", exc_info=True)
|
|
183
|
+
executor = executor_ref()
|
|
184
|
+
if executor is not None: # pragma: no cover
|
|
185
|
+
executor._initializer_failed()
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
idle_tick = monotonic()
|
|
189
|
+
try:
|
|
190
|
+
while True:
|
|
191
|
+
try:
|
|
192
|
+
work_item = work_queue.get(block=True, timeout=0.1)
|
|
193
|
+
except Empty:
|
|
194
|
+
# Check idle timeout
|
|
195
|
+
if idle_timeout >= 0 and monotonic() - idle_tick > idle_timeout:
|
|
196
|
+
break
|
|
197
|
+
continue
|
|
198
|
+
|
|
199
|
+
if work_item is None:
|
|
200
|
+
break
|
|
201
|
+
|
|
202
|
+
work_item.run(ctx)
|
|
203
|
+
del work_item
|
|
204
|
+
|
|
205
|
+
executor = executor_ref()
|
|
206
|
+
if executor is not None:
|
|
207
|
+
executor._idle_semaphore.release()
|
|
208
|
+
del executor
|
|
209
|
+
idle_tick = monotonic()
|
|
210
|
+
finally:
|
|
211
|
+
ctx.finalize()
|
|
212
|
+
executor = executor_ref()
|
|
213
|
+
if executor is None:
|
|
214
|
+
work_queue.put(None)
|
|
215
|
+
else:
|
|
216
|
+
executor._idle_semaphore.acquire(timeout=0)
|
|
217
|
+
if _shutdown or executor._shutdown:
|
|
218
|
+
executor._shutdown = True
|
|
219
|
+
work_queue.put(None)
|
|
220
|
+
del executor
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class ThreadPoolExecutor(_base.Executor):
|
|
224
|
+
"""A thread pool executor with automatic worker scaling and idle timeout.
|
|
225
|
+
|
|
226
|
+
This implementation is compatible with Python 3.11-3.15 and does not depend
|
|
227
|
+
on internal CPython APIs.
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
_counter = itertools.count().__next__
|
|
231
|
+
|
|
232
|
+
@classmethod
|
|
233
|
+
def prepare_context(
|
|
234
|
+
cls, initializer: "Callable | None" = None, initargs: tuple = ()
|
|
235
|
+
) -> tuple["Callable[[], WorkerContext]", "Callable[..., tuple]"]:
|
|
236
|
+
"""Prepare context factory and task resolver.
|
|
237
|
+
|
|
238
|
+
This class method can be overridden to provide a custom WorkerContext.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
initializer: A callable used to initialize worker threads.
|
|
242
|
+
initargs: A tuple of arguments to pass to the initializer.
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
A tuple of (create_context, resolve_task) functions.
|
|
246
|
+
"""
|
|
247
|
+
return WorkerContext.prepare(initializer, initargs)
|
|
248
|
+
|
|
249
|
+
def __init__(
|
|
250
|
+
self,
|
|
251
|
+
max_workers: int | None = 1024,
|
|
252
|
+
thread_name_prefix: str = "",
|
|
253
|
+
initializer=None,
|
|
254
|
+
initargs=(),
|
|
255
|
+
idle_timeout: float | None = 60.0,
|
|
256
|
+
):
|
|
257
|
+
"""Initializes a new ThreadPoolExecutor instance.
|
|
258
|
+
|
|
259
|
+
:type max_workers: int, optional
|
|
260
|
+
:param max_workers: The maximum number of workers to create. Defaults to 1024.
|
|
261
|
+
|
|
262
|
+
:type thread_name_prefix: str, optional
|
|
263
|
+
:param thread_name_prefix: An optional name prefix to give our threads.
|
|
264
|
+
|
|
265
|
+
:type initializer: callable, optional
|
|
266
|
+
:param initializer: A callable used to initialize worker threads.
|
|
267
|
+
|
|
268
|
+
:type initargs: tuple, optional
|
|
269
|
+
:param initargs: A tuple of arguments to pass to the initializer.
|
|
270
|
+
|
|
271
|
+
:type idle_timeout: float, optional
|
|
272
|
+
:param idle_timeout: The maximum amount of time (in seconds) that a worker
|
|
273
|
+
thread can remain idle before it is terminated. If set to None or negative
|
|
274
|
+
value, workers will never be terminated. Defaults to 60 seconds.
|
|
275
|
+
"""
|
|
276
|
+
if max_workers is None:
|
|
277
|
+
max_workers = 1024
|
|
278
|
+
if max_workers <= 0:
|
|
279
|
+
raise ValueError("max_workers must be greater than 0")
|
|
280
|
+
|
|
281
|
+
self._max_workers = max_workers
|
|
282
|
+
self._work_queue: queue.SimpleQueue[_WorkItem | None] = queue.SimpleQueue()
|
|
283
|
+
self._idle_semaphore = Semaphore(0)
|
|
284
|
+
self._threads: set[Thread] = set()
|
|
285
|
+
self._broken = False
|
|
286
|
+
self._shutdown = False
|
|
287
|
+
self._shutdown_lock = Lock()
|
|
288
|
+
self._thread_name_prefix = (
|
|
289
|
+
thread_name_prefix or f"ThreadPoolExecutor-{self._counter()}"
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# Prepare context factory and task resolver
|
|
293
|
+
(
|
|
294
|
+
self._create_worker_context,
|
|
295
|
+
self._resolve_work_item_task,
|
|
296
|
+
) = type(self).prepare_context(initializer, initargs)
|
|
297
|
+
|
|
298
|
+
if idle_timeout is None or idle_timeout < 0:
|
|
299
|
+
self._idle_timeout = -1.0
|
|
300
|
+
else:
|
|
301
|
+
self._idle_timeout = max(0.1, idle_timeout)
|
|
302
|
+
|
|
303
|
+
def submit(
|
|
304
|
+
self,
|
|
305
|
+
fn: "Callable[P, T]",
|
|
306
|
+
/,
|
|
307
|
+
*args: "P.args",
|
|
308
|
+
**kwargs: "P.kwargs",
|
|
309
|
+
) -> "Future[T]":
|
|
310
|
+
if iscoroutinefunction(fn):
|
|
311
|
+
raise TypeError("fn must not be a coroutine function")
|
|
312
|
+
|
|
313
|
+
with self._shutdown_lock, _global_shutdown_lock:
|
|
314
|
+
if self._broken:
|
|
315
|
+
raise BrokenThreadPool(self._broken)
|
|
316
|
+
|
|
317
|
+
if self._shutdown:
|
|
318
|
+
raise RuntimeError("cannot schedule new futures after shutdown")
|
|
319
|
+
if _shutdown:
|
|
320
|
+
# coverage didn't realize that _shutdown is set, add no cover here
|
|
321
|
+
raise RuntimeError( # pragma: no cover
|
|
322
|
+
"cannot schedule new futures after interpreter shutdown"
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
f: Future = Future()
|
|
326
|
+
task = self._resolve_work_item_task(fn, args, kwargs)
|
|
327
|
+
w = _WorkItem(f, task)
|
|
328
|
+
|
|
329
|
+
self._work_queue.put(w)
|
|
330
|
+
self._adjust_thread_count()
|
|
331
|
+
return f
|
|
332
|
+
|
|
333
|
+
submit.__doc__ = _base.Executor.submit.__doc__
|
|
334
|
+
|
|
335
|
+
def _adjust_thread_count(self):
|
|
336
|
+
# If idle threads are available, don't spin new threads
|
|
337
|
+
if self._idle_semaphore.acquire(timeout=0):
|
|
338
|
+
return
|
|
339
|
+
|
|
340
|
+
# Remove dead threads
|
|
341
|
+
threads = self._threads
|
|
342
|
+
dead_threads = [t for t in threads if not t.is_alive()]
|
|
343
|
+
for t in dead_threads:
|
|
344
|
+
threads.discard(t)
|
|
345
|
+
|
|
346
|
+
# When the executor gets lost, the weakref callback will wake up
|
|
347
|
+
# the worker threads.
|
|
348
|
+
def weakref_cb(_, q=self._work_queue):
|
|
349
|
+
q.put(None)
|
|
350
|
+
|
|
351
|
+
num_threads = len(threads)
|
|
352
|
+
if num_threads < self._max_workers:
|
|
353
|
+
t = Thread(
|
|
354
|
+
name=f"{self._thread_name_prefix}_{num_threads}",
|
|
355
|
+
target=_worker,
|
|
356
|
+
args=(
|
|
357
|
+
ref(self, weakref_cb),
|
|
358
|
+
self._work_queue,
|
|
359
|
+
self._create_worker_context,
|
|
360
|
+
self._idle_timeout,
|
|
361
|
+
),
|
|
362
|
+
)
|
|
363
|
+
t.start()
|
|
364
|
+
threads.add(t)
|
|
365
|
+
_threads_queues[t] = self._work_queue
|
|
366
|
+
|
|
367
|
+
def _initializer_failed(self):
|
|
368
|
+
with self._shutdown_lock:
|
|
369
|
+
self._broken = (
|
|
370
|
+
"A thread initializer failed, the thread pool is not usable anymore"
|
|
371
|
+
)
|
|
372
|
+
# Drain work queue and mark pending futures failed
|
|
373
|
+
while True:
|
|
374
|
+
try:
|
|
375
|
+
work_item = self._work_queue.get_nowait()
|
|
376
|
+
except Empty:
|
|
377
|
+
break
|
|
378
|
+
if work_item is not None:
|
|
379
|
+
work_item.future.set_exception(BrokenThreadPool(self._broken))
|
|
380
|
+
|
|
381
|
+
def shutdown(self, wait: bool = True, *, cancel_futures: bool = False):
|
|
382
|
+
with self._shutdown_lock:
|
|
383
|
+
self._shutdown = True
|
|
384
|
+
if cancel_futures:
|
|
385
|
+
# Drain all work items from the queue, and then cancel their
|
|
386
|
+
# associated futures.
|
|
387
|
+
while True:
|
|
388
|
+
try:
|
|
389
|
+
work_item = self._work_queue.get_nowait()
|
|
390
|
+
except Empty:
|
|
391
|
+
break
|
|
392
|
+
if work_item is not None:
|
|
393
|
+
work_item.future.cancel()
|
|
394
|
+
|
|
395
|
+
# Send a wake-up to prevent threads calling
|
|
396
|
+
# _work_queue.get(block=True) from permanently blocking.
|
|
397
|
+
self._work_queue.put(None)
|
|
398
|
+
|
|
399
|
+
if wait:
|
|
400
|
+
for t in self._threads:
|
|
401
|
+
t.join()
|
|
402
|
+
|
|
403
|
+
shutdown.__doc__ = _base.Executor.shutdown.__doc__
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
class AsyncWorkerContext:
|
|
407
|
+
"""Worker context for async pool executor.
|
|
408
|
+
|
|
409
|
+
This class provides a way to customize async worker initialization,
|
|
410
|
+
task execution, and cleanup. It supports both sync and async initializers.
|
|
411
|
+
|
|
412
|
+
Example:
|
|
413
|
+
class DatabaseContext(AsyncWorkerContext):
|
|
414
|
+
async def initialize(self):
|
|
415
|
+
self.db = await create_async_db_connection()
|
|
416
|
+
|
|
417
|
+
async def finalize(self):
|
|
418
|
+
await self.db.close()
|
|
419
|
+
|
|
420
|
+
async def run(self, task):
|
|
421
|
+
fn, args, kwargs = task
|
|
422
|
+
return await fn(self.db, *args, **kwargs)
|
|
423
|
+
"""
|
|
424
|
+
|
|
425
|
+
@classmethod
|
|
426
|
+
def prepare(
|
|
427
|
+
cls, initializer: "Callable | None" = None, initargs: tuple = ()
|
|
428
|
+
) -> tuple["Callable[[], AsyncWorkerContext]", "Callable[..., tuple]"]:
|
|
429
|
+
"""Prepare context factory and task resolver.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
initializer: A callable used to initialize worker threads.
|
|
433
|
+
Can be sync or async.
|
|
434
|
+
initargs: A tuple of arguments to pass to the initializer.
|
|
435
|
+
|
|
436
|
+
Returns:
|
|
437
|
+
A tuple of (create_context, resolve_task) functions.
|
|
438
|
+
"""
|
|
439
|
+
if initializer is not None:
|
|
440
|
+
if not callable(initializer):
|
|
441
|
+
raise TypeError("initializer must be a callable")
|
|
442
|
+
|
|
443
|
+
def create_context() -> "AsyncWorkerContext":
|
|
444
|
+
return cls(initializer, initargs)
|
|
445
|
+
|
|
446
|
+
def resolve_task(fn, args, kwargs) -> tuple:
|
|
447
|
+
return (fn, args, kwargs)
|
|
448
|
+
|
|
449
|
+
return create_context, resolve_task
|
|
450
|
+
|
|
451
|
+
def __init__(self, initializer: "Callable | None" = None, initargs: tuple = ()):
|
|
452
|
+
self.initializer = initializer
|
|
453
|
+
self.initargs = initargs
|
|
454
|
+
|
|
455
|
+
async def initialize(self) -> None:
|
|
456
|
+
"""Initialize the worker context.
|
|
457
|
+
|
|
458
|
+
Called once when the worker thread starts and event loop is ready.
|
|
459
|
+
"""
|
|
460
|
+
if self.initializer is not None:
|
|
461
|
+
if iscoroutinefunction(self.initializer):
|
|
462
|
+
await self.initializer(*self.initargs)
|
|
463
|
+
else:
|
|
464
|
+
self.initializer(*self.initargs)
|
|
465
|
+
|
|
466
|
+
async def finalize(self) -> None:
|
|
467
|
+
"""Finalize the worker context.
|
|
468
|
+
|
|
469
|
+
Called once when the worker thread exits.
|
|
470
|
+
"""
|
|
471
|
+
|
|
472
|
+
async def run(self, task: tuple) -> "T":
|
|
473
|
+
"""Execute a task.
|
|
474
|
+
|
|
475
|
+
Args:
|
|
476
|
+
task: A tuple of (fn, args, kwargs).
|
|
477
|
+
|
|
478
|
+
Returns:
|
|
479
|
+
The result of the task execution.
|
|
480
|
+
"""
|
|
481
|
+
fn, args, kwargs = task
|
|
482
|
+
if iscoroutinefunction(fn):
|
|
483
|
+
return await fn(*args, **kwargs)
|
|
484
|
+
return fn(*args, **kwargs)
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
class _AsyncWorkItem:
|
|
488
|
+
"""A work item for the async pool executor."""
|
|
489
|
+
|
|
490
|
+
def __init__(self, future: Future, task: tuple):
|
|
491
|
+
self.future = future
|
|
492
|
+
self.task = task
|
|
493
|
+
|
|
494
|
+
async def run(self, ctx: AsyncWorkerContext) -> None:
|
|
495
|
+
if not self.future.set_running_or_notify_cancel():
|
|
496
|
+
return
|
|
497
|
+
|
|
498
|
+
try:
|
|
499
|
+
result = await ctx.run(self.task)
|
|
500
|
+
except BaseException as exc:
|
|
501
|
+
self.future.set_exception(exc)
|
|
502
|
+
# Break a reference cycle with the exception 'exc'
|
|
503
|
+
self = None # noqa: F841
|
|
504
|
+
else:
|
|
505
|
+
self.future.set_result(result)
|
|
506
|
+
|
|
507
|
+
__class_getitem__ = classmethod(types.GenericAlias)
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
async def _async_worker(
|
|
511
|
+
executor_ref,
|
|
512
|
+
ready: Event,
|
|
513
|
+
max_workers: int,
|
|
514
|
+
idle_timeout: float,
|
|
515
|
+
create_context: "Callable[[], AsyncWorkerContext]",
|
|
516
|
+
):
|
|
517
|
+
"""Worker function for the async pool executor with idle timeout support."""
|
|
518
|
+
if max_workers <= 0:
|
|
519
|
+
raise ValueError(f"max_workers must be greater than 0, got {max_workers}")
|
|
520
|
+
|
|
521
|
+
ctx = create_context()
|
|
522
|
+
try:
|
|
523
|
+
await ctx.initialize()
|
|
524
|
+
except BaseException:
|
|
525
|
+
_base.LOGGER.critical("Exception in initializer:", exc_info=True)
|
|
526
|
+
executor = executor_ref()
|
|
527
|
+
if executor is not None:
|
|
528
|
+
executor._initializer_failed()
|
|
529
|
+
return
|
|
530
|
+
|
|
531
|
+
semaphore = asyncio.Semaphore(max_workers)
|
|
532
|
+
concurrency = 0
|
|
533
|
+
|
|
534
|
+
def on_task_done(_):
|
|
535
|
+
nonlocal concurrency
|
|
536
|
+
concurrency -= 1
|
|
537
|
+
semaphore.release()
|
|
538
|
+
|
|
539
|
+
loop = asyncio.get_running_loop()
|
|
540
|
+
work_queue = AsyncQueue()
|
|
541
|
+
if (executor := executor_ref()) is not None:
|
|
542
|
+
executor._mark_thread_active(loop, work_queue)
|
|
543
|
+
del executor
|
|
544
|
+
|
|
545
|
+
async def run(tg: asyncio.TaskGroup):
|
|
546
|
+
nonlocal concurrency
|
|
547
|
+
|
|
548
|
+
ready.set()
|
|
549
|
+
idle_tick = monotonic()
|
|
550
|
+
|
|
551
|
+
try:
|
|
552
|
+
while True:
|
|
553
|
+
if await semaphore.acquire():
|
|
554
|
+
semaphore.release()
|
|
555
|
+
try:
|
|
556
|
+
async with timeout(0.1):
|
|
557
|
+
work_item = await work_queue.get()
|
|
558
|
+
if work_item is None:
|
|
559
|
+
break
|
|
560
|
+
|
|
561
|
+
# Create task that runs the work item with context
|
|
562
|
+
async def run_with_context(item):
|
|
563
|
+
await item.run(ctx)
|
|
564
|
+
|
|
565
|
+
task = tg.create_task(run_with_context(work_item))
|
|
566
|
+
task.add_done_callback(on_task_done)
|
|
567
|
+
concurrency += 1
|
|
568
|
+
del work_item
|
|
569
|
+
idle_tick = monotonic()
|
|
570
|
+
except (Empty, TimeoutError):
|
|
571
|
+
pass
|
|
572
|
+
if concurrency > 0:
|
|
573
|
+
idle_tick = monotonic()
|
|
574
|
+
elif idle_timeout >= 0 and monotonic() - idle_tick > idle_timeout:
|
|
575
|
+
break
|
|
576
|
+
finally:
|
|
577
|
+
if (executor := executor_ref()) is not None:
|
|
578
|
+
executor._mark_thread_inactive()
|
|
579
|
+
if _shutdown or executor._shutdown:
|
|
580
|
+
executor._shutdown = True
|
|
581
|
+
work_queue.put_nowait(None)
|
|
582
|
+
del executor
|
|
583
|
+
|
|
584
|
+
try:
|
|
585
|
+
async with asyncio.TaskGroup() as tg:
|
|
586
|
+
await run(tg)
|
|
587
|
+
finally:
|
|
588
|
+
await ctx.finalize()
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
class AsyncPoolExecutor(_base.Executor):
|
|
592
|
+
"""An async pool executor with automatic worker scaling and idle timeout.
|
|
593
|
+
|
|
594
|
+
This executor runs coroutines in a dedicated worker thread with its own
|
|
595
|
+
event loop. It supports concurrent execution of multiple coroutines with
|
|
596
|
+
a semaphore-based limit.
|
|
597
|
+
"""
|
|
598
|
+
|
|
599
|
+
_counter = itertools.count().__next__
|
|
600
|
+
|
|
601
|
+
@classmethod
|
|
602
|
+
def prepare_context(
|
|
603
|
+
cls, initializer: "Callable | None" = None, initargs: tuple = ()
|
|
604
|
+
) -> tuple["Callable[[], AsyncWorkerContext]", "Callable[..., tuple]"]:
|
|
605
|
+
"""Prepare context factory and task resolver.
|
|
606
|
+
|
|
607
|
+
This class method can be overridden to provide a custom AsyncWorkerContext.
|
|
608
|
+
|
|
609
|
+
Args:
|
|
610
|
+
initializer: A callable used to initialize worker threads.
|
|
611
|
+
Can be sync or async.
|
|
612
|
+
initargs: A tuple of arguments to pass to the initializer.
|
|
613
|
+
|
|
614
|
+
Returns:
|
|
615
|
+
A tuple of (create_context, resolve_task) functions.
|
|
616
|
+
"""
|
|
617
|
+
return AsyncWorkerContext.prepare(initializer, initargs)
|
|
618
|
+
|
|
619
|
+
def __init__(
|
|
620
|
+
self,
|
|
621
|
+
max_workers: int | None = None,
|
|
622
|
+
thread_name_prefix: str = "",
|
|
623
|
+
initializer=None,
|
|
624
|
+
initargs=(),
|
|
625
|
+
idle_timeout: float | None = 60.0,
|
|
626
|
+
):
|
|
627
|
+
"""Initializes a new AsyncPoolExecutor instance.
|
|
628
|
+
|
|
629
|
+
:type max_workers: int, optional
|
|
630
|
+
:param max_workers: The maximum number of concurrent coroutines to run.
|
|
631
|
+
Defaults to 262144.
|
|
632
|
+
|
|
633
|
+
:type thread_name_prefix: str, optional
|
|
634
|
+
:param thread_name_prefix: An optional name prefix to give our threads.
|
|
635
|
+
|
|
636
|
+
:type initializer: callable, optional
|
|
637
|
+
:param initializer: A callable used to initialize worker threads.
|
|
638
|
+
Can be a sync function or an async coroutine function.
|
|
639
|
+
|
|
640
|
+
:type initargs: tuple, optional
|
|
641
|
+
:param initargs: A tuple of arguments to pass to the initializer.
|
|
642
|
+
|
|
643
|
+
:type idle_timeout: float, optional
|
|
644
|
+
:param idle_timeout: The maximum amount of time (in seconds) that a worker
|
|
645
|
+
thread can remain idle before it is terminated. If set to None or negative
|
|
646
|
+
value, workers will never be terminated. Defaults to 60 seconds.
|
|
647
|
+
"""
|
|
648
|
+
super().__init__()
|
|
649
|
+
if max_workers is None:
|
|
650
|
+
max_workers = 262144
|
|
651
|
+
if max_workers <= 0:
|
|
652
|
+
raise ValueError("max_workers must be greater than 0")
|
|
653
|
+
if not thread_name_prefix:
|
|
654
|
+
thread_name_prefix = f"AsyncPoolExecutor-{self._counter()}"
|
|
655
|
+
|
|
656
|
+
self._broken = False
|
|
657
|
+
self._shutdown = False
|
|
658
|
+
self._shutdown_lock = Lock()
|
|
659
|
+
self._max_workers = max_workers
|
|
660
|
+
self._thread_name_prefix = thread_name_prefix
|
|
661
|
+
self._thread: Thread | None = None
|
|
662
|
+
self._thread_active = Event()
|
|
663
|
+
|
|
664
|
+
# Prepare context factory and task resolver
|
|
665
|
+
(
|
|
666
|
+
self._create_worker_context,
|
|
667
|
+
self._resolve_work_item_task,
|
|
668
|
+
) = type(self).prepare_context(initializer, initargs)
|
|
669
|
+
|
|
670
|
+
# Wrap (loop, queue) into a list to make they can be accessed in weakref
|
|
671
|
+
# callback even if worker has been changed.
|
|
672
|
+
self._comp: list[_AsyncComponent] = []
|
|
673
|
+
|
|
674
|
+
if idle_timeout is None or idle_timeout < 0:
|
|
675
|
+
self._idle_timeout = -1
|
|
676
|
+
else:
|
|
677
|
+
self._idle_timeout = max(0.1, idle_timeout)
|
|
678
|
+
_async_executors.add(self)
|
|
679
|
+
|
|
680
|
+
def submit(
|
|
681
|
+
self,
|
|
682
|
+
fn: "Callable[P, T]",
|
|
683
|
+
/,
|
|
684
|
+
*args: "P.args",
|
|
685
|
+
**kwargs: "P.kwargs",
|
|
686
|
+
) -> "Future[T]":
|
|
687
|
+
if not iscoroutinefunction(fn):
|
|
688
|
+
raise TypeError("fn must be a coroutine function")
|
|
689
|
+
|
|
690
|
+
with self._shutdown_lock, _global_shutdown_lock:
|
|
691
|
+
if self._broken:
|
|
692
|
+
raise BrokenThreadPool(self._broken)
|
|
693
|
+
|
|
694
|
+
if self._shutdown:
|
|
695
|
+
raise RuntimeError("cannot schedule new futures after shutdown")
|
|
696
|
+
if _shutdown:
|
|
697
|
+
raise RuntimeError( # pragma: no cover
|
|
698
|
+
"cannot schedule new futures after interpreter shutdown"
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
self._start_worker_thread()
|
|
702
|
+
if self._broken:
|
|
703
|
+
raise BrokenThreadPool(self._broken)
|
|
704
|
+
assert self._comp
|
|
705
|
+
|
|
706
|
+
f = Future() # type: ignore
|
|
707
|
+
task = self._resolve_work_item_task(fn, args, kwargs)
|
|
708
|
+
w = _AsyncWorkItem(f, task)
|
|
709
|
+
loop, work_queue = self._comp[0]
|
|
710
|
+
loop.call_soon_threadsafe(work_queue.put_nowait, w)
|
|
711
|
+
|
|
712
|
+
return f
|
|
713
|
+
|
|
714
|
+
submit.__doc__ = _base.Executor.submit.__doc__
|
|
715
|
+
|
|
716
|
+
def _start_worker_thread(self):
|
|
717
|
+
if self._thread_active.is_set():
|
|
718
|
+
return
|
|
719
|
+
|
|
720
|
+
def weakref_cb(_, c=self._comp):
|
|
721
|
+
if not c:
|
|
722
|
+
return
|
|
723
|
+
loop, work_queue = c[0]
|
|
724
|
+
loop.call_soon_threadsafe(work_queue.put_nowait, None)
|
|
725
|
+
|
|
726
|
+
ready = Event()
|
|
727
|
+
|
|
728
|
+
def run_worker():
|
|
729
|
+
loop = asyncio.new_event_loop()
|
|
730
|
+
asyncio.set_event_loop(loop)
|
|
731
|
+
try:
|
|
732
|
+
loop.run_until_complete(
|
|
733
|
+
_async_worker(
|
|
734
|
+
ref(self, weakref_cb),
|
|
735
|
+
ready,
|
|
736
|
+
self._max_workers,
|
|
737
|
+
self._idle_timeout,
|
|
738
|
+
self._create_worker_context,
|
|
739
|
+
)
|
|
740
|
+
)
|
|
741
|
+
finally:
|
|
742
|
+
loop.close()
|
|
743
|
+
|
|
744
|
+
t = Thread(
|
|
745
|
+
name=f"{self._thread_name_prefix or self}_0",
|
|
746
|
+
target=run_worker,
|
|
747
|
+
)
|
|
748
|
+
t.start()
|
|
749
|
+
self._thread = t
|
|
750
|
+
|
|
751
|
+
# Check if initializer has finished or failed
|
|
752
|
+
while not self._broken:
|
|
753
|
+
if ready.is_set():
|
|
754
|
+
break
|
|
755
|
+
ready.wait(0.1)
|
|
756
|
+
|
|
757
|
+
def _mark_thread_active(self, loop: AbstractEventLoop, work_queue: AsyncQueue):
|
|
758
|
+
self._comp.append((loop, work_queue))
|
|
759
|
+
self._thread_active.set()
|
|
760
|
+
|
|
761
|
+
def _mark_thread_inactive(self):
|
|
762
|
+
self._comp.clear()
|
|
763
|
+
self._thread_active.clear()
|
|
764
|
+
|
|
765
|
+
def _cancel_pending_futures(self, loop: AbstractEventLoop, work_queue: AsyncQueue):
|
|
766
|
+
async def drain_and_cancel():
|
|
767
|
+
cancelled = []
|
|
768
|
+
while True:
|
|
769
|
+
try:
|
|
770
|
+
work_item = work_queue.get_nowait()
|
|
771
|
+
if work_item is not None:
|
|
772
|
+
cancelled.append(work_item)
|
|
773
|
+
except asyncio.QueueEmpty:
|
|
774
|
+
break
|
|
775
|
+
return cancelled
|
|
776
|
+
|
|
777
|
+
future = asyncio.run_coroutine_threadsafe(drain_and_cancel(), loop)
|
|
778
|
+
try:
|
|
779
|
+
work_items = future.result(timeout=5.0)
|
|
780
|
+
for work_item in work_items:
|
|
781
|
+
work_item.future.set_exception(BrokenThreadPool(self._broken))
|
|
782
|
+
except Exception:
|
|
783
|
+
pass
|
|
784
|
+
|
|
785
|
+
def _initializer_failed(self):
|
|
786
|
+
self._broken = True
|
|
787
|
+
self._mark_thread_inactive()
|
|
788
|
+
if c := self._comp:
|
|
789
|
+
loop, work_queue = c[0]
|
|
790
|
+
c.clear()
|
|
791
|
+
self._cancel_pending_futures(loop, work_queue)
|
|
792
|
+
|
|
793
|
+
def shutdown(self, wait=True, *, cancel_futures=False):
|
|
794
|
+
with self._shutdown_lock:
|
|
795
|
+
self._shutdown = True
|
|
796
|
+
|
|
797
|
+
if c := self._comp:
|
|
798
|
+
loop, work_queue = c[0]
|
|
799
|
+
c.clear()
|
|
800
|
+
|
|
801
|
+
if cancel_futures:
|
|
802
|
+
self._cancel_pending_futures(loop, work_queue)
|
|
803
|
+
|
|
804
|
+
loop.call_soon_threadsafe(work_queue.put_nowait, None)
|
|
805
|
+
|
|
806
|
+
if wait and (t := self._thread) is not None:
|
|
807
|
+
t.join()
|
|
808
|
+
self._thread = None
|
|
809
|
+
self._mark_thread_inactive()
|
|
810
|
+
|
|
811
|
+
shutdown.__doc__ = _base.Executor.shutdown.__doc__
|
|
@@ -11,20 +11,20 @@ classifiers = [
|
|
|
11
11
|
"Operating System :: OS Independent",
|
|
12
12
|
"Programming Language :: Python",
|
|
13
13
|
"Programming Language :: Python :: 3",
|
|
14
|
-
"Programming Language :: Python :: 3.8",
|
|
15
|
-
"Programming Language :: Python :: 3.9",
|
|
16
|
-
"Programming Language :: Python :: 3.10",
|
|
17
14
|
"Programming Language :: Python :: 3.11",
|
|
18
15
|
"Programming Language :: Python :: 3.12",
|
|
16
|
+
"Programming Language :: Python :: 3.13",
|
|
17
|
+
"Programming Language :: Python :: 3.14",
|
|
18
|
+
"Programming Language :: Python :: 3.15",
|
|
19
19
|
"Topic :: Software Development",
|
|
20
20
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
21
21
|
"Topic :: Utilities",
|
|
22
22
|
]
|
|
23
|
-
requires-python = ">=3.
|
|
23
|
+
requires-python = ">=3.11"
|
|
24
24
|
readme = "README.md"
|
|
25
25
|
dependencies = []
|
|
26
26
|
dynamic = []
|
|
27
|
-
version = "0.0.
|
|
27
|
+
version = "0.0.13"
|
|
28
28
|
|
|
29
29
|
[project.license]
|
|
30
30
|
text = "MIT"
|
|
@@ -54,21 +54,6 @@ excludes = [
|
|
|
54
54
|
source = "file"
|
|
55
55
|
path = "flexexecutor.py"
|
|
56
56
|
|
|
57
|
-
[tool.pdm.dev-dependencies]
|
|
58
|
-
dev = [
|
|
59
|
-
"autoflake>=2.3.1",
|
|
60
|
-
"coverage[toml]>=7.6.1",
|
|
61
|
-
"mypy>=1.11.1",
|
|
62
|
-
"nox>=2024.4.15",
|
|
63
|
-
"pytest>=8.3.2",
|
|
64
|
-
"pytest-asyncio>=0.23.8",
|
|
65
|
-
"pytest-cov>=5.0.0",
|
|
66
|
-
"pytest-mock>=3.14.0",
|
|
67
|
-
"pytest-timeout>=2.3.1",
|
|
68
|
-
"ruoml>=0.11.0",
|
|
69
|
-
"ruff>=0.6.1",
|
|
70
|
-
]
|
|
71
|
-
|
|
72
57
|
[tool.autoflake]
|
|
73
58
|
in-place = true
|
|
74
59
|
recursive = true
|
|
@@ -105,3 +90,19 @@ warn_unused_configs = true
|
|
|
105
90
|
|
|
106
91
|
[tool.pytest.ini_options]
|
|
107
92
|
asyncio_mode = "strict"
|
|
93
|
+
|
|
94
|
+
[dependency-groups]
|
|
95
|
+
dev = [
|
|
96
|
+
"autoflake>=2.3.1",
|
|
97
|
+
"coverage[toml]>=7.6.1",
|
|
98
|
+
"nox>=2024.4.15",
|
|
99
|
+
"pytest>=8.3.2",
|
|
100
|
+
"pytest-asyncio>=0.23.8",
|
|
101
|
+
"pytest-cov>=5.0.0",
|
|
102
|
+
"pytest-mock>=3.14.0",
|
|
103
|
+
"pytest-timeout>=2.3.1",
|
|
104
|
+
"ruff>=0.9.10",
|
|
105
|
+
"typing-extensions>=4.12.2",
|
|
106
|
+
"ty>=0.0.1a34",
|
|
107
|
+
"python-lsp-server>=1.14.0",
|
|
108
|
+
]
|
|
@@ -1,386 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Flexexecutor provides executors that can automatically scale the number of
|
|
3
|
-
workers up and down.
|
|
4
|
-
|
|
5
|
-
Copyright (c) 2020-2024, Leavers.
|
|
6
|
-
License: MIT
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
import asyncio
|
|
10
|
-
import atexit
|
|
11
|
-
import itertools
|
|
12
|
-
from concurrent.futures import Future, ProcessPoolExecutor, _base
|
|
13
|
-
from concurrent.futures.thread import BrokenThreadPool, _WorkItem
|
|
14
|
-
from concurrent.futures.thread import ThreadPoolExecutor as _ThreadPoolExecutor
|
|
15
|
-
from inspect import iscoroutinefunction
|
|
16
|
-
from queue import Empty
|
|
17
|
-
from threading import Event, Lock, Thread
|
|
18
|
-
from time import monotonic
|
|
19
|
-
from weakref import WeakKeyDictionary, ref
|
|
20
|
-
|
|
21
|
-
__all__ = (
|
|
22
|
-
"__version__",
|
|
23
|
-
"AsyncPoolExecutor",
|
|
24
|
-
"BrokenThreadPool",
|
|
25
|
-
"Future",
|
|
26
|
-
"ProcessPoolExecutor",
|
|
27
|
-
"ThreadPoolExecutor",
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
__version__ = "0.0.11"
|
|
31
|
-
|
|
32
|
-
_threads_queues = WeakKeyDictionary() # type: ignore
|
|
33
|
-
_shutdown = False
|
|
34
|
-
_global_shutdown_lock = Lock()
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def _python_exit():
|
|
38
|
-
global _shutdown
|
|
39
|
-
with _global_shutdown_lock:
|
|
40
|
-
_shutdown = True
|
|
41
|
-
items = list(_threads_queues.items())
|
|
42
|
-
for t, q in items:
|
|
43
|
-
q.put(None)
|
|
44
|
-
for t, q in items:
|
|
45
|
-
t.join()
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
atexit.register(_python_exit)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def _worker(executor_ref, work_queue, initializer, initargs, idle_timeout):
|
|
52
|
-
if initializer is not None:
|
|
53
|
-
try:
|
|
54
|
-
initializer(*initargs)
|
|
55
|
-
except BaseException:
|
|
56
|
-
_base.LOGGER.critical("Exception in initializer:", exc_info=True)
|
|
57
|
-
executor = executor_ref()
|
|
58
|
-
if executor is not None: # pragma: no cover
|
|
59
|
-
executor._initializer_failed()
|
|
60
|
-
return
|
|
61
|
-
|
|
62
|
-
idle_tick = -1.0
|
|
63
|
-
try:
|
|
64
|
-
while True:
|
|
65
|
-
if idle_tick == -1.0:
|
|
66
|
-
idle_tick = monotonic()
|
|
67
|
-
elif idle_timeout >= 0 and monotonic() - idle_tick > idle_timeout:
|
|
68
|
-
break
|
|
69
|
-
try:
|
|
70
|
-
work_item = work_queue.get(block=True, timeout=0.1)
|
|
71
|
-
except Empty:
|
|
72
|
-
continue
|
|
73
|
-
if work_item is not None:
|
|
74
|
-
work_item.run()
|
|
75
|
-
del work_item
|
|
76
|
-
|
|
77
|
-
executor = executor_ref()
|
|
78
|
-
if executor is not None:
|
|
79
|
-
executor._idle_semaphore.release()
|
|
80
|
-
del executor
|
|
81
|
-
idle_tick = monotonic()
|
|
82
|
-
continue
|
|
83
|
-
break # pragma: no cover
|
|
84
|
-
finally:
|
|
85
|
-
executor = executor_ref()
|
|
86
|
-
if executor is None:
|
|
87
|
-
work_queue.put(None)
|
|
88
|
-
else:
|
|
89
|
-
executor._idle_semaphore.acquire(timeout=0)
|
|
90
|
-
if _shutdown or executor._shutdown:
|
|
91
|
-
executor._shutdown = True
|
|
92
|
-
work_queue.put(None)
|
|
93
|
-
del executor
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
class ThreadPoolExecutor(_ThreadPoolExecutor):
|
|
97
|
-
def __init__(
|
|
98
|
-
self,
|
|
99
|
-
max_workers=1024,
|
|
100
|
-
thread_name_prefix="",
|
|
101
|
-
initializer=None,
|
|
102
|
-
initargs=(),
|
|
103
|
-
idle_timeout=60.0,
|
|
104
|
-
):
|
|
105
|
-
"""Initializes a new ThreadPoolExecutor instance.
|
|
106
|
-
|
|
107
|
-
:type max_workers: int, optional
|
|
108
|
-
:param max_workers: The maximum number of workers to create. Defaults to 1024.
|
|
109
|
-
|
|
110
|
-
:type thread_name_prefix: str, optional
|
|
111
|
-
:param thread_name_prefix: An optional name prefix to give our threads.
|
|
112
|
-
|
|
113
|
-
:type initializer: callable, optional
|
|
114
|
-
:param initializer: A callable used to initialize worker threads.
|
|
115
|
-
|
|
116
|
-
:type initargs: tuple, optional
|
|
117
|
-
:parm initargs: A tuple of arguments to pass to the initializer.
|
|
118
|
-
|
|
119
|
-
:type idle_timeout: float, optional
|
|
120
|
-
:param idle_timeout: The maximum amount of time (in seconds) that a worker
|
|
121
|
-
thread can remain idle before it is terminated. If set to None or negative
|
|
122
|
-
value, workers will never be terminated. Defaults to 60 seconds.
|
|
123
|
-
"""
|
|
124
|
-
if max_workers is None:
|
|
125
|
-
max_workers = 1024
|
|
126
|
-
super().__init__(max_workers, thread_name_prefix, initializer, initargs)
|
|
127
|
-
if idle_timeout is None or idle_timeout < 0:
|
|
128
|
-
self._idle_timeout = -1
|
|
129
|
-
else:
|
|
130
|
-
self._idle_timeout = max(0.1, idle_timeout)
|
|
131
|
-
|
|
132
|
-
def submit(self, fn, /, *args, **kwargs):
|
|
133
|
-
if iscoroutinefunction(fn):
|
|
134
|
-
raise TypeError("fn must not be a coroutine function")
|
|
135
|
-
|
|
136
|
-
with self._shutdown_lock, _global_shutdown_lock:
|
|
137
|
-
if self._broken:
|
|
138
|
-
raise BrokenThreadPool(self._broken)
|
|
139
|
-
|
|
140
|
-
if self._shutdown:
|
|
141
|
-
raise RuntimeError("cannot schedule new futures after shutdown")
|
|
142
|
-
if _shutdown:
|
|
143
|
-
# coverage didn't realize that _shutdown is set, add no cover here
|
|
144
|
-
raise RuntimeError( # pragma: no cover
|
|
145
|
-
"cannot schedule new futures after interpreter shutdown"
|
|
146
|
-
)
|
|
147
|
-
|
|
148
|
-
f = Future() # type: ignore
|
|
149
|
-
w = _WorkItem(f, fn, args, kwargs)
|
|
150
|
-
|
|
151
|
-
self._work_queue.put(w)
|
|
152
|
-
self._adjust_thread_count()
|
|
153
|
-
return f
|
|
154
|
-
|
|
155
|
-
submit.__doc__ = _base.Executor.submit.__doc__
|
|
156
|
-
|
|
157
|
-
def _adjust_thread_count(self):
|
|
158
|
-
if self._idle_semaphore.acquire(timeout=0):
|
|
159
|
-
return
|
|
160
|
-
threads = self._threads
|
|
161
|
-
dead_threads = [t for t in threads if not t.is_alive()]
|
|
162
|
-
for t in dead_threads:
|
|
163
|
-
threads.remove(t) # type: ignore
|
|
164
|
-
|
|
165
|
-
def weakref_cb(_, q=self._work_queue):
|
|
166
|
-
q.put(None)
|
|
167
|
-
|
|
168
|
-
num_threads = len(threads)
|
|
169
|
-
if num_threads < self._max_workers:
|
|
170
|
-
t = Thread(
|
|
171
|
-
name=f"{self._thread_name_prefix or self}_{num_threads}",
|
|
172
|
-
target=_worker,
|
|
173
|
-
args=(
|
|
174
|
-
ref(self, weakref_cb),
|
|
175
|
-
self._work_queue,
|
|
176
|
-
self._initializer,
|
|
177
|
-
self._initargs,
|
|
178
|
-
self._idle_timeout,
|
|
179
|
-
),
|
|
180
|
-
)
|
|
181
|
-
t.start()
|
|
182
|
-
threads.add(t) # type: ignore
|
|
183
|
-
_threads_queues[t] = self._work_queue
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
class _AsyncWorkItem(_WorkItem):
|
|
187
|
-
async def run(self):
|
|
188
|
-
if not self.future.set_running_or_notify_cancel():
|
|
189
|
-
print("cancelled")
|
|
190
|
-
return
|
|
191
|
-
|
|
192
|
-
try:
|
|
193
|
-
result = await self.fn(*self.args, **self.kwargs)
|
|
194
|
-
self.future.set_result(result)
|
|
195
|
-
except BaseException as exc:
|
|
196
|
-
self.future.set_exception(exc)
|
|
197
|
-
finally:
|
|
198
|
-
del self
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
async def _async_worker(
|
|
202
|
-
executor_ref,
|
|
203
|
-
work_queue,
|
|
204
|
-
initializer,
|
|
205
|
-
initargs,
|
|
206
|
-
max_workers,
|
|
207
|
-
idle_timeout,
|
|
208
|
-
):
|
|
209
|
-
executor = executor_ref()
|
|
210
|
-
if executor is not None:
|
|
211
|
-
executor._running.set()
|
|
212
|
-
|
|
213
|
-
if initializer is not None:
|
|
214
|
-
try:
|
|
215
|
-
initializer(*initargs)
|
|
216
|
-
except BaseException:
|
|
217
|
-
_base.LOGGER.critical("Exception in initializer:", exc_info=True)
|
|
218
|
-
if executor is not None: # pragma: no cover
|
|
219
|
-
executor._running.set()
|
|
220
|
-
executor._initializer_failed()
|
|
221
|
-
return
|
|
222
|
-
|
|
223
|
-
idle_tick = monotonic()
|
|
224
|
-
curr_tasks = set()
|
|
225
|
-
loop = asyncio.get_running_loop()
|
|
226
|
-
asleep = asyncio.sleep
|
|
227
|
-
|
|
228
|
-
try:
|
|
229
|
-
while True:
|
|
230
|
-
if idle_timeout >= 0 and monotonic() - idle_tick > idle_timeout:
|
|
231
|
-
break
|
|
232
|
-
if len(curr_tasks) < max_workers:
|
|
233
|
-
try:
|
|
234
|
-
work_item = work_queue.get(block=True, timeout=0.1)
|
|
235
|
-
if work_item is not None:
|
|
236
|
-
task = loop.create_task(work_item.run())
|
|
237
|
-
curr_tasks.add(task)
|
|
238
|
-
await asleep(0)
|
|
239
|
-
del work_item
|
|
240
|
-
else:
|
|
241
|
-
break
|
|
242
|
-
except Empty:
|
|
243
|
-
pass
|
|
244
|
-
await asleep(0)
|
|
245
|
-
finished_tasks = [t for t in curr_tasks if t.done()]
|
|
246
|
-
for t in finished_tasks:
|
|
247
|
-
curr_tasks.remove(t)
|
|
248
|
-
if curr_tasks:
|
|
249
|
-
idle_tick = monotonic()
|
|
250
|
-
finally:
|
|
251
|
-
while curr_tasks:
|
|
252
|
-
await asleep(0)
|
|
253
|
-
finished_tasks = [t for t in curr_tasks if t.done()]
|
|
254
|
-
for t in finished_tasks:
|
|
255
|
-
curr_tasks.remove(t)
|
|
256
|
-
executor = executor_ref()
|
|
257
|
-
if executor is None:
|
|
258
|
-
work_queue.put(None)
|
|
259
|
-
else:
|
|
260
|
-
executor._running.clear()
|
|
261
|
-
if _shutdown or executor._shutdown:
|
|
262
|
-
executor._shutdown = True
|
|
263
|
-
work_queue.put(None)
|
|
264
|
-
del executor
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
class AsyncWorker(Thread):
|
|
268
|
-
def __init__(
|
|
269
|
-
self,
|
|
270
|
-
name,
|
|
271
|
-
executor_ref,
|
|
272
|
-
work_queue,
|
|
273
|
-
initializer,
|
|
274
|
-
initargs,
|
|
275
|
-
max_workers,
|
|
276
|
-
idle_timeout,
|
|
277
|
-
):
|
|
278
|
-
super().__init__(name=name)
|
|
279
|
-
self._executor_ref = executor_ref
|
|
280
|
-
self._work_queue = work_queue
|
|
281
|
-
self._initializer = initializer
|
|
282
|
-
self._initargs = initargs
|
|
283
|
-
self._max_workers = max_workers
|
|
284
|
-
self._idle_timeout = idle_timeout
|
|
285
|
-
|
|
286
|
-
def run(self):
|
|
287
|
-
loop = asyncio.new_event_loop()
|
|
288
|
-
loop.run_until_complete(
|
|
289
|
-
_async_worker(
|
|
290
|
-
self._executor_ref,
|
|
291
|
-
self._work_queue,
|
|
292
|
-
self._initializer,
|
|
293
|
-
self._initargs,
|
|
294
|
-
self._max_workers,
|
|
295
|
-
self._idle_timeout,
|
|
296
|
-
)
|
|
297
|
-
)
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
class AsyncPoolExecutor(ThreadPoolExecutor):
|
|
301
|
-
_counter = itertools.count().__next__
|
|
302
|
-
|
|
303
|
-
def __init__(
|
|
304
|
-
self,
|
|
305
|
-
max_workers=None,
|
|
306
|
-
thread_name_prefix="",
|
|
307
|
-
initializer=None,
|
|
308
|
-
initargs=(),
|
|
309
|
-
idle_timeout=60.0,
|
|
310
|
-
):
|
|
311
|
-
"""Initializes a new AsyncPoolExecutor instance.
|
|
312
|
-
|
|
313
|
-
:type max_workers: int, optional
|
|
314
|
-
:param max_workers: The maximum number of workers to create. Defaults to 261244.
|
|
315
|
-
|
|
316
|
-
:type thread_name_prefix: str, optional
|
|
317
|
-
:param thread_name_prefix: An optional name prefix to give our threads.
|
|
318
|
-
|
|
319
|
-
:type initializer: callable, optional
|
|
320
|
-
:param initializer: A callable used to initialize worker threads.
|
|
321
|
-
|
|
322
|
-
:type initargs: tuple, optional
|
|
323
|
-
:parm initargs: A tuple of arguments to pass to the initializer.
|
|
324
|
-
|
|
325
|
-
:type idle_timeout: float, optional
|
|
326
|
-
:param idle_timeout: The maximum amount of time (in seconds) that a worker
|
|
327
|
-
thread can remain idle before it is terminated. If set to None or negative
|
|
328
|
-
value, workers will never be terminated. Defaults to 60 seconds.
|
|
329
|
-
"""
|
|
330
|
-
if max_workers is None:
|
|
331
|
-
max_workers = 262144
|
|
332
|
-
if not thread_name_prefix:
|
|
333
|
-
thread_name_prefix = f"AsyncPoolExecutor-{self._counter()}" # type: ignore
|
|
334
|
-
super().__init__(max_workers, thread_name_prefix, initializer, initargs)
|
|
335
|
-
del self._idle_semaphore
|
|
336
|
-
self._running = Event()
|
|
337
|
-
if idle_timeout is None or idle_timeout < 0:
|
|
338
|
-
self._idle_timeout = -1
|
|
339
|
-
else:
|
|
340
|
-
self._idle_timeout = max(0.1, idle_timeout)
|
|
341
|
-
|
|
342
|
-
def submit(self, fn, /, *args, **kwargs):
|
|
343
|
-
if not iscoroutinefunction(fn):
|
|
344
|
-
raise TypeError("fn must be a coroutine function")
|
|
345
|
-
with self._shutdown_lock, _global_shutdown_lock:
|
|
346
|
-
if self._broken:
|
|
347
|
-
raise BrokenThreadPool(self._broken)
|
|
348
|
-
|
|
349
|
-
if self._shutdown:
|
|
350
|
-
raise RuntimeError("cannot schedule new futures after shutdown")
|
|
351
|
-
if _shutdown:
|
|
352
|
-
# coverage didn't realize that _shutdown is set, add no cover here
|
|
353
|
-
raise RuntimeError( # pragma: no cover
|
|
354
|
-
"cannot schedule new futures after interpreter shutdown"
|
|
355
|
-
)
|
|
356
|
-
|
|
357
|
-
f = Future() # type: ignore
|
|
358
|
-
w = _AsyncWorkItem(f, fn, args, kwargs)
|
|
359
|
-
|
|
360
|
-
self._work_queue.put(w)
|
|
361
|
-
self._adjust_thread_count()
|
|
362
|
-
return f
|
|
363
|
-
|
|
364
|
-
submit.__doc__ = _base.Executor.submit.__doc__
|
|
365
|
-
|
|
366
|
-
def _adjust_thread_count(self):
|
|
367
|
-
if self._running.is_set():
|
|
368
|
-
return
|
|
369
|
-
threads = self._threads
|
|
370
|
-
threads.clear() # type: ignore
|
|
371
|
-
|
|
372
|
-
def weakref_cb(_, q=self._work_queue):
|
|
373
|
-
q.put(None)
|
|
374
|
-
|
|
375
|
-
w = AsyncWorker(
|
|
376
|
-
f"{self._thread_name_prefix or self}_0",
|
|
377
|
-
ref(self, weakref_cb),
|
|
378
|
-
self._work_queue,
|
|
379
|
-
self._initializer,
|
|
380
|
-
self._initargs,
|
|
381
|
-
self._max_workers,
|
|
382
|
-
self._idle_timeout,
|
|
383
|
-
)
|
|
384
|
-
w.start()
|
|
385
|
-
threads.add(w) # type: ignore
|
|
386
|
-
_threads_queues[w] = self._work_queue
|
|
File without changes
|