Skip to content

feat(core): unwrap capabilities of nested request managers (#3999) - #4038

Closed
harryautomazione wants to merge 7 commits into
apify:masterfrom
harryautomazione:feat/delegating-request-manager
Closed

feat(core): unwrap capabilities of nested request managers (#3999)#4038
harryautomazione wants to merge 7 commits into
apify:masterfrom
harryautomazione:feat/delegating-request-manager

Conversation

@harryautomazione

Copy link
Copy Markdown
Contributor

Resolves #3999

Summary

When a ThrottlingRequestManager is wrapped inside another request manager (such as RequestManagerTandem created via requestList.toTandem(throttler)), BasicCrawler probed capabilities via a shallow supportsDomainThrottling(manager) check. Because outer wrappers do not directly expose rate-limit methods, domain throttling capabilities (429 backoff, robots.txt crawl delay, stall detection) quietly stopped working.

This PR introduces generic capability delegation:

  1. DelegatingRequestManager Interface: Exports getDelegatedManager() allowing wrappers to expose their inner request manager(s).
  2. RequestManagerTandem & ThrottlingRequestManager Delegation: RequestManagerTandem implements getDelegatedManager(): Promise<IRequestManager> | IRequestManager returning its inner manager without needing to know anything about throttling.
  3. Recursive Unwrapper findDomainThrottlingManager: Traverses delegating managers with an anti-recursion visited set to prevent circular stack overflows.
  4. BasicCrawler Optimization: Resolves findDomainThrottlingManager during crawler setup (_init()) and caches #throttlingManager for O(1) zero-overhead checks.

Testing

  • Added unit tests in test/core/storages/throttling_request_manager.test.ts verifying direct discovery, unwrapping through RequestManagerTandem, and anti-recursion protection for circular delegators.

@janbuchar
janbuchar self-requested a review August 15, 2026 10:58
@harryautomazione
harryautomazione force-pushed the feat/delegating-request-manager branch 6 times, most recently from 48c60ec to da6a474 Compare August 16, 2026 09:08
@B4nan
B4nan changed the base branch from v4 to master August 18, 2026 16:33
@harryautomazione
harryautomazione force-pushed the feat/delegating-request-manager branch from 2c1140a to fc0b2c5 Compare August 20, 2026 09:38
@janbuchar

Copy link
Copy Markdown
Contributor

Closing as superseded by #4061. Thanks for your contribution!

@janbuchar janbuchar closed this Aug 21, 2026
janbuchar added a commit that referenced this pull request Aug 27, 2026
…anager to fix leaky abstraction of ThrottlingRequestManager (#4061)

- closes #3999
- closes #4027
- supersedes #4037
- supersedes #4038
- contributes to #4039

Worth reviewing in order — each change stands alone, with the reasoning
in its own message.

1. `checkReadiness()` replaces `isEmpty()` / `isFinished()` on
`IRequestLoader` and `IRequestManager`.
2. `IRequestManager.recordPacingSignal()` replaces the
`SupportsDomainThrottling` capability probe.
3. The crawler builds the `sameDomainDelaySecs` throttler in its
constructor, which is what puts it *inside* the tandem.
4. `sameDomainDelaySecs` goes in through `recordPacingSignal()` as a
floor covering every domain, so a manager that already paces takes it
instead of getting a second pacer wrapped around it.
5. `ThrottlingRequestManager`'s `inner` is optional — omitted, it opens
the default queue on first use. Same feature as #4037, which is a much
smaller patch on top of the lazy `inner` factory this branch already
has.

On #3999 this takes the first option — wrappers forward — but the
objection there was that it makes `RequestManagerTandem` know throttling
exists. It doesn't: pacing signals are `IRequestManager` members, so the
tandem forwards a `PacingSignal` it never looks inside, and learns
nothing throttling-specific. That deletes the capability-discovery
problem rather than solving it, which is why the delegation interface
from #4038 isn't here.

One capability check did survive that — the guard refusing
`sameDomainDelaySecs` over a `ThrottlingRequestManager` — and it missed
a throttler behind a tandem (#discussion_r3851833671). The fourth change
removes it rather than deepening it: the floor is a `PacingSignal` like
any other, so whatever paces takes it wherever it sits in a composition,
and a manager pacing only *some* of its domains throws rather than
under-applying it. No `instanceof` left anywhere.

#4039 is the shape `sameDomainDelaySecs` was producing on its own:
wrapping happened at first use, by which point the tandem for a
`requestList` already existed, so the pacer went outside it and a list's
requests reached the queue without passing a per-domain clock. The third
commit builds the pacer in the constructor instead, so those requests
are routed by domain like any other. The hand-built form in #4039's
reproduction is untouched — `fetchNextRequest` still takes whatever
`inner` offers without checking the clocks — so #4040 is still needed.

Migration notes in `docs/upgrading/upgrading_v4.md`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Capabilities of a ThrottlingRequestManager are lost when it is wrapped in another manager

4 participants