Tencent/WeKnora · error

read sandbox turn lease: %w

Error message

read sandbox turn lease: %w

What it means

TurnState reads the per-session turn-lease hash from Redis to report whether a chat turn is open and whether its one stale-sandbox rebuild is still unspent. This error wraps any failure of the HGetAll call against the Redis key `weknora:sandbox:session:{<hash>}:turn`, so callers know the turn state could not be determined. It is thrown because acting on stale-binding decisions requires authoritative lease state; guessing would risk tearing down a sandbox mid-turn.

Source

Thrown at internal/sandbox/session_binding_redis.go:351

	}
	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 {
		return false, false, nil
	}
	_ = s.client.PExpire(ctx, s.turnKey(key), sessionTurnLeaseTTL).Err()
	refs, _ := strconv.Atoi(values["refs"])
	if refs <= 0 {
		return false, false, nil
	}
	return true, values["rebuild"] == "1", nil
}

// ConsumeTurnRebuild spends the one rebuild allowed for the current turn.
func (s *RedisSessionSandboxBindingStore) ConsumeTurnRebuild(
	ctx context.Context,
	key SessionSandboxKey,
) error {
	if err := key.Validate(); err != nil {

View on GitHub (pinned to 988cbb0330)

Solutions

  1. Check Redis connectivity and health (PING) from the WeKnora node and fix connection settings (address, password, TLS) in config.
  2. Inspect the wrapped error (errors.Unwrap / %w chain) to distinguish timeouts, auth errors, and read-only replica errors and fix accordingly.
  3. Retry TurnState with backoff; it is a read and safe to re-issue.
  4. If on a replica, ensure reads go to the primary or enable read-replica support in the Redis client.

Example fix

// before
open, canRebuild, err := store.TurnState(ctx, key)
if err != nil {
    return fmt.Errorf("turn state unavailable: %v", err)
}
// after
open, canRebuild, err := store.TurnState(ctx, key)
if err != nil {
    if errors.Is(err, context.DeadlineExceeded) {
        ctx, cancel = context.WithTimeout(context.Background(), 2*time.Second)
        defer cancel()
        open, canRebuild, err = store.TurnState(ctx, key)
    }
    if err != nil {
        return fmt.Errorf("turn state unavailable: %w", err)
    }
}
Defensive patterns

Strategy: retry

Validate before calling

if err := rdb.Ping(ctx).Err(); err != nil {
    return fmt.Errorf("redis unavailable before TurnState: %w", err)
}

Type guard

func isRedisErr(err error) bool {
    var rerr *redis.RedisError
    return errors.As(err, &rerr)
}

Try / catch

open, rebuild, err := store.TurnState(ctx, key)
if err != nil {
    if isRetryableRedisErr(err) {
        open, rebuild, err = store.TurnState(ctxWithRetryTimeout, key)
    }
    if err != nil { return fmt.Errorf("turn state: %w", err) }
}

Prevention

When it happens

Trigger: RedisSessionSandboxBindingStore.TurnState is called while Redis is unreachable, the connection is closed/timed out, the key was migrated or is on a read-only replica, auth fails, or the context is cancelled mid-HGetAll.

Common situations: Redis outage or failover during a chat turn; wrong REDIS address/password in config; Redis under memory pressure evicting connections; network partition between the WeKnora node and Redis cluster; context deadline exceeded under load.

Related errors


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