Skip to content

Retry device registration when a concurrent request wins the race - #135

Merged
dadachi merged 2 commits into
mainfrom
fix/device-registration-race
Oct 4, 2026
Merged

dadachi merged 2 commits into
mainfrom
fix/device-registration-race

Conversation

@dadachi

@dadachi dadachi commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

Problem

Two simultaneous first-time POST /devices with the same (platform, token), for example an app that registers twice at launch, can both pass the uniqueness validation. The unique index then rejects the later INSERT with RecordNotUnique, which surfaces as a 500.

Fix

DevicesController#save_device retries once on RecordNotUnique. The second lookup finds the row the other request created and updates it, returning 200 like any re-registration.

Not changed: reassigning a token to the caller. That is the documented behavior for a device moving to whoever signs in on it. Pushes are delivered to the device (the token), so someone who obtained another person's token could only send their own notifications to that device and stop that person's. They could not read the other person's notifications, which corrects what the earlier review said.

Verification

  • Test-first: the new test commits the same token from a separate PG connection just before this request's INSERT, outside the test transaction, which is a realistic race.
    • On the old code it raised RecordNotUnique.
    • The cleanup runs in after_teardown, after the fixture transaction rolls back. My first version deleted the row inside ensure while the test transaction still held its lock, and it hung; the hung runs were killed and no rows were left behind.
  • Red check: removing the retry brings the RecordNotUnique back.
  • Dev server: 10 trials of two parallel registrations with a fresh token each time.
    • Before the fix: 10 × 500.
    • After the fix: 10 × 201 + 10 × 200.
    • The probe devices were deleted afterwards.
  • bin/rails test: 505 runs, 0 failures. rubocop and brakeman are clean. The AGENTS.md test count is updated.

🤖 Generated with Claude Code

https://claude.ai/code/session_01L4Pma9VAqB5A22KCEEin9R

dadachi and others added 2 commits October 5, 2026 06:58
Two first-time registrations of the same push token could both pass the
uniqueness validation; the unique index then rejected the later INSERT
with RecordNotUnique, a 500. Retry once: the lookup finds the row the
other request created and updates it (200), like any re-registration.

Reassigning a token to the caller stays as is: it's the documented way a
device moves to whoever signs in on it, and pushes go to the device, so
someone who obtained another person's token only redirects their own
notifications to that device.

Also update the AGENTS.md test count (505 tests after #134 and this).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L4Pma9VAqB5A22KCEEin9R
The second connection read :user from configuration_hash, but the key is
:username, so it only worked where the OS user matches the DB user. CI
connects as postgres through DATABASE_URL and failed. Use the test
connection's own conninfo, and only skip the callback if it was added.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L4Pma9VAqB5A22KCEEin9R
@dadachi
dadachi merged commit 9d1cc1f into main Oct 4, 2026
3 checks passed
@dadachi
dadachi deleted the fix/device-registration-race branch October 4, 2026 22:17
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