Tencent/WeKnora · warning
end sandbox turn lease: %w
Error message
end sandbox turn lease: %w
What it means
Returned by RedisSessionSandboxBindingStore.EndTurn when the endTurnScript (HINCRBY refs then DEL at zero) fails to run — same transport/server failure class as BeginTurn. The turn lease is not released; it will linger until the 30-minute TTL expires, temporarily blocking stale-sandbox rebuilds. Wrapped as 'end sandbox turn lease: %w'.
Source
Thrown at internal/sandbox/session_binding_redis.go:335
ttlMS = (30 * time.Minute).Milliseconds()
}
if err := beginTurnScript.Run(ctx, s.client, []string{s.turnKey(key)}, ttlMS).Err(); err != nil {
return fmt.Errorf("begin sandbox turn lease: %w", err)
}
return nil
}
// EndTurn releases one chat-turn lease. The last release drops the lease so
// a later resolve may rebuild a stale sandbox immediately.
func (s *RedisSessionSandboxBindingStore) EndTurn(
ctx context.Context,
key SessionSandboxKey,
) error {
if err := key.Validate(); err != nil {
return err
}
if err := endTurnScript.Run(ctx, s.client, []string{s.turnKey(key)}).Err(); err != nil {
return fmt.Errorf("end sandbox turn lease: %w", err)
}
return nil
}
// TurnState reports whether a chat turn is open and whether its first
// resolve may still rebuild a stale sandbox.
func (s *RedisSessionSandboxBindingStore) TurnState(
ctx context.Context,
key SessionSandboxKey,
) (bool, bool, error) {
if err := key.Validate(); err != nil {
return false, false, err
}
values, err := s.client.HGetAll(ctx, s.turnKey(key)).Result()
if err != nil {
return false, false, fmt.Errorf("read sandbox turn lease: %w", err)
}
if len(values) == 0 {View on GitHub (pinned to 988cbb0330)
Solutions
- Release the lease with a fresh context (background/deadline-extended) rather than the request ctx — cleanup must survive the request's lifecycle
- Retry EndTurn; it is idempotent (script no-ops when the key is gone)
- Accept the bounded impact if one release is lost: the 30-minute TTL self-heals leaked leases
- If leaks are frequent, add a watchdog that calls EndTurn on turn completion via defer and a detached context
Example fix
// before
if err := store.EndTurn(ctx, key); err != nil { return err } // request ctx may be dead by now
// after: cleanup with a detached context so the lease is always released
func handleTurn(key sandbox.SessionSandboxKey, done func() error) error {
if err := done(); err != nil { return err }
endCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := store.EndTurn(endCtx, key); err != nil {
return fmt.Errorf("release turn (lease expires in ≤30m): %w", err)
}
return nil
} Defensive patterns
Strategy: try-catch
Validate before calling
// no meaningful pre-check: the failure mode is transport-level mid-cleanup.
// Instead, guarantee the call happens even on panic:
defer func() {
ectx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = store.EndTurn(ectx, key) // best-effort; TTL bounds any leak at 30m
}() Type guard
func isEndTurnError(err error) bool {
return err != nil && strings.Contains(err.Error(), "end sandbox turn lease")
} Try / catch
endCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := store.EndTurn(endCtx, key); err != nil {
if isEndTurnError(err) {
// non-fatal: the 30-minute lease TTL self-heals; log for ops
log.Printf("turn lease for %s will expire via TTL: %v", key.SessionID, err)
} else {
return err
}
} Prevention
- Never use the request context for EndTurn — turn cleanup outlives the request
- Always EndTurn in a defer so early returns and panics still release the lease
- Remember the refcount model: one EndTurn per successful BeginTurn (extras are no-ops)
- Alert on turn-lease keys persisting near the 30m TTL — a sign of frequent release failures or crashes
When it happens
Trigger: Calling EndTurn(ctx, key) at chat-turn end when Redis is unreachable, ctx cancelled after a long turn, connection dropped, or the server rejects the EVAL (OOM/READONLY/NOAUTH).
Common situations: Turn processing exceeded its request timeout so the context died before cleanup; Redis blip at the exact end of a long turn; process killed between BeginTurn and EndTurn leaving a leaked lease (bounded by sessionTurnLeaseTTL = 30m).
Related errors
- delete sandbox binding: %w
- mark sandbox binding stale: %w
- begin sandbox turn lease: %w
- WEKNORA_REDIS_NAMESPACE must not contain braces
- WEKNORA_REDIS_NAMESPACE must not contain control characters
AI-assisted analysis of Tencent/WeKnora@988cbb0330 (2026-09-02).
Data as JSON: /api/errors/34c3ad2ec11e50a3.
Report an issue: GitHub.