Repository navigation
fix(android): Don't block callers on network status registration - #6537
Merged
0xadam-brown merged 10 commits intoNov 10, 2026
Merged
Conversation
AndroidConnectionStatusProvider held its lock on the SDK executor while registering network and telephony callbacks, which are slow system service calls. The main thread waited on the same lock when Session Replay added an observer during Sentry.init, and when events were captured with a stale connection cache, causing app-start ANRs. Registration now uses its own executor-only lock. The cache lock is held only for field writes, and cache refreshes query the active network outside it, discarding their result if a network callback wrote the cache meanwhile. Observers and child network callbacks are copy-on-write lists, notified without holding a lock. addConnectionStatusObserver schedules registration instead of running it on the caller, and always returns true. Lifecycle callbacks now check registration state on the executor, so backgrounding during the initial registration unregisters the callbacks, and foregrounding before a queued unregistration runs registers them again. Fixes #6509
The cache was four fields (capabilities, network, freshness deadline and a version counter) guarded by a lock, written in four places that each had to remember to bump the version. onAvailable wrote the network without the lock, and the status lookup read the capabilities twice, so a concurrent unregister could make it report DISCONNECTED instead of querying. Replace them with an immutable ConnectionSnapshot in an AtomicReference. Writes swap in a new snapshot with compare-and-set, a refresh stores its result only if the snapshot is unchanged, and readers derive status and type from one snapshot. The capabilities that decide a significant change are now per instance instead of a static array rewritten by every constructor. Refs #6509
Registration now runs on the SDK executor, so a registration queued after close() ran after its cleanup and registered the network and cellular callbacks again, with nothing left to unregister them. close() now sets a flag first that queued registration and foreground work check. Refs #6509
The flag doesn't track connectivity. It only stops child network callbacks from getting onAvailable again when the network callback is registered again on a return to the foreground. Name it for that and document it. Refs #6509
…gging is on Without network capabilities, building the "Cache updated" message queried the ConnectivityManager up to twice more on the caller's thread, even with debug logging off. Refs #6509
…nd exception policies Describe how clients use the provider, which calls may block, and the rules that keep the main thread from waiting: never lock the connection cache, never hold a lock while notifying, and only take the registration lock on the SDK executor. Also document the fields and group them, and make the snapshot's fields private. No behavior change. Refs #6509
Make tests that passed on the pre-fix code fail on it: let close() run before the follow-up call, and keep removeConnectionStatusObserver from waiting in its own setup. Check what a reader racing a network callback gets back, and fail tests whose background notification threads throw or hang. Add tests that getConnectionType, adding or removing an observer during a notification, and reading a fresh cache during another caller's refresh don't wait. Drop a test that pinned the cached capabilities instance, remove a duplicate static-state reset, and shorten names. Refs #6509
📲 Install BuildsAndroid
|
0xadam-brown
marked this pull request as ready for review
November 10, 2026 10:51
0xadam-brown
requested review from
markushi and
romtsn
and removed request for
a team
November 10, 2026 10:51
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 9faedfb. Configure here.
…the network changes during it onAvailable replaces the connection snapshot with one that only has the new network, keeping the old capabilities and expired deadline. A refresh racing it saw the snapshot change, returned that stale snapshot and discarded its own query, so a read during a network handover could report the previous network's type or status. A refresh now gives way only to a fresh snapshot. If the newer snapshot is stale, the refresh stores its result on it, keeping its network. Refs #6509
runningcode
approved these changes
Nov 10, 2026
runningcode
left a comment
Contributor
There was a problem hiding this comment.
Thanks for coming up with a fix and iterating on it to come up with the best solution!
Print the network, a summary of the capabilities the provider reads (transports, internet, validated) and whether the snapshot is still fresh. The full NetworkCapabilities isn't printed, because on API 29+ it can include the Wi-Fi SSID. Refs #6509
0xadam-brown
deleted the
fix/dont-block-main-on-connectivity-registration
branch
November 10, 2026 14:20
runningcode
added a commit
that referenced
this pull request
Nov 10, 2026
Error Prone's NotJavadoc flags Javadoc on methods of local classes, which #6537 added in AndroidConnectionStatusProvider. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8 of 9 tasks
runningcode
added a commit
that referenced
this pull request
Nov 11, 2026
* ref(android): Fix Error Prone warnings in sentry-android-core Error Prone never ran on sentry-android-core. Enabling it reports 59 warnings outside Javadoc, which fail the build under -Werror. - Suppress FutureReturnValueIgnored, JavaUtilDate, and ReferenceEquality where identity is intended, as the sentry module does. - Keep the AnrProfilingIntegration updater a field, since removeCallbacks needs the instance that was posted, and suppress UnnecessaryLambda there. - Read the ANR thread dump and the app start profiling config as UTF-8, Android's default charset. - Remove unused fields and parameters, add missing @OverRide, make long-to-double conversions explicit, and narrow members that are effectively private or protected in a final class. - Explain the two catch blocks that ignore their exception. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ref(android): Fix the Error Prone warnings only main has - ANRWatchDog.monotonicTicker is never read (#6041), since the ticker lambda captures the constructor parameter. Remove the field and keep the parameter. - ActivityFramesTracker.lock is protected in a final class; make it package-private. - LifecycleWatcher reads the session start Date to seed the session window; suppress JavaUtilDate there, as the other Date uses are. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ref(android): Keep the default charset reads as they were Android's default charset is always UTF-8, so DefaultCharset is disabled for the Android modules in #6493 instead of being fixed here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ref(android): Drop the UnnecessaryLambda suppressions #6493 disables UnnecessaryLambda for the Android modules, so the suppressions on the ANRWatchDog and AnrProfilingIntegration fields are redundant. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ref(android): Drop the updater field comment Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(android): Make the local-class method comment a plain comment Error Prone's NotJavadoc flags Javadoc on methods of local classes, which #6537 added in AndroidConnectionStatusProvider. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
0xadam-brown
added a commit
that referenced
this pull request
Nov 11, 2026
runningcode
added a commit
that referenced
this pull request
Nov 11, 2026
…vider NullAway treats AtomicReference.get() as nullable, so the snapshot reads added in #6537 fail once NullAway runs on sentry-android-core. The snapshot is never set to null, so read it through one accessor that suppresses the finding. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
0xadam-brown
added a commit
that referenced
this pull request
Nov 11, 2026
0xadam-brown
added a commit
that referenced
this pull request
Nov 11, 2026
runningcode
added a commit
that referenced
this pull request
Nov 11, 2026
…6493) * build(android): Run Error Prone and NullAway on the Android modules net.ltgt.errorprone dropped Android support in 3.0.0, which we have used since 6.7. Since then it only enables Error Prone and adds it to the processor path for Gradle's JVM source sets. On sentry-android-core and sentry-uitest-android the errorprone dependencies and the NullAway ERROR setting had no effect, and the Java compile tasks ran plain javac. Use the AGP variant API to add the errorprone configuration to each variant's annotation processor configuration and enable Error Prone on its JavaCompile task. Both APIs exist in AGP 9.0.0, the oldest version in the AGP matrix. Pass -XDandroidCompatible=true so Error Prone treats these modules as Android code: it skips checks that don't apply there, such as DefaultCharset, and runs its Android checks. sentry-uitest-android has no hand-written Java, so exclude the view binding classes AGP generates, which trip LabelledBreakTarget. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(android): Resolve NullAway findings in AndroidConnectionStatusProvider NullAway treats AtomicReference.get() as nullable, so the snapshot reads added in #6537 fail once NullAway runs on sentry-android-core. The snapshot is never set to null, so read it through one accessor that suppresses the finding. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
3 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

