Skip to content

Commit

Permalink
finagle, twitter-server: Document Toggles
Browse files Browse the repository at this point in the history
Problem

Toggles needed to be covered in the user guide and Toggles
lacked a few basic primitives to make working with them easier.

Solution

Add sections to the user guide and added a few primitives.

RB_ID=863319
  • Loading branch information
kevinoliver authored and jenkins committed Aug 22, 2016
1 parent 1e8f3e5 commit 9319d50
Show file tree
Hide file tree
Showing 8 changed files with 193 additions and 24 deletions.
120 changes: 118 additions & 2 deletions doc/src/sphinx/Configuration.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
Configuration
=============

Clients and Servers
-------------------

Prior to :doc:`version 6.0 <changelog>`, the ``ClientBuilder``/``ServerBuilder`` API was the
primary method for configuring the modules inside a Finagle :ref:`client <finagle_clients>`
or :ref:`server <finagle_servers>`. We are moving away from this model for various
Expand Down Expand Up @@ -48,7 +51,7 @@ demonstrates how to use ``.configured`` to override TCP socket options provided
you're doing.

Design Principles
-----------------
~~~~~~~~~~~~~~~~~

Finagle has many different components and we tried to faithfully model our configuration
to help understand the constituents and the resulting behavior. For the sake of consistency,
Expand All @@ -74,4 +77,117 @@ the current version of the ``with`` API is designed with the following principle
4. **No experimental and/or advanced features**: While, it's relatively safe to configure
parameters exposed via ``with``-prefixed methods, you should never assume the same about
the `Stack API` (i.e., ``.configured``). It's *easy* to configure basic parameters; it's
*possible* to configure expert-level parameters.
*possible* to configure expert-level parameters.

.. _toggles:

Feature Toggles
---------------

Feature toggles are a commonly used mechanism for modifying system behavior.
For background, here is a `detailed discussion <http://martinfowler.com/articles/feature-toggles.html>`_
of the topic. As implemented in Finagle they provide a good balance of control between
library and service owners which enables library owners to rollout functionality in a
measured and controlled manner.

Concepts
~~~~~~~~

A :finagle-toggle-src:`Toggle <com/twitter/finagle/toggle/Toggle.scala>` is a partial
function from a type-`T` to `Boolean`. These are used to decide whether a feature is
enabled or not for a given request or service configuration.

A :finagle-toggle-src:`ToggleMap <com/twitter/finagle/toggle/ToggleMap.scala>` is
a collection of `Int`-typed `Toggles`. It provides a means of getting a `Toggle`
for a given an identifier as well as an `Iterator` over the metadata for its `Toggles`.
Various basic implementations exist on the `ToggleMap` companion object.

Usage
~~~~~

If a `Toggle` is on the request path, it is recommended that it be stored in
a member variable to avoid unnecessary overhead on the common path. If the `Toggle` is
used only at startup this is unnecessary.

Here is an example :ref:`Filter <filters>` which uses a `Toggle` on the request path:

.. code-block:: scala
package com.example.service
import com.twitter.finagle.{Service, SimpleFilter}
import com.twitter.finagle.http.{Request, Response}
import com.twitter.finagle.toggle.{Toggle, ToggleMap}
import com.twitter.finagle.util.Rng
class ExampleFilter(
toggleMap: ToggleMap,
newBackend: Service[Request, Response])
extends SimpleFilter[Request, Response] {
private[this] val useNewBackend: Toggle[Int] = toggleMap("com.example.service.UseNewBackend")
def apply(req: Request, service: Service[Request, Response]): Future[Response] = {
if (useNewBackend(Rng.threadLocal.nextInt()))
newBackend(req)
else
service(req)
}
}
Note that we pass a `ToggleMap` into the constructor and typically this would be
the instance created via
`StandardToggleMap.apply("com.example.service", com.twitter.finagle.stats.DefaultStatsReceiver)`.
This allows for testing of the code with control of whether the `Toggle` is
enabled or disabled by using `ToggleMap.On` or `ToggleMap.Off`. This could have also been
achieved by passing the `Toggle` into the constructor and then using `Toggle.on` or
`Toggle.off` in tests.

Setting Toggle Values
~~~~~~~~~~~~~~~~~~~~~

Library Owners
^^^^^^^^^^^^^^

The base configuration for `StandardToggles` should be defined in a JSON
configuration file at `resources/com/twitter/toggles/configs/$libraryName.json`.
The :finagle-toggle-src:`JSON schema <com/twitter/finagle/toggle/JsonToggleMap.scala>`
allows for descriptions and comments.

