Skip to content

fix(storage): release upload and download tasks after cancel and pause - #16841

Open
paulb777 wants to merge 3 commits into
mainfrom
pb-storage-cancel-leak
Open

paulb777 wants to merge 3 commits into
mainfrom
pb-storage-cancel-leak

Conversation

@paulb777

@paulb777 paulb777 commented Oct 3, 2026 •

Copy link
Copy Markdown
Member

Problem

StorageUploadTask and StorageDownloadTask wait on try await fetcher.beginFetch(). cancel(),
and pause() on a download, call stopFetching(). Storage doesn't set
stopFetchingTriggersCompletionHandler, so stopFetching() releases the fetcher's completion
handler without calling it. The awaiting continuation never resumes, and the suspended internal
Task keeps the Storage task alive forever:

  • Every cancelled upload or download task is leaked, along with the data passed to putData.
  • Every paused download task is leaked, even after it's resumed and finishes, because the Task
    for the original request stays suspended.

Change

  • Each task class starts its fetch with the completion handler form of beginFetch, in a small
    synchronous startFetch(with:) helper. Calling that form directly from the async code triggers a
    "consider using asynchronous alternative function" warning.
  • The success and failure handling moves into private fetchDidSucceed(with:) and
    fetchDidFail(with:) helpers. It's unchanged apart from dropping a redundant cancelled check at
    the start of the download success path; the locked block after it already makes that check.
  • The completion closure keeps the task alive while the fetch runs, as the suspended Task did, so
    a transfer still finishes when the caller doesn't hold the task. When stopFetching() releases
    the callbacks, the closure goes with them and the task can be deallocated.
  • A nil response body is still delivered as empty Data, as the async API bridged it. A new test
    covers this.
  • Success and failure handling now runs on the fetcher's callback queue (main by default, where the
    progress callbacks already run) instead of the concurrency global executor. It only updates state
    under the lock and dispatches handlers to Storage.callbackQueue, as before.
  • No public API changes.

Setting stopFetchingTriggersCompletionHandler isn't a drop-in alternative. Upload fetchers copy it
to their chunk fetchers, and pauseFetching() stops the chunk fetcher, so pausing an upload would
deliver a final error and break resume.

Testing

Added StorageTaskTests:

  • Upload and download tasks are released after cancel(). These use a test block that never
    responds, so the request is in flight when it's cancelled.
  • A download task is released after pause(), resume(), and success.
  • Three regression tests: upload and download tasks are still released after success, and
    getData still returns empty Data for an empty response.
xcodebuild -scheme FirebaseStorageUnit test -destination 'platform=macOS' \
  -only-testing:FirebaseStorageUnit/StorageTaskTests
  • Without the source change (tests only): the two cancel tests and the pause/resume test fail with
    The task was not released. The three regression tests pass.
  • With the change: all 6 pass.
  • Full suite xcodebuild -scheme FirebaseStorageUnit test -destination 'platform=macOS':
    169 tests, 0 failures.

I also ran a temporary test file (not included) against the Storage emulator:

  • Transfers: a 1 MB chunked round trip, a 4 MB upload pause/resume, and a 32 MB download-to-file
    pause/resume, with contents checked.
  • Cancellation: an in-flight 8 MB upload and an in-flight 32 MB download.

On main, the cancelled tasks and the resumed download were never released. With the change, all
of them are released, and the transfers still succeed with the same contents.

Related PRs

StorageUploadTask and StorageDownloadTask await `fetcher.beginFetch()`.
`cancel()`, and `pause()` on a download, stop the fetcher, and
GTMSessionFetcher then releases its completion handler without calling it.
The awaiting continuation never resumes, so the suspended Task keeps the
storage task, and any data being uploaded, alive forever. A paused download
leaks even after it's resumed and finishes.

Start the fetch with the completion handler form of `beginFetch` instead.
The closure keeps the task alive while the fetch runs, as the suspended Task
did, and is released when the fetch is stopped, so the task can be
deallocated.
@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 3, 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 addresses a memory leak in StorageUploadTask and StorageDownloadTask where tasks and their associated data were not released after being cancelled or paused. The fix refactors the fetch mechanism to use completion handlers instead of try await blocks, and adds unit tests to verify the task release behavior. Feedback on the changes points out a redundant lock check in StorageDownloadTask.swift that can be removed to simplify the code and improve efficiency.

Comment thread FirebaseStorage/Sources/StorageDownloadTask.swift
Remove the separate locked `state == .cancelled` check at the start of
`fetchDidSucceed(with:)`. The locked block that follows already returns
early for a cancelled task, leaving both snapshots nil so nothing fires,
so the extra lock acquisition added nothing. Addresses review feedback.
@paulb777

paulb777 commented Oct 3, 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 addresses a memory leak in StorageUploadTask and StorageDownloadTask where tasks and uploaded data were not released after being cancelled or paused. The implementation refactors both tasks to use beginFetch with a completion handler instead of try await, preventing awaiting tasks from being suspended indefinitely when a fetcher is stopped. Additionally, a new unit test suite StorageTaskTests has been introduced to verify that these tasks are correctly released under various conditions. I have no feedback to provide as there are no review comments.

@paulb777 paulb777 added this to the 13.1.0 - M188 milestone Oct 3, 2026
@ncooke3 ncooke3 modified the milestones: 13.1.0 - M188, 13.2.0 - M189 Oct 14, 2026

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.

2 participants