Skip to content

Retry GCP metadata writes on an operation-level fingerprint conflict - #349

Merged
nuwang merged 3 commits into
mainfrom
gcp-metadata-conflict-retry
Sep 18, 2026
Merged

nuwang merged 3 commits into
mainfrom
gcp-metadata-conflict-retry

Conversation

@nuwang

@nuwang nuwang commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Root cause of the recurring GCP live-suite failures (test_crud_network, test_crud_router, test_crud_vm_firewall*, test_crud_key_pair_service), most recently both attempts of the #348 run and earlier on main.

What was happening

GCP keeps labels for networks, routers, firewalls and key pairs in the project-wide commonInstanceMetadata, and every label write re-uploads that whole document under an optimistic fingerprint. The live suite runs -n 5, so five workers race on one document.

gcp_metadata_save_op has a tenacity retry for exactly this — but its predicate only recognises the conflict as an HttpError carrying the message. A concurrent writer produces it differently: setCommonInstanceMetadata returns 200 with an operation, the operation then completes with CONDITION_NOT_MET, and wait_for_operation raised that as a plain Exception(result['error']). The predicate never fired, so every collision was fatal on the first attempt. Whether a run passed depended on whether xdist's schedule happened to collide — and since that schedule is deterministic for a given test set, the same five tests failed identically on both attempts.

Fix

  • wait_for_operation raises GCPOperationError, a ProviderInternalException carrying the operation's error payload with a codes property.
  • The save predicate retries on CONDITION_NOT_MET from it, alongside the existing HTTP form. The save already re-fetches the metadata (and a fresh fingerprint) on each attempt, so the retry is correct once it fires.

The exception message is unchanged. Callers catching CloudBridgeBaseException now see the typed error instead of the middleware's generic wrapper; nothing in the codebase inspected the old one.

Tests

tests/test_gcp_metadata_save.py drives the real gcp_metadata_save_op and the real wait_for_operation against a fake compute client, offline:

  • a first save whose operation fails with CONDITION_NOT_MET is retried, the metadata is re-fetched, and the second upload carries the new fingerprint;
  • any other operation error is raised once as GCPOperationError with its codes.

The GCP live job on this PR is the end-to-end check.

Changelog under 4.4.2 - unreleased — same heading as #348, so whichever merges second needs a trivial changelog rebase.

GCP keeps labels for networks, routers, firewalls and key pairs in the
project-wide common instance metadata, and every write re-uploads that
document under an optimistic fingerprint. gcp_metadata_save_op retries
when the fingerprint is stale, but its predicate only recognised the
conflict as an HttpError. A concurrent writer produces it differently:
the upload is accepted and the resulting operation completes with
CONDITION_NOT_MET, which wait_for_operation raised as a plain Exception
the retry never matched. Under parallel use every collision therefore
failed on the first attempt - the recurring test_crud_* failures in the
GCP live suite, which runs five workers against one project.

Raise GCPOperationError from wait_for_operation, a
ProviderInternalException carrying the operation's error payload and
codes, and have the save predicate retry on CONDITION_NOT_MET. The save
already re-fetches the metadata (and a fresh fingerprint) on each
attempt, so the existing retry is correct once it fires. The exception
message is unchanged; callers that catch CloudBridgeBaseException see
the typed error instead of the middleware's generic wrapper.
@nuwang
nuwang deployed to cloud-integration September 17, 2026 11:07 — with GitHub Actions Active
@nuwang
nuwang deployed to cloud-integration September 17, 2026 11:07 — with GitHub Actions Active
@nuwang
nuwang deployed to cloud-integration September 17, 2026 11:07 — with GitHub Actions Active
@nuwang
nuwang deployed to cloud-integration September 17, 2026 13:15 — with GitHub Actions Active
@nuwang
nuwang deployed to cloud-integration September 17, 2026 13:15 — with GitHub Actions Active
@nuwang
nuwang deployed to cloud-integration September 17, 2026 13:15 — with GitHub Actions Active
@nuwang
nuwang deployed to cloud-integration September 17, 2026 15:37 — with GitHub Actions Active
@nuwang
nuwang deployed to cloud-integration September 17, 2026 15:37 — with GitHub Actions Active
@nuwang
nuwang deployed to cloud-integration September 17, 2026 15:37 — with GitHub Actions Active
The CB_TEST_TRACE files were emitted by a command placed after pytest in
tox's commands list, and tox stops at the first failing command - so the
trace was printed only for passing runs, and a failing run, the one
whose trace is worth reading, lost it. Emit it from commands_post, which
tox runs regardless of the outcome; the environment still reports the
failure.
Retrying on CONDITION_NOT_MET turned out not to be enough. Instrumenting
the live suite showed why: with five workers labelling resources in one
project, GCP accepts several setCommonInstanceMetadata requests against
the same fingerprint while an earlier one is still pending (one
fingerprint was accepted eleven times), and a later one can complete as
DONE with its change absent from the document - 11 of 56 writes in a
single run, none of them turning up later. The pending window was wide
because the document had grown to ~1,700 orphaned test entries, making
each write take 10-260 seconds, but the behaviour is GCP's and any
concurrent writers can hit it.

So a write now counts as done only when the callback's changes can be
read back, and is redone on fresh metadata otherwise; after the retries
are exhausted it raises MetadataWriteNotApplied. A callback that changes
nothing sends no write, since every write re-uploads the whole document
and moves the fingerprint for everyone else. add_metadata_item checks the
fetched metadata for the key itself and raises DuplicateResourceException
- a key already holding this write's value counts as done, so a retry
after a lost write never appends a second copy - with GCP's own
duplicate-key rejection kept as the backstop. remove_metadata_item
returns False when there was nothing to remove, which its callers had
always tested for; it returned True unconditionally.

The tests drive the real save operation and wait_for_operation against a
fake compute client holding a server-side document whose operations can
apply, conflict, or complete without applying.
@nuwang
nuwang force-pushed the gcp-metadata-conflict-retry branch from c5e6348 to 878a991 Compare September 17, 2026 17:20
@nuwang
nuwang deployed to cloud-integration September 17, 2026 18:11 — with GitHub Actions Active
@nuwang
nuwang deployed to cloud-integration September 17, 2026 18:11 — with GitHub Actions Active
@nuwang
nuwang deployed to cloud-integration September 17, 2026 18:11 — with GitHub Actions Active
@nuwang
nuwang deployed to cloud-integration September 17, 2026 18:11 — with GitHub Actions Active
@nuwang
nuwang merged commit c837404 into main Sep 18, 2026
10 checks passed
@nuwang
nuwang deleted the gcp-metadata-conflict-retry branch September 18, 2026 06:35

This branch was successfully deployed

1 active deployment
cloud-integration — 878a9919 Deployed Sep 17, 2026 by nuwang via Per-cloud integration tests (3.13, gcp) #62
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant