From c28e8c1805638f7bba9fd7cab229f63e7774a58c Mon Sep 17 00:00:00 2001 From: Eddie A Tejeda <669988+eddietejeda@users.noreply.github.com> Date: Sat, 24 Oct 2026 15:44:42 -0700 Subject: [PATCH] Ignore Retry-After when polling results for persistence The Results API answers 202 with Retry-After: 1 while a result persists, but persistence usually finishes ~100ms later. Honoring it made results too large to inline ~1s slower than v1.0.4 (1583ms vs 552ms median for a 56k-row query). Keep the 50ms backoff instead. --- CHANGELOG.md | 2 +- pkg/hotdata/client.go | 15 +++++++-------- pkg/plugin/datasource_test.go | 7 ++++--- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12e087f..60666dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ - Read query results as Arrow directly from `POST /v1/query`, so a fast query takes one request instead of two. Async and too-large results still come from the Results API. - Show the real error for failed queries: read `error_message` from query runs and failed results, and show the `message` and `code` from Hotdata error bodies instead of raw JSON. - Follow pagination on `/v1/databases` and `/v1/information_schema`, so the database picker and SQL autocomplete list every database and table, not just the first page. -- Handle the Results API's `202` (still persisting, honoring `Retry-After`) and `409` (failed) responses. +- Handle the Results API's `202` (still persisting) and `409` (failed) responses. Polling keeps its fast 50ms backoff rather than the server's `Retry-After: 1`, which would add a second to large results. - The health check validates credentials with one `GET /v1/databases?limit=1` instead of listing every database. ## 1.0.4 diff --git a/pkg/hotdata/client.go b/pkg/hotdata/client.go index e5f301e..4640af8 100644 --- a/pkg/hotdata/client.go +++ b/pkg/hotdata/client.go @@ -214,9 +214,12 @@ func (c *Client) pollRun(ctx context.Context, runID, databaseID string) (RunMeta // is database-scoped. The caller must close the reader. // // A result that is still being persisted answers 202 with a JSON body -// {"result_id":…,"status":"processing"} and a Retry-After header, so poll -// until the response is an actual Arrow stream, backing off up to PollMax and -// honoring ctx. A failed result answers 409, which surfaces as an APIError. +// {"result_id":…,"status":"processing"}, so poll until the response is an +// actual Arrow stream, backing off up to PollMax and honoring ctx. A failed +// result answers 409, which surfaces as an APIError. +// +// The 202 carries Retry-After: 1, but persistence usually finishes ~100ms +// later; honoring it added a full second to every result too large to inline. func (c *Client) FetchResultArrow(ctx context.Context, resultID, databaseID string) (io.ReadCloser, error) { // Persistence of a just-minted result usually takes milliseconds, so start // with a much shorter backoff than the run-poll cadence. @@ -258,14 +261,10 @@ func (c *Client) FetchResultArrow(ctx context.Context, resultID, databaseID stri return nil, fmt.Errorf("result not available (status %q): %s", st.Status, msg) } - wait := retryAfterDelay(resp.Header.Get("Retry-After"), interval) - if wait > c.PollMax { - wait = c.PollMax - } select { case <-ctx.Done(): return nil, ctx.Err() - case <-time.After(wait): + case <-time.After(interval): } if interval = interval * 3 / 2; interval > c.PollMax { interval = c.PollMax diff --git a/pkg/plugin/datasource_test.go b/pkg/plugin/datasource_test.go index bf1d374..dfb5bac 100644 --- a/pkg/plugin/datasource_test.go +++ b/pkg/plugin/datasource_test.go @@ -181,11 +181,12 @@ func newContractServer(t *testing.T, opts serverOpts) (*httptest.Server, *atomic w.WriteHeader(http.StatusBadRequest) return } - // The result is not persisted yet: 202 with a JSON placeholder and - // Retry-After before the Arrow stream is served. + // The result is not persisted yet: 202 with a JSON placeholder before + // the Arrow stream is served. Production sends Retry-After: 1, which + // the client deliberately ignores in favor of its faster backoff. if resultFetches.Add(1) <= opts.resultNotReady { w.Header().Set("Content-Type", "application/json") - w.Header().Set("Retry-After", "0") + w.Header().Set("Retry-After", "1") w.WriteHeader(http.StatusAccepted) _, _ = w.Write([]byte(`{"result_id":"rslt1","status":"processing"}`)) return