Repository navigation
feat(autoscaling)!: Add ConcurrencySystem to share one concurrency budget between crawlers
#2322
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: v2
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| import asyncio | ||
|
|
||
| from crawlee import ConcurrencySettings | ||
| from crawlee.autoscaling import ConcurrencySystem | ||
| from crawlee.crawlers import ( | ||
| BeautifulSoupCrawler, | ||
| BeautifulSoupCrawlingContext, | ||
| ParselCrawler, | ||
| ParselCrawlingContext, | ||
| ) | ||
| from crawlee.storages import RequestQueue | ||
|
|
||
|
|
||
| async def main() -> None: | ||
| concurrency_settings = ConcurrencySettings(max_concurrency=10) | ||
|
|
||
| # Each crawler needs a request queue of its own, otherwise they would both | ||
| # consume the default one and process each other's requests. | ||
| first_queue = await RequestQueue.open(alias='first-crawler') | ||
| second_queue = await RequestQueue.open(alias='second-crawler') | ||
|
|
||
| # One budget for both crawlers, instead of one each. | ||
| async with ConcurrencySystem(concurrency_settings) as concurrency_system: | ||
| first_crawler = BeautifulSoupCrawler( | ||
| concurrency_system=concurrency_system, | ||
| request_manager=first_queue, | ||
| ) | ||
| second_crawler = ParselCrawler( | ||
| concurrency_system=concurrency_system, | ||
| request_manager=second_queue, | ||
| ) | ||
|
|
||
| @first_crawler.router.default_handler | ||
| async def first_handler(context: BeautifulSoupCrawlingContext) -> None: | ||
| context.log.info(f'Processing {context.request.url}...') | ||
|
|
||
| @second_crawler.router.default_handler | ||
| async def second_handler(context: ParselCrawlingContext) -> None: | ||
| context.log.info(f'Processing {context.request.url}...') | ||
|
|
||
| await asyncio.gather( | ||
| first_crawler.run(['https://crawlee.dev/']), | ||
| second_crawler.run(['https://crawlee.dev/python/']), | ||
| ) | ||
|
|
||
|
|
||
| if __name__ == '__main__': | ||
| asyncio.run(main()) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -95,3 +95,67 @@ The `Request.forefront` property is removed. Proxy tiers were the only code that | |
| The storage clients now write their JSON data with camelCase keys, the same as Crawlee JS. This covers the metadata, request and record metadata files of `FileSystemStorageClient`, the request and record metadata JSON stored by `RedisStorageClient`, and the request JSON stored by `SqlStorageClient`. For example, `item_count` in a dataset's `__metadata__.json` becomes `itemCount`. The storage metadata documents of `RedisStorageClient` keep their snake_case keys. | ||
|
|
||
| Data written by v1 still loads, so an existing storage directory, Redis instance or database keeps working. Any tooling that reads these files directly needs to handle the camelCase keys. | ||
|
|
||
| ## Concurrency is managed by `ConcurrencySystem` | ||
|
|
||
| Load monitoring and concurrency scaling moved from `AutoscaledPool` to the new `ConcurrencySystem`. A crawler builds a default system from its `concurrency_settings` for each run, so a crawler configured through `concurrency_settings` works as before. To cap the combined concurrency of several crawlers, pass one shared `ConcurrencySystem` to all of them through the new `concurrency_system` option. | ||
|
|
||
| The autoscaling classes now live in the public `crawlee.autoscaling` package, which replaces the private `crawlee._autoscaling`. Import `AutoscaledPool`, `ConcurrencySystem`, `ConcurrencyConsumer`, `SystemInfo` and `LoadRatioInfo` from there. `Snapshotter` and `SystemStatus` stay internal. | ||
|
|
||
| `AutoscaledPool` now only runs the task loop. If you create a pool yourself, note that: | ||
|
|
||
| - `system_status` and `concurrency_settings` are replaced by `concurrency_system`, which has to be active when the pool runs, and `consumer`, which identifies the pool to the system. | ||
| - `run_task_function` receives the id the task is booked under. | ||
| - `current_concurrency` comes from the concurrency system, so a shared system may count the tasks of other pools too. The pool's own tasks are counted by the new `own_concurrency`. | ||
|
|
||
| For example: | ||
|
|
||
| ```python | ||
| # v1 | ||
| from crawlee._autoscaling import AutoscaledPool, Snapshotter, SystemStatus | ||
|
|
||
| async with Snapshotter.from_config() as snapshotter: | ||
| pool = AutoscaledPool( | ||
| system_status=SystemStatus(snapshotter), | ||
| concurrency_settings=ConcurrencySettings(max_concurrency=10), | ||
| run_task_function=run_task, | ||
| is_task_ready_function=is_task_ready, | ||
| is_finished_function=is_finished, | ||
| ) | ||
| await pool.run() | ||
|
|
||
| # v2 | ||
| from crawlee.autoscaling import AutoscaledPool, ConcurrencyConsumer, ConcurrencySystem | ||
|
|
||
| settings = ConcurrencySettings(max_concurrency=10) | ||
|
|
||
| async with ConcurrencySystem(settings) as concurrency_system: | ||
| pool = AutoscaledPool( | ||
| concurrency_system=concurrency_system, | ||
| consumer=ConcurrencyConsumer(id='my-pool'), | ||
| run_task_function=run_task, # Now called with the task id. | ||
| is_task_ready_function=is_task_ready, | ||
| is_finished_function=is_finished, | ||
| ) | ||
| await pool.run() | ||
| ``` | ||
|
|
||
| ## `max_tasks_per_minute` counts the last minute | ||
|
|
||
| `ConcurrencySettings.max_tasks_per_minute` now caps the tasks started over the last 60 seconds, the same as Crawlee JS. In v1, the pool spaced task starts evenly, `60 / max_tasks_per_minute` seconds apart. In v2, tasks start as fast as the concurrency allows until the cap is reached. The next ones wait until the oldest starts age out of the window. The rate over a minute stays the same, but tasks start in bursts. For details, see [Max tasks per minute](../guides/scaling-crawlers#max-tasks-per-minute). | ||
|
|
||
| The value has to be a whole number of at least 1, or infinity, which is the default. A fractional value, such as `0.5` or `7.5`, now raises `ValueError`. | ||
|
|
||
| ## Autoscaling logs changed | ||
|
|
||
| The periodic log of the autoscaling state and the `Waiting for remaining tasks to finish` message are now logged at DEBUG instead of INFO. The crawler's status message at INFO still reports the desired concurrency. | ||
|
|
||
| The state log also has a new format. Instead of raw ratios for every resource, it names only the overloaded ones, with the share of time each spent over its threshold and its limit: | ||
|
|
||
| ```text | ||
| current concurrency 3, desired 5, overloaded: cpu 58% of the time (limit 40%) | ||
| ``` | ||
|
|
||
| To read the concurrency values in code, use `crawler.concurrency_system`. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we should mention the logging changes as well?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you think anything should be added to the “## Autoscaling logs moved to DEBUG” section?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Logger names changed as well, right ( |
||
|
|
||
| The autoscaling loggers moved together with the package. `crawlee._autoscaling.autoscaled_pool`, `crawlee._autoscaling.snapshotter` and `crawlee._autoscaling.system_status` are now `crawlee.autoscaling._autoscaled_pool`, `crawlee.autoscaling._snapshotter` and `crawlee.autoscaling._system_status`. The state log, along with the messages about scaling and free capacity, now comes from the new `crawlee.autoscaling._concurrency_system` logger. To configure all of them at once, use the `crawlee.autoscaling` logger. | ||
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| from ._autoscaled_pool import AutoscaledPool | ||
| from ._concurrency_system import ConcurrencySystem | ||
| from ._types import ConcurrencyConsumer, LoadRatioInfo, SystemInfo | ||
|
|
||
| __all__ = [ | ||
| 'AutoscaledPool', | ||
| 'ConcurrencyConsumer', | ||
| 'ConcurrencySystem', | ||
| 'LoadRatioInfo', | ||
| 'SystemInfo', | ||
| ] |
Uh oh!
There was an error while loading. Please reload this page.