Skip to content

Conversation

@salemalem
Copy link

No description provided.

@salemalem
Copy link
Author

guys?

@TheEdgeOfRage
Copy link
Member

Hey @salemalem

Thank you for your PR 🙏 The whole team is currently out of office on a conference, so we will not be able to review this until next week. Sorry about the delay

@salemalem
Copy link
Author

@TheEdgeOfRage which conference are you referring to? ETH DevCon Argentina?

if maxRetries != 0 && errAttempts >= maxRetries {
return nil, fmt.Errorf("%w. %s", ErrorRetriesExhausted, err.Error())
}
fmt.Fprintln(os.Stderr, "failed to retrieve results. Retrying...\n", err)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why remove the print here?

dune/http.go Outdated
remStr := h.Get("X-RateLimit-Remaining")
resetStr := h.Get("X-RateLimit-Reset")

var lim, rem int
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to shorten variable names :)

Suggested change
var lim, rem int
var limit, remaining int

dune/http.go Outdated
Comment on lines 50 to 51
InitialBackoff: 500 * time.Millisecond,
MaxBackoff: 5 * time.Second,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that rate limits apply on a minute basis, I think it makes sense to bump these a bit higher, as it's very possible that you'll still be rate limited after waiting only a couple of seconds

dune/http.go Outdated
return &RateLimit{Limit: lim, Remaining: rem, Reset: reset}
}

func nextBackoff(attempt int, p RetryPolicy) time.Duration {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And would you mind moving the retry and backoff code into a separate file please (e.g. dune/retries.go) 🙏

Suggested change
func nextBackoff(attempt int, p RetryPolicy) time.Duration {
func (p RetryPolicy) nextBackoff(attempt int) time.Duration {

dune/http.go Outdated
if err != nil {
return nil, fmt.Errorf("failed to read error response body: %w", err)
snippetBytes, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
var er ErrorResponse
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var er ErrorResponse
var errorResp ErrorResponse

dune/http.go Outdated
return nil, fmt.Errorf("failed to read error response body: %w", err)
snippetBytes, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
var er ErrorResponse
_ = json.Unmarshal(snippetBytes, &er)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This unmarshal could fail, since the body is capped at 1024 characters, so you'll need to handle the error here in that case

@TheEdgeOfRage
Copy link
Member

@salemalem I'm sorry about this, I seem to have accidentally deleted the notification for this PR and forgot about it 😬

Yeah, it was DevConnect :)

@salemalem
Copy link
Author

@TheEdgeOfRage ok let me review your suggestions and get back to you.

@cursor
Copy link

cursor bot commented Dec 15, 2025

PR Summary

Adds a configurable retry policy with exponential backoff and enhances HTTP/error handling (APIError, rate limits, Retry-After), applying backoff in execution result polling.

  • HTTP:
    • Introduces APIError and RateLimit with parseRateLimitHeaders to surface status, body snippet, and rate limit metadata.
    • Reworks httpRequest to use defaultRetryPolicy for network errors and retryable statuses (429, 5xx), honoring Retry-After and returning detailed errors.
  • Execution:
    • Updates WaitGetResults to track errAttempts, apply exponential backoff between retries, and reset attempts on success.
  • Retry Policy:
    • Adds dune/retries.go defining RetryPolicy, defaultRetryPolicy, and NextBackoff (exponential with jitter).

Written by Cursor Bugbot for commit 7646e87. Configure here.

@salemalem
Copy link
Author

@TheEdgeOfRage thanks for the thorough review — I’ve addressed your points and aligned with house style:

  • Reintroduced stderr print in execution.WaitGetResults and switched to method-based backoff via defaultRetryPolicy.NextBackoff(...).
  • Renamed rate-limit variables to limit and remaining in parseRateLimitHeaders.
  • Moved retry/backoff into dune/retries.go and changed to func (p RetryPolicy) NextBackoff(int) time.Duration.
  • Bumped backoff windows to reflect minute-level limits: initialBackoff = 2s, maxBackoff = 60s, maxAttempts = 5 (with small jitter). Happy to tune these if you prefer different values.
  • Error parsing in httpRequest:
    • Switched ererrorResp.
    • Guarded json.Unmarshal and fall back to the raw 1KB body snippet if truncated or malformed.
    • Return structured APIError with statusCode, statusText, bodySnippet, rateLimit, and retryAfter.
    • Honor Retry-After when larger than computed backoff; retry for 429, 5xx and network errors.

I’ll resolve the current merge conflicts and push the updates. If you’d like a different jitter strategy or backoff defaults, I can adjust quickly. Thanks again!

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment @cursor review or bugbot run to trigger another review on this PR

dune/http.go Outdated
}
time.Sleep(sleep)
attempt++
continue
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: POST request body empty on retry

When retrying a request after receiving a retryable status code (429, 5xx), the code reuses the same http.Request object without resetting its body. The request body (req.Body) is consumed by http.DefaultClient.Do() on the first attempt. On subsequent retry attempts, the body is empty because it has already been read. POST requests with JSON payloads (like QueryExecute, SQLExecute, QueryPipelineExecute) will send empty bodies on retry, causing silent failures. The fix would require calling req.GetBody() to regenerate the body before each retry.

Additional Locations (1)

Fix in Cursor Fix in Web

dune/http.go Outdated
}

if resp.StatusCode != 200 {
defer resp.Body.Close()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Defer in loop accumulates unclosed response bodies

The defer resp.Body.Close() statement is inside the retry loop. Each time the loop iterates with a non-200 status code, a new defer is added without the previous response body being closed immediately. This keeps multiple response bodies and their associated resources (TCP connections, file descriptors) open until the function returns, which could exhaust connection pool slots or cause "too many open files" errors during extended retry sequences.

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants