Skip to content

Commit a6b6839

Browse files
authored
refactor(scrapy): make AsyncThread timeout configurable (#955)
`AsyncThread.run_coro` hardcoded a 60s timeout. This adds a configurable `default_timeout` constructor argument (default unchanged at 60s), used when a per-call timeout is not given, and documents why each Scrapy consumer (scheduler, HTTP cache) owns its own event-loop thread. Part of splitting the larger Scrapy integration fix (`fix/scrapy-integration`) into reviewable pieces.
1 parent 7c9f2b1 commit a6b6839

1 file changed

Lines changed: 14 additions & 7 deletions

File tree

src/apify/scrapy/_async_thread.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from concurrent import futures
66
from datetime import timedelta
77
from logging import getLogger
8-
from typing import TYPE_CHECKING, Any
8+
from typing import TYPE_CHECKING, Any, Literal
99

1010
if TYPE_CHECKING:
1111
from collections.abc import Coroutine
@@ -14,13 +14,16 @@
1414

1515

1616
class AsyncThread:
17-
"""Class for running an asyncio event loop in a separate thread.
17+
"""Run an asyncio event loop in a dedicated background thread.
1818
19-
This allows running asynchronous coroutines from synchronous code by executingthem on an event loop
20-
that runs in its own dedicated thread.
19+
This lets synchronous Scrapy callbacks drive asynchronous Apify and Crawlee coroutines. The
20+
scheduler and the HTTP cache storage each own their own `AsyncThread`, so the request queue and
21+
the key-value store never share an event loop; they only share the read-only global
22+
`Configuration`. A single shared loop would also work but would couple their lifecycles.
2123
"""
2224

23-
def __init__(self) -> None:
25+
def __init__(self, default_timeout: timedelta = timedelta(seconds=60)) -> None:
26+
self._default_timeout = default_timeout
2427
self._eventloop = asyncio.new_event_loop()
2528

2629
# Start the event loop in a dedicated daemon thread.
@@ -33,7 +36,7 @@ def __init__(self) -> None:
3336
def run_coro(
3437
self,
3538
coro: Coroutine,
36-
timeout: timedelta = timedelta(seconds=60),
39+
timeout: timedelta | Literal['default'] = 'default',
3740
) -> Any:
3841
"""Run a coroutine on an event loop running in a separate thread.
3942
@@ -42,7 +45,8 @@ def run_coro(
4245
4346
Args:
4447
coro: The coroutine to run.
45-
timeout: The maximum number of seconds to wait for the coroutine to finish.
48+
timeout: The maximum time to wait for the coroutine to finish. Pass `'default'` to use the
49+
`default_timeout` passed to the constructor.
4650
4751
Returns:
4852
The result returned by the coroutine.
@@ -52,6 +56,9 @@ def run_coro(
5256
TimeoutError: If the coroutine does not complete within the timeout.
5357
Exception: Any exception raised during coroutine execution.
5458
"""
59+
if timeout == 'default':
60+
timeout = self._default_timeout
61+
5562
if not self._eventloop.is_running():
5663
raise RuntimeError(f'The coroutine {coro} cannot be executed because the event loop is not running.')
5764

0 commit comments

Comments
 (0)