fix(kb): serialize knowledge tree writes - #3358
Open
Sanderhoff-alt wants to merge 1 commit into
Open
Conversation
Sanderhoff-alt
force-pushed
the
fix/kb-serialize-tree-writes
branch
4 times, most recently
from
August 10, 2026 16:05
c30334f to
65582f0
Compare
Knowledge-tree create, delete, and move operations previously read and updated the hierarchy without a common lock. Concurrent opposite moves could both pass cycle detection against the same snapshot and commit a parent loop. Lock the bank row with FOR NO KEY UPDATE before structural reads and writes. This serializes tree writers for one bank without conflicting with the FOR KEY SHARE locks taken by unrelated foreign-key inserts. The second opposite move now observes the first committed parent link and is rejected by the existing cycle guard. Add a deterministic concurrency test that holds the first bank lock, verifies the second move cannot finish early, and checks that only the first move succeeds.
Sanderhoff-alt
force-pushed
the
fix/kb-serialize-tree-writes
branch
from
August 10, 2026 16:15
65582f0 to
716d10c
Compare
Contributor
|
👋 Friendly suggestion from a contributor review: PRs #3358 (serialize writes), #3369 (atomic creation), and #3370 (compensation on failure) all address knowledge-base transaction safety. Consider consolidating these three into a single PR — it would reduce review overhead, prevent merge conflicts between them, and make the KB transaction story coherent. Each PR is valuable individually, but together they form a stronger atomicity guarantee. |
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.
Summary
Serialize knowledge-base structural writes at the bank level so create, delete, and move operations cannot validate or rewrite the hierarchy from incompatible snapshots. This prevents concurrent opposite moves from both passing cycle detection and committing a parent loop.
Problem
Knowledge-base hierarchy checks previously ran without a shared serialization point. Two transactions moving
AunderBandBunderAcould both read the same pre-commit tree, independently pass the existing cycle guard, and then commit a cycle. Create and delete also change the same hierarchy, so serializing move alone would leave the structural-write invariant incomplete.Design
Add
_kp_lock_bank()and acquire a row lock on the owning bank before structural hierarchy reads or writes. Folder creation, page-node insertion, move, and delete all use the same lock, so structural writers for one bank execute against one committed tree state. Writers for different banks remain independent.The lock uses
FOR NO KEY UPDATErather thanFOR UPDATE. It still conflicts with another structural writer taking the same lock, but it does not conflict with theFOR KEY SHARElocks PostgreSQL takes for inserts into tables that reference the bank. This preserves writer serialization without unnecessarily blocking unrelated bank-scoped inserts or introducing the foreign-key lock interaction caused by the stronger lock. The Oracle adapter already rewrites this form to supportedFOR UPDATEsyntax.Move takes the bank lock before validating the destination parent and reading the parent map. Delete takes the same lock before reading and removing a subtree. Create takes it before validating the parent and inserting the new hierarchy row. Non-structural operations such as rename do not take this lock.
Concurrency behavior
A.parent_id = Band rejects the moveA.parent_id = BTest coverage
Add a deterministic regression test that pauses the first request after it acquires the real bank-row lock, verifies the second request reaches the same lock and cannot finish early, then confirms the first move succeeds and the second move is rejected. The test uses events rather than sleeps, making the required interleaving explicit and repeatable.
Validation
./scripts/hooks/lint.shuv run pytest tests/test_knowledge_base.py -q -n 0(40 passed)Scope and tradeoffs
The serialization scope is one bank row, so structural writes within the same bank are intentionally serialized. This is coarser than locking individual paths, but it keeps a uniform lock order across create, delete, and move, makes the tree invariant straightforward to audit, and avoids locking every knowledge node.
This change is intentionally limited to preventing concurrent structural writes from creating a parent loop. It does not add defensive traversal for trees already corrupted before the fix, change page-creation failure compensation, or alter existing exception semantics.