Dynamically changing the values across a cluster is specific to a particular deployment
and can be wired in via a
:finagle-toggle-src:`service-loaded ToggleMap <com/twitter/finagle/toggle/ServiceLoadedToggleMap.scala>`.

Deterministic unit tests can be written that modify a `Toggle`\'s settings via
:finagle-toggle-src:`flag overrides <com/twitter/finagle/toggle/flag/overrides.scala>`
using:

.. code-block:: scala
import com.twitter.finagle.toggle.flag
flag.overrides.let("your.toggle.id.here", fractionToUse) {
// code that uses the flag in this block will have the
// flag's fraction set to `fractionToUse`.
}
Service Owners
^^^^^^^^^^^^^^

At runtime, the in-process `Toggle` values can be modified using TwitterServer's
"/admin/toggles" `API endpoint
<http://twitter.github.io/twitter-server/Admin.html#admin-toggles>`_.
This provides a quick way to try out a change in a limited fashion.

For setting more permanent `Toggle` values, include a JSON configuration file at
`resources/com/twitter/toggles/configs/$libraryName-service.json`.
The :finagle-toggle-src:`JSON schema <com/twitter/finagle/toggle/JsonToggleMap.scala>`
allows for descriptions and comments.

The JSON configuration also supports optional environment-specific overrides via
files that are examined before the non-environment-specific configs.
These environment-specific configs must be placed at
`resources/com/twitter/toggles/configs/$libraryName-service-$environment.json`
where the `environment` from
:finagle-toggle-src:`ServerInfo.apply() <com/twitter/finagle/server/ServerInfo.scala>`
is used to determine which one to load.
8 changes: 8 additions & 0 deletions doc/src/sphinx/Metrics.rst
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,14 @@ These metrics track the state of name resolution and service discovery.

.. include:: metrics/ServiceDiscovery.rst

Toggles
-------

These metrics correspond to :ref:`feature toggles <toggles>`.

**toggles/<libraryName>/checksum**
A gauge summarizing the current state of a `ToggleMap` which may be useful
for comparing state across a cluster or over time.

HTTP
----
Expand Down
2 changes: 2 additions & 0 deletions doc/src/sphinx/ServicesAndFilters.rst
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ Services implement *application logic*. You might, for instance,
define a ``Service[http.Request, http.Response]`` to implement your
application's external API.

.. _filters:

Filters
-------

Expand Down
6 changes: 4 additions & 2 deletions doc/src/sphinx/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,19 @@
version = sbt_versions.release_to_version(release)

