1414if TYPE_CHECKING :
1515 from crawlee .autoscaling import SystemStatus
1616
17+ __all__ = ['ConcurrencySettings' , 'AutoscaledPool' ]
18+
1719logger = getLogger (__name__ )
1820
1921
@@ -63,6 +65,16 @@ def __init__(
6365 self .max_tasks_per_minute = max_tasks_per_minute
6466
6567
68+ class _AutoscaledPoolRun :
69+ def __init__ (self ) -> None :
70+ self .worker_tasks = list [asyncio .Task ]()
71+ """A list of worker tasks currently in progress"""
72+
73+ self .worker_tasks_updated = asyncio .Event ()
74+ self .cleanup_done = asyncio .Event ()
75+ self .result : asyncio .Future = asyncio .Future ()
76+
77+
6678class AutoscaledPool :
6779 """Manages a pool of asynchronous resource-intensive tasks that are executed in parallel.
6880
@@ -131,13 +143,6 @@ def __init__(
131143
132144 self ._autoscale_task = RecurringTask (self ._autoscale , autoscale_interval )
133145
134- self ._worker_tasks = list [asyncio .Task ]()
135- """A list of worker tasks currently in progress"""
136-
137- self ._worker_tasks_updated = asyncio .Event ()
138- self ._cleanup_done = asyncio .Event ()
139- self ._run_result : asyncio .Future = asyncio .Future ()
140-
141146 if desired_concurrency_ratio < 0 or desired_concurrency_ratio > 1 :
142147 raise ValueError ('desired_concurrency_ratio must be between 0 and 1 (non-inclusive)' )
143148
@@ -154,32 +159,33 @@ def __init__(
154159
155160 self ._max_tasks_per_minute = concurrency_settings .max_tasks_per_minute
156161 self ._is_paused = False
157- self ._is_running = False
162+ self ._current_run : _AutoscaledPoolRun | None = None
158163
159164 async def run (self ) -> None :
160165 """Start the autoscaled pool and return when all tasks are completed and `is_finished_function` returns True.
161166
162167 If there is an exception in one of the tasks, it will be re-raised.
163168 """
164- if self ._is_running :
169+ if self ._current_run is not None :
165170 raise RuntimeError ('The pool is already running' )
166171
167- self ._is_running = True
168- self ._cleanup_done .clear ()
172+ run = _AutoscaledPoolRun ()
173+ self ._current_run = run
174+
169175 logger .debug ('Starting the pool' )
170176
171177 self ._autoscale_task .start ()
172178 self ._log_system_status_task .start ()
173179
174180 orchestrator = asyncio .create_task (
175- self ._worker_task_orchestrator (), name = 'autoscaled pool worker task orchestrator'
181+ self ._worker_task_orchestrator (run ), name = 'autoscaled pool worker task orchestrator'
176182 )
177183
178184 try :
179- await self . _run_result
185+ await run . result
180186 except AbortError :
181187 orchestrator .cancel ()
182- for task in self . _worker_tasks :
188+ for task in run . worker_tasks :
183189 if not task .done ():
184190 task .cancel ()
185191 finally :
@@ -195,21 +201,23 @@ async def run(self) -> None:
195201
196202 logger .info ('Waiting for remaining tasks to finish' )
197203
198- for task in self . _worker_tasks :
204+ for task in run . worker_tasks :
199205 if not task .done ():
200206 with suppress (BaseException ):
201207 await task
202208
203- self ._run_result = asyncio .Future ()
204- self ._cleanup_done .set ()
205- self ._is_running = False
209+ run .cleanup_done .set ()
210+ self ._current_run = None
206211
207212 logger .debug ('Pool cleanup finished' )
208213
209214 async def abort (self ) -> None :
210215 """Interrupt the autoscaled pool and all the tasks in progress."""
211- self ._run_result .set_exception (AbortError ())
212- await self ._cleanup_done .wait ()
216+ if not self ._current_run :
217+ raise RuntimeError ('The pool is not running' )
218+
219+ self ._current_run .result .set_exception (AbortError ())
220+ await self ._current_run .cleanup_done .wait ()
213221
214222 def pause (self ) -> None :
215223 """Pause the autoscaled pool so that it does not start new tasks."""
@@ -227,7 +235,10 @@ def desired_concurrency(self) -> int:
227235 @property
228236 def current_concurrency (self ) -> int :
229237 """The number of concurrent tasks in progress."""
230- return len (self ._worker_tasks )
238+ if self ._current_run is None :
239+ return 0
240+
241+ return len (self ._current_run .worker_tasks )
231242
232243 def _autoscale (self ) -> None :
233244 """Inspect system load status and adjust desired concurrency if necessary. Do not call directly."""
@@ -258,16 +269,16 @@ def _log_system_status(self) -> None:
258269 f'{ system_status !s} '
259270 )
260271
261- async def _worker_task_orchestrator (self ) -> None :
272+ async def _worker_task_orchestrator (self , run : _AutoscaledPoolRun ) -> None :
262273 """Launches worker tasks whenever there is free capacity and a task is ready.
263274
264275 Exits when `is_finished_function` returns True.
265276 """
266277 finished = False
267278
268279 try :
269- while not (finished := await self ._is_finished_function ()) and not self . _run_result .done ():
270- self . _worker_tasks_updated .clear ()
280+ while not (finished := await self ._is_finished_function ()) and not run . result .done ():
281+ run . worker_tasks_updated .clear ()
271282
272283 current_status = self ._system_status .get_current_system_info ()
273284 if not current_status .is_system_idle :
@@ -281,44 +292,44 @@ async def _worker_task_orchestrator(self) -> None:
281292 else :
282293 logger .debug ('Scheduling a new task' )
283294 worker_task = asyncio .create_task (self ._worker_task (), name = 'autoscaled pool worker task' )
284- worker_task .add_done_callback (lambda task : self ._reap_worker_task (task ))
285- self . _worker_tasks .append (worker_task )
295+ worker_task .add_done_callback (lambda task : self ._reap_worker_task (task , run ))
296+ run . worker_tasks .append (worker_task )
286297
287298 if math .isfinite (self ._max_tasks_per_minute ):
288299 await asyncio .sleep (60 / self ._max_tasks_per_minute )
289300
290301 continue
291302
292303 with suppress (asyncio .TimeoutError ):
293- await asyncio .wait_for (self . _worker_tasks_updated .wait (), timeout = 0.5 )
304+ await asyncio .wait_for (run . worker_tasks_updated .wait (), timeout = 0.5 )
294305 finally :
295306 if finished :
296307 logger .debug ('`is_finished_function` reports that we are finished' )
297- elif self . _run_result .done () and self . _run_result .exception () is not None :
308+ elif run . result .done () and run . result .exception () is not None :
298309 logger .debug ('Unhandled exception in `run_task_function`' )
299310
300- if self . _worker_tasks :
311+ if run . worker_tasks :
301312 logger .debug ('Terminating - waiting for tasks to complete' )
302- await asyncio .wait (self . _worker_tasks , return_when = asyncio .ALL_COMPLETED )
313+ await asyncio .wait (run . worker_tasks , return_when = asyncio .ALL_COMPLETED )
303314 logger .debug ('Worker tasks finished' )
304315 else :
305316 logger .debug ('Terminating - no running tasks to wait for' )
306317
307- if not self . _run_result .done ():
308- self . _run_result .set_result (object ())
318+ if not run . result .done ():
319+ run . result .set_result (object ())
309320
310- def _reap_worker_task (self , task : asyncio .Task ) -> None :
321+ def _reap_worker_task (self , task : asyncio .Task , run : _AutoscaledPoolRun ) -> None :
311322 """A callback for finished worker tasks.
312323
313324 - It interrupts the run in case of an exception,
314325 - keeps track of tasks in progress,
315326 - notifies the orchestrator
316327 """
317- self . _worker_tasks_updated .set ()
318- self . _worker_tasks .remove (task )
328+ run . worker_tasks_updated .set ()
329+ run . worker_tasks .remove (task )
319330
320- if not task .cancelled () and (exception := task .exception ()) and not self . _run_result .done ():
321- self . _run_result .set_exception (exception )
331+ if not task .cancelled () and (exception := task .exception ()) and not run . result .done ():
332+ run . result .set_exception (exception )
322333
323334 async def _worker_task (self ) -> None :
324335 try :
0 commit comments