Skip to content
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

fix: Speed up identity overrides #4840

Merged
merged 63 commits into from
Jan 6, 2025
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
49142b0
Query only one page of identity overrides for features
zachaysan Nov 18, 2024
9e8d767
Pass feature ids to get_edge_identity_overrides
zachaysan Nov 18, 2024
8d864f2
Paginate the view to get the feature ids for the context
zachaysan Nov 18, 2024
5a5e7e4
Add a test that get_overrides_data gets passed the feature ids
zachaysan Nov 18, 2024
80f6588
simplify caching logic
matthewelwell Nov 18, 2024
678ddd4
Update test to pass
zachaysan Nov 18, 2024
79d46d5
Merge branch 'fix/speed_up_identity_overrides' of github.com:Flagsmit…
zachaysan Nov 18, 2024
33814e5
Add warning docstring
zachaysan Nov 19, 2024
6d0c046
Update mock assertion
zachaysan Nov 19, 2024
cdf378d
Limit based on whether feature ids have been passed in
zachaysan Nov 19, 2024
1a8393b
Remove obsolete argument
zachaysan Nov 19, 2024
415cb51
Add more identity overrides boolean
zachaysan Nov 19, 2024
9044edb
Remove obsolete arg and set more identity overrides
zachaysan Nov 19, 2024
a470918
Set more identity overrides boolean
zachaysan Nov 19, 2024
4fc6b50
Add get_identity_overrides_key_condition_expression and remove argument
zachaysan Nov 19, 2024
d9b52b5
Add more identity overrides
zachaysan Nov 19, 2024
9854471
Remove obsolete argument
zachaysan Nov 19, 2024
3e2c959
Add test for searching based on feature ids
zachaysan Nov 19, 2024
81b402e
Merge branch 'main' into fix/speed_up_identity_overrides
zachaysan Dec 3, 2024
26cd0c9
Remove as_completed
zachaysan Dec 3, 2024
a8c3719
Create get_edge_identity_overrides_for_feature_ids
zachaysan Dec 6, 2024
5a9c5e8
Remove more_identities_overrides and create IdentityOverridesV2List
zachaysan Dec 6, 2024
1c15e36
Use IdentityOverridesQueryResponse
zachaysan Dec 6, 2024
0d84428
Switch to is_num_identity_overrides_complete
zachaysan Dec 6, 2024
15a7138
Update caller to new identities overrides response from client
zachaysan Dec 6, 2024
47b2f1c
Update help text and switch to is_num_identity_overrides_complete
zachaysan Dec 6, 2024
001ca64
Update call assertion
zachaysan Dec 6, 2024
82614bb
Switch to is_num_identity_overrides_complete and test page of identit…
zachaysan Dec 6, 2024
6b74a68
Run test with feature_ids
zachaysan Dec 6, 2024
f1a91b1
Switch from NOTE to TODO with issue linked
zachaysan Dec 6, 2024
0c620a8
Page non-deterministic results
zachaysan Dec 9, 2024
b151941
Merge branch 'main' into fix/speed_up_identity_overrides
zachaysan Dec 9, 2024
aea8974
Query only one page of identity overrides for features
zachaysan Nov 18, 2024
9c37f6f
Pass feature ids to get_edge_identity_overrides
zachaysan Nov 18, 2024
ed79cab
Paginate the view to get the feature ids for the context
zachaysan Nov 18, 2024
4ad8e16
Add a test that get_overrides_data gets passed the feature ids
zachaysan Nov 18, 2024
95a2e61
Update test to pass
zachaysan Nov 18, 2024
036dcdb
simplify caching logic
matthewelwell Nov 18, 2024
2159ace
Add warning docstring
zachaysan Nov 19, 2024
97dfdf9
Update mock assertion
zachaysan Nov 19, 2024
ab56777
Limit based on whether feature ids have been passed in
zachaysan Nov 19, 2024
9c242aa
Remove obsolete argument
zachaysan Nov 19, 2024
a975129
Add more identity overrides boolean
zachaysan Nov 19, 2024
b684fa4
Remove obsolete arg and set more identity overrides
zachaysan Nov 19, 2024
beeefae
Set more identity overrides boolean
zachaysan Nov 19, 2024
6bde39a
Add get_identity_overrides_key_condition_expression and remove argument
zachaysan Nov 19, 2024
0cbb298
Add more identity overrides
zachaysan Nov 19, 2024
71cee1e
Remove obsolete argument
zachaysan Nov 19, 2024
eaa20f6
Add test for searching based on feature ids
zachaysan Nov 19, 2024
55ca9ca
Remove as_completed
zachaysan Dec 3, 2024
9a46b7a
Create get_edge_identity_overrides_for_feature_ids
zachaysan Dec 6, 2024
98c1401
Remove more_identities_overrides and create IdentityOverridesV2List
zachaysan Dec 6, 2024
6dff803
Use IdentityOverridesQueryResponse
zachaysan Dec 6, 2024
9b1ec2d
Switch to is_num_identity_overrides_complete
zachaysan Dec 6, 2024
9de188a
Update caller to new identities overrides response from client
zachaysan Dec 6, 2024
1083745
Update help text and switch to is_num_identity_overrides_complete
zachaysan Dec 6, 2024
d7d6133
Update call assertion
zachaysan Dec 6, 2024
2ce37df
Switch to is_num_identity_overrides_complete and test page of identit…
zachaysan Dec 6, 2024
2513d83
Run test with feature_ids
zachaysan Dec 6, 2024
59d2648
Switch from NOTE to TODO with issue linked
zachaysan Dec 6, 2024
6448a70
Page non-deterministic results
zachaysan Dec 9, 2024
ec70099
Fix conflicts and merge branch 'main' into fix/speed_up_identity_over…
zachaysan Jan 6, 2025
32224e9
Fix conflicts and merge branch 'fix/speed_up_identity_overrides' of g…
zachaysan Jan 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion api/edge_api/identities/edge_identity_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,15 @@
def get_edge_identity_overrides(
environment_id: int,
feature_id: int | None = None,
feature_ids: None | list[int] = None,
limit_feature_identities_to_one_page: bool = False,
) -> typing.List[IdentityOverrideV2]:
override_items = (
ddb_environment_v2_wrapper.get_identity_overrides_by_environment_id(
environment_id=environment_id, feature_id=feature_id
environment_id=environment_id,
feature_id=feature_id,
feature_ids=feature_ids,
limit_feature_identities_to_one_page=limit_feature_identities_to_one_page,
)
)
return [
Expand Down
66 changes: 57 additions & 9 deletions api/environments/dynamodb/wrappers/environment_wrapper.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import typing
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import Any, Iterable

from boto3.dynamodb.conditions import Key
Expand Down Expand Up @@ -66,23 +67,70 @@ def get_identity_overrides_by_environment_id(
self,
environment_id: int,
feature_id: int | None = None,
feature_ids: None | list[int] = None,
limit_feature_identities_to_one_page: bool = False,
) -> typing.List[dict[str, Any]]:
try:
return list(
self.query_get_all_items(
KeyConditionExpression=Key(ENVIRONMENTS_V2_PARTITION_KEY).eq(
str(environment_id),
if not limit_feature_identities_to_one_page:
if feature_ids is not None:
raise NotImplementedError(
"Multiple feature ids is currently not supported "
"when not limiting to a single page of features"
)
& Key(ENVIRONMENTS_V2_SORT_KEY).begins_with(
get_environments_v2_identity_override_document_key(
feature_id=feature_id,
),
return list(
self.query_get_all_items(
KeyConditionExpression=Key(ENVIRONMENTS_V2_PARTITION_KEY).eq(
str(environment_id),
)
& Key(ENVIRONMENTS_V2_SORT_KEY).begins_with(
get_environments_v2_identity_override_document_key(
feature_id=feature_id,
),
)
)
)
)
else:
if feature_ids is None and feature_id:
feature_ids = [feature_id]
assert feature_ids is not None

futures = []
with ThreadPoolExecutor() as executor:
for feature_id in feature_ids:
futures.append(
executor.submit(
self.get_page_of_feature_identities,
environment_id,
feature_id,
)
)

results = []
for future in as_completed(futures):
result = future.result()
for item in result:
results.append(item)

return results

except KeyError as e:
raise ObjectDoesNotExist() from e

def get_page_of_feature_identities(
self, environment_id: int, feature_id: int
) -> list[dict[str, Any]]:
query_response = self.table.query(
KeyConditionExpression=Key(ENVIRONMENTS_V2_PARTITION_KEY).eq(
str(environment_id),
)
& Key(ENVIRONMENTS_V2_SORT_KEY).begins_with(
get_environments_v2_identity_override_document_key(
feature_id=feature_id,
),
)
)
return query_response["Items"]

def update_identity_overrides(
self,
changeset: IdentityOverridesV2Changeset,
Expand Down
9 changes: 6 additions & 3 deletions api/features/features_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,21 @@

def get_overrides_data(
environment: "Environment",
feature_ids: None | list[int] = None,
) -> OverridesData:
"""
Get correct overrides counts for a given environment.

:param project: project to get overrides data for
:return: overrides data getter
:return: overrides data getter dictionary of {feature_id: EnvironmentFeatureOverridesData}
"""
project = environment.project

if project.enable_dynamo_db:
if project.edge_v2_identity_overrides_migrated:
# If v2 migration is complete, count segment overrides from Core
# and identity overrides from DynamoDB.
return get_edge_overrides_data(environment)
return get_edge_overrides_data(environment, feature_ids)
# If v2 migration is not started, in progress, or incomplete,
# only count segment overrides from Core.
# v1 Edge identity overrides are uncountable.
Expand Down Expand Up @@ -71,7 +72,7 @@ def get_core_overrides_data(


def get_edge_overrides_data(
environment: "Environment",
environment: "Environment", feature_ids: None | list[int] = None
) -> OverridesData:
"""
Get the number of identity / segment overrides in a given environment for each feature in the
Expand All @@ -89,6 +90,8 @@ def get_edge_overrides_data(
get_overrides_data_future = executor.submit(
get_edge_identity_overrides,
environment_id=environment.id,
feature_ids=feature_ids,
limit_feature_identities_to_one_page=True,
)
all_overrides_data: OverridesData = {}

Expand Down
7 changes: 4 additions & 3 deletions api/features/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,6 @@ def get_queryset(self):

if environment_id:
page = self.paginate_queryset(queryset)

self.environment = Environment.objects.get(id=environment_id)
q = Q(
feature_id__in=[feature.id for feature in page],
Expand Down Expand Up @@ -205,7 +204,6 @@ def perform_destroy(self, instance):

def get_serializer_context(self):
context = super().get_serializer_context()

feature_states = getattr(self, "_feature_states", {})
project = get_object_or_404(Project.objects.all(), pk=self.kwargs["project_pk"])
context.update(
Expand All @@ -216,7 +214,10 @@ def get_serializer_context(self):
environment = get_object_or_404(
Environment, id=self.request.query_params["environment"]
)
context["overrides_data"] = get_overrides_data(environment)
queryset = self.get_queryset()
page = self.paginate_queryset(queryset)
feature_ids = [feature.id for feature in page]
context["overrides_data"] = get_overrides_data(environment, feature_ids)

return context

Expand Down
33 changes: 33 additions & 0 deletions api/tests/unit/features/test_unit_features_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
VIEW_ENVIRONMENT,
)
from environments.permissions.models import UserEnvironmentPermission
from features.dataclasses import EnvironmentFeatureOverridesData
from features.feature_types import MULTIVARIATE
from features.models import Feature, FeatureSegment, FeatureState
from features.multivariate.models import MultivariateFeatureOption
Expand Down Expand Up @@ -2062,6 +2063,38 @@ def test_list_features_provides_segment_overrides_for_dynamo_enabled_project(
assert response_json["results"][0]["num_identity_overrides"] is None


def test_list_features_calls_get_overrides_data_with_feature_ids(
dynamo_enabled_project: Project,
dynamo_enabled_project_environment_one: Environment,
admin_client_new: APIClient,
mocker: MockerFixture,
) -> None:
# Given
feature = Feature.objects.create(
name="test_feature", project=dynamo_enabled_project
)
url = "%s?environment=%d" % (
reverse(
"api-v1:projects:project-features-list", args=[dynamo_enabled_project.id]
),
dynamo_enabled_project_environment_one.id,
)
mock_get_overrides_data = mocker.patch("features.views.get_overrides_data")
mock_get_overrides_data.return_value = {
feature.id: EnvironmentFeatureOverridesData()
}

# When
response = admin_client_new.get(url)

# Then
assert response.status_code == status.HTTP_200_OK
mock_get_overrides_data.assert_called_once_with(
dynamo_enabled_project_environment_one,
[feature.id],
)


def test_create_segment_override_reaching_max_limit(
admin_client_new: APIClient,
feature: Feature,
Expand Down
Loading