-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
feat(features) Use dataclasses for flagpole instead of pydantic #75859
Conversation
Early timing measurements are looking promising.
This schema was generated with pydantic and will be used in CI to validate that schema and dataclasses are kept compatible, and could be part of a validation flow for flagpole.
🚨 Warning: This pull request contains Frontend and Backend changes! It's discouraged to make changes to Sentry's Frontend and Backend in a single pull request. The Frontend and Backend are not atomically deployed. If the changes are interdependent of each other, they must be separated into two pull requests and be made forward or backwards compatible, such that the Backend or Frontend can be safely deployed independently. Have questions? Please ask in the |
Test Failures Detected: Due to failing tests, we cannot provide coverage reports at this time. ❌ Failed Test Results:Completed 21719 tests with View the full list of failed testspytest
|
"""The value to compare against the condition's evaluation context property.""" | ||
|
||
operator: str = dataclasses.field(default="") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't love this, but it is less gross than having to always provide operator
to every condition constructor throughout tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor nits, but changes look good. Nice work!
description="A brief description or identifier for the segment" | ||
def condition_from_dict(data: Mapping[str, Any]) -> ConditionBase: | ||
operator_kind = ConditionOperatorKind(data.get("operator", "invalid")) | ||
condition_cls = OPERATOR_LOOKUP[operator_kind] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want to raise explicitly here if we get an invalid operator type? Otherwise, won't this be an attribute error on NoneType?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can make the raise explicit. Currently it would raise KeyError
which isn't as informative as it could be.
with pytest.raises(ConditionTypeMismatchException): | ||
not_condition.match( | ||
context=EvaluationContext({"foo": attr_val}), segment_name="test" | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Main reason I went with the assertion util function approach before was because it made it explicit which invalid input failed when modifying condition logic. This is mostly fine, although it does make debugging just a bit harder.
These changes switch flagpole from using pydantic as the base classes to
dataclasses
from stdlib. During the pydantic 2 upgrade the time to parse features shot way up which was unexpected. We have also not been as impressed with feature flag match times and suspected that pydantic might be contributing overhead.The changes of this pull request re-implement flagpole with basic python dataclasses. The new implementation has reduced time to build feature flags, which should help improve the overall performance of our feature flagging. Improvements were measured both with cProfile, and local mini-benchmarks. (scripts provided below)
cProfile results
The cprofile script builds a feature 1000 times and collects profiling data from those operations.
current master (pydantic)
after (dataclasses)
While significantly more functions were called the overall runtime is much better.
flagpole-profile script
Micro benchmark
In the microbenchmark I looked at 3 feature flags (the max, approximate median and smallest features). Each feature would be parsed 10000 times and I looked at the min, mean and max duration for building a Feature object from dictionary data loaded from options seeded with the current feature flag inventory.
before (pydantic)
after (dataclasses)
flagpole-timing script
Again we see significant improvements in runtime without any impact in memory usage.
What we lose?
While moving to dataclasses gives us some gains in performance it has a few drawbacks: