Say why a stream stopped, and give each wait its own setting - #5073
Say why a stream stopped, and give each wait its own setting#5073elias-ba wants to merge 1 commit into
Conversation
|
I have enough context. The changes are:
Now scoping each check: S0 (project scoping): The reaper is an internal Oban cron job (like S1 (authorization): No new user-triggered CRUD actions. The reaper isn't user-invoked; the channel change adds payload fields to existing broadcasts (no new S2 (audit trail): Chat messages are operational/session data, not in the audited config-resource set (credentials, project settings, workflows, webhook auth methods, oauth clients, version control). No matching audit module exists or is expected here. Security Review ✅
|
043e22e to
1bb4437
Compare
aacaf77 to
4428533
Compare
1bb4437 to
46eb44f
Compare
4428533 to
c7821e5
Compare
46eb44f to
34430bf
Compare
c7821e5 to
f7e2565
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## timeout-keep-partial-response #5073 +/- ##
===============================================================
- Coverage 90.6% 90.5% -0.1%
===============================================================
Files 422 422
Lines 20066 20075 +9
===============================================================
- Hits 18178 18175 -3
- Misses 1888 1900 +12 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
34430bf to
5bdb4ad
Compare
592f66a to
be94fda
Compare
5bdb4ad to
9db7ecf
Compare
be94fda to
3c74d7a
Compare
Two problems with one cause: nothing downstream could tell what had gone wrong, because the reason was thrown away before anyone could read it. Tesla's Finch adapter halts a streamed body identically for a mid-stream error, a mid-stream timeout and a clean end - it returns nil in all three cases and binds the error to an unused variable. So a hung Apollo, a severed connection and a genuinely short answer were the same event to us, and all three reported "Stream ended without complete response". We now use our own copy of that adapter which keeps the reason. A sentinel inside the stream is not possible, because the SSE middleware concatenates elements as binaries and would fail on anything else; but the unfold runs in the calling process, so the reason is left there and read back once the stream has been consumed. The copy also passes request_timeout through to Finch, which 1.18.3 drops. The reason arrives as either Finch's transport struct or Mint's, and both carry it under the same key, so it is taken out before being matched on. Matching one struct silently missed the other, which put a dropped connection back on the generic sentence - the exact conflation this change exists to end. APOLLO_TIMEOUT is replaced by three settings that each answer one question: how long to wait to reach Apollo, how long a silence mid-answer is acceptable, and how long a whole request may take. One number could not answer all three, so it had to be sized for the longest of them, which meant a completely hung Apollo held the user on a spinner for five minutes. The staging and prod config is renamed in the same change. Oban's drain window is derived from those same three rather than fixed, so raising one of them cannot leave a deploy killing AI jobs before they finish. OBAN_SHUTDOWN_GRACE_PERIOD_MS overrides it. Apollo also gets its own connection pool. An AI stream holds a connection for the length of an answer, and sharing the default pool meant a handful of them tied up connections other outbound calls were waiting on. It is pinned to http1 deliberately: on http2 Finch reinterprets the idle setting as a deadline for the whole request. The adapter tests run over a raw socket rather than the Tesla mock, and are the first thing in the suite to exercise the lazy stream path at all - which is why this went unnoticed. Checked by reverting the two lines that keep the reason: the tests then report nil where they expect a timeout. The adapter file itself is left out of coverage. It is upstream's code carried until tesla#912 ships, and the lines we changed are covered.
9db7ecf to
6874963
Compare
3c74d7a to
5434625
Compare
Description
Two problems with one cause: nothing downstream could tell what had gone wrong, because the reason was thrown away before anyone could read it.
Tesla's Finch adapter halts a streamed body identically for a mid-stream error, a mid-stream timeout and a clean end. It returns
nilin all three cases and binds the error to an unused variable. So a hung Apollo, a severed connection and a genuinely short answer were the same event to us, and all three reported "Stream ended without complete response". They now say three different things.We use our own copy of that adapter which keeps the reason. A sentinel inside the stream is not possible, because the SSE middleware concatenates elements as binaries and would fail on anything else; but the unfold runs in the calling process, so the reason is left there and read back once the stream has been consumed. The copy also passes
request_timeoutthrough to Finch, which the 1.18.3 we are pinned to drops.The copy is not upstream's file with two edits. It also loses upstream's
build/4clauses, so a multipart or streamed request body would raise rather than work, and it keeps only the newer of upstream's two version-gated error clauses. That is fine for Apollo, which sends JSON, and the moduledoc says what the module is for. Upstream has since fixed the reason and released it, so this whole file goes away when we bump Tesla, tracked in #5080. It is excluded from coverage on the way out, since it is upstream's code and only the lines we changed are ours to test.The reason arrives as either Finch's transport struct or Mint's, and both carry it under the same key, so it is taken out before being matched on. Matching one struct silently missed the other, which put a dropped connection back on the generic sentence: the exact conflation this exists to end.
APOLLO_TIMEOUTis replaced by three settings that each answer one question: how long to wait to reach Apollo, how long a silence mid-answer is acceptable, and how long a whole request may take. One number could not answer all three, so it had to be sized for the longest, which meant a completely hung Apollo held the user on a spinner for five minutes. The staging and prod config is renamed in the same change, and all three have defaults so nothing has to be set.Oban's drain window is derived from those three rather than fixed at the six minutes #5069 set, so raising one of them cannot leave a deploy killing AI jobs before they finish.
OBAN_SHUTDOWN_GRACE_PERIOD_MSoverrides it, and the boot warning now names it.Apollo also gets its own Finch pool, which is how the connect timeout takes effect at all. An AI stream holds a connection for the length of an answer, and sharing the default pool meant a handful of them tied up connections other outbound calls were waiting on. It is pinned to http1 deliberately: on http2 Finch reinterprets the idle setting as a deadline for the whole request.
The adapter tests run over a raw socket rather than the Tesla mock, and are the first thing in the suite to exercise the lazy stream path at all, which is why this went unnoticed. Checked by reverting the two lines that keep the reason: the tests then report
nilwhere they expect a timeout.Closes #4882
Validation steps
(
nc -l <port>is enough), setAPOLLO_IDLE_TIMEOUT_MS=5000, and send amessage. You should get an error in about five seconds rather than waiting
out the whole request budget.
through. Kill the listener mid-answer instead and it should say the
connection was lost. Those two used to read the same.
APOLLO_REQUEST_TIMEOUT_MSabove the drain window and restart, to seethe boot warning name
OBAN_SHUTDOWN_GRACE_PERIOD_MS.Additional notes for the reviewer
lib/lightning/tesla/adapter/finch.exis a copy of upstream's adapter and is most of the diff. Upstream has since fixed and released the bug behind it, so the file goes when we bump Tesla (#5080); it is excluded from coverage for that reason.AI Usage
Please disclose whether you've used AI anywhere in this PR (it's cool, we just
want to know!):
You can read more details in our
Responsible AI Policy
Pre-submission checklist
/reviewwith Claude Code)
(e.g.,
:owner,:admin,:editor,:viewer) — none touched here