From 858ef7b4b71efbbc130891a2daeb0be817e64412 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Tue, 15 Sep 2026 16:44:35 +0200 Subject: [PATCH 1/4] ref(core): Hold the hostname cache on SentryOptions (JAVA-579) HostnameCache was a process-wide static singleton whose constructor reached for JavaMonotonicTicker.getInstance() directly. That was the last place in the SDK hard-coding a ticker rather than taking one from options, so the cache measured its TTL on System.nanoTime() even where options supply an elapsedRealtimeNanos()-backed ticker that counts deep sleep. SentryOptions now holds one instance, built from getMonotonicTicker() the same way RateLimiter(SentryOptions) is. It is wrapped in a LazyEvaluator because resolving the hostname blocks on InetAddress.getLocalHost(), which no Sentry.init should pay for up front; deferring also means the SentryAndroidOptions override is in effect by the time the ticker is read. MainEventProcessor still closes the cache, and now drops both its own reference and the options-held one. Both it and the options outlive Scopes.close(isRestarting = true), so a closed cache left in either place would never refresh the hostname again. MainEventProcessorTest no longer needs Mockito.mockStatic to intercept getInstance(); it sets the cache on options instead. Co-Authored-By: Claude Opus 5 (1M context) --- sentry/api/sentry.api | 4 +- .../main/java/io/sentry/HostnameCache.java | 26 ++-------- .../java/io/sentry/MainEventProcessor.java | 2 +- .../main/java/io/sentry/SentryOptions.java | 33 ++++++++++++ .../main/java/io/sentry/logger/LoggerApi.java | 3 +- .../java/io/sentry/metrics/MetricsApi.java | 3 +- .../java/io/sentry/MainEventProcessorTest.kt | 15 +----- .../test/java/io/sentry/SentryOptionsTest.kt | 51 +++++++++++++++++++ 8 files changed, 95 insertions(+), 42 deletions(-) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 01b068ee689..583f1bb6722 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -694,7 +694,6 @@ public final class io/sentry/Hint { public final class io/sentry/HostnameCache { public fun getHostname ()Ljava/lang/String; - public static fun getInstance ()Lio/sentry/HostnameCache; } public final class io/sentry/HttpBodyType : java/lang/Enum { @@ -3808,6 +3807,7 @@ public class io/sentry/SentryOptions : io/sentry/transport/RateLimiterConfig { public fun getFlushTimeoutMillis ()J public fun getFullyDisplayedReporter ()Lio/sentry/FullyDisplayedReporter; public fun getGestureTargetLocators ()Ljava/util/List; + public fun getHostnameCache ()Lio/sentry/HostnameCache; public fun getIdleTimeout ()Ljava/lang/Long; public fun getIgnoredCheckIns ()Ljava/util/List; public fun getIgnoredErrors ()Ljava/util/List; @@ -3919,6 +3919,7 @@ public class io/sentry/SentryOptions : io/sentry/transport/RateLimiterConfig { public fun isTraceSampling ()Z public fun isTracingEnabled ()Z public fun merge (Lio/sentry/ExternalOptions;)V + public fun resetHostnameCache ()V public fun setAppStartExtender (Lio/sentry/IAppStartExtender;)V public fun setAttachServerName (Z)V public fun setAttachStacktrace (Z)V @@ -3980,6 +3981,7 @@ public class io/sentry/SentryOptions : io/sentry/transport/RateLimiterConfig { public fun setFullyDisplayedReporter (Lio/sentry/FullyDisplayedReporter;)V public fun setGestureTargetLocators (Ljava/util/List;)V public fun setGlobalHubMode (Ljava/lang/Boolean;)V + public fun setHostnameCache (Lio/sentry/HostnameCache;)V public fun setIdleTimeout (Ljava/lang/Long;)V public fun setIgnoredCheckIns (Ljava/util/List;)V public fun setIgnoredErrors (Ljava/util/List;)V diff --git a/sentry/src/main/java/io/sentry/HostnameCache.java b/sentry/src/main/java/io/sentry/HostnameCache.java index 96bf845ef59..bfcfcca5bc9 100644 --- a/sentry/src/main/java/io/sentry/HostnameCache.java +++ b/sentry/src/main/java/io/sentry/HostnameCache.java @@ -1,9 +1,7 @@ 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; import java.util.concurrent.Callable; @@ -29,8 +27,8 @@ * performance purposes, the operation of retrieving the hostname will automatically fail after a * period of time defined by {@link #GET_HOSTNAME_TIMEOUT} without result. * - *

HostnameCache is a singleton and its instance should be obtained through {@link - * HostnameCache#getInstance()}. + *

One instance is held per {@link SentryOptions} and should be obtained through {@link + * SentryOptions#getHostnameCache()}. */ @ApiStatus.Internal public final class HostnameCache { @@ -42,10 +40,6 @@ public final class HostnameCache { /** How long the worker thread may stay idle before it self-terminates. */ private static final long THREAD_KEEP_ALIVE_SECONDS = 30; - private static volatile @Nullable HostnameCache INSTANCE; - private static final @NotNull AutoClosableReentrantLock staticLock = - new AutoClosableReentrantLock(); - private final @NotNull MonotonicTicker ticker; /** Current value for hostname (might change over time). */ @@ -61,22 +55,10 @@ public final class HostnameCache { private final @NotNull ExecutorService executorService; - public static @NotNull HostnameCache getInstance() { - if (INSTANCE == null) { - try (final @NotNull ISentryLifecycleToken ignored = staticLock.acquire()) { - if (INSTANCE == null) { - INSTANCE = new HostnameCache(); - } - } - } - - return INSTANCE; - } - - private HostnameCache() { + HostnameCache(final @NotNull SentryOptions options) { // avoid method refs on Android due to some issues with older AGP setups // noinspection Convert2MethodRef - this(() -> InetAddress.getLocalHost(), JavaMonotonicTicker.getInstance()); + this(() -> InetAddress.getLocalHost(), options.getMonotonicTicker()); } /** diff --git a/sentry/src/main/java/io/sentry/MainEventProcessor.java b/sentry/src/main/java/io/sentry/MainEventProcessor.java index 79b45a30c65..9b5c6128d0a 100644 --- a/sentry/src/main/java/io/sentry/MainEventProcessor.java +++ b/sentry/src/main/java/io/sentry/MainEventProcessor.java @@ -169,7 +169,7 @@ private void setServerName(final @NotNull SentryBaseEvent event) { private void ensureHostnameCache() { if (hostnameCache == null) { - hostnameCache = HostnameCache.getInstance(); + hostnameCache = options.getHostnameCache(); } } diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 2b2f9d370a0..fb4ef96015c 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -535,6 +535,17 @@ public class SentryOptions implements RateLimiterConfig { private final @NotNull LazyEvaluator dateProvider = new LazyEvaluator<>(() -> new SentryAutoDateProvider()); + /** + * Cache of the local hostname, used when {@link #isAttachServerName()} is enabled. + * + *

Evaluated lazily because resolving the hostname blocks on {@code + * InetAddress.getLocalHost()}, which no {@code Sentry.init} should pay for up front. Deferring + * also means {@link #getMonotonicTicker()} is read after subclasses have overridden it. + */ + @ApiStatus.Internal + private final @NotNull LazyEvaluator hostnameCache = + new LazyEvaluator<>(() -> new HostnameCache(this)); + private final @NotNull List performanceCollectors = new ArrayList<>(); /** Performance collector that collect performance stats while transactions run. */ @@ -3140,6 +3151,28 @@ public void setDateProvider(final @NotNull SentryDateProvider dateProvider) { return JavaMonotonicTicker.getInstance(); } + /** Returns the hostname cache, resolving the hostname on first use. */ + @ApiStatus.Internal + public @NotNull HostnameCache getHostnameCache() { + return hostnameCache.getValue(); + } + + @ApiStatus.Internal + public void setHostnameCache(final @NotNull HostnameCache hostnameCache) { + this.hostnameCache.setValue(hostnameCache); + } + + /** + * Discards the cached instance, so that the next {@link #getHostnameCache()} builds a new one. + * + *

Called after the cache has been closed, because these options outlive a restart and a closed + * cache can no longer resolve anything. + */ + @ApiStatus.Internal + public void resetHostnameCache() { + hostnameCache.resetValue(); + } + /** * Adds a ICollector. * diff --git a/sentry/src/main/java/io/sentry/logger/LoggerApi.java b/sentry/src/main/java/io/sentry/logger/LoggerApi.java index 3741ddc6209..139ac615394 100644 --- a/sentry/src/main/java/io/sentry/logger/LoggerApi.java +++ b/sentry/src/main/java/io/sentry/logger/LoggerApi.java @@ -1,6 +1,5 @@ package io.sentry.logger; -import io.sentry.HostnameCache; import io.sentry.IScope; import io.sentry.ISpan; import io.sentry.PropagationContext; @@ -256,7 +255,7 @@ private void setServerName( "server.address", new SentryLogEventAttributeValue(SentryAttributeType.STRING, optionsServerName)); } else if (options.isAttachServerName()) { - final @Nullable String hostname = HostnameCache.getInstance().getHostname(); + final @Nullable String hostname = options.getHostnameCache().getHostname(); if (hostname != null) { attributes.put( "server.address", diff --git a/sentry/src/main/java/io/sentry/metrics/MetricsApi.java b/sentry/src/main/java/io/sentry/metrics/MetricsApi.java index d70b4ab5c97..93564e4ddac 100644 --- a/sentry/src/main/java/io/sentry/metrics/MetricsApi.java +++ b/sentry/src/main/java/io/sentry/metrics/MetricsApi.java @@ -1,6 +1,5 @@ package io.sentry.metrics; -import io.sentry.HostnameCache; import io.sentry.IScope; import io.sentry.ISpan; import io.sentry.PropagationContext; @@ -241,7 +240,7 @@ private void setServerName( "server.address", new SentryLogEventAttributeValue(SentryAttributeType.STRING, optionsServerName)); } else if (options.isAttachServerName()) { - final @Nullable String hostname = HostnameCache.getInstance().getHostname(); + final @Nullable String hostname = options.getHostnameCache().getHostname(); if (hostname != null) { attributes.put( "server.address", diff --git a/sentry/src/test/java/io/sentry/MainEventProcessorTest.kt b/sentry/src/test/java/io/sentry/MainEventProcessorTest.kt index 20c4682d4b5..b3e90fc4277 100644 --- a/sentry/src/test/java/io/sentry/MainEventProcessorTest.kt +++ b/sentry/src/test/java/io/sentry/MainEventProcessorTest.kt @@ -12,7 +12,6 @@ import io.sentry.util.HintUtils import java.lang.RuntimeException import java.net.InetAddress import java.util.concurrent.TimeUnit -import kotlin.test.AfterTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -20,7 +19,6 @@ import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertSame import kotlin.test.assertTrue -import org.mockito.Mockito import org.mockito.kotlin.mock import org.mockito.kotlin.reset import org.mockito.kotlin.times @@ -40,7 +38,6 @@ class MainEventProcessorTest { val getLocalhost = mock() val hostnameCacheTicker = TestMonotonicTicker() lateinit var sentryTracer: SentryTracer - private val hostnameCacheMock = Mockito.mockStatic(HostnameCache::class.java) fun getSut( attachThreads: Boolean = true, @@ -78,20 +75,10 @@ class MainEventProcessorTest { whenever(scopes.options).thenReturn(sentryOptions) sentryTracer = SentryTracer(TransactionContext("", ""), scopes) - val hostnameCache = HostnameCache({ getLocalhost }, hostnameCacheTicker) - hostnameCacheMock.`when` { HostnameCache.getInstance() }.thenReturn(hostnameCache) + sentryOptions.setHostnameCache(HostnameCache({ getLocalhost }, hostnameCacheTicker)) return MainEventProcessor(sentryOptions) } - - fun teardown() { - hostnameCacheMock.close() - } - } - - @AfterTest - fun teardown() { - fixture.teardown() } private val fixture = Fixture() diff --git a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt index 6b47d36e53d..1010a71a095 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -3,9 +3,15 @@ package io.sentry import com.google.common.truth.Truth.assertThat import io.sentry.SentryOptions.RequestSize import io.sentry.logger.ILoggerBatchProcessorFactory +import io.sentry.test.getProperty +import io.sentry.time.MonotonicTicker +import io.sentry.time.TestMonotonicTicker +import io.sentry.util.LazyEvaluator import io.sentry.util.StringUtils import java.io.File +import java.net.InetAddress import java.net.Proxy +import java.util.concurrent.Callable import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -1331,4 +1337,49 @@ class SentryOptionsTest { options.scopesStorageFactory = null assertNull(options.scopesStorageFactory) } + + private fun SentryOptions.peekHostnameCache(): HostnameCache? = + getProperty>("hostnameCache").getProperty("value") + + @Test + fun `constructing options does not resolve the hostname`() { + val options = SentryOptions() + assertThat(options.peekHostnameCache()).isNull() + + // Asserting that peek reports an evaluated cache keeps the check above from passing vacuously. + options.hostnameCache = HostnameCache(Callable { mock() }, TestMonotonicTicker()) + assertThat(options.peekHostnameCache()).isNotNull() + } + + @Test + fun `hostnameCache is created once and reused`() { + val options = SentryOptions() + val cache = HostnameCache(Callable { mock() }, TestMonotonicTicker()) + options.hostnameCache = cache + + assertThat(options.hostnameCache).isSameInstanceAs(cache) + assertThat(options.hostnameCache).isSameInstanceAs(cache) + } + + @Test + fun `resetHostnameCache discards the cached instance`() { + val options = SentryOptions() + options.hostnameCache = HostnameCache(Callable { mock() }, TestMonotonicTicker()) + + options.resetHostnameCache() + + assertThat(options.peekHostnameCache()).isNull() + } + + @Test + fun `hostnameCache measures its lifetime on the options ticker`() { + val ticker = TestMonotonicTicker() + val options = + object : SentryOptions() { + override fun getMonotonicTicker(): MonotonicTicker = ticker + } + + assertThat(options.hostnameCache.getProperty("ticker")) + .isSameInstanceAs(ticker) + } } From 36a856758832bc2490bad56ff8b623fccf9ffea1 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Tue, 15 Sep 2026 16:45:47 +0200 Subject: [PATCH 2/4] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e169d95abb1..524d50870f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### 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)) +- Hold the hostname cache on `SentryOptions` instead of a static singleton, so that it measures its TTL on the ticker the options provide ([#6117](https://github.com/getsentry/sentry-java/pull/6117)) ## 8.60.0 From 7a59f20f1fcbd9ca0e1a6c9922e525c9094a47d8 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Tue, 15 Sep 2026 17:02:29 +0200 Subject: [PATCH 3/4] ref(core): Build the hostname cache from a ticker, not the whole options MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HostnameCache reads exactly one collaborator, so it now takes a MonotonicTicker rather than a SentryOptions it would only call getMonotonicTicker() on. This follows RateLimiter.create(MonotonicTicker, RateLimiterConfig), which names the collaborators a rate limiter actually reads for the same reason; a config interface is unnecessary here because there is only the one. Also drops SentryOptions.setHostnameCache(), which had no production caller. Unlike setDateProvider(), which AndroidOptionsInitializer uses, it was only a test seam, so MainEventProcessorTest overrides getHostnameCache() instead — the way CheckInUtilsTest already overrides getMonotonicTicker(). Co-Authored-By: Claude Opus 5 (1M context) --- sentry/api/sentry.api | 1 - .../main/java/io/sentry/HostnameCache.java | 10 +++- .../main/java/io/sentry/SentryOptions.java | 7 +-- .../java/io/sentry/MainEventProcessorTest.kt | 24 ++++++---- .../test/java/io/sentry/SentryOptionsTest.kt | 46 +++++++------------ 5 files changed, 41 insertions(+), 47 deletions(-) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 583f1bb6722..b48959055a4 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -3981,7 +3981,6 @@ public class io/sentry/SentryOptions : io/sentry/transport/RateLimiterConfig { public fun setFullyDisplayedReporter (Lio/sentry/FullyDisplayedReporter;)V public fun setGestureTargetLocators (Ljava/util/List;)V public fun setGlobalHubMode (Ljava/lang/Boolean;)V - public fun setHostnameCache (Lio/sentry/HostnameCache;)V public fun setIdleTimeout (Ljava/lang/Long;)V public fun setIgnoredCheckIns (Ljava/util/List;)V public fun setIgnoredErrors (Ljava/util/List;)V diff --git a/sentry/src/main/java/io/sentry/HostnameCache.java b/sentry/src/main/java/io/sentry/HostnameCache.java index bfcfcca5bc9..8407495acf2 100644 --- a/sentry/src/main/java/io/sentry/HostnameCache.java +++ b/sentry/src/main/java/io/sentry/HostnameCache.java @@ -55,10 +55,16 @@ public final class HostnameCache { private final @NotNull ExecutorService executorService; - HostnameCache(final @NotNull SentryOptions options) { + /** + * Names the only collaborator a hostname cache reads, rather than taking the whole {@link + * SentryOptions}. + * + * @param ticker the ticker the cache lifetime is measured on + */ + HostnameCache(final @NotNull MonotonicTicker ticker) { // avoid method refs on Android due to some issues with older AGP setups // noinspection Convert2MethodRef - this(() -> InetAddress.getLocalHost(), options.getMonotonicTicker()); + this(() -> InetAddress.getLocalHost(), ticker); } /** diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index fb4ef96015c..8aab34715a7 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -544,7 +544,7 @@ public class SentryOptions implements RateLimiterConfig { */ @ApiStatus.Internal private final @NotNull LazyEvaluator hostnameCache = - new LazyEvaluator<>(() -> new HostnameCache(this)); + new LazyEvaluator<>(() -> new HostnameCache(getMonotonicTicker())); private final @NotNull List performanceCollectors = new ArrayList<>(); @@ -3157,11 +3157,6 @@ public void setDateProvider(final @NotNull SentryDateProvider dateProvider) { return hostnameCache.getValue(); } - @ApiStatus.Internal - public void setHostnameCache(final @NotNull HostnameCache hostnameCache) { - this.hostnameCache.setValue(hostnameCache); - } - /** * Discards the cached instance, so that the next {@link #getHostnameCache()} builds a new one. * diff --git a/sentry/src/test/java/io/sentry/MainEventProcessorTest.kt b/sentry/src/test/java/io/sentry/MainEventProcessorTest.kt index b3e90fc4277..7529d0a3e54 100644 --- a/sentry/src/test/java/io/sentry/MainEventProcessorTest.kt +++ b/sentry/src/test/java/io/sentry/MainEventProcessorTest.kt @@ -27,16 +27,23 @@ import org.mockito.kotlin.whenever class MainEventProcessorTest { class Fixture { - val sentryOptions: SentryOptions = - SentryOptions().apply { - dsn = dsnString - release = "release" - dist = "dist" - sdkVersion = SdkVersion("test", "1.2.3") - } val scopes = mock() val getLocalhost = mock() val hostnameCacheTicker = TestMonotonicTicker() + // Built in getSut() rather than here: the constructor resolves the hostname straight away, + // so it has to run after getLocalhost is stubbed. + lateinit var hostnameCache: HostnameCache + val sentryOptions: SentryOptions = + object : SentryOptions() { + // Qualified: an unqualified name here would resolve to this override, not the field. + override fun getHostnameCache(): HostnameCache = this@Fixture.hostnameCache + } + .apply { + dsn = dsnString + release = "release" + dist = "dist" + sdkVersion = SdkVersion("test", "1.2.3") + } lateinit var sentryTracer: SentryTracer fun getSut( @@ -74,8 +81,7 @@ class MainEventProcessorTest { } whenever(scopes.options).thenReturn(sentryOptions) sentryTracer = SentryTracer(TransactionContext("", ""), scopes) - - sentryOptions.setHostnameCache(HostnameCache({ getLocalhost }, hostnameCacheTicker)) + hostnameCache = HostnameCache({ getLocalhost }, hostnameCacheTicker) return MainEventProcessor(sentryOptions) } diff --git a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt index 1010a71a095..6e54054581c 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -9,9 +9,7 @@ import io.sentry.time.TestMonotonicTicker import io.sentry.util.LazyEvaluator import io.sentry.util.StringUtils import java.io.File -import java.net.InetAddress import java.net.Proxy -import java.util.concurrent.Callable import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -1341,45 +1339,35 @@ class SentryOptionsTest { private fun SentryOptions.peekHostnameCache(): HostnameCache? = getProperty>("hostnameCache").getProperty("value") + /** Options whose hostname cache resolves against a ticker the test controls. */ + private fun optionsWithTicker(ticker: MonotonicTicker): SentryOptions = + object : SentryOptions() { + override fun getMonotonicTicker(): MonotonicTicker = ticker + } + @Test - fun `constructing options does not resolve the hostname`() { - val options = SentryOptions() - assertThat(options.peekHostnameCache()).isNull() + fun `hostname is resolved on first use, not when options are constructed`() { + val ticker = TestMonotonicTicker() + val options = optionsWithTicker(ticker) - // Asserting that peek reports an evaluated cache keeps the check above from passing vacuously. - options.hostnameCache = HostnameCache(Callable { mock() }, TestMonotonicTicker()) - assertThat(options.peekHostnameCache()).isNotNull() - } + assertThat(options.peekHostnameCache()).isNull() - @Test - fun `hostnameCache is created once and reused`() { - val options = SentryOptions() - val cache = HostnameCache(Callable { mock() }, TestMonotonicTicker()) - options.hostnameCache = cache + val cache = options.hostnameCache + // Also keeps the assertion above honest: peek does report a cache once one exists. + assertThat(options.peekHostnameCache()).isSameInstanceAs(cache) assertThat(options.hostnameCache).isSameInstanceAs(cache) - assertThat(options.hostnameCache).isSameInstanceAs(cache) + assertThat(cache.getProperty("ticker")).isSameInstanceAs(ticker) } @Test fun `resetHostnameCache discards the cached instance`() { - val options = SentryOptions() - options.hostnameCache = HostnameCache(Callable { mock() }, TestMonotonicTicker()) + val options = optionsWithTicker(TestMonotonicTicker()) + val cache = options.hostnameCache options.resetHostnameCache() assertThat(options.peekHostnameCache()).isNull() - } - - @Test - fun `hostnameCache measures its lifetime on the options ticker`() { - val ticker = TestMonotonicTicker() - val options = - object : SentryOptions() { - override fun getMonotonicTicker(): MonotonicTicker = ticker - } - - assertThat(options.hostnameCache.getProperty("ticker")) - .isSameInstanceAs(ticker) + assertThat(options.hostnameCache).isNotSameInstanceAs(cache) } } From a403884ee8918fee80b73415d4d3d393004b37f2 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Tue, 15 Sep 2026 17:23:04 +0200 Subject: [PATCH 4/4] ref(core): Stop closing the hostname cache The cache's executor is a single daemon thread with allowCoreThreadTimeOut(true) and a 30 second keep-alive, so it self-terminates once idle and never holds up process exit. close() was switching off something that switches itself off. Scopes.close() already leaves the timer executor running for this reason. Dropping it removes the reason SentryOptions.resetHostnameCache() existed. That method was only there because MainEventProcessor closed a cache the options own, which a re-init with the same options object would then keep handing out shut down. Nothing closes the cache now, so nothing has to undo it. MainEventProcessor no longer implements Closeable, and no longer memoizes the cache either: the options build it on first use, so the field and ensureHostnameCache() were duplicating LazyEvaluator. Both removed methods are on @ApiStatus.Internal types. SentryClientTest's `when client is closed, hostname cache is closed` went with them; it asserted isClosed() on a processor that had never resolved a hostname, which returned true because the cache was still null. Co-Authored-By: Claude Opus 5 (1M context) --- sentry/api/sentry.api | 1 - .../src/main/java/io/sentry/MainEventProcessor.java | 12 +----------- sentry/src/main/java/io/sentry/SentryOptions.java | 11 ----------- sentry/src/test/java/io/sentry/SentryOptionsTest.kt | 11 ----------- 4 files changed, 1 insertion(+), 34 deletions(-) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index b48959055a4..bbeab303330 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -3919,7 +3919,6 @@ public class io/sentry/SentryOptions : io/sentry/transport/RateLimiterConfig { public fun isTraceSampling ()Z public fun isTracingEnabled ()Z public fun merge (Lio/sentry/ExternalOptions;)V - public fun resetHostnameCache ()V public fun setAppStartExtender (Lio/sentry/IAppStartExtender;)V public fun setAttachServerName (Z)V public fun setAttachStacktrace (Z)V diff --git a/sentry/src/main/java/io/sentry/MainEventProcessor.java b/sentry/src/main/java/io/sentry/MainEventProcessor.java index 9b5c6128d0a..6602e8f07bf 100644 --- a/sentry/src/main/java/io/sentry/MainEventProcessor.java +++ b/sentry/src/main/java/io/sentry/MainEventProcessor.java @@ -21,7 +21,6 @@ public final class MainEventProcessor implements EventProcessor { private final @NotNull SentryOptions options; private final @NotNull SentryThreadFactory sentryThreadFactory; private final @NotNull SentryExceptionFactory sentryExceptionFactory; - private volatile @Nullable HostnameCache hostnameCache = null; public MainEventProcessor(final @NotNull SentryOptions options) { this.options = options; @@ -160,16 +159,7 @@ private void setServerName(final @NotNull SentryBaseEvent event) { } if (options.isAttachServerName() && event.getServerName() == null) { - ensureHostnameCache(); - if (hostnameCache != null) { - event.setServerName(hostnameCache.getHostname()); - } - } - } - - private void ensureHostnameCache() { - if (hostnameCache == null) { - hostnameCache = options.getHostnameCache(); + event.setServerName(options.getHostnameCache().getHostname()); } } diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 8aab34715a7..d6ed66f2fcb 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -3157,17 +3157,6 @@ public void setDateProvider(final @NotNull SentryDateProvider dateProvider) { return hostnameCache.getValue(); } - /** - * Discards the cached instance, so that the next {@link #getHostnameCache()} builds a new one. - * - *

Called after the cache has been closed, because these options outlive a restart and a closed - * cache can no longer resolve anything. - */ - @ApiStatus.Internal - public void resetHostnameCache() { - hostnameCache.resetValue(); - } - /** * Adds a ICollector. * diff --git a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt index 6e54054581c..284c0a98cd5 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -1359,15 +1359,4 @@ class SentryOptionsTest { assertThat(options.hostnameCache).isSameInstanceAs(cache) assertThat(cache.getProperty("ticker")).isSameInstanceAs(ticker) } - - @Test - fun `resetHostnameCache discards the cached instance`() { - val options = optionsWithTicker(TestMonotonicTicker()) - val cache = options.hostnameCache - - options.resetHostnameCache() - - assertThat(options.peekHostnameCache()).isNull() - assertThat(options.hostnameCache).isNotSameInstanceAs(cache) - } }