Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
ref(core): Build the hostname cache from a ticker, not the whole options
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) <noreply@anthropic.com>
  • Loading branch information
runningcode and claude committed Sep 15, 2026
commit a4aecfed034d10a32cb23f2707028a741f1cfb56
1 change: 0 additions & 1 deletion sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -3888,7 +3888,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
Expand Down
10 changes: 8 additions & 2 deletions sentry/src/main/java/io/sentry/HostnameCache.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,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);
}

/**
Expand Down
7 changes: 1 addition & 6 deletions sentry/src/main/java/io/sentry/SentryOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -539,7 +539,7 @@ public class SentryOptions implements RateLimiterConfig {
*/
@ApiStatus.Internal
private final @NotNull LazyEvaluator<HostnameCache> hostnameCache =
new LazyEvaluator<>(() -> new HostnameCache(this));
new LazyEvaluator<>(() -> new HostnameCache(getMonotonicTicker()));

private final @NotNull List<IPerformanceCollector> performanceCollectors = new ArrayList<>();

Expand Down Expand Up @@ -3109,11 +3109,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.
*
Expand Down
24 changes: 15 additions & 9 deletions sentry/src/test/java/io/sentry/MainEventProcessorTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,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<IScopes>()
val getLocalhost = mock<InetAddress>()
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(
Expand Down Expand Up @@ -73,8 +80,7 @@ class MainEventProcessorTest {
}
whenever(scopes.options).thenReturn(sentryOptions)
sentryTracer = SentryTracer(TransactionContext("", ""), scopes)

sentryOptions.setHostnameCache(HostnameCache({ getLocalhost }, hostnameCacheTicker))
hostnameCache = HostnameCache({ getLocalhost }, hostnameCacheTicker)

return MainEventProcessor(sentryOptions)
}
Expand Down
46 changes: 17 additions & 29 deletions sentry/src/test/java/io/sentry/SentryOptionsTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1178,45 +1176,35 @@ class SentryOptionsTest {
private fun SentryOptions.peekHostnameCache(): HostnameCache? =
getProperty<LazyEvaluator<HostnameCache>>("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<InetAddress>() }, 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<InetAddress>() }, 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<MonotonicTicker>("ticker")).isSameInstanceAs(ticker)
}

@Test
fun `resetHostnameCache discards the cached instance`() {
val options = SentryOptions()
options.hostnameCache = HostnameCache(Callable { mock<InetAddress>() }, 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<MonotonicTicker>("ticker"))
.isSameInstanceAs(ticker)
assertThat(options.hostnameCache).isNotSameInstanceAs(cache)
}
}
Loading