Skip to content

feat(gateway): implement wake-on-traffic for paused sandboxes#586

Open
furykerry wants to merge 16 commits into
openkruise:masterfrom
furykerry:feat/wake-on-traffic
Open

feat(gateway): implement wake-on-traffic for paused sandboxes#586
furykerry wants to merge 16 commits into
openkruise:masterfrom
furykerry:feat/wake-on-traffic

Conversation

@furykerry

@furykerry furykerry commented Jun 28, 2026

Copy link
Copy Markdown
Member

Ⅰ. Describe what this PR does

This PR implements wake-on-traffic for paused sandboxes in the sandbox-gateway. When a paused sandbox receives incoming traffic, the gateway automatically resumes it and forwards the request once the sandbox is running again.

Key changes:

  1. Wake-on-traffic in filter (pkg/sandbox-gateway/filter/filter.go): When a paused sandbox with WakeOnTraffic=true receives traffic, the filter launches wake in a goroutine with a detached context and returns api.Running to suspend Envoy request processing. The wakeAndContinue goroutine calls Continue(api.Continue) on success or SendLocalReply(503) on failure. Thread-safe completion state machine (mutex-protected) prevents double-reply when async goroutine and Destroy race. Wake eligibility is encapsulated in shouldWakeSandbox() — a single method that checks all preconditions (Paused state, EnableWakeOnTraffic, waker initialized, valid namespace--name format) before deciding via route.WakeOnTraffic or the HasWakeAnnotation fallback. Scoped loggers (logger.With(zap.String("sandboxID", ...))) are used in DecodeHeaders and wakeAndContinue to avoid repeating the field at each call site.

  2. New wake package (pkg/sandbox-gateway/wake/wake.go): Implements Waker that calls sandbox.Resume() to patch the spec and waits for the sandbox to reach Running state via the cache wait reconciler. Wake() runs under the caller's context (with timeout) — Resume itself provides first-writer-wins dedup via retryUpdate, so no singleflight is needed. Uses errors.Is() with sentinel errors for error classification (not string matching).

  3. E2B pause with spec patch (pkg/servers/e2b/pause_resume.go): Adds Pause() that sets Spec.DesiredState=Paused (instead of deleting the sandbox), enabling the wake-on-traffic flow.

  4. Registry route lifecycle (pkg/sandbox-gateway/server/server.go): handleRefresh retains routes for all states except dead and empty string. This ensures the filter has full visibility (including paused, creating, available) for routing and wake decisions.

  5. Sentinel error classification (pkg/utils/utils.go, pkg/sandbox-manager/errors/error.go): Exports sentinel errors (ErrSandboxIsPausing, ErrSandboxIsTerminating, ErrShutdownTimeReached, ErrSandboxPhaseNotAllowed) from IsSandboxResumable. Adds Unwrap() + NewErrorWrap() to managererrors.Error to enable errors.Is() through wrapped errors.

  6. Gateway cache optimization (pkg/cache/cache.go): Restricts the gateway's informer cache to only Sandbox resources via ByObject filtering, reducing memory usage and API server load.

  7. Configuration: Adds ENABLE_WAKE_ON_TRAFFIC env var and wakeTimeoutSeconds config to the gateway filter.

  8. autoResume API support (pkg/servers/e2b/models/sandbox.go, pkg/servers/e2b/create.go): When the E2B create sandbox API is called with "autoResume": {"enabled": true}, the sandbox is automatically annotated with agents.kruise.io/wake-on-traffic=true at creation time. This aligns with the upstream E2B API specification. The E2B SDK maps lifecycle={"auto_resume": True} to this API field. Unit tests cover enabled, disabled, and absent scenarios.

  9. E2E test update (test/e2b/test_wake_on_traffic.py): Updated to use lifecycle={"on_timeout": "pause", "auto_resume": True} instead of manual kubectl annotate for the wake-on-traffic annotation. The wake-timeout-seconds annotation is still set via kubectl since the API only handles wake-on-traffic.

Ⅱ. Does this pull request fix one issue?

