Skip to content

fix(sdks): windowed degraded detection for pool backoff - #1499

Open
Pangjiping wants to merge 5 commits into
mainfrom
fix/sdk-pool-windowed-degradation
Open

fix(sdks): windowed degraded detection for pool backoff#1499
Pangjiping wants to merge 5 commits into
mainfrom
fix/sdk-pool-windowed-degradation

Conversation

@Pangjiping

Copy link
Copy Markdown
Collaborator

Summary

The sandbox pool reconcile loop detected degradation with a consecutive-failure counter that any successful warmup reset to zero (recordSuccess zeroed failureCount, backoffAttempts, and cancelled the active backoff window). Under a sustained high failure rate with interleaved successes (e.g. ~50 failures/s + ~9.4 successes/s), the count could never reach degradedThreshold, so DEGRADED/backoff never triggered and the pool churned for hours.

This PR replaces the counter with rate-based detection over a sliding time window:

  • Failures are recorded with timestamps and age out of a new failure_window knob (default 60s)
  • degradedThreshold now means failures inside the window, not consecutive failures
  • Successful warmups no longer reset the failure count or cancel an active backoff window
  • While DEGRADED, an expired backoff window is auto-renewed as long as the window is still hot — the pool stays paused until the failure rate actually drops below the threshold
  • Recovery is time-based only: the pool returns to HEALTHY when the window drains (backoff attempts / lastError reset then)

Implemented identically in Kotlin, Go, and Python (sync + async); recordSuccess removed in all languages. Docs updated (docs/guides/client-pool.md) noting the change takes effect from the next SDK release.

Tests

  • Kotlin: ./gradlew spotlessApply :sandbox:test :code-interpreter:test
  • Go: go test ./...
  • Python: uv run pytest tests/ + ruff + pyright ✅
  • Docs: pnpm docs:build

New regression tests (fail under the old counter logic):

  • Pool-level interleaved success/failure warmup streams now trigger DEGRADED + backoff (Kotlin SandboxPoolTest, Go TestReconciler_InterleavedCreatesTriggerDegraded, Python test_interleaved_creates_trigger_degraded_backoff)
  • Windowed trigger across time gaps, backoff auto-renewal while hot, drain-based recovery, and escalation-rate tests

Behavior change (public)

  • failure_window new config (default 60s) in all three SDKs
  • degradedThreshold semantics: consecutive → windowed
  • Pools with a high sustained failure rate stay paused (auto-renewed backoff) instead of probing every backoff step

Replace the consecutive-failure counter in the sandbox pool reconciler
with rate-based detection over a sliding failure window. A successful
warmup no longer resets the failure count or cancels an active backoff,
so pools with a sustained high failure rate and interleaved successes
enter DEGRADED and stay paused until the window drains.

- ReconcileState: failures recorded with timestamps, pruned by
  failureWindow (default 60s, new PoolConfig knob); expired backoff is
  renewed while the window is still hot; recovery is time-based only
- Kotlin, Go, and Python (sync + async) aligned; recordSuccess removed
- Regression tests: interleaved success/failure warmup streams now
  trigger backoff; windowed trigger, renewal, and drain-recovery tests
- docs/guides/client-pool.md: document new semantics and config knob
  (effective from the next SDK release)
@github-actions github-actions Bot added documentation Improvements or additions to documentation sdk/go sdk/java sdk/python sdks size/L Denotes a PR that changes 100-499 lines, ignoring generated files. labels Aug 13, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7a62a3c390

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread sdks/sandbox/python/src/opensandbox/pool_types.py Outdated
Comment thread sdks/sandbox/go/pool_reconciler.go Outdated
…multiply

Address review findings on #1499:

- Python: move failure_window to the end of PoolConfig/AsyncPoolConfig fields
  and SandboxPoolSync/SandboxPoolAsync constructor params so existing
  positional construction keeps its meaning; add positional regression tests
- Go/Kotlin: cap the exponential delay in seconds before building the
  duration so sustained renewals (backoffAttempts grows unbounded) can no
  longer overflow time.Duration (Go wraps negative) or Duration capacity
  (Kotlin throws); Python already caps via unbounded ints, regression test
  added for consistency

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 359828cd85

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread sdks/sandbox/go/pool_reconciler.go Outdated
Go snapshot() only compared backoffUntil and returned the cached state,
so a DEGRADED pool with no deficit could keep reporting stale DEGRADED
and an unpruned failure count after the window drained. Extract
refreshLocked (prune/renew/recover) shared by shouldBackoff and snapshot,
and reorder Kotlin/Python snapshots to advance the state machine before
reading state, failure count, and last error so all languages stay
aligned.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ce74346762

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Kotlin's SandboxPool.builder() wrapper exposed degradedThreshold but not
the new failureWindow knob, so Kotlin/Java callers could not set it via
the normal pool builder. Add the pass-through setter plus a forwarding
regression test.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: be97122ad5

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread sdks/sandbox/go/pool_reconciler.go
DefaultSandboxPool.Snapshot() discarded the health state refreshed by
reconcileState.snapshot() (which prunes the window and runs recovery on
read) and returned the cached p.healthState instead, so it could pair
stale DEGRADED with fresh failureCount/backoffActive after the window
drained. Use the refreshed value so Go matches Kotlin/Python snapshot
behavior. Regression test drives DEGRADED then drains the window and
asserts the snapshot self-recovers.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation sdk/go sdk/java sdk/python sdks size/L Denotes a PR that changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant