Skip to content

ref(core): Measure two wall-clock TTLs on a monotonic ticker (JAVA-579) - #6099

Closed
runningcode wants to merge 2 commits into
mainfrom
no/java-579-c6-wall-clock-ttls
Closed

ref(core): Measure two wall-clock TTLs on a monotonic ticker (JAVA-579)#6099
runningcode wants to merge 2 commits into
mainfrom
no/java-579-c6-wall-clock-ttls

Conversation

@runningcode

Copy link
Copy Markdown
Contributor

📜 Description

Audit finding §C6 lists four wall-clock TTL/cleanup sites. Two of them are genuine interval measurements and are fixed here; the other two turn out to have no monotonic remedy, and I explain why below rather than leave them looking forgotten.

Fixed — HostnameCache 5h TTL. It stored an absolute expiry built from System.currentTimeMillis() and compared it against a fresh reading. A backward clock step extended the TTL by the size of the step; a forward step expired the cache early. Now a Deadline on a MonotonicTicker. The field also no longer starts at 0, which on a boot-relative ticker reads as "freshly set" rather than "unset" — it starts as Deadline.passed, so nothing is treated as cached before the first resolve.

Fixed — DefaultCompositePerformanceCollector 30s auto-stop. It decided a transaction had been collecting for 30 seconds by subtracting two options.getDateProvider() readings. Those are wall-clock on every platform, including Android: SentryNanotimeDate.nanoTimestamp() returns millisToNanos(unixDateMillis), not its nanoTime component, so the nanoTime precision that type exists for never entered this comparison. Now a Deadline per CompositeData.

Not fixable — the two file-mtime sites. Both compare against File.lastModified():

  • profiling-trace cleanup in Sentry.java, f.lastModified() < classCreationTimestamp - 5min
  • envelope rotation in CacheStrategy, Arrays.sort(files, comparing lastModified())

A filesystem mtime is a wall-clock value, recorded by a different process at a time we never observed. There is no monotonic quantity to compare it against, so moving either of these to a ticker is not possible — it would mean comparing a tick to an mtime, which is worse than what is there now. Fixing them properly would mean not relying on mtimes at all (writing our own timestamp alongside each file), which is a storage-format change and well outside this item. Leaving them as they are.

Neither fixed value is serialized — they only decide when the SDK stops waiting — so this is not gated behind the v9 work.

💡 Motivation and Context

Audit finding §C6 from the clock-usage audit.

💚 How did you test it?

  • New test in HostnameCacheTest: the hostname is re-resolved once the cache duration has elapsed and not before, driven by advancing a TestMonotonicTicker rather than by sleeping for five hours. This path had no test at all previously, because it was not reachable without a real clock.
  • The two existing 30-second tests in DefaultCompositePerformanceCollectorTest now advance a ticker instead of stubbing dateProvider.now() with a positional sequence of four return values (thenReturn(dates[0], dates[0], dates[0], dates[1])), which was brittle against any change in how often the date provider gets called.
  • Full :sentry module suite: 249 suites, 3548 tests, 0 failures. :sentry-android-core SpanFrameMetricsCollectorTest and MainEventProcessorTest (the other HostnameCache consumer) also pass.

📝 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.

Two behavior details worth a reviewer's eye:

  1. The 30-second boundary is now inclusiveDeadline.hasPassed() is >=, where the old comparison was a strict >. A sample landing exactly on 30.000s now ends collection instead of being kept.
  2. addDataAndCheckTimeout no longer takes a shared nowNanos. That parameter existed so one clock reading was shared across every transaction in a collection round; with a per-transaction Deadline there is nothing to share, and the nanosecond spread across one loop cannot matter to a 30-second budget. Say the word if you would rather keep the shared reading.

TestMonotonicTicker's backing field is now @Volatile, because the collector's timer thread reads a ticker that the test thread advances.

🔮 Next steps

§C6 also mentions the 100ms sampling loop running on java.util.Timer, whose deadlines are wall-clock and whose Object.wait() does not progress in Android deep sleep. That is a scheduling change rather than a TTL one, it needs either a periodic API on ISentryExecutorService or a self-rescheduling task, and six tests in DefaultCompositePerformanceCollectorTest assert directly against an injected mock Timer. It deserves its own PR.

🤖 Generated with Claude Code

Both of these asked the wall clock how much time had passed, so a device
time change lengthened or shortened them:

- HostnameCache kept an absolute expiry built from currentTimeMillis, so
  a backward step extended the 5h TTL by the size of the step and a
  forward step expired it early.
- DefaultCompositePerformanceCollector decided a transaction had been
  collecting for 30s by subtracting two dateProvider readings, which are
  wall-clock on every platform: SentryNanotimeDate.nanoTimestamp()
  returns its unix millis, not its nanoTime component.

Both now hold a Deadline on a MonotonicTicker. Neither value is
serialized, so this changes only when the SDK stops waiting.

Two side effects worth noting for review: the 30s boundary is now
inclusive, where the old strict `>` let a sample land exactly at 30s;
and addDataAndCheckTimeout no longer takes the shared clock reading,
since each transaction owns its own deadline. TestMonotonicTicker's
field is now volatile, as the collector's timer thread reads a ticker
the test thread advances.

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

linear-code Bot commented Sep 11, 2026

Copy link
Copy Markdown

JAVA-579

@sentry

sentry Bot commented Sep 11, 2026

Copy link
Copy Markdown

📲 Install Builds

Android

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

⚙️ sentry-android Build Distribution Settings

@runningcode

Copy link
Copy Markdown
Contributor Author

Split into two PRs, since HostnameCache and DefaultCompositePerformanceCollector are unrelated classes and reviewing them together added nothing:

Same changes, same tests; the @Volatile on TestMonotonicTicker went with #6101, which is the one that needs it. The notes about the two File.lastModified() sites that have no monotonic remedy are carried over to both.

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