# e.g. :issue:`36` :ticket:`8`
# or :src:`BufferingPool <com/twitter/finagle/pool/BufferingPool.scala>`
extlinks = {
'issue': ('https://github.com/twitter/finagle/issues/%s', 'issue #'),
'ex': ('https://github.com/twitter/finagle/blob/finagle-example/src/main/scala/%s', 'Finagle example '),
'api': ('http://twitter.github.io/finagle/docs/#%s', ''),
'util': ('http://twitter.github.io/util/docs/#%s', ''),
'util-stats-src':("https://github.com/twitter/util/blob/master/util-stats/src/main/scala/%s", 'util-stats github repo'),
'util-core-src':("https://github.com/twitter/util/blob/master/util-core/src/main/scala/%s", 'util-core github repo'),
'finagle-mux-src':("https://github.com/twitter/finagle/blob/master/finagle-mux/src/main/scala/%s", 'finagle-mux github repo'),
'util-stats-src':("https://github.com/twitter/util/blob/master/util-stats/src/main/scala/%s", 'util-stats github repo'),
'finagle-http-src':("https://github.com/twitter/finagle/blob/master/finagle-http/src/main/scala/%s", 'finagle-http github repo'),
'finagle-netty4-src':("https://github.com/twitter/finagle/blob/master/finagle-netty4/src/main/scala/%s", 'finagle-netty4 github repo'),
'finagle-mux-src':("https://github.com/twitter/finagle/blob/master/finagle-mux/src/main/scala/%s", 'finagle-mux github repo'),
'finagle-thriftmux-src':("https://github.com/twitter/finagle/blob/master/finagle-thriftmux/src/main/scala/%s", 'finagle-thriftmux github repo'),
'finagle-toggle-src':("https://github.com/twitter/finagle/blob/master/finagle-toggle/src/main/scala/%s", 'finagle-toggle github repo'),
'src':("https://github.com/twitter/finagle/blob/master/finagle-core/src/main/scala/%s", 'finagle-core github repo')
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,24 +151,20 @@ object Toggle {
private[this] val AlwaysTrue: PartialFunction[Any, Boolean] =
{ case _ => true }

private[toggle] def on[T](id: String): Toggle[T] =
/**
* A [[Toggle]] which is defined for all inputs and always returns `true`.
*/
def on[T](id: String): Toggle[T] =
apply(id, AlwaysTrue)

private[this] val AlwaysFalse: PartialFunction[Any, Boolean] =
{ case _ => false }

private[toggle] def off[T](id: String): Toggle[T] =
apply(id, AlwaysFalse)

/**
* A [[Toggle]] which is defined for all inputs and always returns `true`.
*/
val True: Toggle[Any] = on("com.twitter.finagle.toggle.True")

/**
* A [[Toggle]] which is defined for all inputs and always returns `false`.
*/
val False: Toggle[Any] = off("com.twitter.finagle.toggle.False")
def off[T](id: String): Toggle[T] =
apply(id, AlwaysFalse)

/**
* A [[Toggle]] which is defined for no inputs.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,9 @@ object ToggleMap {
fractionAndToggle.get()._2(t)
}

/**
* Create an empty [[Mutable]] instance.
*/
def newMutable(): Mutable = new Mutable {

override def toString: String =
Expand Down Expand Up @@ -441,4 +444,24 @@ object ToggleMap {
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
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -379,4 +379,23 @@ class ToggleMapTest extends FunSuite
assert(Seq(tm0, tm1, tm2) == ToggleMap.components(
ToggleMap.observed(tm0.orElse(tm1).orElse(tm2), NullStatsReceiver)))
}

test("ToggleMap.On") {
val toggle = ToggleMap.On("com.on.toggle")
forAll(IntGen) { i =>
assert(toggle.isDefinedAt(i))
assert(toggle(i))
}
assert(Iterator.empty.sameElements(ToggleMap.On.iterator))
}

test("ToggleMap.Off") {
val toggle = ToggleMap.Off("com.off.toggle")
forAll(IntGen) { i =>
assert(toggle.isDefinedAt(i))
assert(!toggle(i))
}
assert(Iterator.empty.sameElements(ToggleMap.Off.iterator))
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,20 @@ class ToggleTest extends FunSuite
private val IntGen =
Gen.chooseNum(Int.MinValue, Int.MaxValue)

private val on = Toggle.on[Int]("com.twitter.on")
private val off = Toggle.off[Int]("com.twitter.off")

test("True") {
forAll(IntGen) { i =>
assert(Toggle.True.isDefinedAt(i))
assert(Toggle.True(i))
assert(on.isDefinedAt(i))
assert(on(i))
}
}

test("False") {
forAll(IntGen) { i =>
assert(Toggle.False.isDefinedAt(i))
assert(!Toggle.False(i))
assert(off.isDefinedAt(i))
assert(!off(i))
}
}

Expand All @@ -49,17 +52,17 @@ class ToggleTest extends FunSuite
}

test("orElse(Toggle) basics") {
val trueOrFalse = Toggle.True.orElse(Toggle.False)
val trueOrTrue = Toggle.True.orElse(Toggle.True)
val trueOrFalse = on.orElse(off)
val trueOrTrue = on.orElse(on)
Seq(trueOrFalse, trueOrTrue).foreach { tog =>
forAll(IntGen) { i =>
assert(tog.isDefinedAt(i))
assert(tog(i))
}
}

val falseOrFalse = Toggle.False.orElse(Toggle.False)
val falseOrTrue = Toggle.False.orElse(Toggle.True)
val falseOrFalse = off.orElse(off)
val falseOrTrue = off.orElse(on)
Seq(falseOrFalse, falseOrTrue).foreach { tog =>
forAll(IntGen) { i =>
assert(falseOrFalse.isDefinedAt(i))
Expand All @@ -76,13 +79,13 @@ class ToggleTest extends FunSuite
assert(!undef12.isDefinedAt(i))
}

val t = undef12.orElse(Toggle.True)
val t = undef12.orElse(on)
forAll(IntGen) { i =>
assert(t.isDefinedAt(i))
assert(t(i))
}

val f = undef12.orElse(Toggle.False)
val f = undef12.orElse(off)
forAll(IntGen) { i =>
assert(f.isDefinedAt(i))
assert(!f(i))
Expand Down

0 comments on commit 9319d50

Please sign in to comment.