-
Couldn't load subscription status.
- Fork 978
feat: retry on 429 #2244
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
feat: retry on 429 #2244
Changes from all commits
b8e75fa
8c2f4ff
6147653
ae4b2e9
7fe6a48
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,16 +1,22 @@ | ||||||||||||||||||||||||||||
| package runner | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| import ( | ||||||||||||||||||||||||||||
| "context" | ||||||||||||||||||||||||||||
| "fmt" | ||||||||||||||||||||||||||||
| "net/http" | ||||||||||||||||||||||||||||
| "net/http/httptest" | ||||||||||||||||||||||||||||
| "os" | ||||||||||||||||||||||||||||
| "strings" | ||||||||||||||||||||||||||||
| "sync" | ||||||||||||||||||||||||||||
| "sync/atomic" | ||||||||||||||||||||||||||||
| "testing" | ||||||||||||||||||||||||||||
| "time" | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| _ "github.com/projectdiscovery/fdmax/autofdmax" | ||||||||||||||||||||||||||||
| "github.com/projectdiscovery/httpx/common/httpx" | ||||||||||||||||||||||||||||
| "github.com/projectdiscovery/mapcidr/asn" | ||||||||||||||||||||||||||||
| stringsutil "github.com/projectdiscovery/utils/strings" | ||||||||||||||||||||||||||||
| syncutil "github.com/projectdiscovery/utils/sync" | ||||||||||||||||||||||||||||
| "github.com/stretchr/testify/require" | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
@@ -227,10 +233,10 @@ func TestCreateNetworkpolicyInstance_AllowDenyFlags(t *testing.T) { | |||||||||||||||||||||||||||
| runner := &Runner{} | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| tests := []struct { | ||||||||||||||||||||||||||||
| name string | ||||||||||||||||||||||||||||
| allow []string | ||||||||||||||||||||||||||||
| deny []string | ||||||||||||||||||||||||||||
| testCases []struct { | ||||||||||||||||||||||||||||
| name string | ||||||||||||||||||||||||||||
| allow []string | ||||||||||||||||||||||||||||
| deny []string | ||||||||||||||||||||||||||||
| testCases []struct { | ||||||||||||||||||||||||||||
| ip string | ||||||||||||||||||||||||||||
| expected bool | ||||||||||||||||||||||||||||
| reason string | ||||||||||||||||||||||||||||
|
|
@@ -312,3 +318,92 @@ func TestCreateNetworkpolicyInstance_AllowDenyFlags(t *testing.T) { | |||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| func TestRunner_Process_And_RetryLoop(t *testing.T) { | ||||||||||||||||||||||||||||
| var hits1, hits2 int32 | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // srv1: returns 429 for the first 3 requests, and 200 on the 4th request | ||||||||||||||||||||||||||||
| srv1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||||||||||||||||||||||||||
| if atomic.AddInt32(&hits1, 1) != 4 { | ||||||||||||||||||||||||||||
| w.WriteHeader(http.StatusTooManyRequests) | ||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| w.WriteHeader(http.StatusOK) | ||||||||||||||||||||||||||||
| })) | ||||||||||||||||||||||||||||
| defer srv1.Close() | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // srv2: returns 429 for the first 2 requests, and 200 on the 3rd request | ||||||||||||||||||||||||||||
| srv2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||||||||||||||||||||||||||
| if atomic.AddInt32(&hits2, 1) != 3 { | ||||||||||||||||||||||||||||
| w.WriteHeader(http.StatusTooManyRequests) | ||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| w.WriteHeader(http.StatusOK) | ||||||||||||||||||||||||||||
| })) | ||||||||||||||||||||||||||||
| defer srv2.Close() | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| r, err := New(&Options{ | ||||||||||||||||||||||||||||
| Threads: 1, | ||||||||||||||||||||||||||||
| RetryRounds: 2, | ||||||||||||||||||||||||||||
| RetryDelay: 5, | ||||||||||||||||||||||||||||
| Timeout: 3, | ||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||
| require.NoError(t, err) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| output := make(chan Result) | ||||||||||||||||||||||||||||
| retryCh := make(chan retryJob) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| _, drainedCh := r.retryLoop(context.Background(), retryCh, output, r.analyze) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| wg, _ := syncutil.New(syncutil.WithSize(r.options.Threads)) | ||||||||||||||||||||||||||||
| so := r.scanopts.Clone() | ||||||||||||||||||||||||||||
| so.Methods = []string{"GET"} | ||||||||||||||||||||||||||||
| so.TLSProbe = false | ||||||||||||||||||||||||||||
| so.CSPProbe = false | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| seed := map[string]string{ | ||||||||||||||||||||||||||||
| "srv1": srv1.URL, | ||||||||||||||||||||||||||||
| "srv2": srv2.URL, | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| var drainWG sync.WaitGroup | ||||||||||||||||||||||||||||
| drainWG.Add(1) | ||||||||||||||||||||||||||||
| var s1n429, s1n200, s2n429, s2n200 int | ||||||||||||||||||||||||||||
| go func() { | ||||||||||||||||||||||||||||
| defer drainWG.Done() | ||||||||||||||||||||||||||||
| for res := range output { | ||||||||||||||||||||||||||||
| switch res.StatusCode { | ||||||||||||||||||||||||||||
| case http.StatusTooManyRequests: | ||||||||||||||||||||||||||||
| if res.URL == srv1.URL { | ||||||||||||||||||||||||||||
| s1n429++ | ||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||
| s2n429++ | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| case http.StatusOK: | ||||||||||||||||||||||||||||
| if res.URL == srv1.URL { | ||||||||||||||||||||||||||||
| s1n200++ | ||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||
| s2n200++ | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| }() | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| for _, url := range seed { | ||||||||||||||||||||||||||||
| r.process(url, wg, r.hp, httpx.HTTP, so, output, retryCh) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| wg.Wait() | ||||||||||||||||||||||||||||
| <-drainedCh | ||||||||||||||||||||||||||||
| close(output) | ||||||||||||||||||||||||||||
| drainWG.Wait() | ||||||||||||||||||||||||||||
|
Comment on lines
+396
to
+399
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Guard against hangs: time-bound the wait for drainedCh If the retry loop logic regresses, Apply this diff: - wg.Wait()
- <-drainedCh
- close(output)
+ wg.Wait()
+ select {
+ case <-drainedCh:
+ // drained successfully
+ case <-time.After(5 * time.Second):
+ t.Fatalf("retry loop did not drain within timeout")
+ }
+ close(output)📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Verify expected results | ||||||||||||||||||||||||||||
| // srv1: should have 3x 429 responses and no 200 (never succeeds within retries) | ||||||||||||||||||||||||||||
| require.Equal(t, 3, s1n429) | ||||||||||||||||||||||||||||
| require.Equal(t, 0, s1n200) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // srv2: should have 2x 429 responses and 1x 200 (succeeds on 3rd attempt) | ||||||||||||||||||||||||||||
| require.Equal(t, 2, s2n429) | ||||||||||||||||||||||||||||
| require.Equal(t, 1, s2n200) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Don’t ignore the retry loop’s stop handle; avoid a goroutine leak
retryLoopappears to return a stop/cleanup handle (you currently discard it with_). If the loop isn’t explicitly stopped, the goroutine can remain blocked onretryChafter the test finishes, causing leaks and potential flakiness in suite runs.Apply this diff to wire cancellation correctly and ensure cleanup even on early test failures:
If you want to double-check the signature and the semantics of the first return value:
🏁 Script executed:
Length of output: 342
🏁 Script executed:
Length of output: 691
🏁 Script executed:
Length of output: 1518
🏁 Script executed:
Length of output: 1276
Wire up the retryLoop stop handle to prevent goroutine leaks
The test currently invokes retryLoop with a background context and discards its first return value (
stop func()), so the internal goroutine never sees a cancellation signal. This can leave the loop blocked onretryChafter the test ends, leading to leaked goroutines and flakiness.• File: runner/runner_test.go
• Lines: 356–357
Suggested change:
📝 Committable suggestion
🤖 Prompt for AI Agents