Skip to content

fix(functions): thread-safe Android streaming listener lifecycle#9093

Open
mikehardy wants to merge 2 commits into
mainfrom
functions-threadsafety
Open

fix(functions): thread-safe Android streaming listener lifecycle#9093
mikehardy wants to merge 2 commits into
mainfrom
functions-threadsafety

Conversation

@mikehardy

Copy link
Copy Markdown
Collaborator

Description

Synchronizes access to the Android functions streaming listeners SparseArray and defers listener removal until explicit cancel or module invalidate(), so a concurrent unsubscribe does not drop the final stream event.

Also adds OKF coverage-evidence gate requirements (coverage_evidence_gate, Jacoco/lcov evidence package, NYC anti-pattern) so native bridge changes must prove per-file coverage before review closes.

Related issues

N/A — proactive thread-safety hardening for Android streaming callables.

Release Summary

Fixes a race where Android functions streaming could lose the final result when unsubscribe raced with stream completion.

Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
    • Yes
  • My change supports the following platforms;
    • Android
    • iOS
    • Other (macOS, web)
  • My change includes tests;
    • e2e tests added or updated in packages/**/e2e
    • jest tests added or updated in packages/**/__tests__
  • I have updated TypeScript types that are affected by my change.
  • This is a breaking change;
    • Yes
    • No

Test Plan

  • yarn tests:jest --watchman=false packages/functions/__tests__/ — 11/11 pass
  • Android e2e (narrowed harness: app + functions) — 76 passing, 1 pending — /tmp/rnfb-e2e-functions-threadsafety-android-r4.log
  • yarn tests:android:post-e2e-coverage — Jacoco report for NativeRNFBTurboFunctions.java; core sync/defer-removal paths covered; four defensive/teardown branches documented as user-accepted deferrals in .agents/reports/functions-threadsafety/coverage-evidence.md
  • New e2e: native streaming error path; early stream cancel cleanup

@mikehardy mikehardy requested a review from russellwheatley July 8, 2026 23:50
@cursor

cursor Bot commented Jul 8, 2026

Copy link
Copy Markdown

Bugbot is not enabled for this team, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request improves the thread-safety of Android functions streaming by synchronizing access to listener management and adjusting the cleanup logic to prevent lost events. Additionally, it enforces stricter quality standards by integrating a mandatory coverage evidence gate for native bridge and library changes, supported by updated documentation and new e2e test cases.

Highlights

  • Thread-Safety Hardening: Synchronized access to the Android functionsStreamingListeners SparseArray to prevent race conditions during stream lifecycle events.
  • Lifecycle Management: Deferred listener removal until explicit cancellation or module invalidation, ensuring final stream events are correctly emitted even if a race condition occurs.
  • Coverage Evidence Gate: Introduced a mandatory coverage_evidence_gate requiring documented proof of coverage for native bridge and library changes before review completion.
  • E2E Testing: Added new end-to-end tests covering native streaming error paths and early stream cancellation cleanup.
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.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@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 a new coverage_evidence gate to the validation and documentation workflow, detailing evidence requirements and anti-patterns. In the Android codebase, thread synchronization was added around functionsStreamingListeners to prevent race conditions. However, the review feedback correctly identifies that removing the listener cleanup from the success, error, and exception paths in NativeRNFBTurboFunctions introduces memory leaks, as completed or failed listeners are never removed from the static map. The reviewer provides actionable suggestions to safely remove these listeners from the map without triggering a cancellation.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

mikehardy added 2 commits July 8, 2026 19:04
Synchronize access to the streaming listeners SparseArray and defer
removal until explicit cancel or invalidate so final stream events are
not dropped under concurrent unsubscribe.

Add e2e coverage for native streaming errors and early stream cancel.
Add coverage_evidence_gate, mandatory Jacoco/lcov evidence package,
NYC anti-pattern, and subagent return fields so native/lib bridge
changes cannot close review without per-file coverage proof.
@mikehardy mikehardy force-pushed the functions-threadsafety branch from c5187ce to b04fb15 Compare July 9, 2026 00:04
@cursor

cursor Bot commented Jul 9, 2026

Copy link
Copy Markdown

Bugbot is not enabled for this team, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@codecov

codecov Bot commented Jul 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 56.66667% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 58.64%. Comparing base (3933b45) to head (b04fb15).

Additional details and impacted files
@@             Coverage Diff              @@
##               main    #9093      +/-   ##
============================================
+ Coverage     58.61%   58.64%   +0.03%     
- Complexity     1616     1618       +2     
============================================
  Files           503      503              
  Lines         39178    39190      +12     
  Branches       5807     5808       +1     
============================================
+ Hits          22962    22980      +18     
+ Misses        14811    14744      -67     
- Partials       1405     1466      +61     
Flag Coverage Δ
android-native 53.62% <56.67%> (+0.06%) ⬆️
e2e-ts-android 50.02% <56.67%> (+0.05%) ⬆️
e2e-ts-ios 53.89% <ø> (ø)
e2e-ts-macos 41.55% <ø> (+0.07%) ⬆️
ios-native 53.89% <ø> (ø)
jest 49.58% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Object listener = functionsStreamingListeners.get(listenerId);
if (listener != null) {
if (listener instanceof Subscription) {
((Subscription) listener).cancel();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

after putting this through review, it's safer to cancel() outside of lock, if cancel stalls or hangs then this could deadlock which makes sense I think

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The API contract for cancel on a Reactive Streams instance indicates it must be timely and non-obstructive so it would be a contract violation for to hang or stall. In my execution path analysis I couldn't suss out a deadlock path but those are seemingly always surprising, and it never hurts to completely isolate work that strictly requires the lock from any other work. I think contention is more likely and sufficient as an argument against holding the lock either way.

So I definitely take the point. I'm hanging up on this one myself as I re-think it because I feel like the locking pattern growing here (and in response to this comment) is starting to get complicated and "complicated locking" isn't something to play with. I also noticed the same pattern in Firestore on android, and in a couple places on iOS - all exhibiting the same latent / "we have been lucky so far" unlocked listener maps / concurrent teardown windows possibilities

Let me finish my analysis there and the final result will I think be an architecture decision with a generic policy on listener registry design + code patterns, then a refactor of listener registries on both platform in all modules

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

yeah, I'm also in two minds. I feel it would never hang, and would be fixed upstream in that event probably. but then I think - is it worth the risk? 🤔

**Verdict line:** `100% on reachable touched lines` **or** `NOT 100%` with numbered gaps and disposition (fixed / intractable with evidence / user-accepted deferral).

Reviewers treat missing or stale coverage evidence as a **blocking** finding — same bar as missing e2e counts ([change authoring § validation evidence](change-authoring-workflow.md#validation-evidence-blocking)).
Record in `.agents/reports/<item>/coverage-evidence.md` (preferred) or work-queue notes. Orchestrators **must not** close `coverage_evidence_gate` or `review_gate` without this file when lib/native bridge is touched.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

"without this file" - could this be clarified? Review says change to "this evidence package", but it could also be "without changing this file"?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

just the sort of ambiguity that can lead to drift - good catch - I'm changing to "without recording the evidence in one of those locations"

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.

2 participants