Nobody has started this yet — be first.
Business impact
create_checkin's own docstring promises: "Idempotent: re-checking-in the same day returns the existing row rather than raising or creating a duplicate." That promise is only true for requests that don't overlap in time. A mobile app that retries a slow request in the background, a flaky network causing a client to double-submit, or simply a user tapping "check in" twice quickly (a very normal double-tap) can send two requests for the same habit+day close enough together that both read "no existing check-in" before either has written one. The second one then crashes with an unhandled database error -- an ordinary double-tap becomes a 500 instead of the harmless no-op the docstring promises. On-call gets paged for what is, from the user's perspective, nothing going wrong at all.
Problem
create_checkin is a check-then-act race: the "does it already exist?" read and the "create it" write are two separate, non-atomic database operations. app/models.py::CheckIn does have a UniqueConstraint("habit_id", "date", name="uq_checkin_habit_date") -- so the data can never actually end up duplicated -- but the second caller's db.commit() raises sqlalchemy.exc.IntegrityError, which nothing here catches. That exception propagates all the way out as an unhandled 500.
Current behavior
Two overlapping "check in for the same habit+date" requests (a double-tap, a client retry) can both pass the existence check before either commits -- the second commit raises an unhandled sqlalchemy.exc.IntegrityError, surfacing as a 500 instead of the idempotent no-op the docstring promises.
Expected behavior
Two overlapping check-in requests for the same habit and date must both succeed (each returning 201 with the same check-in), exactly as two sequential calls already do today -- overlapping in time must not change the outcome.
Steps to reproduce
cd fastapi/habit_tracker source .venv/bin/activate pytest practice_tickets/tests/test_ticket07_checkin_race.py -v
Why this matters
This is a genuine TOCTOU (time-of-check to time-of-use) bug, not a contrived one -- it's already present in the code exactly as written today; nothing needed to be injected to produce it. It's also directly related to TICKET-05 (bulk check-in backfill): any correct fix here is almost certainly the same primitive TICKET-05's create_checkins_bulk should reuse, since a bulk backfill hitting an already-checked-in date is structurally the same "insert that might collide with an existing row" problem, just without real concurrency involved.
Suggested approach
There are two standard ways to close a check-then-act race against a uniqueness constraint: (a) catch the IntegrityError that the losing commit raises, roll back, and re-fetch/return the row that won the race instead of letting the exception escape, or (b) use the database's own "insert, but do nothing (or return the existing row) on conflict" support (SQLAlchemy's sqlalchemy.dialects.sqlite.insert(...).on_conflict_do_nothing(...) for this SQLite-backed project) so the race can't produce an exception in the first place. Either is a reasonable, real-world fix; think about what happens to db's session state after a failed commit() if you go with (a) -- a session that just raised on commit needs a rollback() before it can be used again.
Acceptance criteria
Verification
pytest practice_tickets/tests/test_ticket07_checkin_race.py -v
Hints (0/2)
Try it without hints first — the reading is the exercise.
Working on this ticket
Work on a branch named for the ticket — that's what you'll submit.
Branch off your fork
$git checkout -b fix/checkin-race-conditionFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin fix/checkin-race-conditionSubmit it below
Paste your fork URL and the branch name, with a short write-up of the root cause.
Questions
Ask about anything unclear in the ticket — the maintainer and anyone who has solved it can answer. Please don't post full solutions.
Sign in to ask a question or reply.
Sign indocker compose up --buildfastapi/habit_trackerRun the project's own suite with pytest -v (or pytest tests/ -q for a quick pass/fail count) from fastapi/habit_tracker -- a fresh clone should show a handful of failures until TICKET-03 and TICKET-06 are fixed.
Work the tickets in practice_tickets/ (TICKET-01 through TICKET-07); each names one pytest file under practice_tickets/tests/:
pytest practice_tickets/tests/test_ticket01_stats_window.py -v # a single ticket
./practice_tickets/run_tickets.sh # all 7, clean pass/fail summary
./practice_tickets/run_tickets.sh 03 07 # just a subset
practice_tickets/tests/ is intentionally outside pyproject.toml's testpaths = ["tests"], so a plain pytest run from the repo root never picks these up -- they're learning exercises, not part of the project's CI-gating regression suite.
Level 1
Fix a bug
Read existing behaviour, correct it.