fix(proxy): isolate and apply 30s timeout to upgrade proxy dialer#4125
fix(proxy): isolate and apply 30s timeout to upgrade proxy dialer#4125glatinone wants to merge 5 commits into
Conversation
|
/kind bugfix |
|
/label bugfix |
|
Hi maintainers, the current automated CI pipeline is blocked on the |
|
Thanks for the PR! This will break clients which currently don't respect this 30s limit. I would make it configurable and backward compatible. |
|
@glatinone first thanks for the proposed change.
Thanks |
Addresses all four structural gates raised by maintainers szuecs and MustafaSaber on PR zalando#4125: 1. CONFIGURABLE TIMEOUT - Remove const defaultDialTimeout (hard-coded 30 s). - Add dialTimeout time.Duration field to upgradeProxy struct. - Add effectiveDialTimeout() method: returns configured value or falls back to defaultUpgradeDialTimeout (30 s) when zero/unset. - Wire Params.Timeout → Proxy.upgradeDialTimeout (new field) → upgradeProxy.dialTimeout in makeUpgradeRequest. Operators can now tune the dial deadline per deployment. 2. REGRESSION TESTS (gate 2 — no change without a test) Four new tests in proxy/upgrade_test.go: TestEffectiveDialTimeoutDefault Zero dialTimeout must fall back to 30 s, not 0 (no-deadline). TestEffectiveDialTimeoutConfigured Explicit dialTimeout is returned unchanged. TestDialBackendHTTPSTimesOutOnStalledHandshake ← primary regression Stalled-TLS-handshake backend: dial must fail in [T, 3T] where T = dialTimeout (250 ms). Proves the fix, shows the bug scenario. TestDialBackendRespectsContextCancellation Context cancel fires before dialTimeout; proves req.Context() propagation is intact. TestDialBackendTimeoutViaParams ← end-to-end wiring test Params.Timeout flows full chain to upgradeProxy; proxy returns 503 within 3×Params.Timeout (300 ms) against stalled backend. 3. BACKWARD COMPATIBILITY Zero-value dialTimeout (unset Params.Timeout) falls back to defaultUpgradeDialTimeout = 30 s, identical to prior behaviour. Signed-off-by: glatinone <93207632+glatinone@users.noreply.github.com>
Addresses all four structural gates raised by maintainers szuecs and MustafaSaber on PR zalando#4125: 1. CONFIGURABLE TIMEOUT - Remove const defaultDialTimeout (hard-coded 30 s). - Add dialTimeout time.Duration field to upgradeProxy struct. - Add effectiveDialTimeout() method: returns configured value or falls back to defaultUpgradeDialTimeout (30 s) when zero/unset. - Wire Params.Timeout → Proxy.upgradeDialTimeout (new field) → upgradeProxy.dialTimeout in makeUpgradeRequest. Operators can now tune the dial deadline per deployment. 2. REGRESSION TESTS (gate 2 — no change without a test) Four new tests in proxy/upgrade_test.go: TestEffectiveDialTimeoutDefault Zero dialTimeout must fall back to 30 s, not 0 (no-deadline). TestEffectiveDialTimeoutConfigured Explicit dialTimeout is returned unchanged. TestDialBackendHTTPSTimesOutOnStalledHandshake ← primary regression Stalled-TLS-handshake backend: dial must fail in [T, 3T] where T = dialTimeout (250 ms). Proves the fix, shows the bug scenario. TestDialBackendRespectsContextCancellation Context cancel fires before dialTimeout; proves req.Context() propagation is intact. TestDialBackendTimeoutViaParams ← end-to-end wiring test Params.Timeout flows full chain to upgradeProxy; proxy returns 503 within 3×Params.Timeout (300 ms) against stalled backend. 3. BACKWARD COMPATIBILITY Zero-value dialTimeout (unset Params.Timeout) falls back to defaultUpgradeDialTimeout = 30 s, identical to prior behaviour. Signed-off-by: glatinone <93207632+glatinone@users.noreply.github.com>
5128f29 to
21233d3
Compare
|
@szuecs @MustafaSaber — quick status update on PR #4125. Root cause of red X:
How the four gates are met:
Next action: I’ll push a rebase-with-signoff so the DCO check passes; once CI re-runs, we should be green. If you prefer a single squashed signed commit instead, I can do that as well. Thanks! |
Signed-off-by: glatinone <93207632+glatinone@users.noreply.github.com>
Addresses all four structural gates raised by maintainers szuecs and MustafaSaber on PR zalando#4125: 1. CONFIGURABLE TIMEOUT - Remove const defaultDialTimeout (hard-coded 30 s). - Add dialTimeout time.Duration field to upgradeProxy struct. - Add effectiveDialTimeout() method: returns configured value or falls back to defaultUpgradeDialTimeout (30 s) when zero/unset. - Wire Params.Timeout → Proxy.upgradeDialTimeout (new field) → upgradeProxy.dialTimeout in makeUpgradeRequest. Operators can now tune the dial deadline per deployment. 2. REGRESSION TESTS (gate 2 — no change without a test) Four new tests in proxy/upgrade_test.go: TestEffectiveDialTimeoutDefault Zero dialTimeout must fall back to 30 s, not 0 (no-deadline). TestEffectiveDialTimeoutConfigured Explicit dialTimeout is returned unchanged. TestDialBackendHTTPSTimesOutOnStalledHandshake ← primary regression Stalled-TLS-handshake backend: dial must fail in [T, 3T] where T = dialTimeout (250 ms). Proves the fix, shows the bug scenario. TestDialBackendRespectsContextCancellation Context cancel fires before dialTimeout; proves req.Context() propagation is intact. TestDialBackendTimeoutViaParams ← end-to-end wiring test Params.Timeout flows full chain to upgradeProxy; proxy returns 503 within 3×Params.Timeout (300 ms) against stalled backend. 3. BACKWARD COMPATIBILITY Zero-value dialTimeout (unset Params.Timeout) falls back to defaultUpgradeDialTimeout = 30 s, identical to prior behaviour. Signed-off-by: glatinone <93207632+glatinone@users.noreply.github.com>
21233d3 to
97e3cde
Compare
|
Can you cleanup the long code comments (just delete them)? Thanks |
… no functional changes Signed-off-by: glatinone <93207632+glatinone@users.noreply.github.com>
|
Hi @szuecs, I have pruned the verbose documentation blocks and redundant default value comments across both upgrade.go and upgrade_test.go as requested, while keeping the essential code logic and security annotations intact. The PR is ready for your review and CI workflow approval. Thanks! |
| flushInterval: p.FlushInterval, | ||
| experimentalUpgrade: p.ExperimentalUpgrade, | ||
| experimentalUpgradeAudit: p.ExperimentalUpgradeAudit, | ||
| upgradeDialTimeout: p.Timeout, |
There was a problem hiding this comment.
This should be another config/flag for upgrade proxy, using the same timeout for proxy & upgrade proxy is not good.
| dialTimeout time.Duration | ||
| } | ||
|
|
||
| func (p *upgradeProxy) effectiveDialTimeout() time.Duration { |
There was a problem hiding this comment.
please just use dialTimeout() also keep in mind we need to have the option to not set timeout at all to be backward compatible, so if if the value is negative maybe give not timeout at all
…unded test goroutines
Root causes addressed:
1. Params.Timeout aliasing (proxy.go): WithParams wired the upgrade
proxy's dial timeout from the general-purpose Params.Timeout field
("TCP client connection timeout for proxy http connections to the
backend"), silently coupling the unrelated regular-HTTP backend
timeout to the upgrade/WebSocket dial path. Added a dedicated
Params.UpgradeDialTimeout field and fixed the wiring to use it.
2. Two-state vs three-state timeout collapse (upgrade.go): the prior
effectiveDialTimeout() treated zero and negative dialTimeout
identically (both fell back to the 30s default), with no way to
explicitly disable the ceiling. Renamed to resolvedDialTimeout()
and implemented the 3-state contract already used elsewhere in
this file for CloseIdleConnsPeriod (negative = explicitly disabled,
zero = unset/use default, positive = as configured), matching
net.Dialer.Timeout == 0 semantics for the disabled case.
3. Unbounded background goroutines in tests (upgrade_test.go): the
stalled-backend goroutines in TestDialBackendHTTPSTimesOutOnStalled
Handshake, TestDialBackendRespectsContextCancellation, and
TestDialBackendTimeoutViaParams used a bare time.Sleep(30s) with no
way to terminate early, so they kept running for the full 30s after
their owning test had already completed and moved on. Added a
`done` channel closed via t.Cleanup in all three, and a select
between <-done and the 30s stall, so the goroutine exits promptly
once the test scope ends instead of leaking for the remainder of
the 30s window.
Also renamed the existing TestEffectiveDialTimeoutDefault/Configured
tests to TestResolvedDialTimeoutDefault/Configured (matching the
method rename) and added TestResolvedDialTimeoutNegativeDisablesTimeout
for the new negative-value case. Updated TestDialBackendTimeoutViaParams
to configure Params.UpgradeDialTimeout instead of the general Timeout
field, matching the corrected wiring.
Wired the new field through Options.UpgradeDialTimeout (skipper.go)
and a new -upgrade-dial-timeout CLI flag (config/config.go), following
the exact same three-layer pattern already used for -timeout-backend.
The flag defaults to 0 so that leaving it unset activates
resolvedDialTimeout()'s own built-in 30s fallback, rather than
duplicating that default as a separate hardcoded value in the CLI
layer.
No local Go toolchain is available in this environment to run
go fmt/go vet/go test -race directly. Every change was checked by hand
against the actual current source (all call sites grepped and
updated, brace balance verified, struct-literal alignment matched to
gofmt column rules) rather than assumed. Verification will run via
the upstream CI on push.
Signed-off-by: glatinone <93207632+glatinone@users.noreply.github.com>
…ng collision Signed-off-by: glatinone <93207632+glatinone@users.noreply.github.com>
788afdf to
c32bb7b
Compare
This Pull Request resolves a critical P0 availability defect regarding unbounded goroutine accumulation within the WebSocket and HTTP upgrade proxy pathways under stalled backend conditions.
By replacing the blocking, context-less net.Dial and tls.Dial calls inside dialBackend() with context-aware DialContext routines, the proxy now correctly propagates req.Context() cancellation and enforces a strict dial timeout.
The timeout has been fully wired into the proxy configuration factory, defaulting to 30 seconds while allowing direct overrides via Params.Timeout. Comprehensive regression and E2E timeout verification tests have been integrated into proxy/upgrade_test.go.