Skip to content

fix(android): Treat an unpopulated connection cache as stale (JAVA-717) - #6029

Draft
runningcode wants to merge 3 commits into
no/java-571-clock-abstractionsfrom
no/java-717-connection-cache-deadline
Draft

fix(android): Treat an unpopulated connection cache as stale (JAVA-717)#6029
runningcode wants to merge 3 commits into
no/java-571-clock-abstractionsfrom
no/java-717-connection-cache-deadline

Conversation

@runningcode

@runningcode runningcode commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

📜 Description

AndroidConnectionStatusProvider used 0 to mean "cache never populated", and compared it against SystemClock.uptimeMillis():

private volatile long lastCacheUpdateTime = 0;
private static final long CACHE_TTL_MS = 2 * 60 * 1000L;

private boolean isCacheValid() {
  return (timeProvider.getCurrentTimeMillis() - lastCacheUpdateTime) < CACHE_TTL_MS;
}

uptimeMillis() is 0 at boot, so 0 does not mean "unset" — it means "populated at boot". For the first two minutes of every boot, an empty cache reads as fresh.

The cache now holds a Deadline, so "never populated" is expired by construction and has no numeric value to get wrong.

Stacked on #6028, which adds the clock types this uses.

💡 Motivation and Context

getConnectionStatus() and getConnectionType() both call updateCache(null) only when isCacheValid() is false. During that two-minute window they skip it and fall through to getConnectionStatusFromCache(), which finds cachedNetworkCapabilities == null and answers from the legacy activeNetworkInfo path instead of NetworkCapabilities. So the answer is not wrong, but the cache is silently doing nothing during exactly the window where an app is most likely to be starting up — and unregisterNetworkCallback() reset the field to 0, reopening the window every time.

Changing clocks does not fix it: elapsedRealtimeNanos() starts at 0 at boot too. Any 0-means-unset long compared against a boot-relative clock has this flaw. Only epoch millis made it safe, because there 0 is 1970 — which is why the bug predates the clock work and is not caused by it.

The provider now takes an ElapsedRealtimeClock in place of ICurrentDateProvider. That is the right guarantee for a TTL: a cache entry should go stale on a real-time schedule whether or not the device was awake.

  • resolves: JAVA-717

💚 How did you test it?

A regression test asserts that a provider constructed at tick 0 populates the cache before reading it. It fails if the deadline is constructed fresh instead of passed, which is the shape of the original bug.

The test file moves from a mocked ICurrentDateProvider to TestTicker, so the TTL tests advance by an amount and a unit rather than by a bare += 60 * 1000L.

One existing test changed. When network is active but not connected with permission, return DISCONNECTED mocked an active network reporting isConnected=false alongside NetworkCapabilities describing a validated WiFi link — opposite worlds. It was green only because the stale empty cache forced the legacy path where the capability mocks were never consulted. buildInfo reports API 24, so with the cache fixed the provider reads capabilities. Its mocks are now consistent with its name, in its own commit so the change is not buried in the fix.

Full sentry-android-core suite: 1640 tests, 0 failures. apiDump produces no diff — io.sentry.android.core.internal is in apiValidation.ignoredPackages.

📝 Checklist

  • I added GH Issue ID & Linear ID
  • I added tests to verify the changes.
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled.
  • I updated the docs if needed.
  • I updated the wizard if needed.
  • Review from the native team if needed.
  • No breaking change or entry added to the changelog.
  • No breaking change for hybrid SDKs or communicated to hybrid SDKs.
  • Public API changes reviewed by another Mobile SDK team member or implemented according to the develop docs spec.

runningcode and others added 2 commits August 31, 2026 17:21
`When network is active but not connected with permission, return
DISCONNECTED` mocked an active network reporting isConnected=false
alongside NetworkCapabilities describing a validated WiFi link. Those
describe opposite worlds.

It passes today only because the empty connection cache reads as fresh
for the first two minutes of every boot (JAVA-717), which forces the
legacy activeNetworkInfo path where the capability mocks are never
consulted. buildInfo reports API 24, so once that bug is fixed the
provider reads capabilities and the test would fail for a reason that
has nothing to do with what it is named after.

Fixing the mocks first keeps that failure from being buried in the
commit that fixes the cache.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
lastCacheUpdateTime used 0 for "never populated" while being compared
against SystemClock.uptimeMillis(), which starts at 0 at boot. For the
first two minutes of every boot the empty cache therefore read as fresh,
so getConnectionStatus() skipped updateCache() and fell through to the
legacy activeNetworkInfo path instead of reading NetworkCapabilities.
The window reopens after every unregisterNetworkCallback(), which reset
the field to 0.

Switching clocks does not fix this on its own: elapsedRealtimeNanos()
also starts at 0 at boot. Any 0-means-unset long compared against a
boot-relative clock has the same flaw; only epoch millis made it safe,
because there 0 is 1970.

The cache now holds a Deadline, so "never populated" is expired by
construction and has no numeric value to get wrong. The provider takes
an ElapsedRealtimeClock in place of ICurrentDateProvider, which is what
a two-minute TTL wants: it must keep counting while the device sleeps.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@linear-code

linear-code Bot commented Aug 31, 2026

Copy link
Copy Markdown

JAVA-717

@sentry

sentry Bot commented Aug 31, 2026

Copy link
Copy Markdown

📲 Install Builds

Android

🔗 App Name App ID Version Configuration
SDK Size io.sentry.tests.size 8.54.0 (1) release

⚙️ sentry-android Build Distribution Settings

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.

1 participant