Skip to content

Instantly share code, notes, and snippets.

@wware
Last active December 3, 2017 16:35
Show Gist options
  • Save wware/70550323453a39bd0efe725ca66b699b to your computer and use it in GitHub Desktop.
Save wware/70550323453a39bd0efe725ca66b699b to your computer and use it in GitHub Desktop.

Feature toggle decorator

This is a feature toggle scheme for the system I maintain at my job. The goal is to be able to easily toggle functions that have been recently pushed to production, so if things go bad, we have an easy mitigation plan.

Create a database table with three columns, Key (string), Value (boolean), LastChanged (timestamp). The purpose of the LastChanged column is just for record keeping to decide when a feature is deemed trustworthy. The several for a given key string present the history of switching the feature on and off, which you can compare to the git history of the feature implementation. To toggle the feature, add a new row to the table with the right Key, and with LastChanged set to NOW(). Here I'm using sqlite3 for quick testing, in the target system we use MySQL for everything.

The function get_from_db(key) does a database lookup. For the given key, it returns the Value for the row with the latest value of LastChanged.

When you decide to trust the new feature, you remove the decorator line above the definition of "feature_one", along with the definition of "old_behavior_one". Until then, you can switch the feature on or off just by adding a new row to the database table.

One thing about how the decorator is written is that it checks the database on each invocation of the feature, as opposed to selecting the old or new behavior just once and sticking with it. There is a performance hit in doing that but it allows long-running processes to be left running, which is a requirement in the target system.

# pylint: disable=unused-argument
# pylint: disable=global-statement
import unittest
from functools import wraps
import sqlite3
#################
# Database stuff
conn = None
SCHEMA = """
CREATE TABLE toggles
(Key VARCHAR(255), Value BOOLEAN, LastChanged DATETIME)
"""
def set_toggle(key, value):
cur = conn.cursor()
cur.execute("""
INSERT INTO toggles (Key, Value, LastChanged)
VALUES ('{0}', {1}, DATETIME('NOW'))
""".format(key, 1 if value else 0))
conn.commit()
def get_from_db(key):
cur = conn.cursor()
cur.execute("""
SELECT Value FROM toggles WHERE Key = '{0}'
ORDER BY LastChanged DESC LIMIT 1
""".format(key))
return cur.fetchone()[0] != 0
###########################################
# Here is the decorator and example uses.
def feature_toggle(key, oldf=None):
def nop(*args, **kwargs):
return None
if oldf is None:
oldf = nop
def decfunc(newf):
@wraps(newf)
def inner(*args, **kwargs):
if get_from_db(key):
return newf(*args, **kwargs)
else:
return oldf(*args, **kwargs)
return inner
return decfunc
def old_behavior_one():
return "old behavior one"
@feature_toggle("my cool new feature", old_behavior_one)
def feature_one():
return "new behavior one"
# Introducing new behavior, there is no old behavior.
@feature_toggle("my other cool feature")
def feature_two():
return "new behavior two"
############################################
# Now let's test it and make sure it works.
class FeatureToggleTest(unittest.TestCase):
def setUp(self):
global conn
conn = sqlite3.connect(":memory:")
cur = conn.cursor()
cur.execute(SCHEMA)
conn.commit()
def tearDown(self):
conn.close()
def test1(self):
set_toggle('my cool new feature', True)
set_toggle('some useless other thing', False)
self.assertEqual(feature_one(), "new behavior one")
def test2(self):
set_toggle('my cool new feature', False)
set_toggle('some useless other thing', False)
self.assertEqual(feature_one(), "old behavior one")
def test3(self):
set_toggle('my other cool feature', True)
self.assertEqual(feature_two(), "new behavior two")
def test4(self):
set_toggle('my other cool feature', False)
self.assertEqual(feature_two(), None)
if __name__ == '__main__':
unittest.main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment