Skip to content

Keep an upload pending until its completion has finished - #457

Open
didiergarcia wants to merge 2 commits into
mainfrom
fix/upload-ownership
Open

didiergarcia wants to merge 2 commits into
mainfrom
fix/upload-ownership

Conversation

@didiergarcia

Copy link
Copy Markdown
Contributor

Fixes #456.

cleanupUploads() removed an upload as soon as its task stopped reporting .running, and flush() only selects batches when no uploads are pending. A real URLSessionUploadTask is already .completed when its completion handler starts, and the handler is what deletes the batch file. A flush in that window selected the same file again; the first completion then deleted it, so the second upload sent an empty body and got a 400 (or, if it read the file first, the batch was sent twice).

  • Each upload carries a flag its completion sets once it has finished with the batch. cleanupUploads() removes an upload when that flag is set, or when the task is .suspended and won't complete. The flag is created before the upload starts, since its completion can run before the upload is added. Disk and memory paths both use it.
  • HTTPClient now always calls an upload's completion. Settings can replace the client mid-upload, and the response handler returned without calling it once the client was released (also when the response wasn't an HTTPURLResponse). Those cases complete with .badSession, which keeps the batch for retry. Without this, an upload could stay pending forever and block later flushes.

A custom HTTPSession whose tasks report .completed without ever calling their completion would now keep that upload pending. URLSession always calls it.

Verified:

  • UploadOwnership_Tests uses real URLSession tasks and holds the first completion while a second flush runs. On main the same file is uploaded twice with a 0-byte second body; with the fix it's uploaded once. The released-client test times out on main and passes with the fix. Both passed 20/20 repeats.
  • cleanupUploads unit tests updated for the new rule, plus cases for a completed task whose completion hasn't finished and a suspended task.
  • sdk-e2e-tests main run locally against this branch: 90 passed, 3 skipped, 0 failed.
  • Full suites pass locally on macOS (238), iOS (254), tvOS (250) and watchOS (233).

URLSession marks a task .completed before running its completion, and
cleanupUploads freed the batch as soon as the task stopped running. A
flush in that window selected the same file again; the first completion
then deleted it, so the second upload sent an empty body (400).

Uploads now carry a flag their completion sets when it's done, and
HTTPClient always calls an upload's completion, including when the
client was released mid-upload.
@didiergarcia
didiergarcia requested a balanced review from Copilot October 27, 2026 19:05

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Copilot review overview

9 open issues
What changed in this PR

Prevents duplicate batch uploads and flush deadlocks by keeping uploads “pending” until their completion handler has finished processing the batch, and by ensuring HTTPClient always invokes the caller completion even if the client is released mid-upload.

Changes:

  • Track upload completion-finalization via a shared Completion flag and update cleanupUploads() removal rules accordingly.
  • Ensure HTTPClient calls the upload completion when self is released and when the response isn’t an HTTPURLResponse.
  • Add/adjust tests to reproduce and verify the race (real URLSessionUploadTask completion window) and the released-client completion case.
File Description
Tests/​Segment-Tests/​UploadOwnership_Tests.swift Adds regression tests for “completed-but-completion-still-running” and released-HTTPClient behaviors.
Tests/​Segment-Tests/​Support/​TestUtilities.swift Updates test session task state handling to be atomic/thread-safe.
Tests/​Segment-Tests/​SegmentDestination_Tests.swift Updates/extends cleanupUploads() tests for the new completion-finalization rule.
Sources/​Segment/​Utilities/​Networking/​HTTPClient.swift Ensures completion is always called (including when client is deallocated or response type is unexpected).
Sources/​Segment/​Plugins/​SegmentDestination.swift Introduces per-upload completion-finalization tracking and updates upload cleanup logic to match.

Review effort: Lite

Comment on lines +48 to +54
// set by the upload's completion once it has finished with the batch.
var completion = Completion()

final class Completion {
@Atomic var finished = false
func finish() { _finished.set(true) }
}
Comment on lines 189 to 193
let uploadTask = httpClient.startBatchUpload(writeKey: analytics.configuration.values.writeKey, batch: url) { [weak self] result in
defer {
completion.finish()
leaveGroup.run()
}
Comment on lines +217 to 218
completion.finish()
cleanupUploads()
Comment on lines 299 to 304
let uploadTask = httpClient.startBatchUpload(writeKey: analytics.configuration.values.writeKey, data: data, batchId: batchId) { [weak self] result in
defer {
completion.finish()
// leave for the url we kicked off.
finish.run()
}
Comment on lines +327 to 328
completion.finish()
cleanupUploads()
} else {
completion(.failure(HTTPClientErrors.statusCode(code: httpResponse.statusCode)))
}
} else {
Comment on lines +157 to +158
// let A's completion and any second upload finish.
RunLoop.main.run(until: Date(timeIntervalSinceNow: 1.0))
@@ -204,12 +214,15 @@ extension SegmentDestination {
// the upload we have here has just finished.
// make sure it gets removed and it's cleanup() called rather
// lets go through and get rid of any tasks that aren't running.
// either they were suspended because a background task took too
// long, or the os orphaned it due to device constraints (like a watch).
// lets go through and get rid of uploads that are done: their completion has finished

This branch has not been deployed

No deployments
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.

Completed upload tasks can release batch ownership before callback cleanup, causing duplicate selection and an empty POST

2 participants