1
0
mirror of https://github.com/square/okhttp.git synced 2026-01-12 10:23:16 +03:00

New datastructure to hold Tags (#9163)

* New datastructure to hold Tags

We have been using MutableMap and Map. These have worked
fine.

But we'd like to soon support mutating tags on an existing
Request object, which I'd like to do without a ton of
collections or concurrency overhead. Instead we can do some
lock-free stuff, which needs a simple data structure as a
starting point.

* Fill in some type parameters
This commit is contained in:
Jesse Wilson
2025-10-23 15:50:01 -04:00
committed by GitHub
parent fc41262525
commit ca9f73e7b6
3 changed files with 283 additions and 0 deletions

View File

@@ -0,0 +1,106 @@
/*
* Copyright (C) 2025 Square, Inc.
*
* 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
import kotlin.reflect.KClass
/**
* An immutable collection of key-value pairs implemented as a singly-linked list.
*
* Build up a collection by starting with [EmptyTags] and repeatedly calling [plus]. Each such call
* returns a new instance.
*
* This collection is optimized for safe concurrent access over a very small number of elements.
*
* This collection and is expected to hold fewer than 10 elements. Each operation is _O(N)_, and so
* building an instance with _N_ elements is _O(N**2)_.
*/
internal sealed class Tags {
/**
* Returns a tags instance that maps [key] to [value]. If [value] is null, this returns a tags
* instance that does not have any mapping for [key].
*/
abstract fun <T : Any> plus(
key: KClass<T>,
value: T?,
): Tags
abstract operator fun <T : Any> get(key: KClass<T>): T?
}
/** An empty tags. This is always the tail of a [LinkedTags] chain. */
internal object EmptyTags : Tags() {
override fun <T : Any> plus(
key: KClass<T>,
value: T?,
): Tags =
when {
value != null -> LinkedTags(key, value, this)
else -> this
}
override fun <T : Any> get(key: KClass<T>): T? = null
override fun toString() = "{}"
}
/**
* An invariant of this implementation is that [next] must not contain a mapping for [key].
* Otherwise, we would have two values for the same key.
*/
private class LinkedTags<K : Any>(
private val key: KClass<K>,
private val value: K,
private val next: Tags,
) : Tags() {
override fun <T : Any> plus(
key: KClass<T>,
value: T?,
): Tags {
// Create a copy of this `LinkedTags` that doesn't have a mapping for `key`.
val thisMinusKey =
when {
key == this.key -> next // Subtract this!
else -> {
val nextMinusKey = next.plus(key, null)
when {
nextMinusKey === next -> this // Same as the following line, but with fewer allocations.
else -> LinkedTags(this.key, this.value, nextMinusKey)
}
}
}
// Return a new `Tags` that maps `key` to `value`.
return when {
value != null -> LinkedTags(key, value, thisMinusKey)
else -> thisMinusKey
}
}
override fun <T : Any> get(key: KClass<T>): T? =
when {
key == this.key -> key.java.cast(value)
else -> next[key]
}
/** Returns a [toString] consistent with [Map], with elements in insertion order. */
override fun toString(): String =
generateSequence<LinkedTags<*>>(seed = this) { it.next as? LinkedTags<*> }
.toList()
.reversed()
.joinToString(prefix = "{", postfix = "}") { "${it.key}=${it.value}" }
}

View File

@@ -579,6 +579,23 @@ class RequestTest {
)
}
@Test
fun requestToStringIncludesTags() {
val request =
Request
.Builder()
.url("https://square.com/".toHttpUrl())
.tag<String>("hello")
.tag<Int>(5)
.build()
assertThat(request.toString()).isEqualTo(
"Request{method=GET, url=https://square.com/, tags={" +
"class kotlin.String=hello, " +
"class kotlin.Int=5" +
"}}",
)
}
@Test
fun gzip() {
val mediaType = "text/plain; charset=utf-8".toMediaType()

View File

@@ -0,0 +1,160 @@
/*
* Copyright (C) 2025 Square, Inc.
*
* 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
import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isNull
import org.junit.jupiter.api.Test
class TagsTest {
@Test
fun emptyTags() {
val tags = EmptyTags
assertThat(tags[String::class]).isNull()
}
@Test
fun singleElement() {
val tags = EmptyTags.plus(String::class, "hello")
assertThat(tags[String::class]).isEqualTo("hello")
}
@Test
fun multipleElements() {
val tags =
EmptyTags
.plus(String::class, "hello")
.plus(Integer::class, 5 as Integer)
assertThat(tags[String::class]).isEqualTo("hello")
assertThat(tags[Integer::class]).isEqualTo(5)
}
/** The implementation retains no nodes from the original linked list. */
@Test
fun replaceFirstElement() {
val tags =
EmptyTags
.plus(String::class, "a")
.plus(Integer::class, 5 as Integer)
.plus(Boolean::class, true)
.plus(String::class, "b")
assertThat(tags[String::class]).isEqualTo("b")
assertThat(tags.toString())
.isEqualTo("{class kotlin.Int=5, class kotlin.Boolean=true, class kotlin.String=b}")
}
/** The implementation retains only the first node from the original linked list. */
@Test
fun replaceMiddleElement() {
val tags =
EmptyTags
.plus(Integer::class, 5 as Integer)
.plus(String::class, "a")
.plus(Boolean::class, true)
.plus(String::class, "b")
assertThat(tags[String::class]).isEqualTo("b")
assertThat(tags.toString())
.isEqualTo("{class kotlin.Int=5, class kotlin.Boolean=true, class kotlin.String=b}")
}
/** The implementation retains all but the first node from the original linked list. */
@Test
fun replaceLastElement() {
val tags =
EmptyTags
.plus(Integer::class, 5 as Integer)
.plus(Boolean::class, true)
.plus(String::class, "a")
.plus(String::class, "b")
assertThat(tags[String::class]).isEqualTo("b")
assertThat(tags.toString())
.isEqualTo("{class kotlin.Int=5, class kotlin.Boolean=true, class kotlin.String=b}")
}
/** The implementation retains no nodes from the original linked list. */
@Test
fun removeFirstElement() {
val tags =
EmptyTags
.plus(String::class, "a")
.plus(Integer::class, 5 as Integer)
.plus(Boolean::class, true)
.plus(String::class, null)
assertThat(tags[String::class]).isNull()
assertThat(tags.toString())
.isEqualTo("{class kotlin.Int=5, class kotlin.Boolean=true}")
}
/** The implementation retains only the first node from the original linked list. */
@Test
fun removeMiddleElement() {
val tags =
EmptyTags
.plus(Integer::class, 5 as Integer)
.plus(String::class, "a")
.plus(Boolean::class, true)
.plus(String::class, null)
assertThat(tags[String::class]).isNull()
assertThat(tags.toString())
.isEqualTo("{class kotlin.Int=5, class kotlin.Boolean=true}")
}
/** The implementation retains all but the first node from the original linked list. */
@Test
fun removeLastElement() {
val tags =
EmptyTags
.plus(Integer::class, 5 as Integer)
.plus(Boolean::class, true)
.plus(String::class, "a")
.plus(String::class, null)
assertThat(tags[String::class]).isNull()
assertThat(tags.toString())
.isEqualTo("{class kotlin.Int=5, class kotlin.Boolean=true}")
}
@Test
fun removeUntilEmpty() {
val tags =
EmptyTags
.plus(Integer::class, 5 as Integer)
.plus(Boolean::class, true)
.plus(String::class, "a")
.plus(String::class, null)
.plus(Integer::class, null)
.plus(Boolean::class, null)
assertThat(tags).isEqualTo(EmptyTags)
assertThat(tags.toString()).isEqualTo("{}")
}
@Test
fun removeAbsentFromEmpty() {
val tags = EmptyTags.plus(String::class, null)
assertThat(tags).isEqualTo(EmptyTags)
assertThat(tags.toString()).isEqualTo("{}")
}
@Test
fun removeAbsentFromNonEmpty() {
val tags =
EmptyTags
.plus(String::class, "a")
.plus(Integer::class, null)
assertThat(tags[String::class]).isEqualTo("a")
assertThat(tags.toString()).isEqualTo("{class kotlin.String=a}")
}
}