Skip to content

fix(publish): return deterministic conflict on concurrent coordinate race - #784

Merged
XiaoSeS merged 2 commits into
mainfrom
fix/concurrent-publish-coordinate-race
Aug 31, 2026
Merged

fix(publish): return deterministic conflict on concurrent coordinate race#784
XiaoSeS merged 2 commits into
mainfrom
fix/concurrent-publish-coordinate-race

Conversation

@FenjuFu

@FenjuFu FenjuFu commented Aug 31, 2026

Copy link
Copy Markdown
Member

What

Closes #617.

Concurrent publishes for the same final (namespace_id, slug, owner_id) / (skill_id, version) coordinate could both pass the check-then-create reads and then race on the database unique constraints. The losing request surfaced an unhandled DataIntegrityViolationException as HTTP 500.

Fix

Follow the codebase's existing concurrency idiom — LabelDefinitionService, ReviewService, and PromotionService all catch (DataIntegrityViolationException) and translate the unique-constraint violation into a deterministic business error. SkillPublishService.publishFromEntries now does the same at its two check-then-create insert points:

  • Skill insertskill(namespace_id, slug, owner_id) unique constraint
  • SkillVersion insertskill_version(skill_id, version) unique constraint

Both translate a violation into a deterministic DomainBadRequestException("error.skill.publish.concurrentConflict") (HTTP 4xx) instead of a 500. No same-transaction re-read is attempted (that would hit the aborted-transaction trap); the losing publish rolls back cleanly and returns a retryable conflict, matching the issue's "one success and one deterministic business conflict" contract. No response-shape change.

i18n

New key error.skill.publish.concurrentConflict added to messages.properties (en) and messages_zh.properties (zh); other locales fall back to the default bundle.

Tests

Two unit tests in SkillPublishServiceTest assert that a DataIntegrityViolationException from the skill insert and from the version insert each surface as error.skill.publish.concurrentConflict rather than propagating as a 500, and that no version save / object upload happens once the coordinate race is lost.

Scope

This eliminates the reported 500 — the core of "they should not produce an internal server error". The issue also raises a broader follow-up: deterministic serialization of interleaved publish/review lifecycle mutations (pending-review withdrawal vs latest_version_id). That is a larger lifecycle change; happy to address it in a separate PR if maintainers want it in scope here.

FenjuFu and others added 2 commits August 31, 2026 14:27
…race

Concurrent publishes for the same (namespace_id, slug, owner_id) or
(skill_id, version) coordinate both pass the check-then-create reads and
race on the database unique constraints. The losing request surfaced an
unhandled DataIntegrityViolationException as HTTP 500.

Translate the constraint violation at both insert points into a
deterministic DomainBadRequestException (error.skill.publish.concurrentConflict),
matching the existing idiom in LabelDefinitionService/ReviewService/
PromotionService. No same-transaction re-read is attempted, so the losing
publish rolls back cleanly and returns a retryable conflict instead of a 500.

Add the i18n key (en/zh) and two unit tests covering the skill-insert and
version-insert races.

Closes #617

Signed-off-by: FenjuFu <92919259+FenjuFu@users.noreply.github.com>
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
@XiaoSeS
XiaoSeS merged commit a73997c into main Aug 31, 2026
10 checks passed
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.

[Bug] Concurrent publishes race on skill coordinate and return 500

2 participants