-
Notifications
You must be signed in to change notification settings - Fork 121
Fix race condition when flagging site as unsupported for application passwords #16237
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
Changes from 2 commits
7599e5c
7fae255
b63ea52
ed4f8fa
e1dcda9
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 |
|---|---|---|
|
|
@@ -85,15 +85,21 @@ final class AlamofireNetworkErrorHandler { | |
| originalRequest: URLRequestConvertible, | ||
| failure: Error? | ||
| ) { | ||
| let retriedRequestIndex = retriedJetpackRequests.firstIndex { retriedRequest in | ||
| let urlRequest = try? originalRequest.asURLRequest() | ||
| let retriedRequest = try? retriedRequest.request.asURLRequest() | ||
| return urlRequest == retriedRequest | ||
| } | ||
| let retriedRequest: RetriedJetpackRequest? = queue.sync(flags: .barrier) { [weak self] in | ||
| guard let self else { return nil } | ||
|
|
||
| let retriedRequestIndex = _retriedJetpackRequests.firstIndex { retriedRequest in | ||
| let urlRequest = try? originalRequest.asURLRequest() | ||
| let retriedRequest = try? retriedRequest.request.asURLRequest() | ||
| return urlRequest == retriedRequest | ||
|
||
| } | ||
|
|
||
| guard let index = retriedRequestIndex else { return } | ||
| guard let index = retriedRequestIndex else { return nil } | ||
|
|
||
| return _retriedJetpackRequests.remove(at: index) | ||
| } | ||
|
|
||
| let retriedRequest = retriedJetpackRequests.remove(at: index) | ||
| guard let retriedRequest else { return } | ||
|
|
||
| if failure == nil { | ||
| let siteID = retriedRequest.request.siteID | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -560,6 +560,52 @@ final class AlamofireNetworkErrorHandlerTests: XCTestCase { | |
| // Then | ||
| XCTAssertTrue(userDefaults.applicationPasswordUnsupportedList.keys.contains(String(siteID))) | ||
| } | ||
|
|
||
| func test_concurrent_flagSiteAsUnsupportedForAppPasswordIfNeeded_no_race_condition() { | ||
| // Given - test for the race condition fix where multiple threads | ||
| // try to remove the same item simultaneously | ||
| let expectation = XCTestExpectation(description: "All concurrent flag operations complete without crash") | ||
| let threadCount = 50 | ||
| let siteID: Int64 = 999 | ||
| let jetpackRequest = createJetpackRequest(siteID: siteID) | ||
| let restRequest = createRESTRequest() | ||
| let error = createNetworkError() | ||
|
|
||
| expectation.expectedFulfillmentCount = threadCount | ||
|
|
||
| // Add a single request to the retry list | ||
| _ = errorHandler.shouldRetryJetpackRequest( | ||
| originalRequest: jetpackRequest, | ||
| convertedRequest: restRequest, | ||
| failure: error | ||
| ) | ||
|
|
||
| // When - multiple threads simultaneously try to flag and remove the SAME item | ||
| // This would cause a race condition in the old code where: | ||
| // 1. Thread A reads the array, finds index 0 | ||
| // 2. Thread B reads the array, finds index 0 | ||
| // 3. Thread A removes at index 0 (succeeds) | ||
| // 4. Thread B tries to remove at index 0 (crashes - array is now empty) | ||
| let group = DispatchGroup() | ||
| for _ in 0..<threadCount { | ||
| group.enter() | ||
| DispatchQueue.global().async { | ||
| // All threads try to remove the same request concurrently | ||
| self.errorHandler.flagSiteAsUnsupportedForAppPasswordIfNeeded( | ||
| originalRequest: jetpackRequest, | ||
| failure: nil | ||
| ) | ||
| group.leave() | ||
| expectation.fulfill() | ||
| } | ||
| } | ||
|
|
||
| // Force threads to start as close together as possible | ||
| group.wait() | ||
|
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. Small note — the group.wait() here doesn’t actually make the threads start together; it just waits for them to finish. In this case, that’s probably fine since the goal is to ensure no crash under load, but I just wanted to flag it in case a true concurrent start was intended.
Contributor
Author
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. I updated the comments in ed4f8fa, sorry for the misleading notes. |
||
|
|
||
| // Then - no crashes should occur (especially no EXC_BREAKPOINT from array index out of bounds) | ||
| wait(for: [expectation], timeout: 5.0) | ||
| } | ||
| } | ||
|
|
||
| // MARK: - Helper Methods | ||
|
|
||
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.
Small notice, might be negligible as
_retriedJetpackRequestscould be a few items, but we could move parsingoriginalRequestoutsidefirstIndexso it only runs once.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.
Good point! I moved the parsing of
originalRequestto outside of thefirstIndexblock in b63ea52.