Skip to content
Merged
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
16 changes: 16 additions & 0 deletions okhttp/api/android/okhttp.api
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,22 @@ public final class okhttp3/Dns$Request {
public fun toString ()Ljava/lang/String;
}

public final class okhttp3/DnsCache {
public final field -delegate Lokhttp3/internal/dns/RealDnsCache;
public synthetic fun <init> (JJJJIILkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (JJJJILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun evictAll ()V
public final fun failureTimeToLive ()J
public final fun hitCount ()I
public final fun maxSize ()I
public final fun maximumTimeToLive ()J
public final fun minimumTimeToLive ()J
public final fun networkCount ()I
public final fun requestCount ()I
public final fun revalidateBeforeExpire ()J
public final fun size ()I
}

public abstract class okhttp3/EventListener {
public static final field Companion Lokhttp3/EventListener$Companion;
public static final field NONE Lokhttp3/EventListener;
Expand Down
16 changes: 16 additions & 0 deletions okhttp/api/jvm/okhttp.api
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,22 @@ public final class okhttp3/Dns$Request {
public fun toString ()Ljava/lang/String;
}

public final class okhttp3/DnsCache {
public final field -delegate Lokhttp3/internal/dns/RealDnsCache;
public synthetic fun <init> (JJJJIILkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (JJJJILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun evictAll ()V
public final fun failureTimeToLive ()J
public final fun hitCount ()I
public final fun maxSize ()I
public final fun maximumTimeToLive ()J
public final fun minimumTimeToLive ()J
public final fun networkCount ()I
public final fun requestCount ()I
public final fun revalidateBeforeExpire ()J
public final fun size ()I
}

public abstract class okhttp3/EventListener {
public static final field Companion Lokhttp3/EventListener$Companion;
public static final field NONE Lokhttp3/EventListener;
Expand Down
149 changes: 149 additions & 0 deletions okhttp/src/commonJvmAndroid/kotlin/okhttp3/DnsCache.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/*
* Copyright (c) 2026 OkHttp Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:OptIn(OkHttpInternalApi::class)
@file:Suppress("PropertyName") // Hide the '-delegate' field from Java language users.

package okhttp3

import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import kotlin.time.TimeSource
import okhttp3.internal.OkHttpInternalApi
import okhttp3.internal.concurrent.TaskRunner
import okhttp3.internal.dns.RealDnsCache

/**
* An in-memory DNS query cache to save time and bandwidth.
*
* Cache Lifetime and Expiration
* -----------------------------
*
* DNS resource records include a time to live (TTL) lifetime for returned records. This class
* honors that setting, but with bounds:
*
* * Records with a time to live lower than [minimumTimeToLive] will be used for
* [minimumTimeToLive]. The default value of this is 10 seconds.
*
* * Records with a time to live greater than [maximumTimeToLive] will be used for
* [maximumTimeToLive]. The default value of this is 300 seconds (5 minutes).
*
* If there is a failure fetching DNS records, or if no DNS records are returned, this failure is
* also cached for [failureTimeToLive]. The default value of this is 10 seconds.
*
* Revalidation
* ------------
*
* If a cached record will expire soon, the cached record is returned immediately, but this cache
* will also asynchronously update the value for the benefit of future requests.
*
* This way if a steady stream of requests are made for the same hostname, none of the requests will
* have to wait for DNS.
*
* The default value of [revalidateBeforeExpire] is 2 seconds.
*
* Request Merging
* ---------------
*
* If the cache receives multiple equivalent queries, it combines them into a single query on the
* underlying transport.
*
* Memory Usage
* ------------
*
* This cache retains [maxSize] entries. It prefers to evict expired entries, and when that is
* insufficient, it evicts the least recently requested entries.
*
* The default [maxSize] is 1,000 entries. Most hostnames will require 3 entries for DNS record
* types `A` (IPv4), `AAAA` (IPv6), and `HTTPS` (service metadata).
*
* Between evictions, the cache may grow to double this configured limit. Each entry consumes about
* 400 bytes of memory. In total, the default cache will use about 800 KiB of memory when it is
* full.
*/
class DnsCache internal constructor(
@OkHttpInternalApi
@JvmField
val `-delegate`: RealDnsCache,
) {
@get:JvmName("minimumTimeToLive")
val minimumTimeToLive: Duration
get() = `-delegate`.minimumTimeToLive

@get:JvmName("maximumTimeToLive")
val maximumTimeToLive: Duration
get() = `-delegate`.maximumTimeToLive

@get:JvmName("failureTimeToLive")
val failureTimeToLive: Duration
get() = `-delegate`.failureTimeToLive

@get:JvmName("revalidateBeforeExpire")
val revalidateBeforeExpire: Duration
get() = `-delegate`.revalidateBeforeExpire

@get:JvmName("maxSize")
val maxSize: Int
get() = `-delegate`.maxSize

@get:JvmName("size")
val size: Int
get() = `-delegate`.size

/** Returns the number of calls made to the DNS server. */
@get:JvmName("networkCount")
val networkCount: Int
get() = `-delegate`.networkCount

/** Returns the number of DNS calls served by the cache. */
@get:JvmName("hitCount")
val hitCount: Int
get() = `-delegate`.hitCount

/**
* Returns the total number of DNS calls handled by this cache. This may be greater than the
* sum of [networkCount] and [hitCount] because revalidation calls do both.
*/
@get:JvmName("requestCount")
val requestCount: Int
get() = `-delegate`.requestCount

constructor(
minimumTimeToLive: Duration = 10.seconds,
maximumTimeToLive: Duration = 300.seconds,
failureTimeToLive: Duration = 10.seconds,
revalidateBeforeExpire: Duration = 2.seconds,
maxEntryCount: Int = 1000,
) : this(
`-delegate` =
RealDnsCache(
taskRunner = TaskRunner.INSTANCE,
timeSource = TimeSource.Monotonic,
minimumTimeToLive = minimumTimeToLive,
maximumTimeToLive = maximumTimeToLive,
failureTimeToLive = failureTimeToLive,
revalidateBeforeExpire = revalidateBeforeExpire,
maxEntryCount = maxEntryCount,
),
)

/**
* Deletes all values stored in the cache. In-flight writes to the cache will complete normally,
* but the corresponding responses will not be stored.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks

*/
fun evictAll() {
`-delegate`.evictAll()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ abstract class MemoryCache<K : Any, V : Any>(
val timeSource: TimeSource.WithComparableMarks,
val maxSize: Int,
) {
val size: Int
get() = entries.size

private val entries = ConcurrentHashMap<K, V>()

init {
Expand Down Expand Up @@ -73,6 +76,10 @@ abstract class MemoryCache<K : Any, V : Any>(
return created
}

fun evictAll() {
entries.clear()
}

/**
* Manually evict [count] entries from this cache.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
package okhttp3.internal.dns

import java.io.IOException
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicReference
import kotlin.time.ComparableTimeMark as Time
import kotlin.time.Duration
Expand All @@ -27,53 +28,16 @@ import kotlin.time.TimeSource
import okhttp3.internal.OkHttpInternalApi
import okhttp3.internal.concurrent.TaskRunner

/**
* A DNS query cache that stores responses according to their [ResourceRecord.timeToLive]. Each
* entry's lifetime is bounded between [minimumTimeToLive] and [maximumTimeToLive]. The cache's size
* is bounded by a [maxEntryCount].
*
* Cache Hits
* ----------
*
* The age of the result impacts how queries are satisfied:
*
* * After [Result.expireAt], the cached result is not used and a call to the underlying transport
* is made.
*
* * After [Result.revalidateAt], the cached result is returned immediately. A call to the
* underlying transport is also made, in order to freshen the cache for a possible future call.
*
* * Otherwise, the cached data is returned immediately.
*
* Failures are cached to prevent error cases from using more resources than success cases. There's
* no server-provided defaults for these so the configuration parameter [failureTimeToLive] must be
* used.
*
* If this receives multiple equivalent queries, it combines them into a single query on the
* underlying transport.
*
* Memory Usage
* ------------
*
* By default, this retains the 1,000 most recently accessed entries. Most hostnames will require 3
* entries (`TYPE_A`, `TYPE_AAAA`, and `TYPE_HTTPS`).
*
* Between evictions the memory cache will grow to double that max, 2,000 entries.
*
* Each entry consumes about 400 bytes of memory.
*
* In total, the default cache will use about 800 KiB of memory.
*/
@OkHttpInternalApi
@OptIn(ExperimentalTime::class) // We know Clock and Instant will be stable in Kotlin 2.3.
class DnsCache(
class RealDnsCache(
private val taskRunner: TaskRunner,
timeSource: TimeSource.WithComparableMarks,
private val minimumTimeToLive: Duration = 10.seconds,
private val maximumTimeToLive: Duration = 300.seconds,
private val failureTimeToLive: Duration = 10.seconds,
private val revalidateBeforeExpire: Duration = 5.seconds,
maxEntryCount: Int = 1000,
internal val minimumTimeToLive: Duration,
internal val maximumTimeToLive: Duration,
internal val failureTimeToLive: Duration,
internal val revalidateBeforeExpire: Duration,
maxEntryCount: Int,
) {
private val cache =
object : MemoryCache<Question, Entry>(
Expand All @@ -96,13 +60,32 @@ class DnsCache(
}
}

private val atomicNetworkCount = AtomicInteger()
private val atomicHitCount = AtomicInteger()
private val atomicRequestCount = AtomicInteger()

internal val size: Int
get() = cache.size
internal val maxSize: Int
get() = cache.maxSize
internal val networkCount: Int
get() = atomicNetworkCount.get()
internal val hitCount: Int
get() = atomicHitCount.get()
internal val requestCount: Int
get() = atomicRequestCount.get()

init {
require(failureTimeToLive >= 0.seconds)
require(minimumTimeToLive >= 0.seconds)
require(maximumTimeToLive >= minimumTimeToLive)
require(revalidateBeforeExpire >= 0.seconds)
}

fun evictAll() {
cache.evictAll()
}

fun wrap(delegate: DnsQuery.Factory) =
DnsQuery.Factory { question ->
val entry = cache.computeIfAbsent(question) { Entry() }
Expand Down Expand Up @@ -165,11 +148,15 @@ class DnsCache(

if (!entry.state.compareAndSet(previous, next)) continue // Lost a race, retry.

atomicRequestCount.incrementAndGet()

if (inFlightCall == null && next.inFlightCall != null) {
atomicNetworkCount.incrementAndGet()
next.inFlightCall.query.enqueue(this)
}

if (useCached) {
atomicHitCount.incrementAndGet()
taskRunner.newQueue().execute("${question.name} dns") {
when (result) {
is Result.Success -> callback.onResponse(result.message)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,16 @@ class MemoryCacheTest {
assertThat(a0).isSameInstanceAs(r1)
}

@Test
fun `evictAll evicts all`() {
cache.access("a")
cache.access("b")

cache.evictAll()
cache.assertAbsent("a")
cache.assertAbsent("b")
}

@Test
fun `evict removes expired records`() {
cache.access("b")
Expand Down
Loading
Loading