-
Notifications
You must be signed in to change notification settings - Fork 9.3k
Start evicting elements from the DNS memory cache #9583
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| /* | ||
| * 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. | ||
| */ | ||
| package okhttp3.internal.dns | ||
|
|
||
| import java.util.PriorityQueue | ||
| import java.util.concurrent.ConcurrentHashMap | ||
| import kotlin.time.ComparableTimeMark as Time | ||
| import kotlin.time.TimeSource | ||
|
|
||
| /** | ||
| * This cache is configured with a target [maxSize], but it will not evict entries until it | ||
| * has twice as many entries. | ||
| * | ||
| * This retains entries in least-recently-used order. | ||
| * | ||
| * This evicts in big batches because each eviction must traverse the entire cache. | ||
| */ | ||
| abstract class MemoryCache<K : Any, V : Any>( | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we have a way to mark global changes that would invalidate the whole cache? Let's say the default network changes, we should not used cached values. I think for network selection we can have different caches. So it's mainly for global changes
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, can do. evictAll() ! |
||
| private val timeSource: TimeSource.WithComparableMarks, | ||
| val maxSize: Int, | ||
| ) { | ||
| private val entries = ConcurrentHashMap<K, V>() | ||
|
|
||
| init { | ||
| require(maxSize >= 0) | ||
| } | ||
|
|
||
| /** | ||
| * Returns the time this value was most recently used, in order to make an eviction decision. This | ||
| * should return null if this element should be evicted immediately. | ||
| */ | ||
| abstract fun lastRequestedAt( | ||
| now: Time, | ||
| value: V, | ||
| ): Time? | ||
|
|
||
| /** | ||
| * Similar to [ConcurrentHashMap.computeIfAbsent], but this will also prune the cache to size if | ||
| * this function grows the cache to double [maxSize]. | ||
| */ | ||
| fun computeIfAbsent( | ||
| key: K, | ||
| computeValue: () -> V, | ||
| ): V { | ||
| if (maxSize == 0) return computeValue() | ||
|
|
||
| val result = entries[key] | ||
| if (result != null) return result | ||
|
|
||
| val created = computeValue() | ||
| val existing = entries.putIfAbsent(key, created) | ||
| if (existing != null) return existing | ||
|
|
||
| // If there's more than 2x as many entries as the max, automatically evict. | ||
| val toEvict = entries.size - maxSize | ||
| if (toEvict >= maxSize) { | ||
| evict(toEvict) | ||
| } | ||
|
|
||
| return created | ||
| } | ||
|
|
||
| /** | ||
| * Manually evict [count] entries from this cache. | ||
| * | ||
| * If the cache is smaller than [count], this evicts everything. | ||
| * | ||
| * Entries that are expired are evicted unconditionally, even if this causes more than [count] | ||
| * entries to be evicted. If [count] or more expired entries are evicted, non-expired entries will | ||
| * not be evicted. | ||
| */ | ||
| fun evict(count: Int) { | ||
| // Note that we call lastRequestedAt() exactly once for each entry. If we called it on-demand in | ||
| // the comparator, we'd break the PriorityQueue's invariant that the sort order is stable. | ||
| var count = count | ||
| val now = timeSource.markNow() | ||
| val entriesToEvict = PriorityQueue<Pair<Time, Map.Entry<K, V>>>(count + 1, NewestFirst) | ||
| val i = entries.entries.iterator() | ||
| while (i.hasNext()) { | ||
| val entry = i.next() | ||
|
|
||
| when (val lastRequestedAt = lastRequestedAt(now, entry.value)) { | ||
| // It's expired, evict unconditionally. | ||
| null -> { | ||
| i.remove() | ||
| count-- | ||
| } | ||
|
|
||
| // It's not expired. Make it a candidate for eviction. | ||
| else -> { | ||
| entriesToEvict.add(lastRequestedAt to entry) | ||
| } | ||
| } | ||
|
|
||
| // If we're above our target eviction count, save the newest entry from eviction. | ||
| if (entriesToEvict.size > count) { | ||
| entriesToEvict.poll() | ||
| } | ||
| } | ||
|
|
||
| for ((_, entry) in entriesToEvict) { | ||
| entries.remove(entry.key, entry.value) | ||
| } | ||
| } | ||
|
|
||
| private object NewestFirst : Comparator<Pair<Time, *>> { | ||
| override fun compare( | ||
| o1: Pair<Time, *>, | ||
| o2: Pair<Time, *>, | ||
| ) = o2.first.compareTo(o1.first) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also considered evicting whenever we were 25% greater than the target. Both 1.25x and 2x have the same big-O runtime. 1.25x is better on memory, 2.x is better on CPU.