{"record":{"id":"9f945f87f2ab1548","repo":"Tencent/WeKnora","slug":"scan-sandbox-bindings-w","errorCode":null,"errorMessage":"scan sandbox bindings: %w","messagePattern":"scan sandbox bindings: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/sandbox/session_binding_redis.go","lineNumber":241,"sourceCode":"// A single-node Redis (what the container wires) answers this completely. On a\n// Redis Cluster, SCAN reaches one node, so bindings living on the others would\n// go unmarked and their sessions would keep the previous image until they end.\nfunc (s *RedisSessionSandboxBindingStore) listTenantBindingKeys(\n\tctx context.Context,\n\ttenantID uint64,\n) ([]SessionSandboxKey, error) {\n\tprefix := fmt.Sprintf(\n\t\t\"weknora:sandbox:session:{%s:%d:\", s.namespace, tenantID,\n\t)\n\tconst suffix = \"}:binding\"\n\tpattern := escapeRedisGlob(prefix) + \"*\" + suffix\n\n\tvar keys []SessionSandboxKey\n\tvar cursor uint64\n\tfor {\n\t\tbatch, next, err := s.client.Scan(ctx, cursor, pattern, redisBindingScanCount).Result()\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"scan sandbox bindings: %w\", err)\n\t\t}\n\t\tfor _, raw := range batch {\n\t\t\tsessionID := strings.TrimSuffix(strings.TrimPrefix(raw, prefix), suffix)\n\t\t\tkey := SessionSandboxKey{TenantID: tenantID, SessionID: sessionID}\n\t\t\tif key.Validate() != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tkeys = append(keys, key)\n\t\t}\n\t\tif next == 0 {\n\t\t\treturn keys, nil\n\t\t}\n\t\tcursor = next\n\t}\n}\n\n// markBindingStale writes the marked binding back only while the stored one\n// still names the same sandbox.","sourceCodeStart":223,"sourceCodeEnd":259,"githubUrl":"https://github.com/Tencent/WeKnora/blob/988cbb03305e055d8ebb7d46d9ac6cc0803cd074/internal/sandbox/session_binding_redis.go#L223-L259","documentation":"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'.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Retry InvalidateByConfig — SCAN with cursor is safe to restart from scratch","Raise the context timeout so a long SCAN across many keys completes","Check node health/logs if on Redis Cluster; remember SCAN covers only the node reached — route to master or scan per node","Verify network stability between app and Redis (look for pooled-conn reset logs)"],"exampleFix":"// before\nn, err := store.InvalidateByConfig(ctx, tenantID, configID)\nif err != nil { return err }\n// after: retry with fresh context\nvar n int\nfor attempt := 0; attempt < 3; attempt++ {\n    scanCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)\n    n, err = store.InvalidateByConfig(scanCtx, tenantID, configID)\n    cancel()\n    if err == nil { break }\n}\nif err != nil { return err }","handlingStrategy":"retry","validationCode":"// pre-check Redis health and give the scan a generous deadline\nscanCtx, cancel := context.WithTimeout(context.Background(), 60*time.Second)\ndefer cancel()\nif err := rdb.Ping(scanCtx).Err(); err != nil {\n    return fmt.Errorf(\"skip invalidation, redis down: %w\", err)\n}","typeGuard":"func isScanError(err error) bool {\n    return err != nil && strings.Contains(err.Error(), \"scan sandbox bindings\")\n}","tryCatchPattern":"n, err := store.InvalidateByConfig(ctx, tenantID, configID)\nif isScanError(err) {\n    // restart from scratch: SCAN is stateless, safe to retry\n    n, err = store.InvalidateByConfig(ctx, tenantID, configID)\n}\nif err != nil { return err }","preventionTips":["Size the context timeout to the tenant's key count, not the default request timeout","Avoid Redis Cluster for this store, or scan each master node individually (documented single-node limitation)","Run InvalidateByConfig from an async job with retries, not inline in a request handler","Keep namespaces free of glob characters — the store escapes them, but simpler namespaces are safer"],"tags":["redis","scan","network","go"],"backgroundTag":"redis-scan-failed","analyzedSha":"988cbb03305e055d8ebb7d46d9ac6cc0803cd074","analyzedAt":"2026-09-02T14:41:08.344Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}