Merge pull request #6527 from Gallaecio/dont-throttle-meta

Replace Slot.throttle with Request.meta['dont_throttle']
This commit is contained in:
Mikhail Korobov 2024-11-08 14:25:37 +05:00 committed by GitHub
commit bcef96570b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 61 additions and 46 deletions

View File

@ -21,9 +21,14 @@ Design goals
How it works
============
AutoThrottle extension adjusts download delays dynamically to make spider send
:setting:`AUTOTHROTTLE_TARGET_CONCURRENCY` concurrent requests on average
to each remote website.
Scrapy allows defining the concurrency and delay of different download slots,
e.g. through the :setting:`DOWNLOAD_SLOTS` setting. By default requests are
assigned to slots based on their URL domain, although it is possible to
customize the download slot of any request.
The AutoThrottle extension adjusts the delay of each download slot dynamically,
to make your spider send :setting:`AUTOTHROTTLE_TARGET_CONCURRENCY` concurrent
requests on average to each remote website.
It uses download latency to compute the delays. The main idea is the
following: if a server needs ``latency`` seconds to respond, a client
@ -47,18 +52,6 @@ effect, but there are some important differences:
AutoThrottle doesn't have these issues.
Disabling throttling on a downloader slot
=========================================
It is possible to disable AutoThrottle for a specific download slot at run time
by setting its ``throttle`` attribute to ``False``, e.g. using
:setting:`DOWNLOAD_SLOTS`.
Note, however, that AutoThrottle still determines the starting delay of every
slot by setting the ``download_delay`` attribute on the running spider. You
might want to set a custom value for the ``delay`` attribute of the slot, e.g.
using :setting:`DOWNLOAD_SLOTS`.
Throttling algorithm
====================
@ -92,6 +85,33 @@ callback, for example, and unable to attend downloads. However, these latencies
should still give a reasonable estimate of how busy Scrapy (and ultimately, the
server) is, and this extension builds on that premise.
.. reqmeta:: autothrottle_dont_adjust_delay
Prevent specific requests from triggering slot delay adjustments
================================================================
AutoThrottle adjusts the delay of download slots based on the latencies of
responses that belong to that download slot. The only exceptions are non-200
responses, which are only taken into account to increase that delay, but
ignored if they would decrease that delay.
You can also set the ``autothrottle_dont_adjust_delay`` request metadata key to
``True`` in any request to prevent its response latency from impacting the
delay of its download slot:
.. code-block:: python
from scrapy import Request
Request("https://example.com", meta={"autothrottle_dont_adjust_delay": True})
Note, however, that AutoThrottle still determines the starting delay of every
download slot by setting the ``download_delay`` attribute on the running
spider. If you want AutoThrottle not to impact a download slot at all, in
addition to setting this meta key in all requests that use that download slot,
you might want to set a custom value for the ``delay`` attribute of that
download slot, e.g. using :setting:`DOWNLOAD_SLOTS`.
Settings
========

View File

@ -668,6 +668,7 @@ are some special keys recognized by Scrapy and its built-in extensions.
Those are:
* :reqmeta:`autothrottle_dont_adjust_delay`
* :reqmeta:`bindaddress`
* :reqmeta:`cookiejar`
* :reqmeta:`dont_cache`

View File

@ -845,12 +845,7 @@ Allows to define concurrency/delay parameters on per slot (domain) basis:
.. code-block:: python
DOWNLOAD_SLOTS = {
"quotes.toscrape.com": {
"concurrency": 1,
"delay": 2,
"randomize_delay": False,
"throttle": False,
},
"quotes.toscrape.com": {"concurrency": 1, "delay": 2, "randomize_delay": False},
"books.toscrape.com": {"delay": 3, "randomize_delay": False},
}
@ -862,9 +857,6 @@ Allows to define concurrency/delay parameters on per slot (domain) basis:
- :setting:`CONCURRENT_REQUESTS_PER_DOMAIN`: ``concurrency``
- :setting:`RANDOMIZE_DOWNLOAD_DELAY`: ``randomize_delay``
There is no global setting for ``throttle``, whose default value is
``None``.
.. setting:: DOWNLOAD_TIMEOUT

