feature flags are flawed: let's make them better - dpc

40
FEATURE FLAGS ARE FLAWED LET'S MAKE THEM BETTER Created by Stephen Young / @young_steveo

Upload: stephen-young

Post on 17-Jan-2017

160 views

Category:

Technology


1 download

TRANSCRIPT

FEATURE FLAGS ARE FLAWED

LET'S MAKE THEM BETTERCreated by Stephen Young / @young_steveo

FEATURE FLAG?I've seen a lot of names for this concept tossed

around: feature bits, �ags, �ippers, switchesand the like. There seems no generally

accepted name yet.

— Martin Fowler

SIMPLE TOGGLEclass AmazingRecipe { public function getSauce() {

$useNewRecipe = false;

// $useNewRecipe = true;

if ($useNewRecipe) { // new formula here } else { // original code here } } }

TRADITIONALFEATURE FLAGS ARE…

FLAWED...ACTUALLY PRETTY USEFUL AND NOT REALLY FLAWED PER SE, BUT THEY COME WITH A FEW

CHALLENGES AND I THINK WE CAN IMPROVE THE MODEL, SO

LET'S MAKE THEM BETTER

SIMPLE TOGGLEclass AmazingRecipe { public function getSauce() { $useNewRecipe = false; // $useNewRecipe = true;

if ($useNewRecipe) { // new formula here } else { // original code here } } }

❌❌

Not Testable

SIMPLE TOGGLE V2class AmazingRecipe { public function getSauce() { if ($this->useNewRecipe()) { // new formula here } else { // original code here } } public function useNewRecipe() { return false; // true } }

Can Be Stubbed

MOCK TOGGLE/** * @dataProvider newFormulaProvider */ public function testGetSauce($useNew, $sauce) { $sut = $this->getMock('AmazingRecipe', ['useNewRecipe']); $sut->expects($this->once()) ->method('useNewRecipe') ->will($this->returnValue($useNew)); $this->assertEquals($sauce, $sut->getSauce()); }

public function newFormulaProvider() { return [ [ false, 'old Recipe' ], [ true, 'new Recipe' ] ]; }

SIMPLE TOGGLE V2class AmazingRecipe { public function getSauce() { if ($this->useNewRecipe()) { // new formula here } else { // original code here } } public function useNewRecipe() { return false; // true } }

❌❌❌

Not MaintainableNot Con�gurableNot My Concern

FEATURE ROUTERclass AmazingRecipe { public function getSauce() { if ($this->useNewRecipe()) { // new formula here } else { // original code here } } public function useNewRecipe() { return Flags::enabled('AmazingRecipie.NewSauceFormula'); } }

FEATURE ROUTERfinal class Flags { protected static $map = []; public static function enabled($key) { if (empty(static::$map)) { // hydrate map } return !empty(static::$map[$key]); } }

TRADITIONAL FEATURE FLAG SYSTEMCurb Long-Lived Feature Branches

Easy To Use (If This Then That)

Testable

TONS OF APPLICATIONSRelease Toggles - change frequently, short livedTimed ReleasesOperations Toggles - sometimes long lived, rarely change

WHAT'S NOT TO LIKE?

CYCLOMATIC COMPLEXITYpublic function getSauce() { if ($this->useNewRecipe()) { if ($this->testSpicyVersion()) { // add spice } if ($this->fixEnabled()) { // fix bug in new code } } else { // original code here if ($this->fixEnabled()) { // fix bug in old code } } }

Complexity: 5

ALL OR NOTHING

CANARY DEPLOYMENTS

COHORTS[…] a cohort is a group of subjects who haveshared a particular event together during a

particular time span.

— Wikipedia

MORE CONDITIONS?if ( Flags::enabled('SomeFeature') && $user->canSeeFeature('SomeFeature') ) { // execute feature code }

class User { public function canSeeFeature($feature) { // check the db or user session? } }

INTRODUCING SWIVELSwivel can enable features for a subset of users.

No more complex control �ow; Swivel takes care ofdetermining code paths.

BASIC CONCEPTS1. Segment users into 10 cohorts. Swivel calls these Buckets.

2. Associate a Feature to a number of active Buckets.

3. Write code that runs when a particular Feature is enabled.Swivel calls these Behaviors.

SEGMENT USERS INTO BUCKETSid user_id bucket_id

1 160 6

2 956 2

3 189 7

4 412 2

ASSOCIATE FEATURES TO BUCKETSid slug buckets

1 "AwesomeSauce" "[ ]"

2 "AwesomeSauce.Spicy" "[1,2]"

3 "AwesomeSauce.Saucy" "[3,4]"

BOOTSTRAP$bucket = 5; // From Session or DB

$map = [ 'AwesomeSauce' => [1,2,3,4], 'AwesomeSauce.Spicy' => [1,2], 'AwesomeSauce.Saucy' => [3,4] ];

$config = new \Zumba\Swivel\Config($map, $bucket);

$swivel = new \Zumba\Swivel\Manager($config);

TOGGLE EXAMPLEclass AmazingRecipe { public function __construct(\Zumba\Swivel\Manager $swivel) { $this->swivel = $swivel; }

public function getSauce() { return $this->swivel->forFeature('AwesomeSauce') ->addBehavior('Spicy', [$this, 'getSpicyFormula']) ->addBehavior('Saucy', [$this, 'getSaucyFormula']) ->defaultBehavior([$this, 'getFormula']) ->execute(); }

protected function getSpicyFormula() { } protected function getSaucyFormula() { } protected function getFormula() { } }

SHORTHAND EXAMPLEclass AmazingRecipe { public function __construct(\Zumba\Swivel\Manager $swivel) { $this->swivel = $swivel; }

public function getSauce() { return $this->swivel->invoke( 'AwesomeSauce.New', [$this, 'getNewSauce'], [$this, 'getOldSauce'] ); } }

METRICS FOR CANARY DEPLOYMENTS

SWIVEL LOGGINGPSR-3 LOGGER AWARE

$config = new \Zumba\Swivel\Config($map, $bucket, $psr3Logger);

// or

$config->setLogger($psr3Logger);

SWIVEL METRICSSTATSD STYLE METRICS INTERFACE

interface MetricsInterface { public function count($context, $source, $value, $metric); public function decrement($context, $source, $metric); public function endMemoryProfile($context, $source, $metric); public function endTiming($context, $source, $metric); public function gauge($context, $source, $value, $metric); public function increment($context, $source, $metric); public function memory($context, $source, $memory, $metric); public function set($context, $source, $value, $metric); public function setNamespace($namespace); public function startMemoryProfile($context, $source, $metric); public function startTiming($context, $source, $metric); public function time($context, $source, \Closure $func, $metric); public function timing($context, $source, $value, $metric); }

METRICS FOR A/B TESTING

MORE APPLICATIONSRelease TogglesTimed ReleasesOperations TogglesExperimentsPermissions?

COMPLEXITY

TRADITIONAL FEATURE FLAGSPROS

Eliminate Long Lived BranchesDisable Problematic Code

CONSComplexity BumpAll Or Nothing

COHORT FEATURE FLAGSPROS

Eliminate Long Lived BranchesDisable Problematic CodeRoll Out FeaturesA/B Testing

CONSComplexity BumpAll Or Nothing

SWIVEL FEATURE FLAGSPROS

Eliminate Long Lived BranchesDisable Problematic CodeRoll Out FeaturesA/B TestingBuilt In LoggingBuilt In Metrics

CONSComplexity Bump

SWIVELJSswivel .forFeature('AwesomeSauce') .addBehavior('Spicy', this.getSpicyFormula) .addBehavior('Saucy', this.getSaucyFormula) .defaultBehavior(this.getFormula) .execute();

COMMUNITY PACKAGESZF2 module: https://github.com/mrferos/mrf-swivel

Yii extension: https://github.com/DanaLuther/yii-swivel

Yii 2.0 extension: https://github.com/DanaLuther/yii2-swivel

CakePHP 2 Plugin: https://github.com/zumba/swivel-cake

ROADMAPBucket RangesCon�gurable GranularityCakePHP 3.0 Plugin

QUESTIONS?

GO FORTH AND TOGGLE!Twitter: @young_steveo

Swivel: https://github.com/zumba/swivel

Joindin: https://legacy.joind.in/17583

THANKS!