Skip to content

Fix a lost-update race in entrecote.add() - #77

Merged
p3k merged 1 commit into
mainfrom
fix-entrecote-add-race-condition
Sep 26, 2026
Merged

p3k merged 1 commit into
mainfrom
fix-entrecote-add-race-condition

Conversation

@p3k

@p3k p3k commented Sep 26, 2026

Copy link
Copy Markdown
Owner

Summary

  • db.get()/db.set() in PupDB each lock, read or write, and unlock on their own. add() called them as two separate steps, leaving a window where a concurrent request for the same key reads the same starting count and both writes land the same incremented value – losing a hit. Real traffic hits the same group/key concurrently (mod_wsgi runs multiple threads per process), so this isn't theoretical.
  • Hold db.process_lock across the whole read-modify-write instead, the same way get_recent() already does, reading/writing the file directly rather than re-entering db.get()/db.set() inside that same lock.

Test plan

  • Added a regression check to entrecote.py's existing smoke test: 50 concurrent add() calls on one key, asserting the final count is exactly 50
  • Confirmed the check actually catches the race: reverted the fix locally first and saw only a handful of the 50 hits survive
  • .venv/bin/python3 entrecote.py passes twice in a row (checked test isolation between runs)
  • sh ./test.sh passes against a live dev server

🤖 Generated with Claude Code

db.get()/db.set() each lock, read or write, and unlock on their own,
so calling them as two separate steps – as add() did – left a window
between them where a concurrent request for the same key read the
same starting count this one just had, and both then wrote back the
same incremented value, losing one hit. Real traffic hits the same
group/key concurrently all the time (mod_wsgi runs multiple threads
per process), so this wasn’t theoretical.

Hold db.process_lock across the whole read-modify-write instead,
exactly like get_recent() already does for its own reasons, reading
and writing the file directly rather than going through db.get()/
db.set() again inside that same lock.

Added a regression check to the existing entrecote.py smoke test:
50 concurrent add() calls on one key, asserting the final count is
exactly 50. Confirmed it actually catches the race by reverting the
fix locally first – the old code lost all but a handful of hits.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@p3k
p3k merged commit e64da40 into main Sep 26, 2026
6 checks passed
@p3k
p3k deleted the fix-entrecote-add-race-condition branch September 26, 2026 11:27
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.

1 participant