Issue #1261: fix: answer 409 when an identity-mapping collision survives the retry - #1312
Open
dereck-symmetry wants to merge 1 commit into
Open
dereck-symmetry wants to merge 1 commit into
dereck-symmetry wants to merge 1 commit into
Conversation
…ves the retry A natural-key collision that survived #1260's one retry raised DataStoreException and reached the caller as the generic 500 with a correlation UUID, the same answer as a datastore outage. The retry's IntegrityError now becomes IdentityMappingConflictException (defined in identity_mapper_storage), both save paths re-raise it ahead of their broad except, and the base answers 409 saying the request may be retried. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
13 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description of Change
Problem. #1260 made a
save_mappingsbatch retry once when a concurrent insert invalidates its pre-read. A collision that survives that retry still raisedDataStoreException. That reacheddefault_exception_handleras a 500 with a correlation UUID and "Internal server error. Please try again later.", the same answer the service gives for a real datastore outage. Callers therefore had no correct retry policy: a conflict their own concurrent traffic caused looked like a broken server. This was criterion 4 of #1216, deliberately left out of #1260.Solution. This follows the shape #1261 suggested:
IdentityMappingConflictExceptionincomponents/lif/identity_mapper_storage/core.py, next toDeleteOutcome. It's deliberately not incomponents/lif/exceptions: 12 projects package that brick, and the Identity Mapper workflow'spaths:doesn't list it (CF/CI: deploy workflowpaths:filters miss packaged bricks — a brick-only change merges green and never rebuilds the image (9 of 11 services affected) #1171).identity_mapper_storage_sql: the retry'sIntegrityErroris converted into the conflict exception (raise ... from e). The bound is unchanged: still exactly one retry.save_mappingandsave_mappingsre-raise it ahead of their broadexcept Exception: raise DataStoreException from e, next toValueError. Without this step the new exception is silently swallowed and nothing changes.identity_mapper_restapi: a handler that returns 409 with "The save collided with a concurrent write of the same mapping. The request may be retried." There's no correlation UUID, and it logs atwarninglike the neighbouring 404/400 handlers.A deliberate choice: no constraint-name check. The acceptance criteria say the 409 must not widen to unrelated
IntegrityErrors. On this table, anIntegrityErrorthat survives the retry can only be the natural key:stron theIdentityMappingDTO, so NOT NULL can't failmapping_idis a generated UUID, and an unrecognized caller-supplied id is already aValueError(400)DataError, notIntegrityError(Identity Mapper: reject oversized identity-mapping fields with 422 instead of a 500 from the database #1300)Matching
uq_identity_mappingby name would need dialect-specific message parsing: MariaDB names the key, SQLite lists the columns. So the reasoning is recorded in a comment at the conversion point instead. Genuine datastore failures still return 500 with their UUID: only the retry'sIntegrityErroris converted, andtest_do_save_mappings_datastore_exceptionstill passes unchanged.How reviewers should test it.
To confirm the base test guards the wiring: remove only the
@app.exception_handler(IdentityMappingConflictException)line, and the 409 test fails. The conflict falls through to the catch-all handler.Related Issues
Closes #1261
Refs #1216
Refs #1131
Type of Change
Project Area(s) Affected
Checklist
uv run ruff check)uv run ruff format)uv run ty check)bases/lif/identity_mapper_restapi/README.md). No pageunder
docs/documents the Identity Mapper's status codes, so there was nothing there to changeTesting
mainIdentityMapperServiceandIdentityMapperSqlStorage(aiosqlite) returns409, a "may be retried" message, nocode, and the pre-read ran exactly twiceassert 500 == 409)save_mappingsraises the conflict when the collision persists; still exactly two pre-readsDataStoreException)save_mappingpath raises the conflictDataStoreException)DataStoreExceptionstill returns 500 with a correlation UUIDThe base test deliberately doesn't mock the service. As the issue warns, the ways this breaks sit between the layers: the storage brick's broad
exceptswallowing the conflict, or the exception landing in theLIFException/catch-all handler. A test that stops at the exception type passes in both cases. Like #1260's tests, the collision is simulated with a patched pre-read that misses the committed row; no second transaction actually runs.The identity-mapper base, service and storage suites pass (78 tests).
pre-commit run --fileson all eight changed files is green, including the full pytest suite.Additional Notes
Shared bricks (#1171).
identity_mapper_storageandidentity_mapper_storage_sqlare packaged only bylif_identity_mapper_apiandlif_identity_mapper_mariadb. Only the API is a running service, and its workflowpaths:already lists both bricks and the base (.github/workflows/lif_identity_mapper_api.yml:9-16).Merge order.
CHANGELOG.mdis shared with #1310, #1311 and #1148: each adds a line at the top of[Unreleased]→### Changed, and whichever merges later keeps both. #1274 touches the identity-mapper workflow files, not this code, so they don't conflict.🤖 Generated with Claude Code