Skip to content

fix(firestore): stop leaking the FirestoreQuery snapshot listener - #16839

Open
paulb777 wants to merge 2 commits into
mainfrom
pb-firestorequery-listener-leak
Open

paulb777 wants to merge 2 commits into
mainfrom
pb-firestorequery-listener-leak

Conversation

@paulb777

@paulb777 paulb777 commented Oct 2, 2026

Copy link
Copy Markdown
Member

Problem

FirestoreQueryObservable stores the closure returned by createListener(with:) in
self.setupListener, and that closure captured self strongly. The resulting cycle
(self → setupListener → self) means deinit, which calls removeListener(), never runs.
Every @FirestoreQuery therefore leaks its observable. The observable's snapshot listener stays
registered, and keeps receiving and reading documents, after the view that owned it is gone. The
snapshot handlers passed to createListener already capture self weakly, so this closure was
the only strong capture.

Change

Capture self weakly in the closure returned by createListener(with:) (2 lines). The closure
is only invoked through the observable (from its initializers and when configuration changes),
so the guard never fires while the observable is alive.

Testing

Added Firestore/Swift/Tests/Unit/PropertyWrapper/FirestoreQueryObservableLifetimeTests.swift.
It uses Swift Testing and is part of the FirebaseFirestoreTests SPM target. The tests swap
Query.addSnapshotListener(_:) for a fake that hands out recording ListenerRegistrations.
Each test drops the last strong reference to the observable, then checks that the observable is
deallocated and that its registration was removed. There are three cases:

  • array results;
  • Result results;
  • array results after a configuration change has replaced the listener.
xcodebuild -scheme FirebaseFirestoreTests test -destination 'platform=macOS' \
  -only-testing:FirebaseFirestoreTests/FirestoreQueryObservableLifetimeTests
xcodebuild -scheme FirebaseFirestoreTests test -destination 'platform=macOS'
  • Without the source change, all 3 new tests fail: the observable outlives its last reference,
    and removeCount == 0.
  • With the change, the full scheme passes: 9 tests in 3 suites. It also passes 30 iterations in
    a row (-test-iterations 30 -run-tests-until-failure), 270/270.
  • ./scripts/style.sh is clean.
  • Unrelated to this change: under -test-iterations stress, "Stream handles (nil, nil) events
    gracefully" in AsyncSequenceTests.swift occasionally fails, even with the new suite skipped.
    Those suites aren't .serialized, and their tests swap the same method concurrently.

#16826 touches the same file on different lines (it merges cleanly). The two CHANGELOG entries
will need a trivial merge.

FirestoreQueryObservable stores the closure returned by
`createListener(with:)` in `setupListener`, and that closure captured
`self` strongly. The resulting cycle (self -> setupListener -> self)
kept `deinit` from ever running, so `removeListener()` was never
called: every `@FirestoreQuery` leaked its observable, and the
observable's snapshot listener kept running, and reading documents,
after the view that owned it went away.

Capture `self` weakly in the returned closure, as the snapshot handlers
passed to it already do, and add tests that check that the observable
is deallocated and removes its listener registration once the last
strong reference to it goes away.
@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 addresses a memory leak in @FirestoreQuery by capturing self weakly inside the listener creation closure. It also adds a comprehensive unit test suite to verify the lifetime and deallocation of FirestoreQueryObservable. The review feedback correctly points out a compilation error in the new test file where weak references are declared using let instead of var.

@paulb777

paulb777 commented Oct 2, 2026

Copy link
Copy Markdown
Member Author

[paulb777-bot] Requesting an automated review, as on the other PRs from this batch.

/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 fixes a memory leak in @FirestoreQuery by capturing self weakly inside the listener creation closure in FirestoreQueryObservable.swift. It also adds comprehensive unit tests to verify the lifetime and deallocation behavior of the observable. The review comments correctly point out that weak references in the new tests must be declared as mutable variables (weak var) instead of constants (weak let) to avoid compiler errors.

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.

3 participants