Skip to content

feat(storage): cancel async transfers when the calling task is cancelled - #16840

Open
paulb777 wants to merge 2 commits into
mainfrom
pb-storage-async-cancellation
Open

paulb777 wants to merge 2 commits into
mainfrom
pb-storage-async-cancellation

Conversation

@paulb777

@paulb777 paulb777 commented Oct 2, 2026 •

Copy link
Copy Markdown
Member

Problem

data(maxSize:), putDataAsync, putFileAsync, and writeAsync wrap the callback APIs in
withCheckedThrowingContinuation and ignore Swift task cancellation. Cancelling the calling task
(for example, when the view of a SwiftUI .task modifier disappears) doesn't stop the transfer:
an upload still completes, and the caller stays suspended until the transfer finishes. These
APIs don't return the StorageTask, so callers can't cancel the transfer themselves.

An earlier attempt, #8529, stalled because onCancel can run before the Storage task exists,
and the fix proposed there needed new public API.

Change

  • The four APIs start their transfer inside withTaskCancellationHandler. onCancel calls
    cancel() on the StorageUploadTask or StorageDownloadTask, which stops the request and
    fails the task with StorageError.cancelled, and that error is thrown to the caller.
  • A small lock-protected StorageTaskCanceller covers every ordering. If onCancel runs before
    the task is created (it runs first when the calling task is already cancelled), the task is
    cancelled as soon as it's created.
  • Results always come through the completion handler APIs, which register their success and
    failure observers before the task starts. The onProgress variants used to add .success and
    .failure observers after starting the task. Now they only add a .progress observer, which
    also takes them off the late-observer path in Storage task observers can receive duplicate terminal callbacks #16813 (fixed for all callers by fix(storage): deliver terminal task events to late observers only once #16820).
  • cancel() does nothing once a task has succeeded or failed, so a late cancellation can't
    resume the continuation a second time.
  • No public API changes: the new helpers are private. getMetadata, updateMetadata,
    delete, list, and downloadURL are unchanged, since their internal tasks have no cancel().

Testing

Added StorageAsyncAwaitTests:

  • Six cancellation tests: data(maxSize:) (cancelled during the request, and with an
    already-cancelled task), putDataAsync with and without onProgress, putFileAsync, and
    writeAsync. Each uses a test block that never responds and cancels the calling task once the
    request has started. It checks that StorageError.cancelled is thrown within 10 seconds and
    that the request was stopped (isFetching == false).
  • Four regression tests for the success paths, including that onProgress is still called.
xcodebuild -scheme FirebaseStorageUnit test -destination 'platform=macOS' \
  -only-testing:FirebaseStorageUnit/StorageAsyncAwaitTests
  • Without the source change (tests only): the six cancellation tests fail with Asynchronous wait failed: Exceeded timeout of 10 seconds, with unfulfilled expectations: "finished", and the five
    that cancel an in-flight request also fail with The request was not stopped. The four success
    tests pass.
  • With the change: all 10 pass.
  • Full suite xcodebuild -scheme FirebaseStorageUnit test -destination 'platform=macOS':
    173 tests, 0 failures.

Note: existing leak after cancel()

While testing this, I confirmed an existing leak that this change makes easier to hit. After
StorageTask.cancel(), upload and download tasks are never deallocated, including the data passed
to putData. Today, only an explicit cancel() hits the leak. With this PR, cancelling the calling
task hits it too.

#16841 fixes the leak separately, so ideally it lands first. The two PRs only overlap in the
CHANGELOG.

Fixes #11786

`data(maxSize:)`, `putDataAsync`, `putFileAsync` and `writeAsync` ignored
Swift task cancellation, so the transfer kept running and the caller kept
waiting for it. Start the transfer inside `withTaskCancellationHandler` and
cancel the Storage task when the calling task is cancelled, which stops the
request and throws `StorageError.cancelled`. A lock-protected canceller also
handles cancellation that arrives before the Storage task exists.

Deliver results through the completion handler APIs, which register their
observers before the task starts, instead of adding success and failure
observers after starting it.

Fixes #11786
@gemini-code-assist

Copy link
Copy Markdown
Contributor
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

@paulb777

paulb777 commented Oct 2, 2026

Copy link
Copy Markdown
Member Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces Swift task cancellation support to FirebaseStorage's asynchronous APIs, including data(maxSize:), putDataAsync, putFileAsync, and writeAsync. When a calling Swift task is cancelled, the underlying storage operation is cancelled and a StorageError.cancelled error is thrown. This behavior is managed by a new thread-safe StorageTaskCanceller helper class and the withCancellableStorageTask function. Additionally, unit tests have been added to verify cancellation and success paths. No review comments were provided, so there is no feedback to address.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FR]: Add cancellation support to Storage async/await APIs

2 participants