Repository navigation
Commit move_children_to_parent's edge swap and version bumps in one transaction (closes #2950) - #3029
Conversation
…ransaction (closes #2950) The store's edge swap committed in its own transaction before the service's version-bump transaction. A concurrent write landing between the two failed the call after the edges had already moved: the frontend rolled the children back while the backend kept them re-parented, and their RelationshipUpdated events never fired. The store method is now move_children_to_parent_in_tx, composed with the rootness refresh, version bumps and buffered events into one NodeService::with_transaction, so a conflict anywhere rolls back every edge and events flush only after commit (ADR-069 §1a/§2). Tests: a forced bump conflict after the edge swap rolls the swap back and emits nothing; a multi-threaded race of a real concurrent write against the real method asserts the call's result always matches backend state (fails within two iterations on the old two-transaction code). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Pragmatic Code Review: PR #3029Review Type: Initial Review (the PR had no earlier reviews or comments) SummaryThis is a small, focused fix that makes the codebase healthier. The store method becomes a Requirements Check (#2950)
Code Review Findings🟢 Suggestion: title refresh moved from best-effort to transactional. This is correct, not a regression. 🟢 Suggestion: the VERSION_CONFLICT mapping still reports 🟢 Suggestion: no deadlock risk found. 🟢 Nit: redundant clones inside an 🟢 Nit: the forced-conflict test rebuilds the production composition by hand. Recommendation: APPROVEAll four acceptance criteria are met. The change is atomic and consistent with ADR-069 and the existing |
malibio
left a comment
There was a problem hiding this comment.
Automated pragmatic-code-review: APPROVE (posted as a comment — GitHub blocks self-approval). Full findings in the review comment above.
with_transaction takes an FnOnce, so the per-call clones of the children, nodes, former parents and service were unnecessary. Skipped (all 🟢): VERSION_CONFLICT (0,0) mapping is pre-existing and needs a typed store error — separate change; forced-conflict test composing the steps by hand is covered by the race test on the real method. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Address-review summaryReview verdict was APPROVE with 🟢 suggestions only. ✅ Addressed (1): moved owned state straight into the ⏭️ Skipped (4):
Tests: 10/10 Re-review: not needed. The only code change is a clone removal with no logic change. |
Summary
NodeService::move_children_to_parentcommitted the store's edge swap and the service's version bump in two separate transactions. A concurrent write to a child landing between them failed the RPC after the edges had moved: the frontend rolled back locally, the backend kept the children re-parented, and theirRelationshipUpdatedevents never fired.SqliteStore::move_children_to_parent→move_children_to_parent_in_tx(&Tx, …). It no longer opens its own transaction. The write guard held by theTxstill covers the max-order read → write.refresh_for_rootness_in_tx, the per-child version bump, and event emission in onewith_transaction. A conflict anywhere rolls back every edge, and events flush only after commit (ADR-069 §1a/§2).with_transactionhelper.Tests
test_move_children_to_parent_bump_conflict_rolls_back_edge_swap: forces a bump conflict after the edge swap inside one transaction. The swap rolls back, the version is unchanged, and no event escapes. (Replaces the previous bump-loop-only F9 test.)test_move_children_to_parent_result_matches_backend_under_concurrent_write: races a realupdate_nodeagainst the real method on a multi-threaded runtime, 50 iterations. The call's result must always match the backend parent and event count. Against the old two-transaction code this fails within 1–2 iterations; with the fix, it passed 5 of 5 runs.cargo test -p nodespace-core --lib: 1761 passed. The pre-push gate passed.🤖 Generated with Claude Code