feat(gateway): implement wake-on-traffic for paused sandboxes#586
feat(gateway): implement wake-on-traffic for paused sandboxes#586furykerry wants to merge 16 commits into
Conversation
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: The full list of commands accepted by this bot can be found here. DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
56f1d12 to
3c12d0a
Compare
Codecov Report❌ Patch coverage is 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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
46fd168 to
02d8fdc
Compare
eacd10c to
6d63285
Compare
6d63285 to
9e742f7
Compare
b09a3d6 to
b71f030
Compare
AiRanthem
left a comment
There was a problem hiding this comment.
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.
- 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>
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>
- 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>
4c2d347 to
e305c9b
Compare
AiRanthem
left a comment
There was a problem hiding this comment.
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'sOnDestroy, 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.
| labelSelector := os.Getenv(EnvLabelSelector) | ||
|
|
||
| s.peerManager = peers.NewMemberlistPeers(s.client, peers.NodePrefixSandboxGateway+nodeName, namespace, labelSelector) | ||
| globalPeerManager = s.peerManager |
There was a problem hiding this comment.
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.
| print(f"wake response headers: {dict(resp.headers)}") | ||
|
|
||
| # Step 7: Assert wake succeeded (not 502/503) | ||
| assert resp.status_code != 502, ( |
There was a problem hiding this comment.
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.
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>
Ⅰ. 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:
Wake-on-traffic in filter (
pkg/sandbox-gateway/filter/filter.go): When a paused sandbox withWakeOnTraffic=truereceives traffic, the filter launches wake in a goroutine with a detached context and returnsapi.Runningto suspend Envoy request processing. ThewakeAndContinuegoroutine callsContinue(api.Continue)on success orSendLocalReply(503)on failure. Thread-safe completion state machine (mutex-protected) prevents double-reply when async goroutine andDestroyrace. Wake eligibility is encapsulated inshouldWakeSandbox()— a single method that checks all preconditions (Paused state,EnableWakeOnTraffic, waker initialized, validnamespace--nameformat) before deciding viaroute.WakeOnTrafficor theHasWakeAnnotationfallback. Scoped loggers (logger.With(zap.String("sandboxID", ...))) are used inDecodeHeadersandwakeAndContinueto avoid repeating the field at each call site.New
wakepackage (pkg/sandbox-gateway/wake/wake.go): ImplementsWakerthat callssandbox.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 viaretryUpdate, so no singleflight is needed. Useserrors.Is()with sentinel errors for error classification (not string matching).E2B pause with spec patch (
pkg/servers/e2b/pause_resume.go): AddsPause()that setsSpec.DesiredState=Paused(instead of deleting the sandbox), enabling the wake-on-traffic flow.Registry route lifecycle (
pkg/sandbox-gateway/server/server.go):handleRefreshretains routes for all states exceptdeadand empty string. This ensures the filter has full visibility (includingpaused,creating,available) for routing and wake decisions.Sentinel error classification (
pkg/utils/utils.go,pkg/sandbox-manager/errors/error.go): Exports sentinel errors (ErrSandboxIsPausing,ErrSandboxIsTerminating,ErrShutdownTimeReached,ErrSandboxPhaseNotAllowed) fromIsSandboxResumable. AddsUnwrap()+NewErrorWrap()tomanagererrors.Errorto enableerrors.Is()through wrapped errors.Gateway cache optimization (
pkg/cache/cache.go): Restricts the gateway's informer cache to only Sandbox resources viaByObjectfiltering, reducing memory usage and API server load.Configuration: Adds
ENABLE_WAKE_ON_TRAFFICenv var andwakeTimeoutSecondsconfig to the gateway filter.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 withagents.kruise.io/wake-on-traffic=trueat creation time. This aligns with the upstream E2B API specification. The E2B SDK mapslifecycle={"auto_resume": True}to this API field. Unit tests cover enabled, disabled, and absent scenarios.E2E test update (
test/e2b/test_wake_on_traffic.py): Updated to uselifecycle={"on_timeout": "pause", "auto_resume": True}instead of manualkubectl annotatefor the wake-on-traffic annotation. Thewake-timeout-secondsannotation is still set via kubectl since the API only handleswake-on-traffic.Ⅱ. Does this pull request fix one issue?
NONE
Ⅲ. Describe how to verify it
go test ./pkg/sandbox-gateway/... ./pkg/cache/... ./pkg/servers/e2b/... ./pkg/utils/...lifecycle={"on_timeout": "pause", "auto_resume": True}(SDK) or"autoResume": {"enabled": true}(raw API)agents.kruise.io/wake-on-traffic=truePOST /sandboxes/{id}/pauseⅣ. Special notes for reviews
DecodeHeadersreturnsapi.Runningwhen wake is triggered, launchingwakeAndContinuein a goroutine. The goroutine uses a detached context (not tied to the Envoy filter lifecycle). On completion, it callsContinue(api.Continue)orSendLocalReply. TheDestroy()method cancels the in-flight wake context when Envoy destroys the filter (stream reset). A mutex-protected completion state machine prevents double-reply races.Waker.Wake()callswakeInternaldirectly under the caller's context. Dedup is handled by Resume'sretryUpdate(first-writer-wins) and refcounted wait viaNewSandboxResumeTask. This avoids the footgun where a canceled caller context would cut short the shared Resume for all coalesced callers.deadand empty-state routes are deleted from the registry. All other states (running,paused,creating,available) are retained for full filter visibility."autoResume": {"enabled": true}(mapped from SDK'slifecycle={"auto_resume": True}), or (2) manually viakubectl annotatefor sandboxes created withoutautoResume. The E2E test uses the API path and verifies the annotation is present immediately after creation.SandboxOnlymode) adds nil guards onGetSandboxController()andGetSandboxSetController()since controller handlers are not set up in that mode.Ⅴ. Test coverage
pkg/sandbox-gateway/filterpkg/sandbox-gateway/wakepkg/servers/e2bpkg/proxy