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.NotificationDispatcher
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.DualHttpClientManagerForRelays
import com.vitorpamplona.amethyst.service.okhttp.EncryptionKeyCache
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.VideoCacheFactory
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
// resolved for an image fetch is reused when a relay handshake or NIP-05 lookup hits the
// 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
// path on first lookup.
val dnsStore = AmethystDnsStore(appContext, amethystDns)
val dnsStore = SurgeDnsStore(appContext, surgeDns)
// manages all the other connections separately from relays.
val okHttpClients =
@@ -221,7 +221,7 @@ class AppModules(
isMobileDataProvider = connManager.isMobileOrNull,
keyCache = keyCache,
scope = applicationIOScope,
dns = amethystDns,
dns = surgeDns,
)
// Offers easy methods to know when connections are happening through Tor or not
@@ -303,7 +303,7 @@ class AppModules(
proxyPortProvider = torManager.activePortOrNull,
isMobileDataProvider = connManager.isMobileOrNull,
scope = applicationIOScope,
dns = amethystDns,
dns = surgeDns,
)
// Connects the INostrClient class with okHttp
@@ -25,7 +25,7 @@ import okhttp3.EventListener
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)
* 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
@@ -35,7 +35,7 @@ import java.io.IOException
* invalidation into its `finish` method alongside its existing timing logging.
*/
class DnsInvalidatingEventListener(
private val dns: AmethystDns,
private val dns: SurgeDns,
) : EventListener() {
override fun callFailed(
call: Call,
@@ -46,7 +46,7 @@ class DnsInvalidatingEventListener(
/** Per-client factory. The listener is stateless, so the same instance serves every call. */
class Factory(
dns: AmethystDns,
dns: SurgeDns,
) : EventListener.Factory {
private val listener = DnsInvalidatingEventListener(dns)
@@ -38,7 +38,7 @@ class DualHttpClientManager(
isMobileDataProvider: StateFlow<Boolean?>,
keyCache: EncryptionKeyCache,
scope: CoroutineScope,
dns: AmethystDns,
dns: SurgeDns,
) : IHttpClientManager {
val factory = OkHttpClientFactory(keyCache, userAgent, dns)
@@ -35,7 +35,7 @@ class DualHttpClientManagerForRelays(
proxyPortProvider: StateFlow<Int?>,
isMobileDataProvider: StateFlow<Boolean?>,
scope: CoroutineScope,
dns: AmethystDns,
dns: SurgeDns,
) : IHttpClientManager {
val factory = OkHttpClientFactoryForRelays(userAgent, dns)
@@ -44,7 +44,7 @@ import java.net.Proxy
class MediaCallEventListener(
private val dispatcher: Dispatcher,
private val connectionPool: ConnectionPool,
private val dns: AmethystDns,
private val dns: SurgeDns,
) : EventListener() {
private var callStartNanos = 0L
private var dnsStartNanos = 0L
@@ -179,7 +179,7 @@ class MediaCallEventListener(
class MediaCallEventListenerFactory(
private val dispatcher: Dispatcher,
private val connectionPool: ConnectionPool,
private val dns: AmethystDns,
private val dns: SurgeDns,
) : EventListener.Factory {
override fun create(call: Call): EventListener = MediaCallEventListener(dispatcher, connectionPool, dns)
}
@@ -35,7 +35,7 @@ import java.util.concurrent.TimeUnit
class OkHttpClientFactory(
keyCache: EncryptionKeyCache,
val userAgent: String,
private val dns: AmethystDns,
private val dns: SurgeDns,
) {
// val logging = LoggingInterceptor()
val keyDecryptor = EncryptedBlobInterceptor(keyCache)
@@ -29,7 +29,7 @@ import java.time.Duration
class OkHttpClientFactoryForRelays(
userAgent: String,
private val dns: AmethystDns,
private val dns: SurgeDns,
) {
companion object {
// 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
* lookup block on `future.get()`. Background refreshes never block any caller.
*/
class AmethystDns(
class SurgeDns(
private val delegate: Dns = Dns.SYSTEM,
private val maxEntries: Int = 2000,
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(
val hostname: String,
val addresses: List<String>,
@@ -26,7 +26,7 @@ import com.fasterxml.jackson.module.kotlin.readValue
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.
*
* 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
* ~55 KB of JSON.
*/
class AmethystDnsStore(
class SurgeDnsStore(
private val context: Context,
private val dns: AmethystDns,
private val dns: SurgeDns,
) {
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
* 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.
*/
fun load() {
@@ -93,7 +93,7 @@ class AmethystDnsStore(
}
companion object {
private const val TAG = "AmethystDnsStore"
private const val TAG = "SurgeDnsStore"
private const val PREFS_NAME = "amethyst_dns_cache"
private const val KEY_SNAPSHOT = "dns_cache_v1"
private val MAPPER = jacksonObjectMapper()
@@ -37,7 +37,7 @@ import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicReference
class AmethystDnsTest {
class SurgeDnsTest {
private fun ip(value: String) = InetAddress.getByName(value)
private class CountingDns(
@@ -71,7 +71,7 @@ class AmethystDnsTest {
@Test
fun `cache hit avoids second upstream call`() {
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 second = dns.lookup("a.example")
@@ -84,7 +84,7 @@ class AmethystDnsTest {
@Test
fun `negative cache short-circuits subsequent lookups`() {
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") }
@@ -97,7 +97,7 @@ class AmethystDnsTest {
val upstream = CountingDns(mapOf("a.example" to listOf(ip("1.2.3.4"))))
val syncRefresh = Executor { it.run() }
val dns =
AmethystDns(
SurgeDns(
delegate = upstream,
positiveTtlMs = 1,
positiveTtlJitterMs = 0,
@@ -117,7 +117,7 @@ class AmethystDnsTest {
fun `expired negative entry does not stale-while-revalidate`() {
val upstream = CountingDns(emptyMap())
val dns =
AmethystDns(
SurgeDns(
delegate = upstream,
positiveTtlJitterMs = 0,
negativeTtlMs = 1,
@@ -142,7 +142,7 @@ class AmethystDnsTest {
}
val syncRefresh = Executor { it.run() }
val dns =
AmethystDns(
SurgeDns(
delegate = upstream,
positiveTtlMs = 1,
positiveTtlJitterMs = 0,
@@ -185,7 +185,7 @@ class AmethystDnsTest {
}
val pool = Executors.newFixedThreadPool(8)
val dns =
AmethystDns(
SurgeDns(
delegate = dynamic,
positiveTtlMs = 1,
positiveTtlJitterMs = 0,
@@ -225,7 +225,7 @@ class AmethystDnsTest {
@Test
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 dns = AmethystDns(delegate = gated)
val dns = SurgeDns(delegate = gated)
val pool = Executors.newFixedThreadPool(8)
try {
@@ -263,7 +263,7 @@ class AmethystDnsTest {
parallelism.decrementAndGet()
}
}
val dns = AmethystDns(delegate = instrumented)
val dns = SurgeDns(delegate = instrumented)
val pool = Executors.newFixedThreadPool(2)
try {
@@ -289,7 +289,7 @@ class AmethystDnsTest {
@Test
fun `invalidate clears cache so next lookup hits upstream`() {
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.invalidate()
@@ -307,7 +307,7 @@ class AmethystDnsTest {
"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("b.example")
@@ -329,7 +329,7 @@ class AmethystDnsTest {
),
)
val dns =
AmethystDns(
SurgeDns(
delegate = upstream,
positiveTtlMs = 60_000,
positiveTtlJitterMs = 0,
@@ -346,7 +346,7 @@ class AmethystDnsTest {
@Test
fun `restore replays cached entries without hitting upstream`() {
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
dns.restore(listOf(DnsCacheRecord("relay.example", listOf("9.9.9.9"), expiresAt)))
@@ -358,7 +358,7 @@ class AmethystDnsTest {
@Test
fun `restore drops entries already expired on disk`() {
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
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`() {
val upstream = CountingDns(mapOf("relay.example" to listOf(ip("1.1.1.1"))))
val dns =
AmethystDns(
SurgeDns(
delegate = upstream,
positiveTtlMs = 60_000,
positiveTtlJitterMs = 0,
@@ -396,7 +396,7 @@ class AmethystDnsTest {
@Test
fun `lookup is case-insensitive`() {
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")
@@ -408,7 +408,7 @@ class AmethystDnsTest {
@Test
fun `invalidate is case-insensitive`() {
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.invalidate("EXAMPLE.com")
@@ -420,7 +420,7 @@ class AmethystDnsTest {
@Test
fun `restore lowercases hostnames so subsequent lookups hit`() {
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(
listOf(
@@ -436,7 +436,7 @@ class AmethystDnsTest {
fun `dirty flag tracks positive writes`() {
val upstream = CountingDns(mapOf("a.example" to listOf(ip("1.2.3.4"))))
val dns =
AmethystDns(
SurgeDns(
delegate = upstream,
positiveTtlMs = 60_000,
positiveTtlJitterMs = 0,
@@ -455,7 +455,7 @@ class AmethystDnsTest {
@Test
fun `failed lookup does not mark cache dirty`() {
val upstream = CountingDns(emptyMap())
val dns = AmethystDns(delegate = upstream)
val dns = SurgeDns(delegate = upstream)
assertFalse(dns.isDirty())
runCatching { dns.lookup("missing.example") }
@@ -471,7 +471,7 @@ class AmethystDnsTest {
}
val syncRefresh = Executor { it.run() }
val dns =
AmethystDns(
SurgeDns(
delegate = upstream,
positiveTtlMs = 1,
positiveTtlJitterMs = 0,
@@ -499,7 +499,7 @@ class AmethystDnsTest {
val upstream = CountingDns(mapOf("a.example" to listOf(ip("1.2.3.4"))))
val rejecting = Executor { throw RejectedExecutionException("test") }
val dns =
AmethystDns(
SurgeDns(
delegate = upstream,
positiveTtlMs = 1,
positiveTtlJitterMs = 0,