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

  1. Release the lease with a fresh context (background/deadline-extended) rather than the request ctx — cleanup must survive the request's lifecycle
  2. Retry EndTurn; it is idempotent (script no-ops when the key is gone)
  3. Accept the bounded impact if one release is lost: the 30-minute TTL self-heals leaked leases
  4. 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

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


AI-assisted analysis of Tencent/WeKnora@988cbb0330 (2026-09-02). Data as JSON: /api/errors/34c3ad2ec11e50a3. Report an issue: GitHub.