fix(advice): an editing token, so a comment-only correction is safe too

The gravity guard caught a correction that changed the gravity, but a
COMMENT-ONLY correction leaves the gravity unchanged - so the late
thread for the old advice still matched and overwrote the fresh one.

Reproduced against the live database:

    fresh advice matched: 1
    stale thread matched: 1     <-- should be 0
    FINAL advice: 'STALE ADVICE'

Every edit now writes a fresh token onto the reading, and the advice
thread carries the token it was given.  A late thread finds a token that
no longer matches, so its write is refused.  The gravity is still
checked as well.

Verified against the live database:

    token changed: True
    fresh advice matched: 1
    stale thread matched: 0     <-- must be 0
    FINAL advice: 'FRESH ADVICE'

174 tests (up from 173).
This commit is contained in:
2026-11-09 18:14:24 +10:00
parent 8da3601941
commit a78aabe3ea
4 changed files with 87 additions and 11 deletions
+10 -5
View File
@@ -91,6 +91,7 @@ def _start_advice(run_id: str, reading_id: str, corrected: bool = False) -> None
points = reading.sg_points
readings.append({
"_id": str(reading._id),
"advice_token": reading.advice_token,
"date": reading.date.isoformat() if reading.date else "",
"sg": points,
"sg_decimal": reading.sg_decimal,
@@ -113,10 +114,11 @@ def _start_advice(run_id: str, reading_id: str, corrected: bool = False) -> None
log.exception("could not gather the reading for the advice")
return
# The gravity the advice is about. The write is conditional on the
# reading still holding it, so a slow advice thread cannot overwrite
# a correction with advice about the old value.
# The gravity and the editing token of the reading being advised
# about. The write is conditional on both, so a slow advice thread
# cannot overwrite a correction with advice about the old value.
advice_sg = latest.get("sg")
advice_token = latest.get("advice_token")
def write(read_id: str, ok: bool, text: str) -> None:
# The client is SHARED, not built per reading. A MongoClient
@@ -131,8 +133,11 @@ def _start_advice(run_id: str, reading_id: str, corrected: bool = False) -> None
try:
shared = get_shared_repository()
written = shared.set_reading_advice(read_id, text, ok,
expected_sg=advice_sg)
written = shared.set_reading_advice(
read_id, text, ok,
expected_sg=advice_sg,
expected_token=advice_token,
)
if not written:
# The reading was corrected while the advice was being
# generated, so the advice no longer matches it. That is