refactor(okhttp): rename AmethystDns -> SurgeDns

"AmethystDns" identified the owner; "SurgeDns" identifies the
behavior. The class is built to absorb sudden bursts of concurrent
DNS work — 700 relays reconnecting, a feed scrolling through dozens
of media hosts, a profile screen requesting a NIP-05 — without
falling over: lock-free reads, single-flight coalescing, and
stale-while-revalidate so recurring hosts never block.

File renames:
- AmethystDns.kt        -> SurgeDns.kt
- AmethystDnsStore.kt   -> SurgeDnsStore.kt
- AmethystDnsTest.kt    -> SurgeDnsTest.kt

Symbol renames in every touchpoint (AppModules, factories,
managers, event listeners): AmethystDns -> SurgeDns,
amethystDns -> surgeDns.
This commit is contained in:
Claude
2026-05-03 21:26:46 +00:00
parent e173daf3c9
commit b9a260f3ce
10 changed files with 45 additions and 45 deletions
@@ -53,12 +53,12 @@ import com.vitorpamplona.amethyst.service.location.LocationState
import com.vitorpamplona.amethyst.service.notifications.AlwaysOnNotificationServiceManager import com.vitorpamplona.amethyst.service.notifications.AlwaysOnNotificationServiceManager
import com.vitorpamplona.amethyst.service.notifications.NotificationDispatcher import com.vitorpamplona.amethyst.service.notifications.NotificationDispatcher
import com.vitorpamplona.amethyst.service.notifications.PokeyReceiver import com.vitorpamplona.amethyst.service.notifications.PokeyReceiver
import com.vitorpamplona.amethyst.service.okhttp.AmethystDns
import com.vitorpamplona.amethyst.service.okhttp.AmethystDnsStore
import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManager import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManager
import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManagerForRelays import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManagerForRelays
import com.vitorpamplona.amethyst.service.okhttp.EncryptionKeyCache import com.vitorpamplona.amethyst.service.okhttp.EncryptionKeyCache
import com.vitorpamplona.amethyst.service.okhttp.OkHttpWebSocket import com.vitorpamplona.amethyst.service.okhttp.OkHttpWebSocket
import com.vitorpamplona.amethyst.service.okhttp.SurgeDns
import com.vitorpamplona.amethyst.service.okhttp.SurgeDnsStore
import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCache import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCache
import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCacheFactory import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCacheFactory
import com.vitorpamplona.amethyst.service.playback.pip.BackgroundMedia import com.vitorpamplona.amethyst.service.playback.pip.BackgroundMedia
@@ -206,12 +206,12 @@ class AppModules(
// Concurrent, caching DNS resolver shared by every OkHttp client built below — a host // Concurrent, caching DNS resolver shared by every OkHttp client built below — a host
// resolved for an image fetch is reused when a relay handshake or NIP-05 lookup hits the // resolved for an image fetch is reused when a relay handshake or NIP-05 lookup hits the
// same host. // same host.
val amethystDns = AmethystDns() val surgeDns = SurgeDns()
// Persists [amethystDns]'s positive cache across process restarts so cold starts don't pay // Persists [surgeDns]'s positive cache across process restarts so cold starts don't pay
// ~700 sync getaddrinfo calls. Restored entries fall through to the stale-while-revalidate // ~700 sync getaddrinfo calls. Restored entries fall through to the stale-while-revalidate
// path on first lookup. // path on first lookup.
val dnsStore = AmethystDnsStore(appContext, amethystDns) val dnsStore = SurgeDnsStore(appContext, surgeDns)
// manages all the other connections separately from relays. // manages all the other connections separately from relays.
val okHttpClients = val okHttpClients =
@@ -221,7 +221,7 @@ class AppModules(
isMobileDataProvider = connManager.isMobileOrNull, isMobileDataProvider = connManager.isMobileOrNull,
keyCache = keyCache, keyCache = keyCache,
scope = applicationIOScope, scope = applicationIOScope,
dns = amethystDns, dns = surgeDns,
) )
// Offers easy methods to know when connections are happening through Tor or not // Offers easy methods to know when connections are happening through Tor or not
@@ -303,7 +303,7 @@ class AppModules(
proxyPortProvider = torManager.activePortOrNull, proxyPortProvider = torManager.activePortOrNull,
isMobileDataProvider = connManager.isMobileOrNull, isMobileDataProvider = connManager.isMobileOrNull,
scope = applicationIOScope, scope = applicationIOScope,
dns = amethystDns, dns = surgeDns,
) )
// Connects the INostrClient class with okHttp // Connects the INostrClient class with okHttp
@@ -25,7 +25,7 @@ import okhttp3.EventListener
import java.io.IOException import java.io.IOException
/** /**
* Drops a host's [AmethystDns] entry whenever an OkHttp call to it fails outright. We hook * Drops a host's [SurgeDns] entry whenever an OkHttp call to it fails outright. We hook
* `callFailed` (final-stage signal after OkHttp has tried every address from the DNS lookup) * `callFailed` (final-stage signal after OkHttp has tried every address from the DNS lookup)
* rather than per-attempt `connectFailed`: a multi-A-record host with one bad IP fires the * rather than per-attempt `connectFailed`: a multi-A-record host with one bad IP fires the
* latter while OkHttp recovers via the next address, and we don't want to invalidate the cache * latter while OkHttp recovers via the next address, and we don't want to invalidate the cache
@@ -35,7 +35,7 @@ import java.io.IOException
* invalidation into its `finish` method alongside its existing timing logging. * invalidation into its `finish` method alongside its existing timing logging.
*/ */
class DnsInvalidatingEventListener( class DnsInvalidatingEventListener(
private val dns: AmethystDns, private val dns: SurgeDns,
) : EventListener() { ) : EventListener() {
override fun callFailed( override fun callFailed(
call: Call, call: Call,
@@ -46,7 +46,7 @@ class DnsInvalidatingEventListener(
/** Per-client factory. The listener is stateless, so the same instance serves every call. */ /** Per-client factory. The listener is stateless, so the same instance serves every call. */
class Factory( class Factory(
dns: AmethystDns, dns: SurgeDns,
) : EventListener.Factory { ) : EventListener.Factory {
private val listener = DnsInvalidatingEventListener(dns) private val listener = DnsInvalidatingEventListener(dns)
@@ -38,7 +38,7 @@ class DualHttpClientManager(
isMobileDataProvider: StateFlow<Boolean?>, isMobileDataProvider: StateFlow<Boolean?>,
keyCache: EncryptionKeyCache, keyCache: EncryptionKeyCache,
scope: CoroutineScope, scope: CoroutineScope,
dns: AmethystDns, dns: SurgeDns,
) : IHttpClientManager { ) : IHttpClientManager {
val factory = OkHttpClientFactory(keyCache, userAgent, dns) val factory = OkHttpClientFactory(keyCache, userAgent, dns)
@@ -35,7 +35,7 @@ class DualHttpClientManagerForRelays(
proxyPortProvider: StateFlow<Int?>, proxyPortProvider: StateFlow<Int?>,
isMobileDataProvider: StateFlow<Boolean?>, isMobileDataProvider: StateFlow<Boolean?>,
scope: CoroutineScope, scope: CoroutineScope,
dns: AmethystDns, dns: SurgeDns,
) : IHttpClientManager { ) : IHttpClientManager {
val factory = OkHttpClientFactoryForRelays(userAgent, dns) val factory = OkHttpClientFactoryForRelays(userAgent, dns)
@@ -44,7 +44,7 @@ import java.net.Proxy
class MediaCallEventListener( class MediaCallEventListener(
private val dispatcher: Dispatcher, private val dispatcher: Dispatcher,
private val connectionPool: ConnectionPool, private val connectionPool: ConnectionPool,
private val dns: AmethystDns, private val dns: SurgeDns,
) : EventListener() { ) : EventListener() {
private var callStartNanos = 0L private var callStartNanos = 0L
private var dnsStartNanos = 0L private var dnsStartNanos = 0L
@@ -179,7 +179,7 @@ class MediaCallEventListener(
class MediaCallEventListenerFactory( class MediaCallEventListenerFactory(
private val dispatcher: Dispatcher, private val dispatcher: Dispatcher,
private val connectionPool: ConnectionPool, private val connectionPool: ConnectionPool,
private val dns: AmethystDns, private val dns: SurgeDns,
) : EventListener.Factory { ) : EventListener.Factory {
override fun create(call: Call): EventListener = MediaCallEventListener(dispatcher, connectionPool, dns) override fun create(call: Call): EventListener = MediaCallEventListener(dispatcher, connectionPool, dns)
} }
@@ -35,7 +35,7 @@ import java.util.concurrent.TimeUnit
class OkHttpClientFactory( class OkHttpClientFactory(
keyCache: EncryptionKeyCache, keyCache: EncryptionKeyCache,
val userAgent: String, val userAgent: String,
private val dns: AmethystDns, private val dns: SurgeDns,
) { ) {
// val logging = LoggingInterceptor() // val logging = LoggingInterceptor()
val keyDecryptor = EncryptedBlobInterceptor(keyCache) val keyDecryptor = EncryptedBlobInterceptor(keyCache)
@@ -29,7 +29,7 @@ import java.time.Duration
class OkHttpClientFactoryForRelays( class OkHttpClientFactoryForRelays(
userAgent: String, userAgent: String,
private val dns: AmethystDns, private val dns: SurgeDns,
) { ) {
companion object { companion object {
// by picking a random proxy port, the connection will fail as it should. // by picking a random proxy port, the connection will fail as it should.
@@ -69,7 +69,7 @@ import java.util.concurrent.atomic.AtomicBoolean
* (unavoidable there's nothing to serve stale yet), and followers waiting on that first * (unavoidable there's nothing to serve stale yet), and followers waiting on that first
* lookup block on `future.get()`. Background refreshes never block any caller. * lookup block on `future.get()`. Background refreshes never block any caller.
*/ */
class AmethystDns( class SurgeDns(
private val delegate: Dns = Dns.SYSTEM, private val delegate: Dns = Dns.SYSTEM,
private val maxEntries: Int = 2000, private val maxEntries: Int = 2000,
private val positiveTtlMs: Long = TimeUnit.HOURS.toMillis(24), private val positiveTtlMs: Long = TimeUnit.HOURS.toMillis(24),
@@ -286,7 +286,7 @@ class AmethystDns(
} }
} }
/** Persistable record. Public so [AmethystDnsStore] can serialize it via Jackson. */ /** Persistable record. Public so [SurgeDnsStore] can serialize it via Jackson. */
data class DnsCacheRecord( data class DnsCacheRecord(
val hostname: String, val hostname: String,
val addresses: List<String>, val addresses: List<String>,
@@ -26,7 +26,7 @@ import com.fasterxml.jackson.module.kotlin.readValue
import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.Log
/** /**
* Persists [AmethystDns]'s positive cache to a small `SharedPreferences` blob so the resolver * Persists [SurgeDns]'s positive cache to a small `SharedPreferences` blob so the resolver
* starts hot after a process restart. * starts hot after a process restart.
* *
* Cold starts are the resolver's worst case every host is a sync `getaddrinfo`. With a * Cold starts are the resolver's worst case every host is a sync `getaddrinfo`. With a
@@ -38,15 +38,15 @@ import com.vitorpamplona.quartz.utils.Log
* list, Coil's image cache, and the system resolver's own state. ~700 entries × ~80 bytes * list, Coil's image cache, and the system resolver's own state. ~700 entries × ~80 bytes
* ~55 KB of JSON. * ~55 KB of JSON.
*/ */
class AmethystDnsStore( class SurgeDnsStore(
private val context: Context, private val context: Context,
private val dns: AmethystDns, private val dns: SurgeDns,
) { ) {
private val prefs by lazy { context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) } private val prefs by lazy { context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) }
/** /**
* Read the persisted snapshot and merge it into the resolver. Existing in-memory entries * Read the persisted snapshot and merge it into the resolver. Existing in-memory entries
* are preserved (see [AmethystDns.restore]). Safe to call once at app start. Blocking I/O * are preserved (see [SurgeDns.restore]). Safe to call once at app start. Blocking I/O
* call from a background thread. * call from a background thread.
*/ */
fun load() { fun load() {
@@ -93,7 +93,7 @@ class AmethystDnsStore(
} }
companion object { companion object {
private const val TAG = "AmethystDnsStore" private const val TAG = "SurgeDnsStore"
private const val PREFS_NAME = "amethyst_dns_cache" private const val PREFS_NAME = "amethyst_dns_cache"
private const val KEY_SNAPSHOT = "dns_cache_v1" private const val KEY_SNAPSHOT = "dns_cache_v1"
private val MAPPER = jacksonObjectMapper() private val MAPPER = jacksonObjectMapper()
@@ -37,7 +37,7 @@ import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.atomic.AtomicReference
class AmethystDnsTest { class SurgeDnsTest {
private fun ip(value: String) = InetAddress.getByName(value) private fun ip(value: String) = InetAddress.getByName(value)
private class CountingDns( private class CountingDns(
@@ -71,7 +71,7 @@ class AmethystDnsTest {
@Test @Test
fun `cache hit avoids second upstream call`() { fun `cache hit avoids second upstream call`() {
val upstream = CountingDns(mapOf("a.example" to listOf(ip("1.2.3.4")))) val upstream = CountingDns(mapOf("a.example" to listOf(ip("1.2.3.4"))))
val dns = AmethystDns(delegate = upstream) val dns = SurgeDns(delegate = upstream)
val first = dns.lookup("a.example") val first = dns.lookup("a.example")
val second = dns.lookup("a.example") val second = dns.lookup("a.example")
@@ -84,7 +84,7 @@ class AmethystDnsTest {
@Test @Test
fun `negative cache short-circuits subsequent lookups`() { fun `negative cache short-circuits subsequent lookups`() {
val upstream = CountingDns(emptyMap()) val upstream = CountingDns(emptyMap())
val dns = AmethystDns(delegate = upstream) val dns = SurgeDns(delegate = upstream)
assertThrows(UnknownHostException::class.java) { dns.lookup("missing.example") } assertThrows(UnknownHostException::class.java) { dns.lookup("missing.example") }
assertThrows(UnknownHostException::class.java) { dns.lookup("missing.example") } assertThrows(UnknownHostException::class.java) { dns.lookup("missing.example") }
@@ -97,7 +97,7 @@ class AmethystDnsTest {
val upstream = CountingDns(mapOf("a.example" to listOf(ip("1.2.3.4")))) val upstream = CountingDns(mapOf("a.example" to listOf(ip("1.2.3.4"))))
val syncRefresh = Executor { it.run() } val syncRefresh = Executor { it.run() }
val dns = val dns =
AmethystDns( SurgeDns(
delegate = upstream, delegate = upstream,
positiveTtlMs = 1, positiveTtlMs = 1,
positiveTtlJitterMs = 0, positiveTtlJitterMs = 0,
@@ -117,7 +117,7 @@ class AmethystDnsTest {
fun `expired negative entry does not stale-while-revalidate`() { fun `expired negative entry does not stale-while-revalidate`() {
val upstream = CountingDns(emptyMap()) val upstream = CountingDns(emptyMap())
val dns = val dns =
AmethystDns( SurgeDns(
delegate = upstream, delegate = upstream,
positiveTtlJitterMs = 0, positiveTtlJitterMs = 0,
negativeTtlMs = 1, negativeTtlMs = 1,
@@ -142,7 +142,7 @@ class AmethystDnsTest {
} }
val syncRefresh = Executor { it.run() } val syncRefresh = Executor { it.run() }
val dns = val dns =
AmethystDns( SurgeDns(
delegate = upstream, delegate = upstream,
positiveTtlMs = 1, positiveTtlMs = 1,
positiveTtlJitterMs = 0, positiveTtlJitterMs = 0,
@@ -185,7 +185,7 @@ class AmethystDnsTest {
} }
val pool = Executors.newFixedThreadPool(8) val pool = Executors.newFixedThreadPool(8)
val dns = val dns =
AmethystDns( SurgeDns(
delegate = dynamic, delegate = dynamic,
positiveTtlMs = 1, positiveTtlMs = 1,
positiveTtlJitterMs = 0, positiveTtlJitterMs = 0,
@@ -225,7 +225,7 @@ class AmethystDnsTest {
@Test @Test
fun `concurrent lookups for the same host coalesce to one upstream call`() { fun `concurrent lookups for the same host coalesce to one upstream call`() {
val gated = GatedDns(mapOf("hot.example" to listOf(ip("9.9.9.9")))) val gated = GatedDns(mapOf("hot.example" to listOf(ip("9.9.9.9"))))
val dns = AmethystDns(delegate = gated) val dns = SurgeDns(delegate = gated)
val pool = Executors.newFixedThreadPool(8) val pool = Executors.newFixedThreadPool(8)
try { try {
@@ -263,7 +263,7 @@ class AmethystDnsTest {
parallelism.decrementAndGet() parallelism.decrementAndGet()
} }
} }
val dns = AmethystDns(delegate = instrumented) val dns = SurgeDns(delegate = instrumented)
val pool = Executors.newFixedThreadPool(2) val pool = Executors.newFixedThreadPool(2)
try { try {
@@ -289,7 +289,7 @@ class AmethystDnsTest {
@Test @Test
fun `invalidate clears cache so next lookup hits upstream`() { fun `invalidate clears cache so next lookup hits upstream`() {
val upstream = CountingDns(mapOf("a.example" to listOf(ip("1.2.3.4")))) val upstream = CountingDns(mapOf("a.example" to listOf(ip("1.2.3.4"))))
val dns = AmethystDns(delegate = upstream) val dns = SurgeDns(delegate = upstream)
dns.lookup("a.example") dns.lookup("a.example")
dns.invalidate() dns.invalidate()
@@ -307,7 +307,7 @@ class AmethystDnsTest {
"b.example" to listOf(ip("5.6.7.8")), "b.example" to listOf(ip("5.6.7.8")),
), ),
) )
val dns = AmethystDns(delegate = upstream) val dns = SurgeDns(delegate = upstream)
dns.lookup("a.example") dns.lookup("a.example")
dns.lookup("b.example") dns.lookup("b.example")
@@ -329,7 +329,7 @@ class AmethystDnsTest {
), ),
) )
val dns = val dns =
AmethystDns( SurgeDns(
delegate = upstream, delegate = upstream,
positiveTtlMs = 60_000, positiveTtlMs = 60_000,
positiveTtlJitterMs = 0, positiveTtlJitterMs = 0,
@@ -346,7 +346,7 @@ class AmethystDnsTest {
@Test @Test
fun `restore replays cached entries without hitting upstream`() { fun `restore replays cached entries without hitting upstream`() {
val upstream = CountingDns(mapOf("relay.example" to listOf(ip("9.9.9.9")))) val upstream = CountingDns(mapOf("relay.example" to listOf(ip("9.9.9.9"))))
val dns = AmethystDns(delegate = upstream) val dns = SurgeDns(delegate = upstream)
val expiresAt = System.currentTimeMillis() + 60_000 val expiresAt = System.currentTimeMillis() + 60_000
dns.restore(listOf(DnsCacheRecord("relay.example", listOf("9.9.9.9"), expiresAt))) dns.restore(listOf(DnsCacheRecord("relay.example", listOf("9.9.9.9"), expiresAt)))
@@ -358,7 +358,7 @@ class AmethystDnsTest {
@Test @Test
fun `restore drops entries already expired on disk`() { fun `restore drops entries already expired on disk`() {
val upstream = CountingDns(mapOf("relay.example" to listOf(ip("9.9.9.9")))) val upstream = CountingDns(mapOf("relay.example" to listOf(ip("9.9.9.9"))))
val dns = AmethystDns(delegate = upstream) val dns = SurgeDns(delegate = upstream)
val expiredAt = System.currentTimeMillis() - 1_000 val expiredAt = System.currentTimeMillis() - 1_000
dns.restore(listOf(DnsCacheRecord("relay.example", listOf("9.9.9.9"), expiredAt))) dns.restore(listOf(DnsCacheRecord("relay.example", listOf("9.9.9.9"), expiredAt)))
@@ -371,7 +371,7 @@ class AmethystDnsTest {
fun `restore does not overwrite a fresh in-memory entry`() { fun `restore does not overwrite a fresh in-memory entry`() {
val upstream = CountingDns(mapOf("relay.example" to listOf(ip("1.1.1.1")))) val upstream = CountingDns(mapOf("relay.example" to listOf(ip("1.1.1.1"))))
val dns = val dns =
AmethystDns( SurgeDns(
delegate = upstream, delegate = upstream,
positiveTtlMs = 60_000, positiveTtlMs = 60_000,
positiveTtlJitterMs = 0, positiveTtlJitterMs = 0,
@@ -396,7 +396,7 @@ class AmethystDnsTest {
@Test @Test
fun `lookup is case-insensitive`() { fun `lookup is case-insensitive`() {
val upstream = CountingDns(mapOf("example.com" to listOf(ip("1.2.3.4")))) val upstream = CountingDns(mapOf("example.com" to listOf(ip("1.2.3.4"))))
val dns = AmethystDns(delegate = upstream) val dns = SurgeDns(delegate = upstream)
dns.lookup("Example.COM") dns.lookup("Example.COM")
dns.lookup("example.com") dns.lookup("example.com")
@@ -408,7 +408,7 @@ class AmethystDnsTest {
@Test @Test
fun `invalidate is case-insensitive`() { fun `invalidate is case-insensitive`() {
val upstream = CountingDns(mapOf("example.com" to listOf(ip("1.2.3.4")))) val upstream = CountingDns(mapOf("example.com" to listOf(ip("1.2.3.4"))))
val dns = AmethystDns(delegate = upstream) val dns = SurgeDns(delegate = upstream)
dns.lookup("example.com") dns.lookup("example.com")
dns.invalidate("EXAMPLE.com") dns.invalidate("EXAMPLE.com")
@@ -420,7 +420,7 @@ class AmethystDnsTest {
@Test @Test
fun `restore lowercases hostnames so subsequent lookups hit`() { fun `restore lowercases hostnames so subsequent lookups hit`() {
val upstream = CountingDns(mapOf("example.com" to listOf(ip("9.9.9.9")))) val upstream = CountingDns(mapOf("example.com" to listOf(ip("9.9.9.9"))))
val dns = AmethystDns(delegate = upstream) val dns = SurgeDns(delegate = upstream)
dns.restore( dns.restore(
listOf( listOf(
@@ -436,7 +436,7 @@ class AmethystDnsTest {
fun `dirty flag tracks positive writes`() { fun `dirty flag tracks positive writes`() {
val upstream = CountingDns(mapOf("a.example" to listOf(ip("1.2.3.4")))) val upstream = CountingDns(mapOf("a.example" to listOf(ip("1.2.3.4"))))
val dns = val dns =
AmethystDns( SurgeDns(
delegate = upstream, delegate = upstream,
positiveTtlMs = 60_000, positiveTtlMs = 60_000,
positiveTtlJitterMs = 0, positiveTtlJitterMs = 0,
@@ -455,7 +455,7 @@ class AmethystDnsTest {
@Test @Test
fun `failed lookup does not mark cache dirty`() { fun `failed lookup does not mark cache dirty`() {
val upstream = CountingDns(emptyMap()) val upstream = CountingDns(emptyMap())
val dns = AmethystDns(delegate = upstream) val dns = SurgeDns(delegate = upstream)
assertFalse(dns.isDirty()) assertFalse(dns.isDirty())
runCatching { dns.lookup("missing.example") } runCatching { dns.lookup("missing.example") }
@@ -471,7 +471,7 @@ class AmethystDnsTest {
} }
val syncRefresh = Executor { it.run() } val syncRefresh = Executor { it.run() }
val dns = val dns =
AmethystDns( SurgeDns(
delegate = upstream, delegate = upstream,
positiveTtlMs = 1, positiveTtlMs = 1,
positiveTtlJitterMs = 0, positiveTtlJitterMs = 0,
@@ -499,7 +499,7 @@ class AmethystDnsTest {
val upstream = CountingDns(mapOf("a.example" to listOf(ip("1.2.3.4")))) val upstream = CountingDns(mapOf("a.example" to listOf(ip("1.2.3.4"))))
val rejecting = Executor { throw RejectedExecutionException("test") } val rejecting = Executor { throw RejectedExecutionException("test") }
val dns = val dns =
AmethystDns( SurgeDns(
delegate = upstream, delegate = upstream,
positiveTtlMs = 1, positiveTtlMs = 1,
positiveTtlJitterMs = 0, positiveTtlJitterMs = 0,