Skip to content

SDK-341 Retain and pause task runner on 401 JWT#1004

Merged
sumeruchat merged 8 commits intofeature/SDK-335-retry-logicfrom
feature/SDK-341-retain-pause-on-401
Feb 26, 2026
Merged

SDK-341 Retain and pause task runner on 401 JWT#1004
sumeruchat merged 8 commits intofeature/SDK-335-retry-logicfrom
feature/SDK-341-retain-pause-on-401

Conversation

@sumeruchat
Copy link
Copy Markdown
Contributor

What

When autoRetry is 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

  • IterableAPICallTaskProcessor conditionally returns .failureWithRetry on 401 JWT when autoRetry=true (previously always .failureWithNoRetry)
  • IterableTaskRunner gains authPaused state (separate from network paused) that halts processing on JWT 401
  • Runner subscribes to .iterableAuthTokenRefreshed notification to resume after auth is restored
  • AuthManager posts .iterableAuthTokenRefreshed when a new token is received
  • Fixed scheduleNext() to set running=false before the paused guard (prevents "Already running" race condition on resume)
  • Wired autoRetry flag from localStorage through DependencyContainerTaskRunnerTaskProcessor

Impact

  • Breaking changes: None — all gated behind autoRetry flag (defaults to false)
  • Dependencies: Depends on SDK-339 (included in this branch)
  • Performance: No impact — paused runner does no work until notified

Testing

How to test:

  1. Run offline-events tests: xcodebuild test -scheme swift-sdk -destination 'platform=iOS Simulator,name=iPhone 17 Pro' -only-testing:offline-events-tests
  2. All 61 tests pass including 3 new tests:
    • testRetainTaskOn401WhenAutoRetryEnabled — task kept in DB, runner paused
    • testDeleteTaskOn401WhenAutoRetryDisabled — task deleted (legacy behavior preserved)
    • testResumeAfterAuthTokenRefreshed — runner resumes and processes retained task after auth refresh

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>
@sumeruchat sumeruchat changed the base branch from feature/SDK-335-retry-logic to feature/SDK-339-flag-adoption-logic February 6, 2026 15:56
@codecov
Copy link
Copy Markdown

codecov Bot commented Feb 6, 2026

Codecov Report

❌ Patch coverage is 97.43590% with 1 line in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (feature/SDK-335-retry-logic@1c95bed). Learn more about missing BASE report.

Files with missing lines Patch % Lines
...l/api-client/Request/OfflineRequestProcessor.swift 50.00% 1 Missing ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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>
@sumeruchat sumeruchat force-pushed the feature/SDK-341-retain-pause-on-401 branch from 09e95b3 to 645733f Compare February 6, 2026 16:07
sumchattering and others added 2 commits February 16, 2026 15:51
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>
Comment thread swift-sdk/Internal/IterableTaskRunner.swift Outdated
Co-authored-by: Cursor <cursoragent@cursor.com>
@sumeruchat
Copy link
Copy Markdown
Contributor Author

Addressed both comments:

  • autoRetry as let: Changed to var on IterableTaskRunner and wired up the propagation chain via didSet on RequestHandler.autoRetryOfflineRequestProcessor.autoRetryIterableTaskRunner.autoRetry. Updates from checkRemoteConfiguration() now flow down immediately.
  • (null) in Frameworks: Stale Xcode-generated duplicates — cleaned up and restored the UITests Frameworks phase to its original refs.

Base automatically changed from feature/SDK-339-flag-adoption-logic to feature/SDK-335-retry-logic February 20, 2026 16:24
…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>
Comment on lines 168 to 180
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 {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

@sumeruchat sumeruchat Feb 24, 2026

Choose a reason for hiding this comment

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

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.

Comment on lines +42 to +44
} else if autoRetry && IterableAPICallTaskProcessor.isJWTAuthFailure(sendRequestError: sendRequestError) {
ITBInfo("JWT auth failure, retaining task for retry")
result.resolve(with: .failureWithRetry(retryAfter: nil, detail: sendRequestError))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This looks good.

}
}

static func isJWTAuthFailure(sendRequestError: SendRequestError) -> Bool {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is it been accessed from other places too? Why static?

Copy link
Copy Markdown
Contributor Author

@sumeruchat sumeruchat Feb 24, 2026

Choose a reason for hiding this comment

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

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.

Comment on lines 15 to +26
@@ -21,7 +22,8 @@ class IterableTaskRunner: NSObject {
self.dateProvider = dateProvider
self.connectivityManager = connectivityManager
self.persistenceContext = persistenceContextProvider.newBackgroundContext()

self.autoRetry = autoRetry

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Seems very similar change to IterableApiCallTaskProcessor. Are these two parallel processor, online and offline? Just thinking

Copy link
Copy Markdown
Contributor Author

@sumeruchat sumeruchat Feb 24, 2026

Choose a reason for hiding this comment

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

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.

…n-401

* feature/SDK-335-retry-logic:
  Expand CLAUDE.md with comprehensive inline documentation (#1007)
  SDK-358: Prepare for Release 6.6.7 (#1005)
  SDK-290 Fix iOS SDK Header Exports for Objective-C++ Compatibility (#1002)
@sumeruchat sumeruchat merged commit 46ca7ce into feature/SDK-335-retry-logic Feb 26, 2026
6 of 7 checks passed
@sumeruchat sumeruchat deleted the feature/SDK-341-retain-pause-on-401 branch February 26, 2026 13:10
joaodordio pushed a commit that referenced this pull request Mar 23, 2026
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>
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.

4 participants