Skip to content

#2134 fetcher.thread.timeout: cancel the okhttp call, bounded helper pool for other protocols - #2135

Draft
GGraziadei wants to merge 1 commit into
apache:mainfrom
GGraziadei:perf/fetch-timeout-call-cancel
Draft

#2134 fetcher.thread.timeout: cancel the okhttp call, bounded helper pool for other protocols#2135
GGraziadei wants to merge 1 commit into
apache:mainfrom
GGraziadei:perf/fetch-timeout-call-cancel

Conversation

@GGraziadei

@GGraziadei GGraziadei commented Sep 6, 2026

Copy link
Copy Markdown
Member

Fixes #2134.

fetcher.thread.timeout ran the protocol call on a single-thread executor owned by each FetcherThread and abandoned it with future.cancel(true) when the deadline passed. okhttp does not honour the interrupt while connecting or reading, so the helper stayed blocked on the socket and the FetcherThread queued behind it at its next fetch: one host dribbling bytes could take a thread out of service for as long as http.timeout. The option also doubled the bolt's thread count, and the robots.txt lookup was not covered by the timeout at all.

Change

okhttp protocol

  • fetcher.thread.timeout is applied as a per-call deadline with Call.timeout(), enforced by okio's shared watchdog thread: on expiry the call is cancelled, the socket closed and the fetching thread gets an InterruptedIOException immediately, classified as "Socket timeout fetching" as before.
  • The deadline is clamped to topology.message.timeout.secs, so that it can never loosen the client-level callTimeout derived from it (a warning is logged).
  • With http.content.partial.as.trimmed the content received before the deadline is kept and flagged as trimmed for "time", which is what that option already did for the call timeout. Documented.
  • Protocol gains default boolean supportsFetchTimeout() (false), backward compatible for external and user protocols. okhttp returns true when the deadline is configured; DelegatorProtocol only when every delegate does, since the bolt cannot know in advance which delegate a URL is routed to.

