Skip to content

fix(provider): confirm against the API server before skipping a status write - #10409

Open
nobbs wants to merge 6 commits into
envoyproxy:mainfrom
nobbs:fix/gatewayclass-generation-flake
Open

nobbs wants to merge 6 commits into
envoyproxy:mainfrom
nobbs:fix/gatewayclass-generation-flake

Conversation

@nobbs

@nobbs nobbs commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

What this PR does / why we need it:

The status updater decides whether a write is needed by comparing the desired status with the object from the manager's cache, and skips the write when they are equal. Right after its own write, the cache can still hold the previous version of the object. When two updates for one object arrive out of order, the updater writes the older status and then skips the newer one:

write status S2          the cache catches up, S2
write status S1, late    API server S1, cache still S2
write status S2          equal to the cache, "status unchanged", skipped
the object keeps S1 until something else updates it

This is how GatewayClassObservedGenerationBump flakes (117 failed conformance jobs between 12 and 26 Oct): the Gateway API runner stores a GatewayClass status from an older snapshot (fixed in #10408), and the corrective write is skipped. Any kind with more than one writer, or with out-of-order updates, can end up the same way.

This PR records the resourceVersion of each of the updater's writes, keyed by UID. When the status looks unchanged but the cache shows another resourceVersion than the recorded write, the updater reads the object from the API server before skipping:

 newObj := update.Mutator.Mutate(obj)
+if isStatusEqual(obj, newObj) && u.cacheMayLag(obj) { // a recorded write, another resourceVersion in the cache
+	// read obj from the API server and mutate it again
+}
 if isStatusEqual(obj, newObj) {
 	// skip
 }
 ...
+u.lastWritten[newObj.GetUID()] = lastWrite{resourceVersion: newObj.GetResourceVersion(), at: time.Now()}

A ticker on the updater's goroutine drops records older than a minute, since a record only matters until the cache has seen the write. The extra read happens only when a write is about to be skipped while the cache may be behind.

Evidence:

  • Before: TestStatusUpdateIsNotSkippedWhileCacheLagsBehindOwnWrite, the first unit test for the status updater, runs the sequence above against a fake API server with a cache the test holds behind, and the object ends at S1.
    After: it ends at S2.
  • TestStatusUpdateRetriedAfterConflictIsNotSkippedWhileCacheLags makes the write after the API-server read conflict once; the retry still meets the stale cache and writes S2. TestStatusUpdateRecordsExpire checks that a sweep a minute after writing 100 GatewayClasses leaves no records.
  • The envtest loop from fix(gatewayapi): keep an older translation from overwriting a newer GatewayClass status #10408, with the interleaving forced and without the runner change: 8 of 8 iterations ended at the old observedGeneration before, 0 of 8 after.
  • The provider unit tests (also under -race) and the envtest integration tests pass.

Merge danger: a two-way door. The blast radius is every status write the provider makes. In the case above the updater reads the object from the API server once before deciding, and it keeps a record of each write for one to two minutes. Otherwise nothing changes.

Which issue(s) this PR fixes:

Fixes #9691


PR Checklist

  • Authorship & ownership: Coding agents / AI assistants are welcome, but I have reviewed every change, understand how and why it works, can explain and maintain it, and take full responsibility for this PR. I have not submitted generated output I do not understand.
  • DCO: All commits are signed off (git commit -s). See DCO: Sign your work.
  • API agreed first: If this PR contains API changes (changes under /api), the API was discussed and agreed before the implementation. The API change can be in a separate PR, or in the same PR, but the API must be agreed before implementation. N/A if this PR does not contain API changes. (N/A: no API changes.)
  • Required checks pass: make generate gen-check, make lint, and the unit-test/coverage build pass. (Flaky e2e failures are not considered breakages, but gen-check, lint, and coverage MUST pass.) (make lint and the provider unit and integration tests pass locally. gen-check and the full coverage run are left to CI.)
  • Tests added/updated: New/changed code is covered by appropriate tests. N/A if this PR does not contain code changes.
  • Docs: User-facing changes update the docs, either in this PR or a follow-up PR. N/A if this PR does not contain user-facing changes. (N/A: a status fix with no documented behavior to change.)
  • Release notes: For any non-trivial change, added a release-note fragment under release-notes/current/<section>/<pr-number>-<slug>.md (see release-notes/current/README.md for sections and naming). N/A if this PR does not contain non-trivial changes.
  • Generated files committed: Ran make gen-check and committed the result if API/helm charts/modules changed. (N/A: nothing generated changed.)
  • Scope & compatibility: The PR is reasonably scoped (no unrelated changes) and preserves backward compatibility, or any breaking change is called out above and documented in release-notes/current/breaking_changes/.
  • Codex review: Requested a Codex review and addressed all of its comments.
  • Copilot review: Requested a Copilot review and addressed all of its comments.

@netlify

netlify Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for cerulean-figolla-1f9435 ready!

Name Link
🔨 Latest commit 14a957f
🔍 Latest deploy log https://app.netlify.com/projects/cerulean-figolla-1f9435/deploys/6ae4fbb627c51a49a4acb406
😎 Deploy Preview https://deploy-preview-10409--cerulean-figolla-1f9435.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-30T20:21:43.193159Z 14a957f New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cf0f1c3cd2

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@codecov

codecov Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 78.94737% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.19%. Comparing base (314be21) to head (14a957f).

Files with missing lines Patch % Lines
internal/provider/kubernetes/status_updater.go 78.94% 4 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main   #10409   +/-   ##
=======================================
  Coverage   82.19%   82.19%           
=======================================
  Files         277      277           
  Lines       44014    44032   +18     
=======================================
+ Hits        36178    36193   +15     
- Misses       7835     7838    +3     
  Partials        1        1           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@nobbs
nobbs marked this pull request as draft October 26, 2026 15:15
@nobbs

nobbs commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@nobbs
nobbs marked this pull request as ready for review October 26, 2026 15:49

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7f136544ad

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".


// lastWriteTTL is how long a record of a status write is kept. The record is only needed until the
// cache has seen the write, which takes far less.
const lastWriteTTL = time.Minute

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve lag guards until the cache catches up

When the cache remains behind the last write past this TTL and the next sweep (one to two minutes), expireLastWrites removes the only signal that forces an API-server read. In the same S2 → stale-cache S1 → corrective S2 sequence covered by the new test, delaying the correction until after expiry makes cacheMayLag false, so the updater treats cached S2 as current and leaves server-side S1 in place; the deduplicating status store may never emit S2 again. Cleanup therefore needs to distinguish records whose resource version has been observed by the cache, or otherwise retain a bounded guard that survives prolonged cache lag.

Useful? React with 👍 / 👎.

@nobbs
nobbs force-pushed the fix/gatewayclass-generation-flake branch from 7f13654 to 064d62d Compare October 27, 2026 07:19
@zirain zirain added this to the Backlog milestone Oct 27, 2026
@nobbs
nobbs force-pushed the fix/gatewayclass-generation-flake branch from 064d62d to 03a6c97 Compare October 29, 2026 07:45
nobbs added 6 commits October 30, 2026 21:18
…s write

The status updater compares the desired status with the object from the
manager's cache and skips the write when they are equal. Right after its
own write, the cache can still hold the previous version of the object.
When updates for an object arrive out of order, as when the Gateway API
runner stores a status computed from an older snapshot, the updater wrote
the older status and then skipped the newer one, because the cache still
showed the newer status from its write before. The object kept the older
status, as seen in the flaky GatewayClassObservedGenerationBump
conformance test.

Remember the resourceVersion of the updater's last write per object.
When the cache is behind it and the status looks unchanged, read the
object from the API server before deciding. The entry is dropped once
the cache has caught up or the object was read from the server.

Signed-off-by: Alexej Disterhoft <alexej@disterhoft.de>
Signed-off-by: Alexej Disterhoft <alexej@disterhoft.de>
…onfirmed

When the cache looked behind the status updater's last write, the updater
dropped its record of that write before checking the API server. If the
following write then conflicted, the retry met the same stale cache
without the record, took the cached status for the current one, and
skipped the write.

Keep the record until the server confirms the status is unchanged, the
object is gone, or a write succeeds.

Signed-off-by: Alexej Disterhoft <alexej@disterhoft.de>
The status updater keyed its last-write records by kind and name. Extension
policy kinds are unstructured, and two of them can share a Kind in
different API groups, so their objects of the same namespace and name
shared a record. An update for one could consume the other's record, and
that object's next correction was then compared with its stale cache and
skipped.

Include the API group in the key.

Signed-off-by: Alexej Disterhoft <alexej@disterhoft.de>
A last-write record was only dropped by the next update of the same
object. The last update of most objects is a write, and updates for
deleted objects never arrive, so the records of every object that ever
had its status written stayed for the life of the process.

Record when each write happened and drop records older than a minute, at
most once a minute. A record is only needed until the cache has seen the
write, which takes far less.

Signed-off-by: Alexej Disterhoft <alexej@disterhoft.de>
…ater

Key the records by UID instead of group, kind and name, since no two
objects share a UID. Records are now only added by writes and only
dropped when they expire. The early drops, once the cache showed the
write or the API server confirmed the status, only spared an API read
in rare cases.

Sweep expired records from a ticker on the Start goroutine instead of
throttling a sweep inside apply, which needs neither a sweep timestamp
nor an injected clock.

Signed-off-by: Alexej Disterhoft <alexej@disterhoft.de>
@nobbs
nobbs force-pushed the fix/gatewayclass-generation-flake branch from 03a6c97 to 14a957f Compare October 30, 2026 20:18

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TestGatewayAPIConformance/GatewayClassObservedGenerationBump is flaky

2 participants