bytebase/bytebase · warning · ErrSavedQueryEtagMismatch
saved query policy etag mismatch
Error message
saved query policy etag mismatch
What it means
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.
Source
Thrown at backend/store/saved_query.go:378
}
if expectedEtag != currentEtag {
return false, ErrSavedQueryEtagMismatch
}
if _, err := tx.ExecContext(ctx,
`UPDATE saved_query SET bindings = $1 WHERE resource_id = $2 AND project = $3`,
marshalled, resourceID, projectID); err != nil {
return false, errors.Wrapf(err, "failed to update bindings for saved query %s", resourceID)
}
if err := tx.Commit(); err != nil {
return false, errors.Wrap(err, "failed to commit transaction")
}
return true, nil
}
// ErrSavedQueryEtagMismatch reports that the policy moved under a
// compare-and-swap write; the caller refetches and reapplies.
var ErrSavedQueryEtagMismatch = errors.New("saved query policy etag mismatch")
// CreateSavedQuery creates a new saved query under an active project.
func (s *Store) CreateSavedQuery(ctx context.Context, create *SavedQueryMessage) (*SavedQueryMessage, error) {
payloadStr, err := protojson.Marshal(&storepb.SavedQueryPayload{Database: create.Database})
if err != nil {
return nil, err
}
tx, err := s.GetDB().BeginTx(ctx, nil)
if err != nil {
return nil, errors.Wrap(err, "failed to begin transaction")
}
defer tx.Rollback()
// A saved query is a new child row, so its project must be active.
if err := requireActiveProject(ctx, tx, create.ProjectID); err != nil {
return nil, err
}View on GitHub (pinned to 1870550677)
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
Example fix
// before
applied, err := s.store.SetSavedQueryBindings(ctx, projectID, id, bindings, staleEtag)
// after
policy, err := s.store.GetSavedQueryPolicy(ctx, projectID)
if err != nil { return err }
applied, err := s.store.SetSavedQueryBindings(ctx, projectID, id, recomputeBindings(policy), policy.Etag) Defensive patterns
Strategy: retry
Validate before calling
policy, err := store.GetSavedQueryPolicy(ctx, projectID)
if err != nil { return err }
// use policy.Etag for SetSavedQueryBindings / SetSavedQueryPolicy Try / catch
for i := 0; i < 3; i++ {
policy, _ := store.GetSavedQueryPolicy(ctx, projectID)
_, err := store.SetSavedQueryBindings(ctx, projectID, id, bindings, policy.Etag)
if !errors.Is(err, store.ErrSavedQueryEtagMismatch) { break }
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- iam policy etag mismatch
- ABORTED
- CodeAborted
- source folder cannot be empty
- failed to create missing plan check run for approval input v
AI-assisted analysis of bytebase/bytebase@1870550677 (2026-09-06).
Data as JSON: /api/errors/a0282d3bc549330b.
Report an issue: GitHub.