{"record":{"id":"a0282d3bc549330b","repo":"bytebase/bytebase","slug":"saved-query-policy-etag-mismatch","errorCode":null,"errorMessage":"saved query policy etag mismatch","messagePattern":"saved query policy etag mismatch","errorType":"exception","errorClass":"ErrSavedQueryEtagMismatch","httpStatus":null,"severity":"warning","filePath":"backend/store/saved_query.go","lineNumber":378,"sourceCode":"\t}\n\tif expectedEtag != currentEtag {\n\t\treturn false, ErrSavedQueryEtagMismatch\n\t}\n\n\tif _, err := tx.ExecContext(ctx,\n\t\t`UPDATE saved_query SET bindings = $1 WHERE resource_id = $2 AND project = $3`,\n\t\tmarshalled, resourceID, projectID); err != nil {\n\t\treturn false, errors.Wrapf(err, \"failed to update bindings for saved query %s\", resourceID)\n\t}\n\tif err := tx.Commit(); err != nil {\n\t\treturn false, errors.Wrap(err, \"failed to commit transaction\")\n\t}\n\treturn true, nil\n}\n\n// ErrSavedQueryEtagMismatch reports that the policy moved under a\n// compare-and-swap write; the caller refetches and reapplies.\nvar ErrSavedQueryEtagMismatch = errors.New(\"saved query policy etag mismatch\")\n\n// CreateSavedQuery creates a new saved query under an active project.\nfunc (s *Store) CreateSavedQuery(ctx context.Context, create *SavedQueryMessage) (*SavedQueryMessage, error) {\n\tpayloadStr, err := protojson.Marshal(&storepb.SavedQueryPayload{Database: create.Database})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ttx, err := s.GetDB().BeginTx(ctx, nil)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to begin transaction\")\n\t}\n\tdefer tx.Rollback()\n\n\t// A saved query is a new child row, so its project must be active.\n\tif err := requireActiveProject(ctx, tx, create.ProjectID); err != nil {\n\t\treturn nil, err\n\t}","sourceCodeStart":360,"sourceCodeEnd":396,"githubUrl":"https://github.com/bytebase/bytebase/blob/1870550677fe08f0d2a78c07acd27541464eb945/backend/store/saved_query.go#L360-L396","documentation":"ErrSavedQueryEtagMismatch (backend/store/saved_query.go:378) reports that a saved-query policy or bindings write failed its compare-and-swap: expectedEtag != currentEtag at saved_query.go:362. Like the IAM policy etag guard, it prevents a stale writer from clobbering a concurrent revocation; callers should refetch the policy and reapply.","triggerScenarios":"Calling SetSavedQueryPolicy or SetSavedQueryBindings with an etag that no longer matches the stored policy etag because another writer (e.g. a concurrent UpdateSavedQueryPolicy or binding revocation) changed it after your read.","commonSituations":"Two admins revoke the same saved query's sharing simultaneously; a client applies bindings computed from a policy fetched minutes ago; tests that simulate concurrent writes intentionally trigger the mismatch.","solutions":["Refetch the saved query policy to get the fresh etag, recompute the desired bindings on it, and retry SetSavedQueryBindings","Handle the mapped connect.CodeAborted in saved_query_service.go:802 by prompting the client to refresh and resubmit","Implement a bounded retry loop (refetch -> reapply -> write) to converge under contention","Avoid long-lived cached etags in clients; fetch the policy immediately before each write"],"exampleFix":"// before\napplied, err := s.store.SetSavedQueryBindings(ctx, projectID, id, bindings, staleEtag)\n// after\npolicy, err := s.store.GetSavedQueryPolicy(ctx, projectID)\nif err != nil { return err }\napplied, err := s.store.SetSavedQueryBindings(ctx, projectID, id, recomputeBindings(policy), policy.Etag)","handlingStrategy":"retry","validationCode":"policy, err := store.GetSavedQueryPolicy(ctx, projectID)\nif err != nil { return err }\n// use policy.Etag for SetSavedQueryBindings / SetSavedQueryPolicy","typeGuard":null,"tryCatchPattern":"for i := 0; i < 3; i++ {\n\tpolicy, _ := store.GetSavedQueryPolicy(ctx, projectID)\n\t_, err := store.SetSavedQueryBindings(ctx, projectID, id, bindings, policy.Etag)\n\tif !errors.Is(err, store.ErrSavedQueryEtagMismatch) { break }\n}","preventionTips":["Fetch the policy immediately before binding writes; never reuse stale etags","Handle CodeAborted in saved_query_service.go by refreshing the client","Use errors.Is against store.ErrSavedQueryEtagMismatch rather than string matching"],"tags":["go","etag","optimistic-locking","concurrency","saved-queries"],"backgroundTag":"optimistic-lock-conflict","analyzedSha":"1870550677fe08f0d2a78c07acd27541464eb945","analyzedAt":"2026-09-06T21:16:13.665Z","contentChangedAt":"2026-09-06T21:16:13.665Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}