NONE

Ⅲ. Describe how to verify it

  1. Run unit tests:
    go test ./pkg/sandbox-gateway/... ./pkg/cache/... ./pkg/servers/e2b/... ./pkg/utils/...
  2. Run E2E test:
    pytest test/e2b/test_wake_on_traffic.py
  3. Manual verification:
    • Create a sandbox with lifecycle={"on_timeout": "pause", "auto_resume": True} (SDK) or "autoResume": {"enabled": true} (raw API)
    • Verify the sandbox is annotated with agents.kruise.io/wake-on-traffic=true
    • Pause it via POST /sandboxes/{id}/pause
    • Send traffic to the paused sandbox
    • Verify the sandbox is automatically resumed and the request is forwarded

Ⅳ. Special notes for reviews

  • Async wake: DecodeHeaders returns api.Running when wake is triggered, launching wakeAndContinue in a goroutine. The goroutine uses a detached context (not tied to the Envoy filter lifecycle). On completion, it calls Continue(api.Continue) or SendLocalReply. The Destroy() method cancels the in-flight wake context when Envoy destroys the filter (stream reset). A mutex-protected completion state machine prevents double-reply races.
  • No singleflight: Waker.Wake() calls wakeInternal directly under the caller's context. Dedup is handled by Resume's retryUpdate (first-writer-wins) and refcounted wait via NewSandboxResumeTask. This avoids the footgun where a canceled caller context would cut short the shared Resume for all coalesced callers.
  • handleRefresh: Only dead and empty-state routes are deleted from the registry. All other states (running, paused, creating, available) are retained for full filter visibility.
  • Annotation lifecycle: Wake-on-traffic annotations can be set in two ways: (1) via the E2B create API with "autoResume": {"enabled": true} (mapped from SDK's lifecycle={"auto_resume": True}), or (2) manually via kubectl annotate for sandboxes created without autoResume. The E2E test uses the API path and verifies the annotation is present immediately after creation.
  • The cache optimization (SandboxOnly mode) adds nil guards on GetSandboxController() and GetSandboxSetController() since controller handlers are not set up in that mode.

Ⅴ. Test coverage

Package Coverage
pkg/sandbox-gateway/filter 82.8%
pkg/sandbox-gateway/wake 92.1%
pkg/servers/e2b 87.7%
pkg/proxy 90.2%

@kruise-bot
kruise-bot requested review from AiRanthem and zmberg June 28, 2026 06:06
@kruise-bot

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please ask for approval from furykerry by writing /assign @furykerry in a comment. For more information see:The Kubernetes Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@codecov

codecov Bot commented Jun 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 70.04049% with 74 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.92%. Comparing base (dee78fb) to head (be385e8).
⚠️ Report is 4 commits behind head on master.

Files with missing lines Patch % Lines
pkg/sandbox-gateway/filter/filter.go 66.16% 33 Missing and 12 partials ⚠️
pkg/sandbox-gateway/server/server.go 37.50% 10 Missing ⚠️
pkg/sandbox-gateway/wake/wake.go 88.63% 3 Missing and 2 partials ⚠️
...g/sandbox-gateway/controller/gateway_controller.go 0.00% 4 Missing ⚠️
pkg/cache/cache.go 40.00% 2 Missing and 1 partial ⚠️
pkg/cache/index.go 25.00% 2 Missing and 1 partial ⚠️
pkg/cache/cachetest/cachetest.go 0.00% 1 Missing ⚠️
...controller/sandboxclaim/sandboxclaim_controller.go 0.00% 1 Missing ⚠️
pkg/sandbox-manager/core.go 0.00% 1 Missing ⚠️
pkg/servers/e2b/routes.go 66.66% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #586      +/-   ##
==========================================
- Coverage   80.06%   79.92%   -0.15%     
==========================================
  Files         229      230       +1     
  Lines       17861    18063     +202     
==========================================
+ Hits        14300    14436     +136     
- Misses       2976     3027      +51     
- Partials      585      600      +15     
Flag Coverage Δ
unittests 79.92% <70.04%> (-0.15%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@furykerry
furykerry force-pushed the feat/wake-on-traffic branch 9 times, most recently from 46fd168 to 02d8fdc Compare June 28, 2026 14:26
@furykerry
furykerry force-pushed the feat/wake-on-traffic branch from eacd10c to 6d63285 Compare July 3, 2026 01:34
@furykerry
furykerry force-pushed the feat/wake-on-traffic branch from 6d63285 to 9e742f7 Compare July 8, 2026 09:00
@furykerry
furykerry force-pushed the feat/wake-on-traffic branch 2 times, most recently from b09a3d6 to b71f030 Compare July 11, 2026 01:24
Comment thread pkg/sandbox-gateway/wake/wake.go Outdated
Comment thread pkg/sandbox-gateway/filter/filter.go

@AiRanthem AiRanthem left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Review notes on the wake-on-traffic changes (local go build ./..., go test for pkg/sandbox-gateway/... + pkg/utils/proxyutils/..., and -race on wake/filter all pass). Four inline notes below: one under-implemented gap, one simplification (drop singleflight), one log nit, and one RBAC least-privilege nit.

Comment thread docs/proposals/20260627-wake-on-traffic-via-spec-patch.md Outdated
Comment thread pkg/sandbox-gateway/wake/wake.go Outdated
Comment thread pkg/sandbox-gateway/filter/filter.go Outdated
Comment thread config/sandbox-gateway/rbac.yaml Outdated
furykerry added a commit to furykerry/agents that referenced this pull request Jul 14, 2026
- Remove singleflight from Waker; Resume already provides first-writer-wins
  via retryUpdate. Singleflight added a footgun: if the first caller's
  context was canceled (Envoy Destroy), the shared Resume's Wait was cut
  short for all coalesced callers.
- Simplify Wake() to call wakeInternal directly under the caller's context
  (with timeout derived from caller's ctx, not context.Background())
- Remove type result struct wrapper
- Update proposal doc: annotation lifecycle is manual (kubectl annotate),
  not via the removed autoResume API parameter
- Remove singleflight-specific tests (TestWakeSingleflightCoalesces,
  TestWakeSingleflightDifferentSandboxes, TestWakeSingleflightCallerCancel)

Addresses CR comments openkruise#1, openkruise#3, openkruise#4, openkruise#5 on PR openkruise#586.

Signed-off-by: 守辰 <shouchen.zz@alibaba-inc.com>
furykerry added a commit that referenced this pull request Jul 15, 2026
kill() sends a DELETE request that returns 204 immediately, but the
underlying pod deletion is asynchronous — the gateway's informer cache
may still serve the route for a few hundred milliseconds, causing
is_running() to return True right after kill().

This race was observed in CI run #29330433277 on PR #586:
  FAILED test/e2b/test_sandbox.py::test_is_running - assert not True

Replace the immediate assertion with a polling loop (30s timeout, 1s
interval) that waits for is_running() to return False.

Signed-off-by: 守辰 <shouchen.zz@alibaba-inc.com>
Implement wake-on-traffic support in the sandbox-gateway Envoy Go filter,
allowing incoming HTTP requests to automatically resume paused sandboxes.

Key changes:
- Add wake-on-traffic annotation constants and config in sandbox-gateway
- Implement Wake() flow in filter: detect paused sandbox, call sandbox.Resume(),
  update registry, then forward request via ORIGINAL_DST
- Add informer cache fallback (HasWakeAnnotation) for annotation sync timing
- Set wake annotations at sandbox creation via autoResume in create.go
- Support AutoResume object format matching E2B SDK lifecycle parameter
- Add comprehensive unit tests for wake filter, waker, and config
- Add E2E test (test_wake_on_traffic.py) with access token auth
- Exclude wake-on-traffic test from non-gateway CI runs

Signed-off-by: 守辰 <shouchen.zz@alibaba-inc.com>
… test

- Add singleflight.Group to coalesce concurrent Wake calls for the
  same sandbox, reducing duplicate API server Resume requests
- Use DoChan + select pattern so callers can bail out independently
  while shared work continues under a detached context
- Add 3 new concurrency tests: same-sandbox coalescing, different-
  sandbox isolation, and caller cancellation
- Expand E2E test to verify wake annotation lifecycle:
  - Check wake-on-traffic and wake-timeout-seconds annotations
    are set on the Sandbox CR after creation
  - Check annotations persist after wake (not stripped by Resume)
- Add _get_sandbox_annotations() and _request_gateway_until_forwarded()
  helpers for robust E2E verification

Signed-off-by: 守辰 <shouchen.zz@alibaba-inc.com>
furykerry added 12 commits July 15, 2026 10:04
- DecodeHeaders now launches wake in a goroutine with detached context
  and returns api.Running to suspend Envoy request processing
- wakeAndContinue runs asynchronously: on success sets upstream metadata
  and calls Continue; on failure sends LocalReply (503)
- Add thread-safe completion state machine (mutex-protected) to prevent
  double-reply when async goroutine and Destroy race
- Add Destroy() to cancel in-flight wake context on stream reset
- Add isEnvoyStreamGonePanic helper to detect Envoy stream-gone panics
- Update mock callbacks to track Continue calls and signal completion
  via channel for async test synchronization
- Update TestDecodeHeadersWakeOnTrafficCacheFallback to expect
  api.Running and wait for async completion

Signed-off-by: 守辰 <shouchen.zz@alibaba-inc.com>
- TestIsEnvoyStreamGonePanic: table-driven test for panic detection
- TestDestroyCancelsContext: verify Destroy cancels in-flight wake context
- TestWakeAndContinueSuccess: verify async wake success path with
  Continue callback and upstream metadata set

Coverage improved from 63.5% to 81.8% for filter package.

Signed-off-by: 守辰 <shouchen.zz@alibaba-inc.com>
Add a sandboxOnly bool parameter to NewCache/NewCacheWithHealth,
SetupCacheControllersWithManager, and AddIndexesToCache. When true,
only Sandbox-related controllers (SandboxWaitReconciler and
SandboxCustomReconciler) and Sandbox field indexes are registered,
skipping Checkpoint, PVC, and SandboxSet controllers/indexes.

This allows sandbox-gateway to use cache.NewCache(mgr, true) without
requiring clientgoscheme registration, since corev1 types (PVC, PV)
are no longer needed.

Signed-off-by: 守辰 <shouchen.zz@alibaba-inc.com>
…affic test

GET /kruise/api/sandboxes returned 405 because the sandbox-manager only
registers POST /sandboxes (create) and GET /v2/sandboxes (list). The
manager has a dedicated GET /health endpoint that returns 200 OK — use
that instead for the gateway port-forward liveness check.

The test_sandbox_with_pod_ip failure is a separate flake (sandbox pod
died during pause/resume cycle), not related to this fix.

Signed-off-by: 守辰 <shouchen.zz@alibaba-inc.com>
…t endpoints

Remove the AutoResumeConfig struct, autoResume fields from NewSandboxRequest,
NewSandboxRequestExtension, and SetTimeoutRequest. Remove updateWakeAnnotations
method and its calls from ResumeSandbox and ConnectSandbox handlers. Remove
related validation and annotation-setting code from create flow.

Wake-on-traffic annotations are now set directly on the Sandbox CR (e.g. via
kubectl) rather than through the autoResume API parameter. The E2E test is
updated to set annotations via kubectl after sandbox creation.

Signed-off-by: 守辰 <shouchen.zz@alibaba-inc.com>
…check

The health endpoint was only registered as GET /health, not as
GET /kruise/api/health. When the E2E test sends a health check through
the sandbox-gateway (which forwards /kruise/api/* to the manager), the
manager returned 404 because the path was not registered.

Register the health handler under both /health and /kruise/api/health
so it works both directly and through the gateway.

Signed-off-by: 守辰 <shouchen.zz@alibaba-inc.com>
- Remove singleflight from Waker; Resume already provides first-writer-wins
  via retryUpdate. Singleflight added a footgun: if the first caller's
  context was canceled (Envoy Destroy), the shared Resume's Wait was cut
  short for all coalesced callers.
- Simplify Wake() to call wakeInternal directly under the caller's context
  (with timeout derived from caller's ctx, not context.Background())
- Remove type result struct wrapper
- Update proposal doc: annotation lifecycle is manual (kubectl annotate),
  not via the removed autoResume API parameter
- Remove singleflight-specific tests (TestWakeSingleflightCoalesces,
  TestWakeSingleflightDifferentSandboxes, TestWakeSingleflightCallerCancel)

Addresses CR comments openkruise#1, openkruise#3, openkruise#4, openkruise#5 on PR openkruise#586.

Signed-off-by: 守辰 <shouchen.zz@alibaba-inc.com>
Encapsulate the wake eligibility logic into a single method instead
of using a temporary shouldWake flag variable. All preconditions
(Paused state, EnableWakeOnTraffic, waker != nil, valid sandbox ID
format) are checked upfront before the route.WakeOnTraffic /
HasWakeAnnotation decision. Also removes the redundant inner
route.State != Running re-check.

Signed-off-by: 守辰 <shouchen.zz@alibaba-inc.com>
…nd wakeAndContinue

Construct a child logger via logger.With(zap.String("sandboxID", ...))
at the top of each function and use it for all log calls, removing
repeated zap.String("sandboxID", sandboxID) fields at each call site.

Signed-off-by: 守辰 <shouchen.zz@alibaba-inc.com>
Table-driven test covers all pure branches (state, enable flag, nil
waker, invalid ID, route.WakeOnTraffic) plus annotation fallback
cases with a real informer cache.

Signed-off-by: 守辰 <shouchen.zz@alibaba-inc.com>
When the E2B create sandbox API is called with
"autoResume": {"enabled": true}, automatically annotate the sandbox
with agents.kruise.io/wake-on-traffic=true so the sandbox-gateway will
resume it on incoming traffic after it is paused.

This aligns with the upstream E2B API specification:
https://e2b.dev/docs/api-reference/sandboxes/create-sandbox

Changes:
- Add SandboxAutoResumeConfig struct and AutoResume field to
  NewSandboxRequest
- In basicSandboxCreateModifier, set AnnotationWakeOnTraffic=true
  when request.AutoResume.Enabled is true
- Update proposal doc annotation lifecycle section
- Add unit tests for parsing and annotation injection

Signed-off-by: 守辰 <shouchen.zz@alibaba-inc.com>
…-traffic

The E2B create sandbox API now accepts autoResume={enabled:true} which
sets the wake-on-traffic annotation automatically. Update the E2E test to
use lifecycle={auto_resume:True} instead of manual kubectl annotate for
the wake-on-traffic annotation. The wake-timeout-seconds annotation is
still set via kubectl since the API only handles wake-on-traffic.

Signed-off-by: 守辰 <shouchen.zz@alibaba-inc.com>
@furykerry
furykerry force-pushed the feat/wake-on-traffic branch from 4c2d347 to e305c9b Compare July 15, 2026 02:04

@AiRanthem AiRanthem left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Requesting changes. Verified against the current head by tracing the concrete code paths (not coverage inference). There are 3 blocking issues plus 3 non-blocking notes, all left as inline comments:

  • Blocking 1: access-token auth is bypassed on the paused wake path.
  • Blocking 2: Destroy() is not Envoy's OnDestroy, so the cancel-on-destroy never fires.
  • Blocking 3: peer refresh deletes Paused routes, breaking wake-on-traffic in multi-replica topologies.

Suggested review order: filter.go (auth order, paused wake branch, destroy lifecycle) -> server.go (handleRefresh, globalPeerManager) -> api.go (Pause/Resume/syncRoute) -> wake.go (deadline, timeout annotation) -> create.go + test_wake_on_traffic.py.

Comment thread pkg/sandbox-gateway/filter/filter.go Outdated
Comment thread pkg/sandbox-gateway/filter/filter.go Outdated
Comment thread pkg/sandbox-gateway/server/server.go Outdated
labelSelector := os.Getenv(EnvLabelSelector)

s.peerManager = peers.NewMemberlistPeers(s.client, peers.NodePrefixSandboxGateway+nodeName, namespace, labelSelector)
globalPeerManager = s.peerManager

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Blocking: Paused routes get deleted by peer refresh, breaking wake-on-traffic.

Wiring the wake path to peer sync via globalPeerManager surfaces a problem in handleRefresh (lines 182-192, not shown in this diff): it Updates only Running routes and Deletes every other state, including Paused.

The manager pushes Paused routes on pause (PauseSandbox -> syncRoute -> SyncRouteWithPeers, pkg/sandbox-manager/api.go:326,300-302), and manager + gateway share one memberlist mesh (same agents.kruise.io/peer=true selector + namespace; GetPeers() returns all alive members regardless of the sm- / sg- prefix). So a Paused refresh reaches the gateway and deletes the route. The local controller wrote all states (gateway_controller.go:63-65), but nothing re-adds it until the Sandbox changes again. Traffic then hits sandbox not found at the initial registry lookup and wake is never attempted.

The delete-non-Running behavior is pre-existing (this PR only adds globalPeerManager here), but wake-on-traffic now depends on Paused routes surviving peer sync, so it must be addressed. Suggested fix: in handleRefresh, Update (with resourceVersion guard) for Running / Paused / Creating / Available; Delete only for Dead or an explicit deletion marker; decide the empty-state case explicitly instead of a blanket non-Running delete.

Comment thread pkg/sandbox-gateway/wake/wake.go
Comment thread pkg/sandbox-gateway/server/server.go Outdated
print(f"wake response headers: {dict(resp.headers)}")

# Step 7: Assert wake succeeded (not 502/503)
assert resp.status_code != 502, (

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Non-blocking: success oracle is loose.

The loop polls until any non-502/503 response, and these final assertions only exclude 502/503, so 200/401/404/500 would pass the HTTP check. The test does additionally assert the sandbox reaches Running and the annotations persist (lines ~200, 209-213), which partially covers correctness, but the HTTP oracle should assert an explicit expected 2xx / known app response, and ideally verify that the first suspended request is actually forwarded rather than relying on later polling requests to mask a failure.

@AiRanthem
AiRanthem dismissed their stale review July 15, 2026 06:29

Superseded by inline comments below for readability.

1. Blocking: Move access-token auth before wake path
   - Token validation now runs after route lookup but before
     any state/wake handling, preventing unauthorized requests
     from waking paused sandboxes.

2. Blocking: Rename Destroy() to OnDestroy(DestroyReason)
   - Destroy() was part of the Config interface, not StreamFilter.
     Envoy calls OnDestroy(DestroyReason) via shim.go, so the custom
     cancel logic was never invoked. The embedded PassThroughStreamFilter
     provided a no-op that silently masked the gap.
   - Added compile-time interface assertion:
     var _ api.StreamFilter = (*sandboxFilter)(nil)

3. Blocking: Retain Paused routes in handleRefresh
   - handleRefresh previously deleted all non-Running routes, including
     Paused. This broke wake-on-traffic because traffic to a paused
     sandbox hit 'sandbox not found' at registry lookup.
   - Now retains all states except Dead and empty (unset), matching
     the local controller's behavior.

4. Non-blocking: Thread-safe globalPeerManager lifecycle
   - Added RWMutex protection around the package-level globalPeerManager.
   - Clear the global on Stop() to prevent stale references.

5. Updated TestHandleRefresh_AvailableState to reflect new retention
   behavior (Available routes are now updated, not deleted).

Signed-off-by: 守辰 <shouchen.zz@alibaba-inc.com>
Signed-off-by: 守辰 <shouchen.zz@alibaba-inc.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants