adapter: add LaunchDarkly reconnect integration test and migrate to upstream 3.1.1#37026
Conversation
3a96074 to
006218f
Compare
006218f to
59a7e8d
Compare
e76f589 to
7fffddc
Compare
7fffddc to
414a4f7
Compare
def-
left a comment
There was a problem hiding this comment.
We should actually verify if the test would found the bug.
@def- I could stack a PR on this that has an older version of the 3.0 SDK and see if it fails. Would that be a reasonable approach? |
|
Sure, works for me! |
Move launchdarkly-server-sdk from the MaterializeInc/rust-server-sdk fork back to upstream crates.io 3.1.1, restoring the launchdarkly-sdk-transport + MetricsTransport setup and dropping the [patch.crates-io] override. The fork existed for launchdarkly/rust-server-sdk#116: a StreamingDataSource /eventsource StreamClosed bug where a non-Eof stream error left the data source stuck with no reconnect, silently breaking LD sync. A prior upgrade to upstream 3.0.1 had to be reverted (incident-984) because that bug was still unfixed upstream. The fixes have since landed, rust-server-sdk#168 and rust-eventsource-client#134/#135, and 3.1.1 resolves eventsource-client to 0.17.5, which carries them. Use the rustls + aws-lc-rs features (hyper-rustls-native-roots, crypto-aws-lc-rs), now the upstream defaults, instead of the prior attempt's native-tls/crypto-openssl, avoiding the OpenSSL path. The transport build_https() call is identical either way. Because the SDK now builds a rustls client, the workspace links both rustls provider features (aws_lc_rs via our features, ring transitively via the hyper-rustls chain). With both enabled rustls cannot select a process-default provider on its own and panics on first client build. Install aws-lc-rs explicitly (idempotently) at the top of the environmentd, clusterd, balancerd, sqllogictest, and testdrive entrypoints. orchestratord already installs it. deny.toml gains skips for the duplicate versions the transport stack pulls (older tower/rustls-native-certs; newer rand/rand_core/getrandom/cpufeatures) and re-adds the launchdarkly-sdk-transport wrapper. Adds MetricsTransport unit tests, including test_metric_frozen_on_midstream_ error, modeling the exact incident-984 failure mode (200 OK then a mid-stream timeout): they assert the last_sse_time_seconds gauge freezes so the staleness alert can detect a stuck data source. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
414a4f7 to
0a4f3d9
Compare
0a4f3d9 to
6439427
Compare
incident-984 was a runtime failure: the LaunchDarkly data source stopped reconnecting after its streaming connection died, silently wedging flag sync. The existing test/launchdarkly nightly covers value sync, persistence, targeting, and the kill switch, but nothing exercises reconnect after a mid-stream failure. This adds test/launchdarkly-reconnect, which reproduces the incident deterministically against a mock and needs no real LaunchDarkly credentials. The failure mode matters. The incident signature was a mid-body read timeout on a silently-dead connection, hyper::Error(Body, Kind(TimedOut)), which the eventsource client surfaces to the data source as a stream error. A TCP RST or a clean FIN does NOT reproduce it: those are retried inside the eventsource client on every SDK version and never reach the data source (verified empirically, nightly MaterializeInc#17018 stayed green with an RST-based mock even on the exact incident-era dependency stack). The mock serves an initial flag value (2 GiB) on the first TWO streaming connections and then goes silent holding each open, so the transport read timeout ends the stream. Two, because environmentd creates a short-lived bootstrap LD client at boot (load_remote_system_parameters) before the long-lived sync client, and the stall must hit the sync client (verified empirically, nightly MaterializeInc#17020: with only the first connection stalled, the sync client landed on connection two and got the updated value without any reconnect being exercised). Every later (reconnecting) client gets the updated value (3 GiB) plus heartbeats. Production changes, both hidden test knobs: - --launchdarkly-base-uri (env MZ_LAUNCHDARKLY_BASE_URI) overrides the SDK's streaming/polling/events endpoints with a single base URL via the SDK's ServiceEndpointsBuilder::relay_proxy, letting tests point the SDK at the mock. Also generally useful for relay-proxy setups. - MZ_LAUNCHDARKLY_READ_TIMEOUT overrides the transport's streaming read timeout (default 300s) so the test can trigger the timeout path in seconds. mzcompose.py boots environmentd against the mock with a 5s read timeout and asserts SHOW max_result_size reaches 3GB, which can only happen if the sync client's data source reconnected after the timeout. A regressed SDK stays stuck at 2GB and the assertion times out. Wired into the nightly pipeline. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6439427 to
aaf2f27
Compare
@def- @ggevay - this throwaway PR #37460 fails the reconnect test I propose that we merge this PR #37026 if all looks good to both of you. |
def-
left a comment
There was a problem hiding this comment.
Thanks for checking, no complaints from QA side
|
Thank you very much for the thorough testing! This looks good to me, just some minor comments from Claude and me: I reviewed this PR together with #37025. Since this PR carries both commits of the stack, I'm posting all findings here. Nothing blocking in either commit. On the reconnect test commit1. Unparsable 2. Knob plumbing asymmetry (confirming it's deliberate). 3. The mock comment slightly overstates its guarantee. "the sync client always receives at least one stall-then-timeout cycle" is not strictly true: if the bootstrap client's stalled connection ever timed out before its initialization completed, its reconnect would consume the second stall, and the sync client would then get 3 GiB on its very first connection. This doesn't undermine the test, though. Consuming both stalls itself requires a working post-timeout reconnect, the very behavior under test, in code shared by both clients, so a regressed SDK cannot pass this way. Just worth a small wording tweak in the comment. 4. Nits (take or leave):
On the migration commit (#37025)5. The rustls provider pin is five copy-pasted call sites with an empirically chosen scope. The PR body says "each binary that builds an LD client", but neither testdrive nor clusterd builds an LD client (clusterd has no LaunchDarkly usage at all, 6. The 300s read timeout is a load-bearing magic constant. It must stay above LaunchDarkly's streaming heartbeat interval (around 3 minutes, going by LD's documentation), or healthy idle streams would reconnect spuriously (benign now that reconnects work, but worth stating). The comment this stack adds explains the override mechanism, not the relationship to the heartbeat interval. One sentence on the constant's origin would help. Same constant appears in 7. The new transport auto-detects proxy env vars, worth a comment. 8. Minor Cargo.lock churn: 9. Nit: |
Fold in the non-blocking review comments from MaterializeInc#37026: - Log an error instead of silently falling back to the default when MZ_LAUNCHDARKLY_READ_TIMEOUT fails to parse, so a typo surfaces at the source rather than as an unexplained downstream timeout. - Document the 300s streaming read timeout's origin (must stay above LD's ~3-minute heartbeat interval) in both call sites. - Note that HyperTransport auto-detects HTTP_PROXY/HTTPS_PROXY/NO_PROXY, behavior the old hyper-tls setup did not have. - Reword the mock's STALL_CONNECTIONS comment: stalling two connections does not guarantee the sync client eats a stall, it makes 3 GiB unreachable without a working post-timeout reconnect either way. - Handle BrokenPipeError on the mock's stall-branch write. - Use a single dedented testdrive string. - Pin launchdarkly-sdk-transport to the three-component 0.1.4. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Good point - addressed it.
It's intentional here. I'm also not sure how long we'll want to keep all of these tests beyond de-risking the migration off our fork.
The comments have been clarified.
Fixed it since I was pushing a commit.
Once we're fully migrated over to rustls, some additional cleanup here would be great. I'm not going to address this now because it would increase the size / scope of the PR.
I added some comments here.
Noted
Good catch but leaving it.
fixed Thanks - the latest commit 4250039 addresses most of these. |
Merge this PR to move to LaunchDarkly mainline 3.1.1
Part 2 of 3 in the LaunchDarkly upstream-SDK stack. Stacked on #37025.
What this does
Adds
test/launchdarkly-reconnect, an integration test that reproduces incident-984 deterministically against a mock, with no real LaunchDarkly credentials.The failure mode matters. The incident signature was a mid-body read timeout on a silently-dead connection (
hyper::Error(Body, Kind(TimedOut))), which the eventsource client surfaces to the data source as a stream error. A TCP RST or clean FIN does not reproduce it — those are retried inside the eventsource client on every SDK version and never reach the data source. We verified this empirically: an earlier RST-based version of this mock stayed green (nightly #17018) even on the exact incident-era dependency stack.mock_ld.py— mocks the LD streaming API. The first streaming client gets an initial flag value (2 GiB), then the connection goes silent while held open (no FIN, no RST), so the SDK's transport read timeout ends the stream — the exact incident error class. Every reconnecting client gets an updated value (3 GiB) plus heartbeats.mzcompose.py— boots environmentd against the mock with a 5s read timeout and assertsSHOW max_result_sizereaches3GB, which can only happen if the data source reconnected after the timeout.Production changes, both hidden test knobs:
--launchdarkly-base-uri(envMZ_LAUNCHDARKLY_BASE_URI) — overrides the SDK's endpoints with a single base URL (ServiceEndpointsBuilder::relay_proxy), letting tests point the SDK at the mock.MZ_LAUNCHDARKLY_READ_TIMEOUT— overrides the transport's streaming read timeout (default 300s) so the test can trigger the timeout path in seconds.Against the fixed 3.1.1 SDK (#37025) this should pass green; #37460 proves it goes red on the pre-fix stack.
Stacking note: GitHub can't base a fork PR on another fork branch, so this targets
mainand its diff also contains #37025's migration commit. Review the top commit (adapter: add LaunchDarkly reconnect integration test).