View File

@ -36,13 +36,10 @@ class Slot:
concurrency: int,
delay: float,
randomize_delay: bool,
*,
throttle: bool | None = None,
):
self.concurrency: int = concurrency
self.delay: float = delay
self.randomize_delay: bool = randomize_delay
self.throttle = throttle
self.active: set[Request] = set()
self.queue: deque[tuple[Request, Deferred[Response]]] = deque()
@ -67,15 +64,13 @@ class Slot:
return (
f"{cls_name}(concurrency={self.concurrency!r}, "
f"delay={self.delay:.2f}, "
f"randomize_delay={self.randomize_delay!r}, "
f"throttle={self.throttle!r})"
f"randomize_delay={self.randomize_delay!r})"
)
def __str__(self) -> str:
return (
f"<downloader.Slot concurrency={self.concurrency!r} "
f"delay={self.delay:.2f} randomize_delay={self.randomize_delay!r} "
f"throttle={self.throttle!r} "
f"len(active)={len(self.active)} len(queue)={len(self.queue)} "
f"len(transferring)={len(self.transferring)} "
f"lastseen={datetime.fromtimestamp(self.lastseen).isoformat()}>"
@ -146,8 +141,7 @@ class Downloader:
slot_settings.get("delay", delay),
)
randomize_delay = slot_settings.get("randomize_delay", self.randomize_delay)
throttle = slot_settings.get("throttle", None)
new_slot = Slot(conc, delay, randomize_delay, throttle=throttle)
new_slot = Slot(conc, delay, randomize_delay)
self.slots[key] = new_slot
return key, self.slots[key]

View File

@ -64,7 +64,11 @@ class AutoThrottle:
) -> None:
key, slot = self._get_slot(request, spider)
latency = request.meta.get("download_latency")
if latency is None or slot is None or slot.throttle is False:
if (
latency is None
or slot is None
or request.meta.get("autothrottle_dont_adjust_delay", False) is True
):
return
olddelay = slot.delay

View File

@ -8,5 +8,5 @@ class SlotTest(unittest.TestCase):
slot = Slot(concurrency=8, delay=0.1, randomize_delay=True)
self.assertEqual(
repr(slot),
"Slot(concurrency=8, delay=0.10, randomize_delay=True, throttle=None)",
"Slot(concurrency=8, delay=0.10, randomize_delay=True)",
)

View File

@ -80,7 +80,6 @@ def test_params():
"concurrency": 1,
"delay": 2,
"randomize_delay": False,
"throttle": False,
}
settings = {
"DOWNLOAD_SLOTS": {

View File

@ -157,17 +157,24 @@ def test_startdelay_definition(min_spider, min_setting, start_setting, expected)
@pytest.mark.parametrize(
("meta", "slot", "throttle"),
("meta", "slot"),
(
({}, None, None),
({"download_latency": 1.0}, None, None),
({"download_slot": "foo"}, None, None),
({"download_slot": "foo"}, "foo", None),
({"download_latency": 1.0, "download_slot": "foo"}, None, None),
({"download_latency": 1.0, "download_slot": "foo"}, "foo", False),
({}, None),
({"download_latency": 1.0}, None),
({"download_slot": "foo"}, None),
({"download_slot": "foo"}, "foo"),
({"download_latency": 1.0, "download_slot": "foo"}, None),
(
{
"download_latency": 1.0,
"download_slot": "foo",
"autothrottle_dont_adjust_delay": True,
},
"foo",
),
),
)
def test_skipped(meta, slot, throttle):
def test_skipped(meta, slot):
crawler = get_crawler()
at = build_from_crawler(AutoThrottle, crawler)
spider = TestSpider()
@ -178,9 +185,7 @@ def test_skipped(meta, slot, throttle):
crawler.engine.downloader = Mock()
crawler.engine.downloader.slots = {}
if slot is not None:
_slot = Mock()
_slot.throttle = throttle
crawler.engine.downloader.slots[slot] = _slot
crawler.engine.downloader.slots[slot] = object()
at._adjust_delay = None # Raise exception if called.
at._response_downloaded(None, request, spider)