Skip to content

Commit e96214e

Browse files
committed
Build an computeIfAbsent() mechanism for tags
The Tags type is still immutable, but an AtomicReference<Tag> is mutable.
1 parent ca9f73e commit e96214e

File tree

2 files changed

+90
-0
lines changed
  • okhttp/src

2 files changed

+90
-0
lines changed

okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/Tags.kt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package okhttp3.internal
1717

18+
import java.util.concurrent.atomic.AtomicReference
1819
import kotlin.reflect.KClass
1920

2021
/**
@@ -104,3 +105,28 @@ private class LinkedTags<K : Any>(
104105
.reversed()
105106
.joinToString(prefix = "{", postfix = "}") { "${it.key}=${it.value}" }
106107
}
108+
109+
internal fun <T : Any> AtomicReference<Tags>.computeIfAbsent(
110+
type: KClass<T>,
111+
compute: () -> T,
112+
): T {
113+
var computed: T? = null
114+
115+
while (true) {
116+
val tags = get()
117+
118+
// If the element is already present. Return it.
119+
val existing = tags[type]
120+
if (existing != null) return existing
121+
122+
if (computed == null) {
123+
computed = compute()
124+
}
125+
126+
// If we successfully add the computed element, we're done.
127+
val newTags = tags.plus(type, computed)
128+
if (compareAndSet(tags, newTags)) return computed
129+
130+
// We lost the race. Possibly to other code that was putting a *different* key. Try again!
131+
}
132+
}

okhttp/src/jvmTest/kotlin/okhttp3/internal/TagsTest.kt

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package okhttp3.internal
1818
import assertk.assertThat
1919
import assertk.assertions.isEqualTo
2020
import assertk.assertions.isNull
21+
import java.util.concurrent.atomic.AtomicReference
2122
import org.junit.jupiter.api.Test
2223

2324
class TagsTest {
@@ -157,4 +158,67 @@ class TagsTest {
157158
assertThat(tags[String::class]).isEqualTo("a")
158159
assertThat(tags.toString()).isEqualTo("{class kotlin.String=a}")
159160
}
161+
162+
@Test
163+
fun computeIfAbsentWhenEmpty() {
164+
val tags = EmptyTags
165+
val atomicTags = AtomicReference<Tags>(tags)
166+
assertThat(atomicTags.computeIfAbsent(String::class) { "a" }).isEqualTo("a")
167+
assertThat(atomicTags.get()[String::class]).isEqualTo("a")
168+
}
169+
170+
@Test
171+
fun computeIfAbsentWhenPresent() {
172+
val tags = EmptyTags.plus(String::class, "a")
173+
val atomicTags = AtomicReference(tags)
174+
assertThat(atomicTags.computeIfAbsent(String::class) { "b" }).isEqualTo("a")
175+
assertThat(atomicTags.get()[String::class]).isEqualTo("a")
176+
}
177+
178+
@Test
179+
fun computeIfAbsentWhenDifferentKeyRaceLostDuringCompute() {
180+
val tags = EmptyTags
181+
val atomicTags = AtomicReference<Tags>(tags)
182+
val result =
183+
atomicTags.computeIfAbsent(String::class) {
184+
// 'Race' by making another computeIfAbsent call. In practice this would be another thread.
185+
assertThat(atomicTags.computeIfAbsent(Integer::class) { 5 as Integer }).isEqualTo(5)
186+
"a"
187+
}
188+
assertThat(result).isEqualTo("a")
189+
assertThat(atomicTags.get()[String::class]).isEqualTo("a")
190+
assertThat(atomicTags.get()[Integer::class]).isEqualTo(5)
191+
}
192+
193+
@Test
194+
fun computeIfAbsentWhenSameKeyRaceLostDuringCompute() {
195+
val tags = EmptyTags
196+
val atomicTags = AtomicReference<Tags>(tags)
197+
val result =
198+
atomicTags.computeIfAbsent(String::class) {
199+
// 'Race' by making another computeIfAbsent call. In practice this would be another thread.
200+
assertThat(atomicTags.computeIfAbsent(String::class) { "b" }).isEqualTo("b")
201+
"a"
202+
}
203+
assertThat(result).isEqualTo("b")
204+
assertThat(atomicTags.get()[String::class]).isEqualTo("b")
205+
}
206+
207+
@Test
208+
fun computeIfAbsentOnlyComputesOnceAfterRaceLost() {
209+
var computeCount = 0
210+
val tags = EmptyTags
211+
val atomicTags = AtomicReference<Tags>(tags)
212+
val result =
213+
atomicTags.computeIfAbsent(String::class) {
214+
computeCount++
215+
// 'Race' by making another computeIfAbsent call. In practice this would be another thread.
216+
assertThat(atomicTags.computeIfAbsent(Integer::class) { 5 as Integer }).isEqualTo(5)
217+
"a"
218+
}
219+
assertThat(result).isEqualTo("a")
220+
assertThat(computeCount).isEqualTo(1)
221+
assertThat(atomicTags.get()[Integer::class]).isEqualTo(5)
222+
assertThat(atomicTags.get()[String::class]).isEqualTo("a")
223+
}
160224
}

0 commit comments

Comments
 (0)