This PR's diff looks more involved than it actually is. Most of the changes are new docs and tests. Prod changes are limited to introducing a new
ConnectionSnapshotthat combines a number of previously distinct fields + minor revisions in bookkeeping needed to update that snapshot vs the previous fields.📜 Description
AndroidConnectionStatusProviderno longer makes callers wait while it registers its network and telephony callbacks with system services. That's good because one of those callers isSentry.init.The problem
Before this change, AndroidConnectionStatusProvider used one lock to guard three code paths: callback registration, connection cache refreshes, and adding or removing observers. The SDK executor held that lock across
ConnectivityManager.registerDefaultNetworkCallbackand, on API 31+,TelephonyManager.registerTelephonyCallback. Both are calls into system services that can occasionally block for seconds.The main thread waited on the same lock in two places:
If the system service registrations took a while, the main thread would stall. Scenario (1) in particular could lead to ANRs during app startup. Oof.
A road not taken
Any fix has to isolate the slow part (registration) so that the main thread can return quickly when reading cached network info or adding observers.
The narrowest approach would be to give registration its own lock and keep a second lock for the cache and observers. But a refresh would then either hold the cache lock while querying Android, making readers wait on that query, or need a version counter that every write must remember to update. Even worse, two locks would need to document order they're taken in to avoid deadlock, which would be easy to get wrong.
That's^^ the approach my clanker initially implemented (see commit 1). But there's a better way...
A better approach (and the one this PR implements)
The approach I ultimately landed on was to avoid having the main thread hold a lock entirely, and instead rely on atomically publishing cached network info (see commit 2). I did so by introducing a new
ConnectionSnapshotclass, which is immutable and can be safely shared across threads. I stored observers in copy-on-write lists rather than having them be explicitly locked, and I confined registration to the the SDK executor (it's still defensively guarded by its own lock).Callers that need to access cached network info or register observers can now return quickly. Either of approach would have allowed as much, of course, but the benefit of introducing the snapshot it that it also:
Bonus points
It took me a while to figure out exactly what policies AndroidConnectionStatusProvider implemented, and so I've updated the docs to make that clearer. I've also renamed a few things that threw me for a loop (eg,
isConnected->onAvailableForwarded, as it's actually a flag to make sure the provider only forwardsonAvailablecalls to clients at most once, and shouldn't be used to determine whether the network is currently connected).Reviewing by commit
In case it's helpful, I got my clanker to break up this work into coherent commits. Personally, I'd limit myself to reviewing the final diff, but up to you.
onAvailablewrote the network without the lock, and the status lookup read the capabilities twice.isConnectedtoonAvailableForwardedRisks and costs
Performance and memory: Negligible.
Crash risk: No new risks, and one existing risk removed.
The new copy-on-write lists eliminate a latent
ConcurrentModificationException, which could happen under the previous approach when an observer removed itself on the notifying thread.Leaks: No new risks.
The
closedflag covers the one potential leak asynchronous registration would otherwise add.Two leaks predate this PR and are unchanged:
close()is never cleared.Behavior changes: None visible to users under normal operation.
💡 Motivation and Context
App-start ANRs reported in #6509, a regression from #6146 (8.60.0 through 8.63.0).
SentryAndroid.initblocks main thread onAndroidConnectionStatusProviderlock held acrossregisterTelephonyCallbackbinder call (8.60.0+) #6509💚 How did you test it?
📝 Checklist
sendDefaultPIIis enabled.🔮 Next steps
Possible follow-ups for
AndroidConnectionStatusProvider, in separate PRs:(Existing issues I noticed while implementing the current PR . Out of scope for now.)
ConnectivityManageron the caller's thread.