Third section of #141. start no longer fetches the identity provider's
discovery document before it serves. Setup returns a Config whose two
discovery values are empty, and Discover runs beside the server from
before the listener opens: it fetches the document, publishes the
verifier and then the sign-in endpoints, logs "identity provider
discovery succeeded; sign-in is available", and returns. Until it has,
/login and /callback answer a styled 503 "Sign-in unavailable" and
/register "Registration unavailable", each with Retry-After: 5 and a
"Try again" action, and a sign-in request wakes the loop's wait. A
document whose issuer is not the configured one, or that names no
end-session endpoint (or one that does not parse as an absolute URL),
publishes nothing that sign-in reads and is retried at the cap.
The two values sit behind two atomic.Pointers in auth.Config. The
sign-in value holds a pointer to the verifier value it was published
with, and is stored only after the verifier, from the same document, so
a request never pairs endpoints from one document with a verifier from
another. Re-derivation of roles reads the verifier alone, so it runs
while sign-in still waits for an end-session endpoint; POST /logout
before discovery ends the console's session, lands on /, and revokes
nothing. A Config built by hand, as most tests build theirs, reads its
exported fields as it did.
internal/retryloop is the loop both discovery and, in a later section,
the Temporal connection run: waits from 0.5 seconds doubling to a
15-second cap, plus up to half again of jitter; a wake ends a wait no
sooner than a second after the last attempt began; a failure marked
AtCap makes only the next wait's base the cap, and a wake does not end
that wait. Its log decay writes the first failure, a changed error at
most once per 30 seconds, otherwise one line per 5 minutes, Warn until
the escalation time and Error from it, with the line at the escalation
time written at once; every line's attempts is the attempts so far.
The htmx 5xx toast reads "Server error. Try again later."
Corrections to the change text, made while building: the wait after
an AtCap failure is the cap plus the jitter (the design's bound of
about 23 seconds after an outage already counted the jitter), and every
log line's attempts is the number of attempts so far (one meaning on
every line, matching elapsed), where the design said the cap alone and
the attempts since the last line.
internal/server/testdata/routes-not-ready.golden is new: it differs from
routes.golden in the four sign-in rows only. routes.golden and
route_pages.golden do not change.
Full suite on reset databases: 4422 pass, 15 skip, none failed. The console booted on the
test stack and logged the discovery success. go test -race -p 1 over
internal/retryloop, internal/auth and internal/server after
reset-test-db.sh: every package passes, no data race. make lint passes. Breaks, each caught:
- LoginHandler's gate after RenewToken
- the gate without the wake
- the gate answers through http.Error
- the gate answers 500 in place of 503
- the gate without Retry-After
- /register without the gate
- the sign-in value published from a document without an end-session endpoint
- the sign-in value stored outside the issuer check
- the verifier published only together with the sign-in value
- the verifier published from a document with another issuer
- an unusable document returned as a plain error
- the attempt on http.DefaultClient
- LogoutHandler before publication answers 500
- LogoutHandler before publication redirects without destroying the session
- LogoutHandler with the verifier and no end-session endpoint revokes nothing
- refreshIdentity ends the session when no verifier is published
- Run without the cap
- Run without the jitter
- a wake that ends an AtCap wait
- the wait ignores ctx.Done()
- the discovery holder is a plain field in place of an atomic.Pointer
- a Failure's level ignored
- the decay without its five-minute rule
- the escalation never reached
- the changed-text rule without its thirty-second floor
- a wake that ignores the one-second gap
- answer() without the Retry-After line
- re-derivation reads the exported fields in place of the holder
- a level Error line written at once whatever the last line
- an AtCap failure ratchets the doubling to the cap
- the discovery log lines without the issuer attribute
- the failure lines with a fixed attempts attribute
- an end-session endpoint that does not parse or is not absolute is accepted
- Run attempts once on a context that has ended
- a Failure returned by pointer drops its message and level