-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
finagle-base-http: add MapBackedHeaderMap #805
Changes from 5 commits
7c009f6
00280d9
2f1d9d3
801ac60
69864fd
c141122
90efed3
f1d0fc1
de48cd9
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,99 @@ | ||
package com.twitter.finagle.http.headers | ||
|
||
import com.twitter.finagle.http.HeaderMap | ||
import java.util.function.BiConsumer | ||
import scala.collection.AbstractIterator | ||
|
||
/** | ||
* Mutable, thread-safe [[HeaderMap]] implementation, backed by | ||
* a mutable [[Map[String, Header]]], where the map key | ||
* is forced to lower case | ||
*/ | ||
private[http] trait JMapBackedHeaderMap extends HeaderMap { | ||
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. This feels like it should be an |
||
import HeaderMap._ | ||
|
||
// In general, Map's that are not thread safe are not | ||
// durable to concurrent modification and can result in infinite loops | ||
// and exceptions. | ||
// As such, we synchronize on the underlying collection when performing | ||
// accesses to avoid this. In the common case of no concurrent access, | ||
// this should be cheap. | ||
protected val underlying: java.util.Map[String, Header.Root] | ||
|
||
private def foreachConsumer[U](f: ((String, String)) => U): | ||
BiConsumer[String, Header.Root] = new BiConsumer[String, Header.Root](){ | ||
def accept(key: String, header: Header.Root): Unit = header.iterator.foreach( | ||
nv => f(nv.name, nv.value) | ||
) | ||
} | ||
|
||
final override def foreach[U](f: ((String, String)) => U): Unit = | ||
underlying.forEach(foreachConsumer(f)) | ||
|
||
// ---- HeaderMap ----- | ||
|
||
// Validates key and value. | ||
final def add(key: String, value: String): this.type = { | ||
validateName(key) | ||
addUnsafe(key, foldReplacingValidateValue(key, value)) | ||
} | ||
|
||
// Does not validate key and value. | ||
def addUnsafe(key: String, value: String): this.type | ||
|
||
// Validates key and value. | ||
final def set(key: String, value: String): this.type = { | ||
validateName(key) | ||
setUnsafe(key, foldReplacingValidateValue(key, value)) | ||
} | ||
|
||
// Does not validate key and value. | ||
def setUnsafe(key: String, value: String): this.type | ||
|
||
// ---- Map/MapLike ----- | ||
|
||
def -=(key: String): this.type = removed(key) | ||
def +=(kv: (String, String)): this.type = set(kv._1, kv._2) | ||
|
||
|
||
def get(key: String): Option[String] | ||
|
||
/** | ||
* Underlying headers eagerly copied to an array, without synchronizing | ||
* on the underlying collection. | ||
*/ | ||
private[this] def copyHeaders: Iterator[Header.Root] = underlying.values.toArray(new Array[Header.Root](underlying.size)).iterator | ||
|
||
final def iterator: Iterator[(String, String)] = underlying.synchronized { | ||
copyHeaders.flatMap(_.iterator.map(nv => (nv.name, nv.value))) | ||
} | ||
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. This looks like |
||
|
||
def removed(key: String): this.type | ||
|
||
final override def keys: Set[String] = keysIterator.toSet | ||
|
||
final override def keysIterator: Iterator[String] = underlying.synchronized { | ||
//the common case has a single element in Headers. Prevent unneeded List | ||
//allocations for that case (don't flatMap) | ||
val valuesIterator = copyHeaders | ||
var currentEntries: Iterator[String] = Iterator.empty | ||
new AbstractIterator[String]{ | ||
def hasNext: Boolean = currentEntries.hasNext || valuesIterator.hasNext | ||
def next(): String = | ||
if (currentEntries.hasNext) currentEntries.next() | ||
else { | ||
val h = valuesIterator.next() | ||
if (h.next == null) h.name | ||
else { | ||
currentEntries = h.iterator.map(nv => nv.name).toList.distinct.iterator | ||
currentEntries.next() | ||
} | ||
} | ||
} | ||
} | ||
|
||
private[finagle] final override def nameValueIterator: Iterator[HeaderMap.NameValue] = | ||
underlying.synchronized { | ||
copyHeaders.flatMap(_.iterator) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
package com.twitter.finagle.http.headers | ||
|
||
import com.twitter.finagle.http.HeaderMap | ||
import scala.annotation.tailrec | ||
|
||
/** | ||
* Mutable, thread-safe [[HeaderMap]] implementation, backed by | ||
* a mutable [[Map[String, Header]]], where the map key | ||
* is forced to lower case | ||
*/ | ||
final private[http] class JTreeMapBackedHeaderMap extends JMapBackedHeaderMap { | ||
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. What is the value of separating this from 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. fwiw, I'm not strongly opinionated about this, it's just that having an abstract backing map that is synchronized on in two different traits/classes makes me queasy. 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. It made sense when the HashMap variety still existed. Not so much anymore. |
||
|
||
override val underlying: java.util.TreeMap[String, Header.Root] = | ||
new java.util.TreeMap[String, Header.Root](JTreeMapBackedHeaderMap.SharedComparitor) | ||
|
||
def getAll(key: String): Seq[String] = underlying.synchronized { | ||
underlying.get(key.toLowerCase) match { | ||
case null => Nil | ||
case r: Header.Root => r.values | ||
} | ||
} | ||
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. Indentation is off. |
||
|
||
// Does not validate key and value. | ||
def addUnsafe(key: String, value: String): this.type = underlying.synchronized { | ||
def header = Header.root(key, value) | ||
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.
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. It's a def for that reason, but it can go inside the match. The only reason it's outside now is neater indentation, so that's not much of a reason. 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. Oh, I didn't notice it was a def. :) Maybe just inline it where As an aside, do you know if that def allocates a lambda, or does that get lifted out into the object itself? 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. Yes, I inlined it. As for the allocation, I assumed it gets hoysted to a method all the way to the top, especially since it doesn't capture anything and doesn't escape scope, but I'm not 100% sure either way. |
||
underlying.get(key) match { | ||
case null => underlying.put(key, header) | ||
case h => h.add(key, value) | ||
} | ||
this | ||
} | ||
|
||
// Does not validate key and value. | ||
def setUnsafe(key: String, value: String): this.type = underlying.synchronized { | ||
underlying.put(key, Header.root(key, value)) | ||
this | ||
} | ||
|
||
def get(key: String): Option[String] = underlying.synchronized { | ||
Option(underlying.get(key)).map(_.value) | ||
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. Let's do a |
||
} | ||
|
||
def removed(key: String): this.type = underlying.synchronized { | ||
underlying.remove(key) | ||
this | ||
} | ||
} | ||
|
||
|
||
object JTreeMapBackedHeaderMap { | ||
|
||
val SharedComparitor = new java.util.Comparator[String] { | ||
martijnhoekstra marked this conversation as resolved.
Show resolved
Hide resolved
|
||
def compare(key1: String, key2: String): Int = { | ||
// Shorter strings are always less, regardless of their content | ||
val lenthDiff = key1.length - key2.length | ||
if (lenthDiff != 0) lenthDiff | ||
else { | ||
@tailrec | ||
def go(i: Int): Int = { | ||
if (i == key1.length) 0 // end, they are equal. | ||
else { | ||
val char1 = HeadersHash.hashChar(key1.charAt(i)) | ||
val char2 = HeadersHash.hashChar(key2.charAt(i)) | ||
val diff = char1 - char2 | ||
if (diff == 0) go(i + 1) | ||
else diff | ||
} | ||
} | ||
go(0) | ||
} | ||
} | ||
} | ||
|
||
def apply(headers: (String, String)*): HeaderMap = { | ||
val result = new JTreeMapBackedHeaderMap | ||
headers.foreach(t => result.add(t._1, t._2)) | ||
result | ||
} | ||
} |
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.
If you move
cur
into the definition ofAbstractIterator
you can avoid lifting it to the the heap viascala.runtime.ObjectRef
.