Skip to content

Commit 903a267

Browse files
feat: [DGP-730] jitter TestAPI polling from 0.5 to 1.5 of the pollInterval (#401)
1 parent a265818 commit 903a267

File tree

2 files changed

+108
-2
lines changed

2 files changed

+108
-2
lines changed

pkg/apiclients/testapi/testapi.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"context"
77
"errors"
88
"fmt"
9+
"math/rand"
910
"net/http"
1011
"net/url"
1112
"sync"
@@ -24,6 +25,7 @@ type config struct {
2425
APIVersion string
2526
Logger *zerolog.Logger
2627
lowLevelClientOptions []ClientOption // Options for the oapi-codegen client
28+
jitterFunc func(time.Duration) time.Duration
2729
}
2830

2931
// ConfigOption allows setting custom parameters during construction
@@ -85,6 +87,13 @@ func WithCustomRequestEditorFn(fn RequestEditorFn) ConfigOption {
8587
}
8688
}
8789

90+
// WithJitterFunc allows setting a custom jitter function for polling.
91+
func WithJitterFunc(fn func(time.Duration) time.Duration) ConfigOption {
92+
return func(c *config) {
93+
c.jitterFunc = fn
94+
}
95+
}
96+
8897
type client struct {
8998
lowLevelClient ClientWithResponsesInterface
9099
config config
@@ -235,6 +244,7 @@ func NewTestClient(serverBaseUrl string, options ...ConfigOption) (TestClient, e
235244
cfg := config{
236245
PollInterval: DefaultPollInterval,
237246
APIVersion: DefaultAPIVersion,
247+
jitterFunc: Jitter,
238248
}
239249

240250
for _, opt := range options {
@@ -441,7 +451,9 @@ func contextCanceledError(operationDescription string, contextError error) error
441451

442452
// Query the job endpoint until we're redirected to its 'related' link containing results
443453
func (h *testHandle) pollJobToCompletion(ctx context.Context) (*uuid.UUID, error) {
444-
ticker := time.NewTicker(h.client.config.PollInterval)
454+
cfg := h.client.config
455+
456+
ticker := time.NewTicker(cfg.PollInterval)
445457
defer ticker.Stop()
446458

447459
getJobParams := &GetJobParams{Version: h.client.config.APIVersion}
@@ -463,6 +475,7 @@ func (h *testHandle) pollJobToCompletion(ctx context.Context) (*uuid.UUID, error
463475
if stopPolling {
464476
return nil, jobErr
465477
}
478+
ticker.Reset(cfg.jitterFunc(cfg.PollInterval))
466479
continue
467480

468481
case http.StatusSeeOther:
@@ -478,6 +491,7 @@ func (h *testHandle) pollJobToCompletion(ctx context.Context) (*uuid.UUID, error
478491
Str("orgID", h.orgID.String()).
479492
Str("jobID", h.jobID.String()).
480493
Msg("Job polling returned 404 Not Found, continuing polling in case of delayed job creation")
494+
ticker.Reset(cfg.jitterFunc(cfg.PollInterval))
481495
continue
482496

483497
default:
@@ -691,3 +705,13 @@ func (r *testResult) handleFindingsError(err error, partialFindings []FindingDat
691705
func ptr[T any](v T) *T {
692706
return &v
693707
}
708+
709+
// Jitter returns a random duration between 0.5 and 1.5 of the given duration.
710+
func Jitter(d time.Duration) time.Duration {
711+
if d <= 0 {
712+
return d
713+
}
714+
minDur := int64(float64(d) * 0.5)
715+
maxDur := int64(float64(d) * 1.5)
716+
return time.Duration(rand.Int63n(maxDur-minDur) + minDur)
717+
}

pkg/apiclients/testapi/testapi_test.go

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ func Test_StartTest_Success(t *testing.T) {
180180
// Create our test client
181181
testHTTPClient := newTestHTTPClient(t, server)
182182
testClient, err := testapi.NewTestClient(server.URL,
183-
testapi.WithPollInterval(1*time.Second), // short poll interval for faster tests
183+
testapi.WithPollInterval(1*time.Second),
184184
testapi.WithCustomHTTPClient(testHTTPClient),
185185
)
186186
require.NoError(t, err)
@@ -1047,3 +1047,85 @@ func assertTestFinishedWithOutcomeErrorsAndWarnings(t *testing.T, result testapi
10471047
assert.Nil(t, result.GetWarnings())
10481048
}
10491049
}
1050+
1051+
// TestJitter verifies times returned are 0.5-1.5 times the original value,
1052+
// and invalid times are return unmodified.
1053+
func TestJitter(t *testing.T) {
1054+
t.Parallel()
1055+
t.Run("returns original duration for zero or negative input", func(t *testing.T) {
1056+
t.Parallel()
1057+
assert.Equal(t, time.Duration(0), testapi.Jitter(0))
1058+
assert.Equal(t, time.Duration(-1), testapi.Jitter(-1))
1059+
})
1060+
1061+
t.Run("returns duration within 0.5x to 1.5x of input", func(t *testing.T) {
1062+
t.Parallel()
1063+
duration := 100 * time.Millisecond
1064+
minDur := time.Duration(float64(duration) * 0.5)
1065+
maxDur := time.Duration(float64(duration) * 1.5)
1066+
1067+
for range 100 {
1068+
jittered := testapi.Jitter(duration)
1069+
assert.GreaterOrEqual(t, jittered, minDur)
1070+
assert.LessOrEqual(t, jittered, maxDur)
1071+
}
1072+
})
1073+
}
1074+
1075+
// Test_Wait_CallsJitter ensures Jitter is called while polling for job completion.
1076+
func Test_Wait_CallsJitter(t *testing.T) {
1077+
t.Parallel()
1078+
ctx := context.Background()
1079+
1080+
testData := setupTestScenario(t)
1081+
1082+
params := testapi.StartTestParams{
1083+
OrgID: testData.OrgID.String(),
1084+
Subject: testData.TestSubjectCreate,
1085+
}
1086+
1087+
// Mock Jitter
1088+
var jitterCalled bool
1089+
jitterFunc := func(d time.Duration) time.Duration {
1090+
jitterCalled = true
1091+
return d
1092+
}
1093+
1094+
// Mock server handler
1095+
handlerConfig := TestAPIHandlerConfig{
1096+
OrgID: testData.OrgID,
1097+
JobID: testData.JobID,
1098+
TestID: testData.TestID,
1099+
APIVersion: testapi.DefaultAPIVersion,
1100+
PollCounter: testData.PollCounter,
1101+
JobPollResponses: []JobPollResponseConfig{
1102+
{Status: testapi.Pending}, // First poll
1103+
{ShouldRedirect: true}, // Second poll, redirects
1104+
},
1105+
FinalTestResult: FinalTestResultConfig{
1106+
Outcome: testapi.Pass,
1107+
},
1108+
}
1109+
handler := newTestAPIMockHandler(t, handlerConfig)
1110+
server, cleanup := startMockServer(t, handler)
1111+
defer cleanup()
1112+
1113+
// Act
1114+
testHTTPClient := newTestHTTPClient(t, server)
1115+
testClient, err := testapi.NewTestClient(server.URL,
1116+
testapi.WithPollInterval(1*time.Second),
1117+
testapi.WithCustomHTTPClient(testHTTPClient),
1118+
testapi.WithJitterFunc(jitterFunc),
1119+
)
1120+
require.NoError(t, err)
1121+
1122+
handle, err := testClient.StartTest(ctx, params)
1123+
require.NoError(t, err)
1124+
require.NotNil(t, handle)
1125+
1126+
err = handle.Wait(ctx)
1127+
require.NoError(t, err)
1128+
1129+
// Assert
1130+
assert.True(t, jitterCalled)
1131+
}

0 commit comments

Comments
 (0)