Skip to content

fix(form): select document family for update to avoid race conditions - #2645

Merged
winged merged 3 commits into
projectcaluma:mainfrom
winged:enforce_document_update_locking
Aug 26, 2026
Merged

fix(form): select document family for update to avoid race conditions#2645
winged merged 3 commits into
projectcaluma:mainfrom
winged:enforce_document_update_locking

Conversation

@winged

@winged winged commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Concurrent updates to different answers in the same document family can
recalculate the same dependent answers. Without a common lock, either
transaction may calculate results without seeing the other transaction's
uncommitted changes and later overwrite those results.

So we lock the family's root document row before reading the existing answer,
validating, updating, and recalculating. Writers using these API entry points
now acquire the same row lock, so PostgreSQL serializes their work within a
document family. The lock is held until the surrounding transaction commits.

To ensure all GraphQL answer-save mutations use the locking path,
refactor the answer serializers to use the common API as well.

@winged
winged force-pushed the enforce_document_update_locking branch 2 times, most recently from 617475e to bcbb326 Compare August 25, 2026 14:15
@winged
winged requested review from anehx and nlzet August 25, 2026 14:15
@winged
winged force-pushed the enforce_document_update_locking branch 2 times, most recently from ba83f9a to e5b1371 Compare August 25, 2026 15:07
winged added 2 commits August 25, 2026 17:31
Concurrent updates to different answers in the same document family can
recalculate the same dependent answers. Without a common lock, either
transaction may calculate results without seeing the other transaction's
uncommitted changes and later overwrite those results.

So we lock the family's root document row before reading the existing answer,
validating, updating, and recalculating. Writers using these API entry points
now acquire the same row lock, so PostgreSQL serializes their work within a
document family. The lock is held until the surrounding transaction commits.

To ensure all GraphQL answer-save mutations use the locking path,
refactor the answer serializers to use the common API as well.
SaveAnswerLogic.get_new_answer() now already extracts file input and
updates the file relation after creating or updating the answer. Remove
the duplicate handling from create() and update() so file relations are
processed exactly once through the common save path.
@winged
winged force-pushed the enforce_document_update_locking branch from e5b1371 to c683532 Compare August 25, 2026 15:32

@nlzet nlzet left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To make it complete there are currently 3 GraphQL paths that bypass locking:

  • DocumentSerializer::update (inherited parent update so will not lock)
  • RemoveAnswerSerializer::update
  • RemoveDocumentSerializer::update

Comment thread caluma/caluma_form/serializers.py Outdated
Ensure document updates and answer/document removals acquire the
family-level `SELECT FOR UPDATE` lock.

Route these mutations through the API layer and add regression tests
for locking, single-pass validation, and metadata preservation.
@winged
winged force-pushed the enforce_document_update_locking branch from cf33f0b to 86b593b Compare August 26, 2026 11:57

@nlzet nlzet left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍 very nice

@winged
winged merged commit 5a5e116 into projectcaluma:main Aug 26, 2026
12 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.

2 participants