SDK-341 Retain and pause task runner on 401 JWT#1004
SDK-341 Retain and pause task runner on 401 JWT#1004sumeruchat merged 8 commits intofeature/SDK-335-retry-logicfrom
Conversation
Wire the autoRetry flag from backend getRemoteConfiguration response through RemoteConfiguration model, LocalStorage, and RequestHandler. Defaults to false when missing from the API response for backward compatibility. This flag will gate the new retain-and-pause-on-401 behavior in subsequent PRs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## feature/SDK-335-retry-logic #1004 +/- ##
==============================================================
Coverage ? 70.06%
==============================================================
Files ? 112
Lines ? 9141
Branches ? 0
==============================================================
Hits ? 6405
Misses ? 2736
Partials ? 0 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
When autoRetry is enabled and an offline-queued task receives a 401 JWT error, retain the task in the DB and pause the task runner instead of deleting it. Processing resumes when a new auth token is received via the .iterableAuthTokenRefreshed notification. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
09e95b3 to
645733f
Compare
Cover additional scenarios: multiple queued tasks with resume, all JWT error codes (BadAuthorizationHeader, JwtUserIdentifiersMismatched), non-JWT 401 (BadApiKey) and 500 errors are not retained, and auth refresh with empty DB is a no-op. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… tester Add a mock API server using URLProtocol interception to test the offline retry flow (retain-and-pause on 401 JWT) without a real JWT-enabled API key. New panels: - Remote Config Override: toggle mock server, override offlineMode/autoRetry - Updated Offline Retry: mock API response mode, JWT auth init, event log Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Addressed both comments:
|
…n-401 Resolve conflict in RequestHandler.swift: keep didSet on autoRetry to propagate value to offlineProcessor, and change offlineProcessor from let to var since OfflineRequestProcessor is a struct. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| if retrievedAuthToken != nil { | ||
| let isRefreshQueued = queueAuthTokenExpirationRefresh(retrievedAuthToken, onSuccess: onSuccess) | ||
| if !isRefreshQueued { | ||
| onSuccess?(authToken) | ||
| authToken = retrievedAuthToken | ||
| storeAuthToken() | ||
| } else { | ||
| authToken = retrievedAuthToken | ||
| storeAuthToken() | ||
| onSuccess?(authToken) | ||
| } | ||
| NotificationCenter.default.post(name: .iterableAuthTokenRefreshed, object: nil) | ||
| } else { |
There was a problem hiding this comment.
There is a possibility of app passing nil as auth token. But probably this is correct approach. Retries might not need to be engaged in those cases.
There was a problem hiding this comment.
Good catch, so the notification only fires when retrievedAuthToken != nil, so if the app returns nil the task runner stays paused. That's intentional: if the app can't provide a valid token, there's no point retrying the queued requests since they'd just get 401'd again. The AuthManager's retry policy will keep trying to get a token, and once it succeeds the notification fires and the runner resumes.
| } else if autoRetry && IterableAPICallTaskProcessor.isJWTAuthFailure(sendRequestError: sendRequestError) { | ||
| ITBInfo("JWT auth failure, retaining task for retry") | ||
| result.resolve(with: .failureWithRetry(retryAfter: nil, detail: sendRequestError)) |
| } | ||
| } | ||
|
|
||
| static func isJWTAuthFailure(sendRequestError: SendRequestError) -> Bool { |
There was a problem hiding this comment.
Is it been accessed from other places too? Why static?
There was a problem hiding this comment.
Yes so it's called from IterableTaskRunner.processTaskResultInQueue() to check if a failure is a JWT auth error before setting authPaused = true. Since the TaskRunner doesn't have an instance of the processor at that point (a new processor is created per-task in execute()), a static method was the cleanest way to share that logic.
| @@ -21,7 +22,8 @@ class IterableTaskRunner: NSObject { | |||
| self.dateProvider = dateProvider | |||
| self.connectivityManager = connectivityManager | |||
| self.persistenceContext = persistenceContextProvider.newBackgroundContext() | |||
|
|
|||
| self.autoRetry = autoRetry | |||
|
|
|||
There was a problem hiding this comment.
Seems very similar change to IterableApiCallTaskProcessor. Are these two parallel processor, online and offline? Just thinking
There was a problem hiding this comment.
Yes exactly the IterableTaskRunner is the offline queue runner and IterableAPICallTaskProcessor processes individual API call tasks within that queue. Both need autoRetry because the processor decides whether to return .failureWithRetry vs .failureWithNoRetry on 401 JWT, and the runner decides whether to set authPaused = true based on the result. The propagation chain is: RequestHandler.autoRetry (didSet) → OfflineRequestProcessor.autoRetry → IterableTaskRunner.autoRetry, so remote config updates flow down immediately.
Co-authored-by: Sumeru Chatterjee <sumeru.chatterjee@me.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
What
When
autoRetryis enabled and an offline-queued task gets a 401 JWT error, the task is now retained in the DB and the task runner pauses — instead of deleting the task permanently. Processing resumes when a new auth token arrives.Changes
IterableAPICallTaskProcessorconditionally returns.failureWithRetryon 401 JWT whenautoRetry=true(previously always.failureWithNoRetry)IterableTaskRunnergainsauthPausedstate (separate from networkpaused) that halts processing on JWT 401.iterableAuthTokenRefreshednotification to resume after auth is restoredAuthManagerposts.iterableAuthTokenRefreshedwhen a new token is receivedscheduleNext()to setrunning=falsebefore the paused guard (prevents "Already running" race condition on resume)autoRetryflag fromlocalStoragethroughDependencyContainer→TaskRunner→TaskProcessorImpact
autoRetryflag (defaults tofalse)Testing
How to test:
xcodebuild test -scheme swift-sdk -destination 'platform=iOS Simulator,name=iPhone 17 Pro' -only-testing:offline-events-teststestRetainTaskOn401WhenAutoRetryEnabled— task kept in DB, runner pausedtestDeleteTaskOn401WhenAutoRetryDisabled— task deleted (legacy behavior preserved)testResumeAfterAuthTokenRefreshed— runner resumes and processes retained task after auth refresh