Files
werkator/docs/prs/2026-10-12-PR#155-gitea-status-retry.md
claude-code faa86e060b
werkator build succeeded after 03:08
werkator/deploy build succeeded after 00:09
A commit status lost on a dead connection is sent once more (v1.41.6) (#155)
Co-authored-by: claude-code <claude-code@javagil.de>
2026-10-12 15:13:51 +02:00

4.1 KiB

PR#155: A commit status lost on a dead connection is sent once more

WARNING: This document describes only the change applied in this PR. It may already be outdated once the next PR is merged. Historic PR-documentation is not maintained along with new PRs — treat it as a snapshot, not as current documentation.

Problem

werkcrappit PR#68 stayed unmerged although werkrevisor gave GREEN and scheduled the merge. Its review build claude-code/demo-links@ai-review succeeded on 2026-10-12 at 04:44:31 UTC, after 34 s. Gitea got "build queued" and "build running" for ai-review/gate on the head, but never the final success. The combined status stayed pending, so Gitea never merged.

The journal of mih09 holds the cause, at the very moment the build ended:

Could not publish Gitea status for 4a3109bc…: I/O error on POST request for
"https://git.javagil.de/api/v1/repos/mi/werkcrappit/statuses/4a3109bc…": HTTP/1.1 header parser received no bytes

GiteaClient keeps one JDK HttpClient and with it a pool of open connections. A request answered by no byte at all is what a pooled connection looks like that the forge's proxy had closed meanwhile. The JDK client never repeats a POST by itself, and publishStatus tried exactly once and only logged the failure. Nothing sends a lost status later, so one dropped connection kept a required check pending for good.

Non-Goals

  • No reconciliation of statuses already lost: a restart of the build sends them again.
  • No retry of an HTTP error answer: the forge decided, and the same request would get the same answer.
  • No change of the connection pool or of its keep-alive time: the proxy's own time is not known to Werkator.

The Scenarios

Feature: commit statuses reach the forge

Scenario#155.01: A status whose request is answered by nothing is sent once more!

So that one dead pooled connection does not leave a required check pending for good.

  • Given Werkator publishes a commit status
    • and the first request is answered by nothing at all
  • When that request fails with an I/O error
  • Then Werkator sends the same status once more, half a second later
    • and the status arrives
Verified by

Scenario#155.02: A second request answered by nothing ends the attempts!

So that a forge that cannot be reached costs at most two requests per status.

  • Given Werkator publishes a commit status
    • and every request is answered by nothing at all
  • When the second request fails with an I/O error too
  • Then Werkator sends no third request
    • and logs a warning
Verified by

Scenario#155.03: A status the forge answered with an HTTP error is not sent again!

So that a status the forge refused is not repeated: the same request would get the same answer.

  • Given Werkator publishes a commit status
  • When Gitea answers with HTTP 500
  • Then Werkator sends no second request
    • and logs a warning
Verified by

The Solution

  • publishStatus catches ResourceAccessException, Spring's I/O error, apart from the other client errors. retryStatus waits 500 ms, logs the retry and sends the same body once more.
  • Sending a status twice is harmless: Gitea records the same state again.
  • The POST itself moved into postStatus, so both attempts share it and each method stays small.
  • The retry waits on the thread that publishes the status, and only after an I/O error. For "build queued" that is the watcher's thread, which then polls half a second later.
  • WireMock's Fault.EMPTY_RESPONSE stands for the dead connection in the tests.
  • Werkbaum: a node under #pr.
  • Version 1.41.6, with an entry in the release notes.