fix(api): persist nested column changes on optimistic-locking PATCH#28837
fix(api): persist nested column changes on optimistic-locking PATCH#28837harshach wants to merge 5 commits into
Conversation
The If-Match (optimistic-locking) PATCH path resolved the base EntityUpdater instead of the entity-specific updater, so column tag/description changes were silently dropped: the server returned 200 with the change echoed in the body but never persisted it (top-level fields were unaffected). Delegate the optimistic getUpdater to the overridden 4-arg getUpdater so nested entitySpecificUpdate logic runs. Also closes related write-path gaps and wires up the client: - route lineage ES writes through the staged index during reindex (LineageUtil) - add conflicts(Proceed) to the ElasticSearch updateByFqnPrefix path (OS parity) - emit ETag on PUT/POST/PATCH responses for optimistic-concurrency edit chains - Python SDK: send If-Match + retry on 412 in patch_column_tags/descriptions Tests: OptimisticLockingColumnPatchIT (column tag/desc persist under If-Match, stale If-Match rejected), ETagResponseFilterTest (mutation ETag + GET 304), and a concurrent-modification retry case in test_ometa_patch.py. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
✅ PR checks passedThe linked issue has a description and all required Shipping project fields set. Thanks! |
- LineageUtilTest: stub SearchRepository.getWriteIndexName (delegating to the real IndexMapping) so the two domain-lineage tests pass now that doAddLineageToSearch routes lineage writes through getWriteIndexName - TrackedREST.patch: match the new REST.patch(..., headers=None) signature (basedpyright incompatible-override error) - patch_mixin: satisfy basedpyright — cast Optional[T] on returns, guard the Optional fullyQualifiedName in log messages, and localize the mixin-provided get_suffix attribute access Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Fix dead-code/contract bug in patch_column_tags/patch_column_descriptions: on the final retry a 412 now breaks to the warn + return None path instead of re-raising, so the post-loop warning is reachable and the methods keep their "return None on persistent contention" contract (no new exception surfaced to callers) - Derive the If-Match ETag client-side from the already-fetched instance (mirrors the server's EntityETag.generateETag) instead of issuing an extra GET per column patch; removes the extra round-trip and the read-vs-instance TOCTOU. Verified the computed ETag matches the server's byte-for-byte - Drop the now-unused REST.get_etag and _request(return_response=...) helpers Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
🟡 Playwright Results — all passed (18 flaky)✅ 4266 passed · ❌ 0 failed · 🟡 18 flaky · ⏭️ 88 skipped
🟡 18 flaky test(s) (passed on retry)
How to debug locally# Download playwright-test-results-<shard> artifact and unzip
npx playwright show-trace path/to/trace.zip # view trace |
Per review: the client-derived If-Match couples to the server's number formatting, so a mismatch would 412 on every attempt and silently drop the write after exhausting retries — the persistence-loss class this PR fixes. - On a 412 against an UNCHANGED instance (the same derived ETag was already rejected), treat the conditional header as unusable and fall back to a non-conditional write (last-write-wins) so the change is not dropped. Real contention (the instance changed) still retries with If-Match. - Make the retry-exhaustion warning state the change was NOT persisted so it is greppable/alertable. - Add an integration test that stubs the ETag helper to an always-rejected value and asserts the column change still persists via the fallback. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code Review ✅ Approved 3 resolved / 3 findingsRoutes optimistic-locking PATCH requests through entity-specific updaters to prevent silent data loss, and hardens column-tagging concurrency with SDK-level ETag retries. ✅ 3 resolved✅ Bug: Final-attempt 412 raises instead of graceful None; warning is dead code
✅ Performance: Column patch now issues an extra GET per call for the ETag
✅ Edge Case: Client-derived ETag couples to server float formatting; mismatch silently drops writes
OptionsDisplay: compact → Showing less information. Comment with these commands to change:
Was this helpful? React with 👍 / 👎 | Gitar |
|



Describe your changes:
Fixes #28876
I made the
If-Match(optimistic-locking) PATCH path run the entity-specific updater, because it was resolving the baseEntityUpdaterand silently dropping nested column tag/description changes — the server returned200with the change echoed in the body but never persisted it (top-level fields were unaffected, which hid the bug). I also closed two adjacent search write-path gaps and wired the Python SDK to actually exercise ETags so concurrent column-tag patches are safe instead of last-write-wins.Type of change:
High-level design:
EntityRepositoryhas threegetUpdateroverloads. Normal PATCH uses the 4-arg overload, which each repository overrides to return its entity-specific updater (e.g.TableUpdater, whoseentitySpecificUpdaterunsupdateColumns). The optimistic PATCH used the 5-arg overload, which no repository overrides, so it fell back to the baseEntityUpdaterwhoseentitySpecificUpdateis a no-op — dropping every nested column change underIf-Match, for all entities with a custom updater. Fix: the base 5-arggetUpdaternow delegates to the overridden 4-arg one and setsuseOptimisticLockingvia a setter.getWriteIndexName) so edges aren't dropped on the alias swap mid-reindex; the ElasticSearchupdateByFqnPrefixgainsconflicts(Proceed)for parity with the OpenSearch path.ETagResponseFilternow emits the ETag on PUT/POST/PATCH responses; the Python SDK'spatch_column_tags/patch_column_descriptionscapture the ETag, sendIf-Match, and retry on412. It stays opt-in so bulk ingestion keeps its whole-array last-write-wins behavior.Tests:
Use cases covered
If-Matchpersists (previously dropped silently).If-Matchis rejected with412instead of last-write-wins.304.Unit tests
openmetadata-service/.../resources/filters/ETagResponseFilterTest.java— 6/6 passing locally.Backend integration tests
openmetadata-integration-tests/.../it/tests/OptimisticLockingColumnPatchIT.java(column tag + description persist underIf-Match; staleIf-Matchrejected).Ingestion integration tests
ingestion/tests/integration/ometa/test_ometa_patch.py— concurrent-modification retry case; the existingtest_patch_column_tags/test_patch_column_descriptionnow run through the newIf-Matchpath.Playwright (UI) tests
Manual testing performed
Reproduced the bug on a live stack (MySQL + Elasticsearch 9.3): a column tag/description PATCH with
If-Matchreturned200but did not persist, while the same PATCH without it did. Verified server-sideIf-Matchenforcement (stale →412) and the SDK's read →If-Match→412→ refetch → retry algorithm live. The running container predates the fix, so the end-to-end ❌→✅ flip is covered by the new integration tests in CI.UI screen recording / screenshots:
Not applicable.
Checklist:
Fixes <issue-number>: <short explanation>Fixes #<issue-number>above.🤖 Generated with Claude Code