{"record":{"id":"01ac1d92329929b9","repo":"Tencent/WeKnora","slug":"mark-sandbox-binding-stale-w","errorCode":null,"errorMessage":"mark sandbox binding stale: %w","messagePattern":"mark sandbox binding stale: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/sandbox/session_binding_redis.go","lineNumber":284,"sourceCode":"\tif err := validateBindingMatch(key, expected.Provider, expected.SandboxID); err != nil {\n\t\treturn false, err\n\t}\n\tmarked := expected\n\tmarked.StaleAt = &staleAt\n\tpayload, err := json.Marshal(marked)\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"encode stale sandbox binding: %w\", err)\n\t}\n\twrote, err := markBindingStaleIfMatchScript.Run(\n\t\tctx,\n\t\ts.client,\n\t\t[]string{s.bindingKey(key)},\n\t\tstring(expected.Provider),\n\t\texpected.SandboxID,\n\t\tpayload,\n\t).Int64()\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"mark sandbox binding stale: %w\", err)\n\t}\n\treturn wrote != 0, nil\n}\n\n// escapeRedisGlob quotes the characters SCAN's MATCH treats as wildcards. The\n// namespace is operator-supplied and only screened for braces and control\n// characters, so a namespace containing \"*\" would otherwise widen the pattern\n// past the workspace it is meant to anchor.\nfunc escapeRedisGlob(literal string) string {\n\tvar out strings.Builder\n\tout.Grow(len(literal))\n\tfor _, r := range literal {\n\t\tswitch r {\n\t\tcase '\\\\', '*', '?', '[', ']', '^':\n\t\t\tout.WriteByte('\\\\')\n\t\t}\n\t\tout.WriteRune(r)\n\t}","sourceCodeStart":266,"sourceCodeEnd":302,"githubUrl":"https://github.com/Tencent/WeKnora/blob/988cbb03305e055d8ebb7d46d9ac6cc0803cd074/internal/sandbox/session_binding_redis.go#L266-L302","documentation":"Returned by markBindingStale when the markBindingStaleIfMatch Lua script Run fails — Redis unreachable, context cancelled, server-side rejection (READONLY, OOM, NOAUTH), or script execution error. The stale marker was not written. Wrapped as 'mark sandbox binding stale: %w'. Returning (false, nil) only means the stored binding no longer matches the expected provider/sandbox ID (already rebound or deleted) — that is not an error.","triggerScenarios":"Calling InvalidateByConfig which fans out markBindingStale per key when Redis is degraded, ctx times out across the fan-out, or the replica rejects writes during failover.","commonSituations":"Failover in progress while config invalidation runs; context budget consumed by earlier keys in the invalidation loop leaving no time for later ones; Redis maxmemory blocking writes; ACL revoking write access to the app user.","solutions":["Retry the invalidation — the script is conditional and idempotent per key","Give the invalidation job a generous context timeout scaled to tenant key count","Check redis-server logs for READONLY/OOM/NOAUTH and fix server-side state","Verify the app's Redis ACL user has WRITE on the weknora:* key pattern"],"exampleFix":"// before\nok, err := store.InvalidateByConfig(ctx, tenantID, configID)\nif err != nil { return err }\n// after: per-run retry since the script is match-conditional and safe to re-run\nfor attempt := 0; attempt < 3; attempt++ {\n    if _, err = store.InvalidateByConfig(ctx, tenantID, configID); err == nil {\n        break\n    }\n    if !errors.Is(err, context.DeadlineExceeded) && !isTransientRedis(err) {\n        break\n    }\n    time.Sleep(time.Duration(attempt+1) * 250 * time.Millisecond)\n}\nif err != nil { return err }","handlingStrategy":"retry","validationCode":"// confirm write access and headroom before a large invalidation fan-out\nif err := rdb.Ping(ctx).Err(); err != nil { return err }\nif info, err := rdb.Info(ctx, \"memory\").Result(); err == nil && strings.Contains(info, \"maxmemory_human\") {\n    // log memory section; alert if near maxmemory\n    log.Printf(\"redis memory section: %s\", info)\n}","typeGuard":"func isMarkStaleTransportError(err error) bool {\n    return err != nil && strings.Contains(err.Error(), \"mark sandbox binding stale\")\n}\n// (false, nil) means the binding changed under us — expected, not an error","tryCatchPattern":"wrote, err := store.InvalidateByConfig(ctx, tenantID, configID)\nif err != nil {\n    if isMarkStaleTransportError(err) && errors.Is(err, context.DeadlineExceeded) {\n        // re-run: script is match-conditional, so re-marking is safe\n        wrote, err = store.InvalidateByConfig(ctx, tenantID, configID)\n    }\n    if err != nil { return err }\n}\nlog.Printf(\"marked %d bindings stale\", wrote)","preventionTips":["Run config invalidation as a background job with its own long timeout and retries","Grant the app's Redis ACL user WRITE on weknora:* patterns","Alert on maxmemory and READONLY states before they block stale-marking","Schedule invalidations outside Redis failover windows when possible"],"tags":["redis","lua-script","write-failure","go"],"backgroundTag":"redis-write-failed","analyzedSha":"988cbb03305e055d8ebb7d46d9ac6cc0803cd074","analyzedAt":"2026-09-02T14:41:08.344Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}