Tencent/WeKnora · error

read sandbox turn lease

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.

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.

Appendix: 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)