Skip to content

Device login: concurrent polls of a code no longer hold the pool - #832

Queued
schmitch wants to merge 1 commit into
mainfrom
claude/beautiful-cori-sa3n2n
Queued

schmitch wants to merge 1 commit into
mainfrom
claude/beautiful-cori-sa3n2n

Conversation

@schmitch

@schmitch schmitch commented Nov 3, 2026

Copy link
Copy Markdown
Member

Fixes #794

Problem

poll locked the request row FOR UPDATE. For an approved request, collect then asked the pool for a second connection (user_dao::find_user_by_id(&state.pool, …), and repository_dao::find_repository_by_name(&state.pool, …) for a scope that names repositories). Meanwhile every other poll of the same device_code held a connection while it waited in PostgreSQL for the row lock. With about as many concurrent polls as the pool has connections, the lock holder could not get its second connection. After the acquire timeout it answered 503, and the next poll took the lock and needed a second connection too. Every other database-backed request on the instance waited for the acquire timeout and failed the same way, until the request expired.

Change

  • device_auth_dao::lock_for_poll uses FOR UPDATE NOWAIT and returns PollLock::{Locked, Busy, Unknown}. When another transaction holds the row (SQLSTATE 55P03) the answer is Busy, which comes back at once.
  • poll answers Busy with slow_down (RFC 8628 §3.5) and drops the aborted transaction, so a concurrent poll never waits for the lock while it holds a connection. The interval is not raised in this case, because the row is locked.
  • collect reads the user and resolves the scope's repositories on the transaction's own connection, so a poll never waits for a second connection while it holds the lock. repository_dao::find_repository_by_name now takes any PgExecutor (same query, so existing callers are unchanged).
  • .sqlx is regenerated for the NOWAIT query.
  • The module doc and the deployment guide's endpoint table name the new slow_down case.

Test

crates/server/tests/device_auth.rs, concurrent_polls_of_an_approved_code_leave_the_pool_alone: on a server whose pool has 2 connections (database.max_connections = 2, the default 10 s acquire timeout), six concurrent polls of one approved code, with a scope naming a repository so the repository lookup runs. Alongside them, a signed-in request lists the user's tokens. The test checks that exactly one poll gets the token, that the others answer slow_down or expired_token, and that every poll and the request beside them answer within 5 s. The harness gets common::sized_pool(max_connections, acquire_timeout), which builds the pool the way main does.

Without the fix the test fails: a request beside the polls took 10.051845858s, which is the acquire timeout. With it, all 9 device_auth tests pass. cargo fmt --check, cargo clippy --workspace --all-targets -D warnings, cargo sqlx prepare --check and cargo xtask changelog --check pass too.

Changelog

No fragment. The device login is new in 2.2.0 (#649) and has not been released, so 2.2.0's notes have no bug to list as fixed. Its feature entry (docs/changes/2.2.0/649.feature.md) needs no change. The issue moved from 2.3.0 to the 2.2.0 milestone as a release blocker.


Generated by Claude Code

A poll locked its request FOR UPDATE and, for an approved one, asked the
pool for a second connection to read the user and the scope's
repositories, while every other poll of the same code held a connection
waiting for that lock. A handful of concurrent polls took the whole pool,
and every request of the instance waited for the acquire timeout and
failed, until the request expired.

The poll now locks the request FOR UPDATE NOWAIT and answers slow_down
at once when another poll holds it, and collect reads the user and the
repositories on the transaction's own connection, so a poll never waits
for a connection or a lock while it holds one. find_repository_by_name
takes any executor for that.

The test polls one approved code six times at once on a pool of two
connections: one poll gets the token, every poll answers within seconds,
and a request beside them is served (before: 10 s, the acquire timeout).

Fixes #794

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012e6DJackg4JuAo1ir7BJjS
@schmitch
schmitch added this pull request to the merge queue Nov 3, 2026
Any commits made after this event will not be merged.

This branch has not been deployed

No deployments
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.

Device login: concurrent polls of an approved code hold every database connection, and the instance stops answering

2 participants