Tencent/WeKnora · error

scan sandbox bindings: %w

Error message

scan sandbox bindings: %w

What it means

Returned by listTenantBindingKeys (used by InvalidateByConfig) when client.Scan fails while paging through the workspace's binding keys with MATCH pattern and COUNT 200. SCAN errors are rare but occur on connection loss, timeouts, or cluster misrouting. The invalidation pass aborts so no bindings get marked stale. Wrapped as 'scan sandbox bindings: %w'.

Source

Thrown at internal/sandbox/session_binding_redis.go:241

// A single-node Redis (what the container wires) answers this completely. On a
// Redis Cluster, SCAN reaches one node, so bindings living on the others would
// go unmarked and their sessions would keep the previous image until they end.
func (s *RedisSessionSandboxBindingStore) listTenantBindingKeys(
	ctx context.Context,
	tenantID uint64,
) ([]SessionSandboxKey, error) {
	prefix := fmt.Sprintf(
		"weknora:sandbox:session:{%s:%d:", s.namespace, tenantID,
	)
	const suffix = "}:binding"
	pattern := escapeRedisGlob(prefix) + "*" + suffix

	var keys []SessionSandboxKey
	var cursor uint64
	for {
		batch, next, err := s.client.Scan(ctx, cursor, pattern, redisBindingScanCount).Result()
		if err != nil {
			return nil, fmt.Errorf("scan sandbox bindings: %w", err)
		}
		for _, raw := range batch {
			sessionID := strings.TrimSuffix(strings.TrimPrefix(raw, prefix), suffix)
			key := SessionSandboxKey{TenantID: tenantID, SessionID: sessionID}
			if key.Validate() != nil {
				continue
			}
			keys = append(keys, key)
		}
		if next == 0 {
			return keys, nil
		}
		cursor = next
	}
}

// markBindingStale writes the marked binding back only while the stored one
// still names the same sandbox.

View on GitHub (pinned to 988cbb0330)

Solutions

  1. Retry InvalidateByConfig — SCAN with cursor is safe to restart from scratch
  2. Raise the context timeout so a long SCAN across many keys completes
  3. Check node health/logs if on Redis Cluster; remember SCAN covers only the node reached — route to master or scan per node
  4. Verify network stability between app and Redis (look for pooled-conn reset logs)

Example fix

// before
n, err := store.InvalidateByConfig(ctx, tenantID, configID)
if err != nil { return err }
// after: retry with fresh context
var n int
for attempt := 0; attempt < 3; attempt++ {
    scanCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    n, err = store.InvalidateByConfig(scanCtx, tenantID, configID)
    cancel()
    if err == nil { break }
}
if err != nil { return err }
Defensive patterns

Strategy: retry

Validate before calling

// pre-check Redis health and give the scan a generous deadline
scanCtx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
if err := rdb.Ping(scanCtx).Err(); err != nil {
    return fmt.Errorf("skip invalidation, redis down: %w", err)
}

Type guard

func isScanError(err error) bool {
    return err != nil && strings.Contains(err.Error(), "scan sandbox bindings")
}

Try / catch

n, err := store.InvalidateByConfig(ctx, tenantID, configID)
if isScanError(err) {
    // restart from scratch: SCAN is stateless, safe to retry
    n, err = store.InvalidateByConfig(ctx, tenantID, configID)
}
if err != nil { return err }

Prevention

When it happens

Trigger: Calling InvalidateByConfig(ctx, tenantID, configID) when: the Redis connection drops mid-scan; ctx deadline exceeded over many batches; Redis Cluster where the node serving the SCAN errors; OOM/auth errors on the server.

Common situations: Very large keyspaces extending the scan past a request timeout; cluster mode where SCAN hits a single node (documented limitation — plus errors when that node fails); transient network blip during config invalidation after rotating an API key.

Related errors


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