fix(actions): lock the objective row before computing effective_closed - #74
Merged
Merged
Conversation
Inside upsertAction's transaction, the first statement is now SELECT ... FOR SHARE on the objective row (via tx.instance.oneOrNone — never .one, which would crash-loop the indexer on a drifted objective). The FOR SHARE lock conflicts with the backend's row lock when Objectives.complete_objective/2 flips objectives.is_completed, so the two writers serialize on that row: whichever commits first, the loser re-reads the committed state under READ COMMITTED and the stale-snapshot insert that produced a chain-open action under a completed objective is impossible. effective_closed is derived from the post-lock read, with the null-objective fallback to the top-of-function fetch preserved. The backend bounds the wait with lock_timeout 5s on its finalize transaction plus Oban retry.
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
Closes the dual-writer race against the backend's objective completion. Inside
upsertAction's transaction, the first statement is now:The
FOR SHARElock conflicts with the backend's row lock whenObjectives.complete_objective/2flipsobjectives.is_completed, so the two writers serialize on that row: whichever commits first, the loser re-reads the committed state under READ COMMITTED, and the stale-snapshot insert (indexer reads incomplete → backend completes → indexer insertseffective_closed=false) is impossible.effective_closedis derived from the post-lock read; the null-objective fallback to the top-of-function fetch is preserved (a drifted objective must never crash-loop the indexer).tx.instance.oneOrNone, never.one— pg-promise's.onethrows on zero rows, which would roll the block back and retry forever.lock_timeouton its finalize transaction plus Oban retry.last_tx/last_eos_accountwere already stamped on both create and update paths (fix(provenance): stamp last_tx/last_eos_account on objective and action writes #71); no change needed.Verified
yarn format(StandardJS) clean.git diffshows onlysrc/updaters/community.js.Deploy after the backend schema lands (migration → restart event-source → backend app).