Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

### Internal

- Measure the hostname cache TTL on a monotonic ticker, so that a device time change no longer shortens or extends it ([#6100](https://github.com/getsentry/sentry-java/pull/6100))

## 8.56.0

### Behavioral Changes
Expand Down
42 changes: 29 additions & 13 deletions sentry/src/main/java/io/sentry/HostnameCache.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package io.sentry;

import io.sentry.time.Deadline;
import io.sentry.time.JavaMonotonicTicker;
import io.sentry.time.MonotonicTicker;
import io.sentry.util.AutoClosableReentrantLock;
import io.sentry.util.Objects;
import java.net.InetAddress;
Expand Down Expand Up @@ -42,14 +45,16 @@ public final class HostnameCache {
private static final @NotNull AutoClosableReentrantLock staticLock =
new AutoClosableReentrantLock();

/** Time for which the cache is kept. */
private final long cacheDuration;
/** Time for which the cache is kept, in milliseconds. */
private final long cacheDurationMillis;

private final @NotNull MonotonicTicker ticker;

/** Current value for hostname (might change over time). */
@Nullable private volatile String hostname;

/** Time at which the cache should expire. */
private volatile long expirationTimestamp;
/** When the cached hostname goes stale. */
private volatile @NotNull Deadline cacheFreshUntil;

/** Whether a cache update thread is currently running or not. */
private final @NotNull AtomicBoolean updateRunning = new AtomicBoolean(false);
Expand All @@ -74,22 +79,34 @@ private HostnameCache() {
this(HOSTNAME_CACHE_DURATION);
}

HostnameCache(long cacheDuration) {
HostnameCache(long cacheDurationMillis) {
// avoid method refs on Android due to some issues with older AGP setups
// noinspection Convert2MethodRef
this(cacheDuration, () -> InetAddress.getLocalHost());
this(cacheDurationMillis, () -> InetAddress.getLocalHost());
}

HostnameCache(long cacheDurationMillis, final @NotNull Callable<InetAddress> getLocalhost) {
this(cacheDurationMillis, getLocalhost, JavaMonotonicTicker.getInstance());
}

/**
* Sets up a cache for the hostname.
*
* @param cacheDuration cache duration in milliseconds.
* @param cacheDurationMillis cache duration in milliseconds.
* @param getLocalhost a callback to obtain the localhost address - this is mostly here because of
* testability
* @param ticker the ticker the cache lifetime is measured on
*/
HostnameCache(long cacheDuration, final @NotNull Callable<InetAddress> getLocalhost) {
this.cacheDuration = cacheDuration;
HostnameCache(
long cacheDurationMillis,
final @NotNull Callable<InetAddress> getLocalhost,
final @NotNull MonotonicTicker ticker) {
this.cacheDurationMillis = cacheDurationMillis;
this.getLocalhost = Objects.requireNonNull(getLocalhost, "getLocalhost is required");
this.ticker = Objects.requireNonNull(ticker, "ticker is required");
// Nothing resolved yet, so the cache is stale rather than fresh until updateCache says
// otherwise.
this.cacheFreshUntil = Deadline.passed(ticker);
// A single thread executor whose worker thread times out while idle, so no thread is kept
// alive between the infrequent cache refreshes.
final @NotNull ThreadPoolExecutor executor =
Expand Down Expand Up @@ -122,8 +139,7 @@ boolean isClosed() {
*/
@Nullable
public String getHostname() {
if (expirationTimestamp < System.currentTimeMillis()
&& updateRunning.compareAndSet(false, true)) {
if (cacheFreshUntil.hasPassed() && updateRunning.compareAndSet(false, true)) {
updateCache();
}

Expand All @@ -136,7 +152,7 @@ private void updateCache() {
() -> {
try {
hostname = getLocalhost.call().getCanonicalHostName();
expirationTimestamp = System.currentTimeMillis() + cacheDuration;
cacheFreshUntil = Deadline.after(ticker, cacheDurationMillis, TimeUnit.MILLISECONDS);
} finally {
updateRunning.set(false);
}
Expand All @@ -156,7 +172,7 @@ private void updateCache() {
}

private void handleCacheUpdateFailure() {
expirationTimestamp = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(1);
cacheFreshUntil = Deadline.after(ticker, 1, TimeUnit.SECONDS);
}

private static final class HostnameCacheThreadFactory implements ThreadFactory {
Expand Down
17 changes: 17 additions & 0 deletions sentry/src/test/java/io/sentry/HostnameCacheTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package io.sentry

import com.google.common.truth.Truth.assertThat
import io.sentry.test.getProperty
import io.sentry.time.TestMonotonicTicker
import java.net.InetAddress
import java.util.concurrent.ThreadPoolExecutor
import java.util.concurrent.TimeUnit
Expand All @@ -23,6 +24,22 @@ class HostnameCacheTest {
assertThat(cache.hostname).isEqualTo("myhost")
}

@Test
fun `hostname is re-resolved only once the cache duration has elapsed`() {
val ticker = TestMonotonicTicker()
val address = mock<InetAddress>()
whenever(address.canonicalHostName).thenReturn("first", "second")
val cache = HostnameCache(TimeUnit.HOURS.toMillis(5), { address }, ticker)

assertThat(cache.hostname).isEqualTo("first")

ticker.advance(4, TimeUnit.HOURS)
assertThat(cache.hostname).isEqualTo("first")

ticker.advance(1, TimeUnit.HOURS)
assertThat(cache.hostname).isEqualTo("second")
}

@Test
fun `worker thread times out while idle instead of staying alive`() {
val cache = getSut()
Expand Down
Loading