Stop the retry backoff from outliving its deadline

A fetch runs under a 30-second per-source budget, but the retry transport
slept without reference to it, so backoff could convert a reportable
provider status into an opaque "context deadline exceeded".

Three changes. The final attempt no longer sleeps: nothing follows it, so
the wait only spent the caller's deadline to return a response already in
hand — up to a second per failed request, across sixteen drivers.
Retry-After is now honoured, in both the delta-seconds and HTTP-date
forms; ignoring it meant retrying a 429 after 250ms and earning another
429, spending the whole retry budget in under a second. And a wait is
skipped entirely when it exceeds the remaining deadline or a 5s cap,
because a retry that lands after the deadline cannot succeed — the
throttled response is surfaced instead so the caller reports what the
provider actually said.

The type moves from google_workspace.go to driver.go, which is where the
other shared driver machinery lives; sixteen drivers construct it and
none of them are Google Workspace. It had no tests, so it has them now.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
This commit is contained in:
Aurélien Sibiril
2026-07-25 09:32:38 +02:00
parent 433aca28cb
commit 92beb20dcc
3 changed files with 399 additions and 45 deletions

View File

@@ -21,10 +21,8 @@
package drivers
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"time"
@@ -50,49 +48,6 @@ func NewGoogleWorkspaceDriver(httpClient *http.Client) *GoogleWorkspaceDriver {
}
}
// retryRoundTripper retries requests that receive 5xx or 429 responses
// with exponential backoff.
type retryRoundTripper struct {
next http.RoundTripper
maxRetries int
}
func (rt *retryRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
transport := rt.next
if transport == nil {
transport = http.DefaultTransport
}
var lastResp *http.Response
for attempt := range rt.maxRetries {
resp, err := transport.RoundTrip(req)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusTooManyRequests && resp.StatusCode < 500 {
return resp, nil
}
// Buffer and re-attach the body so the caller can still read it
// if this turns out to be the final (retry-exhausted) response.
body, _ := io.ReadAll(resp.Body)
_ = resp.Body.Close()
resp.Body = io.NopCloser(bytes.NewReader(body))
lastResp = resp
backoff := time.Duration(250*(1<<attempt)) * time.Millisecond
select {
case <-req.Context().Done():
return nil, req.Context().Err()
case <-time.After(backoff):
}
}
return lastResp, nil
}
func (d *GoogleWorkspaceDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error) {
adminService, err := admin.NewService(ctx, option.WithHTTPClient(d.httpClient))
if err != nil {