-
Notifications
You must be signed in to change notification settings - Fork 0
CXP-533 Improve event feeds context handling #133
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: main
Are you sure you want to change the base?
Changes from all commits
241ff32
5feef6b
33291fc
a39ea9b
6485858
408e9fc
c55843a
ac9596a
9bbf031
278535e
d737007
314bb97
66b1ff2
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 |
|---|---|---|
|
|
@@ -32,6 +32,14 @@ const ( | |
| reportsMaxRetries = 5 | ||
| reportsInitialBackoff = 500 * time.Millisecond | ||
| reportsMaxBackoff = 30 * time.Second | ||
|
|
||
| // reportsPerAttemptTimeout bounds a single ListActivities call, not the retry loop as a | ||
| // whole, so a genuinely hung request is retried like any other transient error instead of | ||
| // being confused with the caller's overall lookup deadline expiring. | ||
| reportsPerAttemptTimeout = 25 * time.Second | ||
|
JavierCarnelli-ConductorOne marked this conversation as resolved.
|
||
|
|
||
| // reportsLookback matches Google's Reports retention window; shared by the event feeds. | ||
| reportsLookback = 180 * 24 * time.Hour | ||
|
JavierCarnelli-ConductorOne marked this conversation as resolved.
|
||
| ) | ||
|
|
||
| // reportsRateLimiter is a simple token-bucket limiter built on the standard library only | ||
|
|
@@ -92,38 +100,93 @@ func (l *reportsRateLimiter) refillLocked() { | |
| l.lastRefill = l.now() | ||
| } | ||
|
|
||
| // listActivitiesFunc mirrors GoogleWorkspaceClient.ListActivities so retryListActivities can be | ||
| // tested with a fake implementation. | ||
| type listActivitiesFunc func(ctx context.Context, userKey, applicationName, eventName, startTime, pageToken, filters string, maxResults int64) (*reportsAdmin.Activities, error) | ||
|
|
||
| // listActivitiesRateLimited waits for the shared filter-query budget, then calls | ||
| // client.ListActivities, retrying with exponential backoff on 429/503 — both are transient, | ||
| // SDK-retryable conditions, not connector bugs (see patterns-error-handling.md). | ||
| // SDK-retryable conditions, not connector bugs (see patterns-error-handling.md). It imposes no | ||
| // per-attempt timeout: use listActivitiesRateLimitedBounded for callers that set their own | ||
| // lookupCtx sub-deadline and want a hung attempt retried instead of just waited out. | ||
| func listActivitiesRateLimited( | ||
| ctx context.Context, | ||
| client *gwclient.GoogleWorkspaceClient, | ||
| userKey, applicationName, eventName, startTime, pageToken string, | ||
| maxResults int64, | ||
| ) (*reportsAdmin.Activities, error) { | ||
| return listActivitiesFilteredRateLimited(ctx, client, userKey, applicationName, eventName, startTime, pageToken, "", maxResults) | ||
| return retryListActivities( | ||
|
JavierCarnelli-ConductorOne marked this conversation as resolved.
|
||
| ctx, sharedReportsRateLimiter, client.ListActivities, | ||
| 0, reportsMaxRetries, reportsInitialBackoff, reportsMaxBackoff, | ||
| userKey, applicationName, eventName, startTime, pageToken, "", maxResults, | ||
| ) | ||
| } | ||
|
|
||
| // listActivitiesRateLimitedBounded is listActivitiesRateLimited plus reportsPerAttemptTimeout | ||
| // applied to every attempt. Reserved for the event feed callers (usage/google-login/saml), which | ||
| // always wrap the call in their own bounded lookupCtx, so a hung attempt can be told apart from | ||
| // the caller's own deadline expiring and retried instead of failing the lookup outright. | ||
| func listActivitiesRateLimitedBounded( | ||
| ctx context.Context, | ||
| client *gwclient.GoogleWorkspaceClient, | ||
| userKey, applicationName, eventName, startTime, pageToken string, | ||
| maxResults int64, | ||
| ) (*reportsAdmin.Activities, error) { | ||
| return listActivitiesFilteredRateLimitedBounded(ctx, client, userKey, applicationName, eventName, startTime, pageToken, "", maxResults) | ||
| } | ||
|
|
||
| // listActivitiesFilteredRateLimited is listActivitiesRateLimited plus an optional Reports API | ||
| // `filters` expression (e.g. "client_id==<id>"), for callers that need to scope a lookup to one | ||
| // specific app rather than an entire app-type. | ||
| func listActivitiesFilteredRateLimited( | ||
| // listActivitiesFilteredRateLimitedBounded is listActivitiesRateLimitedBounded plus an optional | ||
| // Reports API `filters` expression (e.g. "client_id==<id>"), for callers that need to scope a | ||
| // lookup to one specific app rather than an entire app-type. | ||
| func listActivitiesFilteredRateLimitedBounded( | ||
| ctx context.Context, | ||
| client *gwclient.GoogleWorkspaceClient, | ||
| userKey, applicationName, eventName, startTime, pageToken, filters string, | ||
| maxResults int64, | ||
| ) (*reportsAdmin.Activities, error) { | ||
| backoff := reportsInitialBackoff | ||
| return retryListActivities( | ||
| ctx, sharedReportsRateLimiter, client.ListActivities, | ||
| reportsPerAttemptTimeout, reportsMaxRetries, reportsInitialBackoff, reportsMaxBackoff, | ||
| userKey, applicationName, eventName, startTime, pageToken, filters, maxResults, | ||
| ) | ||
| } | ||
|
|
||
| // retryListActivities holds the retry/backoff/per-attempt-timeout policy, parameterized so tests | ||
| // can drive it with a fake call and short durations. perAttemptTimeout == 0 means "no per-attempt | ||
| // cap" — the caller's own ctx is used as-is and a DeadlineExceeded from it is never retried as | ||
| // a hung attempt. | ||
| func retryListActivities( | ||
|
Contributor
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. retryListActivities is up to 14 params now, 7 of which just get passed straight through to |
||
| ctx context.Context, | ||
| limiter *reportsRateLimiter, | ||
| call listActivitiesFunc, | ||
| perAttemptTimeout time.Duration, | ||
| maxRetries int, | ||
| initialBackoff, maxBackoff time.Duration, | ||
| userKey, applicationName, eventName, startTime, pageToken, filters string, | ||
| maxResults int64, | ||
| ) (*reportsAdmin.Activities, error) { | ||
| applyPerAttemptTimeout := perAttemptTimeout > 0 | ||
| backoff := initialBackoff | ||
| for attempt := 0; ; attempt++ { | ||
| if err := sharedReportsRateLimiter.Wait(ctx); err != nil { | ||
| if err := limiter.Wait(ctx); err != nil { | ||
| return nil, fmt.Errorf("google-workspace-connector: context cancelled waiting for reports api quota: %w", err) | ||
| } | ||
|
|
||
| resp, err := client.ListActivities(ctx, userKey, applicationName, eventName, startTime, pageToken, filters, maxResults) | ||
| attemptCtx := ctx | ||
| cancel := func() {} | ||
| if applyPerAttemptTimeout { | ||
| attemptCtx, cancel = context.WithTimeout(ctx, perAttemptTimeout) | ||
| } | ||
| resp, err := call(attemptCtx, userKey, applicationName, eventName, startTime, pageToken, filters, maxResults) | ||
| cancel() | ||
| if err == nil { | ||
| return resp, nil | ||
| } | ||
| if attempt >= reportsMaxRetries || !isRetryableReportsError(err) { | ||
|
|
||
| // ctx still being live means attemptCtx's own timeout fired, not the caller's deadline — | ||
| // treat that like a retryable 429/503 rather than "out of time." | ||
| hungAttempt := applyPerAttemptTimeout && errors.Is(err, context.DeadlineExceeded) && ctx.Err() == nil | ||
| if attempt >= maxRetries || (!isRetryableReportsError(err) && !hungAttempt) { | ||
| return nil, err | ||
|
Comment on lines
+186
to
190
Contributor
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. Rate-limit storms now surface as a bare When the retry budget is exhausted this returns // usage_event_feed.go:145, saml_event_feed.go:77, google_login_event_feed.go:69
if errors.Is(err, context.DeadlineExceeded) && ctx.Err() == nil {
// skip this one app instead of failing the whole batch
return nil, nil
}Concrete scenario: Google rate-limits the tenant for a minute. The backoff sleeps alone are Before this PR the 429 propagated as a real error and the cursor was preserved for retry, so the batch was retried rather than dropped. Suggest making the hung-attempt case explicit rather than inferring it from the error type at the call site — e.g. have |
||
| } | ||
|
|
||
|
|
@@ -134,8 +197,8 @@ func listActivitiesFilteredRateLimited( | |
| return nil, ctx.Err() | ||
| } | ||
| backoff *= 2 | ||
| if backoff > reportsMaxBackoff { | ||
| backoff = reportsMaxBackoff | ||
| if backoff > maxBackoff { | ||
| backoff = maxBackoff | ||
| } | ||
| } | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.