From 6243e241c37ab80d19fa901e5666de4a7f71cb94 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Thu, 1 Oct 2026 01:59:26 +0000 Subject: [PATCH] Declare which Link failures are retryable --- docs/sandbox-link-protocol.md | 4 +++- internal/sandboxlink/protocol.go | 6 ++++++ internal/sandboxlink/serve.go | 6 +++--- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/docs/sandbox-link-protocol.md b/docs/sandbox-link-protocol.md index e533e840..b89a27b7 100644 --- a/docs/sandbox-link-protocol.md +++ b/docs/sandbox-link-protocol.md @@ -20,7 +20,7 @@ The Sandbox I/O service runs `sandboxlink.Serve` with a `ServeConfig`: - `URL`, `Credential` and `Resource` come from the [bootstrap input](sandbox-bootstrap.md). The credential identifies the service, so the Hello carries no peer ID. `ServerInstanceID` is a new ID whenever the service starts without its operation and handle registries. - `Services` holds one handler per offered service and version. A handler receives the `Bind`, which carries the authorized binding including a File stream's exports, and the stream. It owns the stream and returns when it is done with it. Its context ends when the attachment closes or `Serve` returns. -- `Serve` reconnects with jittered exponential backoff, sending the same `ServerInstanceID`, whenever the link drops. It returns when its context ends or when the relay refuses the Hello with any failure other than `ServiceUnavailable` or `LimitExceeded`, for example `AuthenticationFailed` after the credential is withdrawn or `StaleGeneration` after a newer sandbox took over the resource. Before returning it cancels every handler's context and waits for the handlers. +- `Serve` reconnects with jittered exponential backoff, sending the same `ServerInstanceID`, whenever the link drops. It returns when its context ends or when the relay refuses the Hello with a failure that is not [retryable](#failures), for example `AuthenticationFailed` after the credential is withdrawn or `StaleGeneration` after a newer sandbox took over the resource. Before returning it cancels every handler's context and waits for the handlers. - `OnAttachmentLost` fires when an attachment's last open stream ends while the attachment is still open, such as when the link drops. `OnAttachmentRestored` fires when a stream binds a lost attachment again. `OnAttachmentClosed` fires with the reason when the relay reports `AttachmentClosed`. Losing a socket is not closing an attachment: the service keeps an attachment's state until it is closed. - Closing is final. A `Bind` the relay sent before a close can arrive after the `AttachmentClosed`, so `Serve` refuses a `Bind` for an attachment closed within the last `sandboxlink.HandshakeTimeout` with `LeaseExpired`. A stream of a closed attachment that ends later never marks another attachment lost. @@ -277,6 +277,8 @@ The relay copies each direction through a 32 KiB buffer and holds at most one 25 | 11 | `LimitExceeded` | A link's stream limit or its limit of renewals being decided is reached | | 12 | `ProtocolViolation` | A message is malformed, not allowed where it arrived, or carries a request ID that does not increase | +`ServiceUnavailable` and `LimitExceeded` are transient: the same request may succeed later, and `Code.Retryable` reports them. Every other code is final: repeating the request with the same credential, attachment and generation fails again. + ## Verification `go test ./internal/sandboxlink/...` covers the golden frames, decode rejection, and the relay's authorization, generation, lease, revocation, renewal bound and reconnect behavior, including orderly end and abort propagation. `go test -run '^$' -fuzz FuzzDecode ./internal/sandboxlink` fuzzes the decoder. diff --git a/internal/sandboxlink/protocol.go b/internal/sandboxlink/protocol.go index abcae331..a9f4b57d 100644 --- a/internal/sandboxlink/protocol.go +++ b/internal/sandboxlink/protocol.go @@ -252,6 +252,12 @@ func (c Code) String() string { func (c Code) Error() string { return "sandbox link: " + c.String() } +// Retryable reports whether the same request may succeed later. +// ServiceUnavailable and LimitExceeded are transient; every other code is +// final, and repeating the request with the same credential, attachment and +// generation fails again. +func (c Code) Retryable() bool { return c == ServiceUnavailable || c == LimitExceeded } + // Error is a typed Link failure with its effect. Cause is the local error that // produced it, such as a transport failure or a canceled context; it is never // sent. diff --git a/internal/sandboxlink/serve.go b/internal/sandboxlink/serve.go index 65df23f2..98964e07 100644 --- a/internal/sandboxlink/serve.go +++ b/internal/sandboxlink/serve.go @@ -54,8 +54,8 @@ type ServeConfig struct { // Serve connects to the relay and serves bound streams until parent ends. It // reconnects with backoff, sending the same ServerInstanceID, until the relay -// refuses the Hello with a failure other than ServiceUnavailable or -// LimitExceeded; it then returns that *Error. Before returning it cancels +// refuses the Hello with a failure that is not [Code.Retryable]; it then +// returns that *Error. Before returning it cancels // every handler's context and waits for the handlers. func Serve(parent context.Context, cfg ServeConfig) error { hello := ServeHello{Version: Version, Credential: cfg.Credential, Resource: cfg.Resource, ServerInstanceID: cfg.ServerInstanceID} @@ -95,7 +95,7 @@ func Serve(parent context.Context, cfg ServeConfig) error { return stop(parent.Err()) } var refused *Error - if !connected && errors.As(err, &refused) && refused.Code != ServiceUnavailable && refused.Code != LimitExceeded { + if !connected && errors.As(err, &refused) && !refused.Code.Retryable() { return stop(refused) } if cfg.OnDisconnected != nil {