A dedicated IDLE Processing State for Idle Retries, handled like RE_REQUESTED
GStraccini-bot updates a pull request’s branch only when it processes that pull request, and it moves on
to another open pull request in the same repository only when one closes (checkForOtherPullRequests).
Pull requests can therefore sit outdated, waiting for a branch update that never starts. Each hour, the
idle-pull-requests Worker gives the first due Idle Pull Request an Idle Retry
(github_pull_requests_idle_view, in its round-robin order across repositories). It marks that pull request with a new ProcessingState, IDLE, and
doesn’t reuse RE_REQUESTED. IDLE exists so that a time-based Idle Retry can be told apart from an
Event-driven re-request in the database, the processing counts, and the dashboards. The bot deliberately
handles it exactly like RE_REQUESTED: IDLE sits in the same claim list, and the pull request handler
doesn’t read the state. Don’t “simplify” it away as redundant, and don’t give it its own bot logic without
revisiting this decision.
Consequences
IDLEis a value ofgithub_pull_requests.ProcessingStateonly. Othergithub_*tables keep their enums, even though the bot’s claim list containingIDLEis shared across all of them.- The webhooks schema owns the enum (
Sql/0038). Every hardcoded list of states has to includeIDLEas a pending state, or idle pull requests silently vanish from counts:- webhooks:
processing-state.php,openapi.json - projects-monitor: badge, state lists, presets
- webhooks:
- Linear backoff with a cap:
github_pull_requests.IdleRetryCount(default 0) increases by 1 on every Idle Retry. APROCESSEDopen pull request is due onceUpdatedAt <= NOW() - INTERVAL (IdleRetryCount + 1) DAY, and only whileIdleRetryCount < 10. That gives 10 retries over about 55 idle days. The step, base and cap are literals in the view. - The view shows every Idle Pull Request, not just the due ones: every open
PROCESSEDpull request idle for at least 1 day, each with anIdleStatus:DUE: the worker takes the first of theseWAITING: inside its backoff windowGAVE_UP: at the cap
Dashboards can therefore show retries that are pending and pull requests the automation gave up on. Since
Sql/0039the view also joinsgithub_repositories(most recent row per owner/name) to exposeRepositoryPrivate. It is a left join (Sql/0040): a repository with no row there yieldsNULL, and the pull request stays an Idle Pull Request. The inner joinSql/0039started with dropped most of them, since most repositories predate the rows this service writes. - Round-robin across repositories (
Sql/0042): the view is ordered byRepositoryIdleRank(the pull request’s position among its repository’s idle ones: oldestDUEfirst, thenWAITING, thenGAVE_UP), thenRepositoryLastPullRequestUpdate(the most recentUpdatedAtamong all of the repository’s pull requests), thenUpdatedAt. Ordered byUpdatedAtalone, the repository with the oldest pull requests got every Idle Retry in a row; now each repository’s first pull request is listed before any repository’s second, and the repository that has been quiet the longest goes first. Once the bot processes an Idle Retry, that repository’s last update jumps to that moment and it moves to the back of the line. The activity key is deliberately any pull request update, not only Idle Retries: a repository with daily activity is already being moved along by the normal workflow (a closing pull request re-requests the next one), so it waits; a pull request stuck in the middle of that chain is a manual check. The value is derived fromgithub_pull_requestsrather than stored ongithub_repositories, which has no unique key per repository and no row at all for most of them. - Least retried first (
Sql/0043):IdleStatus(DUE,WAITING,GAVE_UP) and thenIdleRetryCountcome before the round-robin keys, andIdleRetryCountcomes before age insideRepositoryIdleRank. Ranked by age alone, a retried pull request won: its longer backoff made it older than the never-retried ones next to it by the time it wasDUEagain, so it got its third Idle Retry while they waited for their first. Now everyDUEpull request with 0 retries comes before any with 1, and so on, round-robin inside each count. A much-retried pull request can wait while newly idle ones keep arriving; that is the intent — a first nudge for everyone before a second for anyone. - Reset on real activity: every Event-driven write sets
IdleRetryCount = 0. That includesedited, and the repository-wide writes for branch protection rules and rulesets. The bot’s ownRE_REQUESTEDinupsertPullRequest, the next-pull-request chain, resets it too. The reset is an explicit column in each writer’sUPDATE. A trigger would cover future writers automatically, but it can’t beSQL SECURITY INVOKER(see ADR-0001) and it’s invisible when reading the PHP. - The worker doesn’t bump
UpdatedAt(it writesUpdatedAt = UpdatedAt), so the pull request stays near the front of the bot’sORDER BY UpdatedAt ASCclaim queue. LeavingPROCESSEDis what takes it out of the due set. When the bot finishes it,UpdatedAtupdates automatically, which starts the next, longer wait. - Rollout order: webhooks first, then gstraccini-bot-service.
- The bot’s reset writes
IdleRetryCount, so it can’t deploy before migration 0038 adds that column.upsertPullRequestwould hitdie()on the unknown column. - The reverse window is harmless:
IDLErows, at most one per hour, wait unclaimed until the bot learns the state. - Inside webhooks’ own deploy, the worker can run before 0038. MariaDB’s strict
sql_modemakes that write fail, and the worker retries on restart. Under a non-strict mode, a stranded row would show up asProcessingState = '', so check for that after the deploy.
- The bot’s reset writes