-
Notifications
You must be signed in to change notification settings - Fork 397
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
refactor(feature-flags): optimize UX and maintenance #563
refactor(feature-flags): optimize UX and maintenance #563
Conversation
Codecov Report
@@ Coverage Diff @@
## develop #563 +/- ##
========================================
Coverage 99.95% 99.95%
========================================
Files 113 113
Lines 4477 4533 +56
Branches 243 245 +2
========================================
+ Hits 4475 4531 +56
Partials 2 2
Continue to review full report at Codecov.
|
* develop: feat(params): expose high level max_age, raise_on_transform_error (aws-powertools#567) fix(parser): apigw wss validation check_message_id; housekeeping (aws-powertools#553) chore(deps-dev): bump isort from 5.9.2 to 5.9.3 (aws-powertools#574) feat(data-classes): decode json_body if based64 encoded (aws-powertools#560) chore(deps-dev): bump mkdocs-material from 7.2.0 to 7.2.1 (aws-powertools#566)
f025789
to
d902018
Compare
Rough understanding of the schema as I'm going through before documenting it ## Schema Rules
# `features` MUST be present
# `feature_default_value` MUST be present
# MUST be at least ONE feature
# `rules` MIGHT be present
# `rule_name` must be present
# `rule_default_value` must be BOOL |
We also need to look into how to make it less error prone when creating feature flags whether that's for prod use or unit testing. There are number of conditions when validation could fail and crafting the dictionary by hand will likely lead to us causing production issues. |
@risenberg-cyberark @heitorlessa - this is looking good. Are we still labeling this as beta? In case there are some small refactoring later. |
After this refactoring, I'd go with not labelling as beta, unless something major shows up during the load testing. After launch, we could also consider a generic Two things pending then we merge: 1/ Exception Handling: InvalidSchema over ConfigurationError, and catch Store exceptions and raise accordingly, 2 Load testing: I'm slightly concerned about performance so need to check whether Store cache is sufficient or if we need to reduce the amount of |
Exceptions are now improved. Just need an extra test for condition values that are not just string to be sure we got it right. I'll load test tomorrow, merge it, and get on with the docs with @am29d to launch it by Friday EOD/Monday in the worst case scenario (life happens!) |
@heitorlessa minor patch update diff --git a/aws_lambda_powertools/utilities/feature_flags/__init__.py b/aws_lambda_powertools/utilities/feature_flags/__init__.py
index 1edbea1..5514a50 100644
--- a/aws_lambda_powertools/utilities/feature_flags/__init__.py
+++ b/aws_lambda_powertools/utilities/feature_flags/__init__.py
@@ -1,5 +1,4 @@
-"""Advanced feature toggles utility
-"""
+"""Advanced feature toggles utility"""
from .appconfig import AppConfigStore
from .base import StoreProvider
from .exceptions import ConfigurationStoreError
diff --git a/aws_lambda_powertools/utilities/feature_flags/appconfig.py b/aws_lambda_powertools/utilities/feature_flags/appconfig.py
index ea3f5dd..6dd6292 100644
--- a/aws_lambda_powertools/utilities/feature_flags/appconfig.py
+++ b/aws_lambda_powertools/utilities/feature_flags/appconfig.py
@@ -69,10 +69,13 @@ class AppConfigStore(StoreProvider):
"""
try:
# parse result conf as JSON, keep in cache for self.max_age seconds
- config = self._conf_store.get(
- name=self.name,
- transform=TRANSFORM_TYPE,
- max_age=self.cache_seconds,
+ config = cast(
+ dict,
+ self._conf_store.get(
+ name=self.name,
+ transform=TRANSFORM_TYPE,
+ max_age=self.cache_seconds,
+ ),
)
if self.envelope:
@@ -80,6 +83,6 @@ class AppConfigStore(StoreProvider):
data=config, envelope=self.envelope, jmespath_options=self.jmespath_options
)
- return cast(dict, config)
+ return config
except (GetParameterError, TransformParameterError) as exc:
raise ConfigurationStoreError("Unable to get AWS AppConfig configuration file") from exc |
When doing the load testing I found a silent bug - We're swallowing all types of exceptions, including AccessDenied when fetching config from AppConfig. I'll work on a generic |
First load test with a single feature being evaluated
Second load test with 100 features that are all evaluated to True**
Third load test without Tracer
Fourth load test without Tracer with a 5m duration
Code snippet: from aws_lambda_powertools import Tracer
from aws_lambda_powertools.utilities.typing import LambdaContext
from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver
from aws_lambda_powertools.utilities.feature_flags import FeatureFlags, AppConfigStore
app_config = AppConfigStore(
application="powertools",
environment="test",
name="test_conf_name",
cache_seconds=600,
)
tracer = Tracer()
app = ApiGatewayResolver()
feature_flags: FeatureFlags = FeatureFlags(store=app_config)
@app.get("/hello")
def hello():
username = app.current_event.get_query_string_value(
"username", default_value="lessa"
)
ctx = {"username": f"{username}"}
# is_my_feature_active: bool = feature_flags.evaluate(
# name="my_feature", context=ctx, default=False
# )
all_features: list[str] = feature_flags.get_enabled_features(context=ctx)
return {"message": f"{username}", "all_enabled_features": all_features}
# return {"message": f"{username}", "my_feature_enabled": is_my_feature_active}
@tracer.capture_lambda_handler
def lambda_handler(event, context: LambdaContext):
return app.resolve(event, context) Load test for 60s but with AppConfig cache set to 5s to check on overhead after warm
|
Happy with the performance and we can merge now. This is now maintainable, it has docstrings to aid customers using it as well as maintainers, I've also created a schema specification, and it's easier to extend the feature schema, plus more compact and concise. After launch, when time allows, we can easily add a AnalyticsProvider where one could bring their own to track who's using features that match conditions. Thanks a lot @michaelbrewer for the help in the refactoring too. @am29d we're now ready to move onto the docs, then release it. |
We discussed about using memoize to speed up this, but @risenberg-cyberark pointed out that the rules could be really large in some cases and the memory used may have the opposite effect. This may be something to revisit if performance test aren't great. |
Issue #, if available: #494
Description of changes:
Checklist
New UX
Using envelope argument to pluck feature flags in an arbitrary key from fetched config
Changes
Major changes made during initial doc writing - Due to the size, we're splitting this PR to separate actual docs.
Renamefeature_toggles
tofeature_flags
Refactor error loggers to contextual exceptionsConsider an additional fields to improve exception handling such as error type, feature, etcString concatenation ended up slowing things down, dropping itConsider fine grained ConfigurationError instead of a single ConfigurationErrorRename it toInvalidSchemaError
Simplify schema by removingfeatures
keyMake each rule inrules
unique by using Dict over List, and removerule_name
Review long and redundant names such as"feature": { "feature_default_value"...}
,"rules": [{"rule_name"...}]
Renamefeature_default_Value
todefault
Renamevalue_when_applies
towhen_match
Refactor schema into multiple validators to ease maintenanceUse module logger over classlogger
Create classes for each validation over multiple methodsAdd typing extensions as feature flags and other features become difficult to maintain without things like TypedDict, improved generics for mypy, etc.- Note: MyPy doesn't support TypedDic when you use a variable name as a dict key; ignoring it
RenameAction
toRuleAction
enumSuggest what RuleActions are valid when invalid is providedFeatureFlags
ConfigurationStore
toFeatureFlags
rules_context
tocontext
Renameget_feature
method toevaluate
Renameget_all_enabled_feature_toggles
toget_enabled_features
StoreProvider
SchemaFetcher
toStoreProvider
Usebase.py
for interfaces for consistency (e.g. Metrics, Tracer, etc.)AppConfigFetcher
toAppConfigStore
AppConfig construct parameter names for consistency (e.g.configuration_name
->name
,service
->application
)Renamevalue_if_missing
param todefault
to match Python consistency (e.g.os.getenv("VAR", default=False)
)Renameget_configuration
method toget_json_configuration
Store sectionFeatureFlags sectionSchema sectionAdd schema specificationTime allowing
test_is_rule_matched_no_matches
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.