Fetcher bolts

  • New package-private FetchTimeoutHelpers, owned by FetcherBolt and SimpleFetcherBolt. Its call() runs both the robots.txt lookup and the fetch: on the calling thread when the protocol enforces the timeout itself, otherwise on a helper thread from one bounded pool per bolt (fetcher.thread.timeout.helpers, default 2 × fetcher.threads.number, 2 for SimpleFetcherBolt; threads created on demand, released after a minute idle).
  • A deadline on the helper path throws a typed TimeoutException; a full pool rejects at once with SaturatedException, reported as FETCH_ERROR with fetch.exception "No fetch helper available". Helpers are shared by all hosts, so a host that never answers can make fetches of other hosts fail this way until its helpers time out; stated in the docs.
  • With the default okhttp protocol no helper thread is ever created.
  • New fetchhelpers gauge and fetch.timeout / fetch.helper.rejected counters.
  • fetcher.thread.timeout is now defined in Constants; the public alias FetcherBolt.FETCH_TIMEOUT_PARAM_KEY (Add bolt-level timeout for fetcher threads #1861) is removed. The configuration key is unchanged.
  • configuration.adoc documents fetcher.thread.timeout, which was missing, and the new helpers key.

Behaviour

protocol fetcher.thread.timeout set before after
okhttp (default) yes fetch abandoned on a per-thread helper that stays blocked; 50 extra threads call cancelled, socket closed, no helper threads
okhttp no unchanged unchanged
other (Playwright, custom) yes per-thread helper; a stuck helper pinned its FetcherThread bounded shared pool; stuck helpers do not pin threads; explicit saturation error
any yes robots.txt lookup outside the timeout covered

Exception classification now also maps InterruptedIOException (okhttp's own timeouts) to "Socket timeout fetching" regardless of the option; previously such failures carried the exception class name in fetch.exception.

Tests

  • HttpProtocolFetchTimeoutTest: a 5s response is cancelled at the 1s deadline; a 60s deadline is clamped to a 1s message timeout; partial content kept or failed depending on http.content.partial.as.trimmed.
  • FetchTimeoutHelpersTest: every branch of call() (off, self-timing protocol, helper thread, exception propagation, typed timeout with interrupt, saturation, bound from config, shutdown).
  • Fetcher bolt tests, run for both bolts, with a test protocol that hangs and ignores interruption: a stuck fetch followed by two fast ones on one fetcher thread yields two pages and one FETCH_ERROR (before: all three timed out); bounded pool with one rejection; hanging robots.txt reported at the timeout (before: the thread blocked for good).
  • FetcherBoltTest: no helper threads with okhttp; slow robots.txt bounded by the deadline with okhttp.
  • The existing testThreadTimeout passes unchanged through the okhttp path.

For all changes

  • Is there a issue associated with this PR? Is it referenced in the commit message?
  • Does your PR title start with #XXXX where XXXX is the issue number you are trying to resolve?
  • Has your PR been rebased against the latest commit within the target branch (typically main)?
  • Is your initial contribution a single, squashed commit?
  • Is the code properly formatted with mvn git-code-format:format-code -Dgcf.globPattern="**/*" -Dskip.format.code=false?

For code changes

  • Have you ensured that the full suite of tests is executed via mvn clean verify?
  • Have you written or updated unit tests to verify your changes?
  • If adding new dependencies to the code, are these dependencies licensed in a way that is compatible for inclusion under ASF 2.0? (no new dependencies)
  • If applicable, have you updated the LICENSE file, including the main LICENSE file? (not applicable)
  • If applicable, have you updated the NOTICE file, including the main NOTICE file? (not applicable)

…elper pool for other protocols

The bolt-level fetch timeout ran the protocol call on a single-thread
executor owned by each FetcherThread and abandoned it with
future.cancel(true) on expiry. okhttp does not honour the interrupt while
connecting or reading, so the helper stayed blocked on the socket and the
FetcherThread queued behind it at its next fetch: a host that dribbles
bytes could take a thread out of service for as long as http.timeout.
It also doubled the thread count of the bolt whenever the option was on,
and the robots.txt lookup was not covered by the timeout at all.

okhttp: fetcher.thread.timeout is applied as a per-call deadline
(Call.timeout()), enforced by okio's shared watchdog: on expiry the call
is cancelled, the socket closed and the fetching thread gets an
InterruptedIOException at once, classified as "Socket timeout fetching".
The deadline is clamped to topology.message.timeout.secs so that it can
never loosen the client-level callTimeout. With
http.content.partial.as.trimmed the content received before the deadline
is kept and flagged as trimmed for "time", as it already was for the
call timeout. Protocol gains a default supportsFetchTimeout() (false);
okhttp returns true when configured, DelegatorProtocol only when every
delegate does.

Bolts: the timeout machinery moves to a new package-private
FetchTimeoutHelpers owned by FetcherBolt and SimpleFetcherBolt. Its
call() runs both the robots.txt lookup and the fetch on the calling
thread when the protocol enforces the timeout itself, otherwise on a
helper thread from one bounded pool per bolt (fetcher.thread.timeout.helpers,
default 2 x fetcher.threads.number, 2 for SimpleFetcherBolt, threads
created on demand and released after a minute idle). A deadline throws
a typed TimeoutException; a full pool rejects at once with
SaturatedException, reported as FETCH_ERROR "No fetch helper available".
With the default protocol no helper thread is ever created. New
fetchhelpers gauge and fetch.timeout / fetch.helper.rejected counters.

The fetcher.thread.timeout key is defined in Constants; the public alias
FetcherBolt.FETCH_TIMEOUT_PARAM_KEY (apache#1861) is removed. Documentation
for the parameter, which was missing, is added to configuration.adoc.

Tests: HttpProtocolFetchTimeoutTest (cancellation at the deadline,
clamp to the message timeout, partial content), FetchTimeoutHelpersTest
(every branch of call()), fetcher bolt tests with a protocol that hangs
and ignores interruption (stuck fetch not blocking the following ones,
bounded pool with rejection, hanging robots.txt reported at the
timeout), slow robots.txt bounded with okhttp, no helper threads with
okhttp, delegator capability.

Fixes apache#2134.
@GGraziadei
GGraziadei marked this pull request as draft September 6, 2026 22:26
@GGraziadei

Copy link
Copy Markdown
Member Author

Moving on DRAFT
Pending discussion and design doc on dev@

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.

fetcher.thread.timeout: cancel the okhttp call instead of interrupting a helper thread

1 participant