-
-
Notifications
You must be signed in to change notification settings - Fork 461
[ANR] Update Connection Status cache in the background #4832
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
53a1054
e520ff7
7ae75a2
e44381d
8dba1d8
5e5a8d5
7a980e8
dfce0b8
f358548
c7698bc
0cce8e9
3d90c07
e69534d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -54,6 +54,7 @@ public final class AndroidConnectionStatusProvider | |
| private static final @NotNull AutoClosableReentrantLock childCallbacksLock = | ||
| new AutoClosableReentrantLock(); | ||
| private static final @NotNull List<NetworkCallback> childCallbacks = new ArrayList<>(); | ||
| private static final AtomicBoolean isUpdatingCache = new AtomicBoolean(false); | ||
|
|
||
| private static final int[] transports = { | ||
| NetworkCapabilities.TRANSPORT_WIFI, | ||
|
|
@@ -146,6 +147,12 @@ private boolean isNetworkEffectivelyConnected( | |
| : ConnectionStatus.DISCONNECTED; | ||
| } | ||
|
|
||
| // If cache is being updated, and there's no cached data, return UNKNOWN status until cache is | ||
| // updated | ||
| if (isUpdatingCache.get()) { | ||
| return ConnectionStatus.UNKNOWN; | ||
| } | ||
|
|
||
| // Fallback to legacy method when NetworkCapabilities not available | ||
| final ConnectivityManager connectivityManager = | ||
| getConnectivityManager(context, options.getLogger()); | ||
|
|
@@ -163,6 +170,12 @@ private boolean isNetworkEffectivelyConnected( | |
| return getConnectionType(capabilities); | ||
| } | ||
|
|
||
| // If cache is being updated, and there's no cached data, return UNKNOWN status until cache is | ||
| // updated | ||
| if (isUpdatingCache.get()) { | ||
| return null; | ||
| } | ||
|
|
||
| // Fallback to legacy method when NetworkCapabilities not available | ||
| return getConnectionType(context, options.getLogger(), buildInfoProvider); | ||
| } | ||
|
|
@@ -268,10 +281,19 @@ private void updateCacheAndNotifyObservers( | |
|
|
||
| // Only notify observers if something meaningful changed | ||
| if (shouldUpdate) { | ||
| updateCache(networkCapabilities); | ||
|
|
||
| final @NotNull ConnectionStatus status = getConnectionStatusFromCache(); | ||
| try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { | ||
| cachedNetworkCapabilities = networkCapabilities; | ||
| lastCacheUpdateTime = timeProvider.getCurrentTimeMillis(); | ||
| final @NotNull ConnectionStatus status = getConnectionStatusFromCache(); | ||
| options | ||
| .getLogger() | ||
| .log( | ||
| SentryLevel.DEBUG, | ||
| "Cache updated - Status: " | ||
| + status | ||
| + ", Type: " | ||
| + getConnectionTypeFromCache()); | ||
|
|
||
| for (final @NotNull IConnectionStatusObserver observer : | ||
| connectionStatusObservers) { | ||
| observer.onConnectionStatusChanged(status); | ||
|
|
@@ -349,78 +371,89 @@ private boolean hasSignificantTransportChanges( | |
| } | ||
|
|
||
| @SuppressLint({"NewApi", "MissingPermission"}) | ||
| private void updateCache(@Nullable NetworkCapabilities networkCapabilities) { | ||
| try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { | ||
| try { | ||
| if (networkCapabilities != null) { | ||
| cachedNetworkCapabilities = networkCapabilities; | ||
| } else { | ||
| if (!Permissions.hasPermission(context, Manifest.permission.ACCESS_NETWORK_STATE)) { | ||
| options | ||
| .getLogger() | ||
| .log( | ||
| SentryLevel.INFO, | ||
| "No permission (ACCESS_NETWORK_STATE) to check network status."); | ||
| cachedNetworkCapabilities = null; | ||
| lastCacheUpdateTime = timeProvider.getCurrentTimeMillis(); | ||
| return; | ||
| } | ||
| private void updateCache() { | ||
| try { | ||
| if (!Permissions.hasPermission(context, Manifest.permission.ACCESS_NETWORK_STATE)) { | ||
| options | ||
| .getLogger() | ||
| .log(SentryLevel.INFO, "No permission (ACCESS_NETWORK_STATE) to check network status."); | ||
| try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { | ||
| cachedNetworkCapabilities = null; | ||
| lastCacheUpdateTime = timeProvider.getCurrentTimeMillis(); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.M) { | ||
| cachedNetworkCapabilities = null; | ||
| lastCacheUpdateTime = timeProvider.getCurrentTimeMillis(); | ||
| return; | ||
| } | ||
| if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.M) { | ||
| try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { | ||
| cachedNetworkCapabilities = null; | ||
| lastCacheUpdateTime = timeProvider.getCurrentTimeMillis(); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| // Fallback: query current active network | ||
| final ConnectivityManager connectivityManager = | ||
| getConnectivityManager(context, options.getLogger()); | ||
| if (connectivityManager != null) { | ||
| final Network activeNetwork = connectivityManager.getActiveNetwork(); | ||
|
|
||
| cachedNetworkCapabilities = | ||
| activeNetwork != null | ||
| ? connectivityManager.getNetworkCapabilities(activeNetwork) | ||
| : null; | ||
| } else { | ||
| cachedNetworkCapabilities = | ||
| null; // Clear cached capabilities if connectivity manager is null | ||
| } | ||
| // Avoid concurrent updates | ||
| if (!isUpdatingCache.getAndSet(true)) { | ||
| // Fallback: query current active network in the background | ||
| if (options.getThreadChecker().isMainThread()) { | ||
| submitSafe(() -> updateCacheFromConnectivityManager(), () -> isUpdatingCache.set(false)); | ||
| } else { | ||
| updateCacheFromConnectivityManager(); | ||
| } | ||
| lastCacheUpdateTime = timeProvider.getCurrentTimeMillis(); | ||
| } | ||
|
|
||
| options | ||
| .getLogger() | ||
| .log( | ||
| SentryLevel.DEBUG, | ||
| "Cache updated - Status: " | ||
| + getConnectionStatusFromCache() | ||
| + ", Type: " | ||
| + getConnectionTypeFromCache()); | ||
| } catch (Throwable t) { | ||
| options.getLogger().log(SentryLevel.WARNING, "Failed to update connection status cache", t); | ||
| } catch (Throwable t) { | ||
| options.getLogger().log(SentryLevel.WARNING, "Failed to update connection status cache", t); | ||
| try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { | ||
stefanosiano marked this conversation as resolved.
Show resolved
Hide resolved
stefanosiano marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| cachedNetworkCapabilities = null; | ||
| lastCacheUpdateTime = timeProvider.getCurrentTimeMillis(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @RequiresApi(api = Build.VERSION_CODES.M) | ||
| private void updateCacheFromConnectivityManager() { | ||
| final ConnectivityManager connectivityManager = | ||
| getConnectivityManager(context, options.getLogger()); | ||
| if (connectivityManager != null) { | ||
| final @Nullable NetworkCapabilities capabilities = | ||
| getNetworkCapabilities(connectivityManager); | ||
|
|
||
| try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { | ||
| cachedNetworkCapabilities = capabilities; | ||
| lastCacheUpdateTime = timeProvider.getCurrentTimeMillis(); | ||
|
|
||
| if (capabilities != null) { | ||
| options | ||
| .getLogger() | ||
| .log( | ||
| SentryLevel.DEBUG, | ||
| "Cache updated - Status: " | ||
| + getConnectionStatusFromCache() | ||
| + ", Type: " | ||
| + getConnectionTypeFromCache()); | ||
| } | ||
| } | ||
| } | ||
| isUpdatingCache.set(false); | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: Infinite flag: caching functions return unknowns foreverIf an exception occurs in |
||
|
|
||
| private boolean isCacheValid() { | ||
| return (timeProvider.getCurrentTimeMillis() - lastCacheUpdateTime) < CACHE_TTL_MS; | ||
| } | ||
|
|
||
| @Override | ||
| public @NotNull ConnectionStatus getConnectionStatus() { | ||
| if (!isCacheValid()) { | ||
| updateCache(null); | ||
| updateCache(); | ||
| } | ||
| return getConnectionStatusFromCache(); | ||
| } | ||
|
|
||
| @Override | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The refactored 🤖 Prompt for AI Agent Did we get this right? 👍 / 👎 to inform future reviews.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @stefanosiano this is my concern as well -- I think many things won't work as we expect them to with this change of behavior. I think we should probably specifically solve this for Replay (and maybe continuous profiling?). We could reuse your
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @romtsn So you're suggesting to return Because I think the issue of stale cache status happens just when the app goes to foreground, as that's the only time where we don't have the network listener updating the cache directly. So in
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the last known cached status, and if it's not available then UNKNOWN. which may (or may not) be the correct one
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I think this sounds good, too. Just need to make sure we don't acquire the lock from |
||
| public @Nullable String getConnectionType() { | ||
| if (!isCacheValid()) { | ||
| updateCache(null); | ||
| updateCache(); | ||
| } | ||
| return getConnectionTypeFromCache(); | ||
| } | ||
|
|
@@ -490,7 +523,7 @@ public void onForeground() { | |
| () -> { | ||
| // proactively update cache and notify observers on foreground to ensure connectivity | ||
| // state is not stale | ||
| updateCache(null); | ||
| updateCache(); | ||
|
|
||
| final @NotNull ConnectionStatus status = getConnectionStatusFromCache(); | ||
| if (status == ConnectionStatus.DISCONNECTED) { | ||
|
|
@@ -575,6 +608,14 @@ public NetworkCapabilities getCachedNetworkCapabilities() { | |
| } | ||
| } | ||
|
|
||
| @RequiresApi(Build.VERSION_CODES.M) | ||
| @SuppressLint("MissingPermission") | ||
| private static @Nullable NetworkCapabilities getNetworkCapabilities( | ||
| final @NotNull ConnectivityManager connectivityManager) { | ||
| final Network activeNetwork = connectivityManager.getActiveNetwork(); | ||
| return activeNetwork != null ? connectivityManager.getNetworkCapabilities(activeNetwork) : null; | ||
| } | ||
|
|
||
| /** | ||
| * Check the connection type of the active network | ||
| * | ||
|
|
@@ -603,14 +644,7 @@ public NetworkCapabilities getCachedNetworkCapabilities() { | |
| boolean cellular = false; | ||
|
|
||
| if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.M) { | ||
|
|
||
| final Network activeNetwork = connectivityManager.getActiveNetwork(); | ||
| if (activeNetwork == null) { | ||
| logger.log(SentryLevel.INFO, "Network is null and cannot check network status"); | ||
| return null; | ||
| } | ||
| final NetworkCapabilities networkCapabilities = | ||
| connectivityManager.getNetworkCapabilities(activeNetwork); | ||
| final NetworkCapabilities networkCapabilities = getNetworkCapabilities(connectivityManager); | ||
| if (networkCapabilities == null) { | ||
| logger.log(SentryLevel.INFO, "NetworkCapabilities is null and cannot check network type"); | ||
| return null; | ||
|
|
@@ -820,12 +854,19 @@ public static List<NetworkCallback> getChildCallbacks() { | |
| } | ||
|
|
||
| private void submitSafe(@NotNull Runnable r) { | ||
| submitSafe(r, null); | ||
| } | ||
|
|
||
| private void submitSafe(final @NotNull Runnable r, final @Nullable Runnable onError) { | ||
| try { | ||
| options.getExecutorService().submit(r); | ||
| } catch (Throwable e) { | ||
| options | ||
| .getLogger() | ||
| .log(SentryLevel.ERROR, "AndroidConnectionStatusProvider submit failed", e); | ||
| if (onError != null) { | ||
| onError.run(); | ||
| } | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.