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

  1. Refetch the saved query policy to get the fresh etag, recompute the desired bindings on it, and retry SetSavedQueryBindings
  2. Handle the mapped connect.CodeAborted in saved_query_service.go:802 by prompting the client to refresh and resubmit
  3. Implement a bounded retry loop (refetch -> reapply -> write) to converge under contention
  4. 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

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


AI-assisted analysis of bytebase/bytebase@1870550677 (2026-09-06). Data as JSON: /api/errors/a0282d3bc549330b. Report an issue: GitHub.