Tencent/WeKnora · error

begin sandbox turn lease: %w

Error message

begin sandbox turn lease: %w

What it means

Returned by RedisSessionSandboxBindingStore.BeginTurn when the beginTurnScript (HINCRBY refs + PEXPIRE) fails to run — Redis unreachable, context cancelled, or server rejection. The chat-turn lease was not opened, so the caller must assume no rebuild allowance exists for the turn. Wrapped as 'begin sandbox turn lease: %w'.

Source

Thrown at internal/sandbox/session_binding_redis.go:320

	}
	return out.String()
}

// BeginTurn opens a chat-turn lease. The first increment of a session's
// refcount allows the next resolve to rebuild a stale sandbox.
func (s *RedisSessionSandboxBindingStore) BeginTurn(
	ctx context.Context,
	key SessionSandboxKey,
) error {
	if err := key.Validate(); err != nil {
		return err
	}
	ttlMS := sessionTurnLeaseTTL.Milliseconds()
	if ttlMS <= 0 {
		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
}

View on GitHub (pinned to 988cbb0330)

Solutions

  1. Check redis-cli ping and REDIS_* env config
  2. Ensure ctx has remaining budget when BeginTurn is called — budget a dedicated timeout for the lease step
  3. Retry BeginTurn once; a duplicate increment is recoverable by a matching EndTurn
  4. Inspect server logs for OOM/READONLY/NOAUTH and address the server-side condition

Example fix

// before
if err := store.BeginTurn(ctx, key); err != nil { return err }
// after: dedicated short-timeout context + one retry
turnCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
err := store.BeginTurn(turnCtx, key)
cancel()
if err != nil {
    time.Sleep(100 * time.Millisecond)
    turnCtx, cancel2 := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel2()
    if rerr := store.BeginTurn(turnCtx, key); rerr != nil {
        return fmt.Errorf("open turn: %w", rerr)
    }
}
Defensive patterns

Strategy: retry

Validate before calling

// ensure the key is valid and Redis reachable before opening the turn
if err := key.Validate(); err != nil { return err }
if err := rdb.Ping(ctx).Err(); err != nil {
    return fmt.Errorf("cannot open turn, redis down: %w", err)
}

Type guard

func isBeginTurnError(err error) bool {
    return err != nil && strings.Contains(err.Error(), "begin sandbox turn lease")
}

Try / catch

if err := store.BeginTurn(ctx, key); err != nil {
    if isBeginTurnError(err) {
        // one retry with a fresh short timeout; a duplicate increment is closed by EndTurn
        rctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
        defer cancel()
        if rerr := store.BeginTurn(rctx, key); rerr != nil {
            return fmt.Errorf("turn lease unavailable: %w", rerr)
        }
    } else {
        return err
    }
}
defer func() {
    ectx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()
    _ = store.EndTurn(ectx, key)
}()

Prevention

When it happens

Trigger: Calling BeginTurn(ctx, key) at chat-turn start when Redis is down or the ctx is already expired/cancelled; connection pool exhaustion under concurrency; server OOM or read-only replica rejecting the write.

Common situations: Redis restarts mid-conversation; per-request timeouts too short so ctx is nearly spent by the time the turn begins; a network partition during peak load; wrong Redis credentials after a rotation.

Related errors


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