
com.twitter.finagle.toggle.ToggleMap.scala Maven / Gradle / Ivy
The newest version!
package com.twitter.finagle.toggle
import com.twitter.finagle.stats.StatsReceiver
import com.twitter.finagle.toggle.Toggle.Metadata
import com.twitter.io.Charsets
import com.twitter.logging.Logger
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicReference
import java.util.zip.CRC32
import scala.annotation.varargs
import scala.collection.JavaConverters._
import scala.collection.{breakOut, immutable, mutable}
/**
* A collection of Int-typed [[Toggle toggles]] which can be
* used to build a form of feature toggles which allow for modifying
* behavior without changing code.
*
* Expected usage is for code to have [[Toggle toggles]] passed into
* their constructors instead of dynamically creating new [[Toggle toggles]]
* on every call.
*
* @see [[Toggle]]
* @see [[ServiceLoadedToggleMap]] and [[StandardToggleMap]] for typical usage
* entry points.
* @see [[http://martinfowler.com/articles/feature-toggles.html Feature Toggles]]
* for detailed discussion on the topic.
*/
abstract class ToggleMap { self =>
/**
* Get a [[Toggle]] for this `id`.
*
* The `Toggle.isDefined` method should return `false` if the
* [[ToggleMap]] does not know about that [[Toggle]]
* or it is currently not "operating" on that `id`.
*
* @param id the identifying name of the `Toggle`.
* These should generally be fully qualified names to avoid conflicts
* between libraries. For example, "com.twitter.finagle.CoolThing".
*/
def apply(id: String): Toggle[Int]
def iterator: Iterator[Toggle.Metadata]
/**
* Creates a [[ToggleMap]] which uses `this` before `that`.
*
* [[apply]] returns a [[Toggle]] that uses the [[Toggle]] from `this`
* if it `isDefinedAt` for the input, before trying `that`.
*
* [[iterator]] includes metadata from both `self` and `that`,
* with `self`'s metadata taking precedence on conflicting ids.
* Note however that if a `ToggleMetadata.description` is not defined on `self`,
* the description from `that` will be preferred. This is done because many
* sources of `ToggleMaps` do not have a description defined and we want to
* surface that information.
*/
def orElse(that: ToggleMap): ToggleMap = {
new ToggleMap with ToggleMap.Composite {
override def toString: String =
s"${self.toString}.orElse(${that.toString})"
def apply(id: String): Toggle[Int] = {
self(id).orElse(that(id))
}
def iterator: Iterator[Metadata] = {
val byName = mutable.Map.empty[String, Toggle.Metadata]
that.iterator.foreach { md =>
byName.put(md.id, md)
}
self.iterator.foreach { md =>
val mdWithDesc = md.description match {
case Some(_) => md
case None => md.copy(description =
byName.get(md.id).flatMap(ToggleMap.MdDescFn))
}
byName.put(md.id, mdWithDesc)
}
byName.valuesIterator
}
def components: Seq[ToggleMap] = {
Seq(self, that)
}
}
}
}
object ToggleMap {
private[this] val MetadataOrdering: Ordering[Toggle.Metadata] =
new Ordering[Toggle.Metadata] {
def compare(x: Metadata, y: Metadata): Int = {
val ids = Ordering.String.compare(x.id, y.id)
if (ids != 0) ids
else Ordering.Double.compare(x.fraction, y.fraction)
}
}
/**
* Creates a [[ToggleMap]] with a `Gauge`, "checksum", which summarizes the
* current state of the `Toggles` which may be useful for comparing state
* across a cluster or over time.
*
* @param statsReceiver in typical usage by [[StandardToggleMap]], will be
* scoped to "toggles/\$libraryName".
*/
def observed(toggleMap: ToggleMap, statsReceiver: StatsReceiver): ToggleMap = {
new Proxy with Composite {
private[this] val checksum = statsReceiver.addGauge("checksum") {
// crc32 is not a cryptographic hash, but good enough for our purposes
// of summarizing the current state of the ToggleMap. we only need it
// to be efficient to compute and have small changes to the input affect
// the output.
val crc32 = new CRC32()
// need a consistent ordering, forcing the sort before computation
iterator.toIndexedSeq.sorted(MetadataOrdering).foreach { md =>
crc32.update(md.id.getBytes(Charsets.Utf8))
// convert the md's fraction to a Long and then feed each
// byte into the crc
val f = java.lang.Double.doubleToLongBits(md.fraction)
crc32.update((0xff & f ).toInt)
crc32.update((0xff & (f >> 8) ).toInt)
crc32.update((0xff & (f >> 16)).toInt)
crc32.update((0xff & (f >> 24)).toInt)
crc32.update((0xff & (f >> 32)).toInt)
crc32.update((0xff & (f >> 40)).toInt)
crc32.update((0xff & (f >> 48)).toInt)
crc32.update((0xff & (f >> 56)).toInt)
}
crc32.getValue.toFloat
}
protected def underlying: ToggleMap = toggleMap
override def toString: String =
s"observed($toggleMap, $statsReceiver)"
def components: Seq[ToggleMap] =
Seq(underlying)
}
}
/**
* A marker interface in support of [[components(ToggleMap)]]
*/
private trait Composite {
def components: Seq[ToggleMap]
}
/**
* For some administrative purposes, it can be useful to get at the
* component `ToggleMaps` that may make up a [[ToggleMap]].
*
* For example:
* {{{
* val toggleMap1: ToggleMap = ...
* val toggleMap2: ToggleMap = ...
* val combined = toggleMap1.orElse(toggleMap2)
* assert(Seq(toggleMap1, toggleMap2) == ToggleMap.components(combined))
* }}}
*/
def components(toggleMap: ToggleMap): Seq[ToggleMap] = {
toggleMap match {
case composite: Composite =>
composite.components.flatMap(components)
case _ =>
Seq(toggleMap)
}
}
/**
* The [[ToggleMap]] interface is read only and this
* is the mutable side of it.
*
* Implementations are expected to be thread-safe.
*/
trait Mutable extends ToggleMap {
/**
* Add or replace the [[Toggle]] for this `id` with a
* [[Toggle]] that returns `true` for a `fraction` of the inputs.
*
* @param id the identifying name of the `Toggle`.
* These should generally be fully qualified names to avoid conflicts
* between libraries. For example, "com.twitter.finagle.CoolThing".
* @param fraction must be within `0.0–1.0`, inclusive. If not, the operation
* is ignored.
*/
def put(id: String, fraction: Double): Unit
/**
* Remove the [[Toggle]] for this `id`.
*
* This is a no-op for missing values.
*
* @param id the identifying name of the `Toggle`.
* These should generally be fully qualified names to avoid conflicts
* between libraries. For example, "com.twitter.finagle.CoolThing".
*/
def remove(id: String): Unit
}
/**
* Create a [[Toggle]] where `fraction` of the inputs will return `true.`
*
* @param id the name of the Toggle which is used to mix
* where along the universe of Ints does the range fall.
* @param fraction the fraction, from 0.0 - 1.0 (inclusive), of Ints
* to return `true`. If outside of that range, a
* `java.lang.IllegalArgumentException` will be thrown.
*/
private[toggle] def fractional(id: String, fraction: Double): Toggle[Int] = {
Toggle.validateFraction(id, fraction)
// we want a continuous range within the space of Int.MinValue
// to Int.MaxValue, including overflowing Max.
// By mapping the range to a Long and then mapping this into the
// space of Ints we create a Toggle that is both space efficient
// as well as quick to respond to `apply`.
// within a range of [0, Int.MaxValue*2]
val range: Long = ((1L << 32) * fraction).toLong
// We want to use `id` as an input into the function so
// that ints have different likelihoods depending on the toggle's id.
// Without this, every Toggle's range would start at 0.
// The input to many toggle's may be something consistent per node,
// say a machine name. So without the offset, nodes that hash to
// close to 0 will be much more likely to have most or all toggles
// turned on. by using the id as an offset, we can shift this and
// make them be more evenly distributed.
val start = id.hashCode
val end: Int = (start + range).toInt
if (range == 0) {
Toggle.off(id) // 0%
} else if (start == end) {
Toggle.on(id) // 100%
} else if (start <= end) {
// the range is contiguous without overflows.
Toggle(id, { case i => i >= start && i <= end })
} else {
// the range overflows around Int.MaxValue
Toggle(id, { case i => i >= start || i <= end })
}
}
/**
* Create a [[ToggleMap]] out of the given [[ToggleMap ToggleMaps]].
*
* If `toggleMaps` is empty, [[NullToggleMap]] will be returned.
*/
@varargs
def of(toggleMaps: ToggleMap*): ToggleMap = {
val start: ToggleMap = NullToggleMap
toggleMaps.foldLeft(start) { case (acc, tm) =>
acc.orElse(tm)
}
}
/**
* A [[ToggleMap]] implementation based on immutable [[Toggle.Metadata]].
*/
class Immutable(
metadata: immutable.Seq[Toggle.Metadata])
extends ToggleMap {
private[this] val toggles: immutable.Map[String, Toggle[Int]] =
metadata.map { md =>
md.id -> fractional(md.id, md.fraction)
}(breakOut)
override def toString: String =
s"ToggleMap.Immutable@${System.identityHashCode(this)}"
def apply(id: String): Toggle[Int] =
toggles.get(id) match {
case Some(t) => t
case None => Toggle.Undefined
}
def iterator: Iterator[Toggle.Metadata] =
metadata.iterator
}
private[this] val log = Logger.get()
private[this] val NoFractionAndToggle = (Double.NaN, Toggle.Undefined)
private class MutableToggle(id: String) extends Toggle[Int](id) {
private[this] val fractionAndToggle =
new AtomicReference[(Double, Toggle[Int])](NoFractionAndToggle)
override def toString: String = s"MutableToggle($id)"
private[ToggleMap] def currentFraction: Double =
fractionAndToggle.get()._1
private[ToggleMap] def setFraction(fraction: Double): Unit = {
val fAndT: (Double, Toggle[Int]) = if (Toggle.isValidFraction(fraction)) {
(fraction, fractional(id, fraction))
} else {
NoFractionAndToggle
}
fractionAndToggle.set(fAndT)
}
def isDefinedAt(t: Int): Boolean =
fractionAndToggle.get()._2.isDefinedAt(t)
def apply(t: Int): Boolean =
fractionAndToggle.get()._2(t)
}
/**
* Create an empty [[Mutable]] instance.
*/
def newMutable(): Mutable = new Mutable {
override def toString: String =
s"ToggleMap.Mutable@${System.identityHashCode(this)}"
// There will be minimal updates, so we can use a low concurrency level,
// which makes the footprint smaller.
private[this] val toggles =
new ConcurrentHashMap[String, MutableToggle](32, 0.75f, 1)
private[this] def toggleFor(id: String): MutableToggle = {
val curr = toggles.get(id)
if (curr != null) {
curr
} else {
val newToggle = new MutableToggle(id)
val prev = toggles.putIfAbsent(id, newToggle)
if (prev == null)
newToggle
else
prev
}
}
def apply(id: String): Toggle[Int] =
toggleFor(id)
def iterator: Iterator[Toggle.Metadata] = {
val source = toString
toggles.asScala.collect {
case (id, toggle) if Toggle.isValidFraction(toggle.currentFraction) =>
Toggle.Metadata(id, toggle.currentFraction, None, source)
}.toIterator
}
def put(id: String, fraction: Double): Unit = {
if (Toggle.isValidFraction(fraction)) {
log.info(s"Mutable Toggle id='$id' set to fraction=$fraction")
toggleFor(id).setFraction(fraction)
} else {
log.warning(s"Mutable Toggle id='$id' ignoring invalid fraction=$fraction")
}
}
def remove(id: String): Unit = {
log.info(s"Mutable Toggle id='$id' removed")
toggleFor(id).setFraction(Double.NaN)
}
}
/**
* A [[ToggleMap]] that is backed by a `com.twitter.app.GlobalFlag`,
* [[flag.overrides]].
*
* Its [[Toggle Toggles]] will reflect changes to the underlying `Flag` which
* enables usage in tests.
*
* Fractions that are out of range (outside of `[0.0-1.0]`) will be
* ignored.
*/
val flags: ToggleMap = new ToggleMap {
override def toString: String = "ToggleMap.Flags"
private[this] def fractions: Map[String, Double] =
flag.overrides()
private[this] class FlagToggle(id: String) extends Toggle[Int](id) {
private[this] val fractionAndToggle =
new AtomicReference[(Double, Toggle[Int])](NoFractionAndToggle)
override def toString: String = s"FlagToggle($id)"
def isDefinedAt(t: Int): Boolean =
fractions.get(id) match {
case Some(f) if Toggle.isValidFraction(f) => true
case _ => false
}
def apply(t: Int): Boolean = {
fractions.get(id) match {
case Some(f) if Toggle.isValidFraction(f) =>
val prev = fractionAndToggle.get()
val toggle =
if (f == prev._1) {
// we can use the cached toggle since the fraction matches
prev._2
} else {
val newToggle = fractional(id, f)
fractionAndToggle.compareAndSet(prev, (f, newToggle))
newToggle
}
toggle(t)
case _ =>
throw new IllegalStateException(s"$this not defined for input: $t")
}
}
}
def apply(id: String): Toggle[Int] =
new FlagToggle(id)
def iterator: Iterator[Toggle.Metadata] = {
val source = toString
fractions.iterator.collect { case (id, f) if Toggle.isValidFraction(f) =>
Toggle.Metadata(id, f, None, source)
}
}
}
/**
* A [[ToggleMap]] that proxies work to `underlying`.
*/
trait Proxy extends ToggleMap {
protected def underlying: ToggleMap
override def toString: String = underlying.toString
def apply(id: String): Toggle[Int] = underlying(id)
def iterator: Iterator[Metadata] = underlying.iterator
}
private val MdDescFn: Toggle.Metadata => Option[String] =
md => md.description
/**
* A [[ToggleMap]] which returns [[Toggle.on]] for all `ids`.
*
* @note [[ToggleMap.iterator]] will always be empty.
*/
val On: ToggleMap = new ToggleMap {
def apply(id: String): Toggle[Int] = Toggle.on(id)
def iterator: Iterator[Metadata] = Iterator.empty
}
/**
* A [[ToggleMap]] which returns [[Toggle.off]] for all `ids`.
*
* @note [[ToggleMap.iterator]] will always be empty.
*/
val Off: ToggleMap = new ToggleMap {
def apply(id: String): Toggle[Int] = Toggle.off(id)
def iterator: Iterator[Metadata] = Iterator.empty
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy