Feature Flag Using Python Decorators
In this blog I will be discussing about a use case where you can use Python decorators as feature flag to disable and enable a function execution. This is helpful when you write a new function and later want to disable if there is any issue with the functionality or not required.
For this demo, I am using python dict() as data sources to know whether we should disable a function or not.
Demo
In this demo, we have two function i.e. feature_old and feature_new. feature_old is existing well tested code which has been running for long whereas feature_new is newly developed feature. Though it is well tested, developer wants to have a mechanism to disable this as and when required i.e. when some race condition is being caught impacting the overall health of the system.
I am using python decorators to control this.
Below is the code snippet before introducing the feature_new.
from typing import Callable, Union
import logging
import inspect
# Defining Logging
logger = logging.getLogger("feature-flag-using-deco")
logger.setLevel(logging.DEBUG)
conHandler = logging.StreamHandler()
conHandler.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s %(name)s.%(funcName)-25s %(levelname)-7s: %(message)s')
conHandler.setFormatter(formatter)
logger.addHandler(conHandler)
def feature_old():
logger.info("This is old feature and must run.")
def main():
feature_old()
if __name__ == '__main__':
main()
Below is code which has feature_new with a control mechanism.
from typing import Callable, Union
import logging
import inspect
# Defining Logging
logger = logging.getLogger("feature-flag-using-deco")
logger.setLevel(logging.DEBUG)
conHandler = logging.StreamHandler()
conHandler.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s %(name)s.%(funcName)-25s %(levelname)-7s: %(message)s')
conHandler.setFormatter(formatter)
logger.addHandler(conHandler)
validation_flag = {
"feature_new": False
}
def validation_feature_flag(func: Callable) -> Callable:
def internal_function(*args, **kwargs) -> Union[Callable, bool]:
if not validation_flag.get(func.__name__, False):
logger.warning(f'Skipping function {str(func.__name__).upper()} as feature is turned off.')
return True
return func(*args, **kwargs)
return internal_function
def feature_old():
logger.info("This is old feature and must run.")
@validation_feature_flag
def feature_new():
logger.info(f"Running function: {inspect.stack()[0][3]}.")
def main():
feature_old()
feature_new()
if __name__ == '__main__':
main()
Lets understand this code.
validation_feature_flag is a python decorator function which will decide whether the a function should be executed or not. By default execution of feature or function will be turned off if the function name is not found in feature flag data store i.e. validation_flag dict. Lookup as below:
if not validation_flag.get(func.__name__, False)
Now to add feature flag functionality, we need to annotate our new function with this decorator function.
@validation_feature_flag
def feature_new():
What this will do is, whenever there is call to execute feature_new, as per the behavior of python decorator, validation_feature_flag will be executed first. In validation_feature_flag we will check for the presence of calling function name in validation_flag dict. If it is not present, a default value i.e. False will be returned and execution of feature_new will be skipped.
If we need to enable the execution of new feature, we need to enable or set its value to True in validation_flag dict.
validation_flag = {
"feature_new": True
}
Sample Execution
When feature_new is not present in validation_flag or set to False.
2021–04–01 13:48:48,025 feature-flag-using-deco.feature_old INFO : This is old feature and must run.
2021–04–01 13:48:48,025 feature-flag-using-deco.internal_function WARNING: Skipping function FEATURE_NEW as feature is turned off.
When feature_new is present in validation_flag and set to True.
2021–04–01 13:49:53,834 feature-flag-using-deco.feature_old INFO : This is old feature and must run.
2021–04–01 13:49:53,842 feature-flag-using-deco.feature_new INFO : Running function: feature_new.
Making it production ready
Updating feature flag value in dict will need you to restart you application. Though it give you a flexibility to some extent for example you don’t need to comment your code in all places where it is being used, but again this is not something you want to do in production i.e. restarting your application or redeploying with new value of feature flag. You need something which can act in runtime.
In order to do that you can use some external source where you can upload this values as JSON, for Eg: AWS S3 or use external tools like Launch Darkly which return boolean for a flag. However, if this new feature is being called very frequently, you will end up in making frequent calls to these external system. You can further optimize this by caching future flag value in memory for some time.
The sample code is available here.