Skip to content

Commit 43c99a2

Browse files
authored
perf(Segment Membership): Members list improvements (#7924)
1 parent 37a682f commit 43c99a2

3 files changed

Lines changed: 119 additions & 7 deletions

File tree

api/segment_membership/services.py

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,8 @@ def compute_segment_counts_for_project(
129129
f"SELECT {seg.id} AS segment_id, "
130130
f"i.environment_id AS env_key, count() AS c "
131131
f"FROM IDENTITIES AS i FINAL "
132-
f"WHERE i.environment_id IN %(env_keys)s AND ({predicate}) "
132+
f"WHERE i.environment_id IN %(env_keys)s "
133+
f"AND i.is_deleted = false AND ({predicate}) "
133134
f"GROUP BY i.environment_id"
134135
)
135136

@@ -186,7 +187,10 @@ def get_segment_members_page(
186187
)
187188
return []
188189

189-
conditions = ["i.environment_id = %(env_key)s"]
190+
conditions = [
191+
"i.environment_id = %(env_key)s",
192+
"i.is_deleted = false",
193+
]
190194
params: dict[str, Any] = {"env_key": environment.api_key, "limit": limit}
191195
if cursor:
192196
conditions.append("i.identifier > %(cursor)s")
@@ -196,13 +200,29 @@ def get_segment_members_page(
196200
params["q"] = q
197201
conditions.append(f"({predicate})")
198202

199-
sql = (
200-
"SELECT i.identifier, i.identity_key, i.traits "
201-
"FROM IDENTITIES AS i FINAL "
203+
# Why two queries? `traits` is a wide JSON column split over many per-path files.
204+
# That fans out into thousands of object-storage requests, which is what
205+
# makes a cold read slow. Delegating the `traits` read to an outer query
206+
# limits them to the page.
207+
#
208+
# Besides that, bypass FINAL, which is heavy, in favour of LIMIT 1 BY i.identifier,
209+
# which is (tolerably) less correct but substantially faster.
210+
inner_sql = (
211+
"SELECT i.identifier "
212+
"FROM IDENTITIES AS i "
202213
f"WHERE {' AND '.join(conditions)} "
203-
"ORDER BY i.identifier ASC "
214+
"ORDER BY i.identifier ASC, i.inserted_at DESC "
215+
"LIMIT 1 BY i.identifier "
204216
"LIMIT %(limit)s"
205217
)
218+
sql = (
219+
"SELECT m.identifier, m.identity_key, m.traits "
220+
"FROM IDENTITIES AS m "
221+
"WHERE m.environment_id = %(env_key)s AND m.is_deleted = false "
222+
f"AND m.identifier IN ({inner_sql}) "
223+
"ORDER BY m.identifier ASC, m.inserted_at DESC "
224+
"LIMIT 1 BY m.identifier"
225+
)
206226
log_comment = (
207227
"flagsmith:segment_membership:read"
208228
f":org_{segment.project.organisation_id}"

api/tests/unit/segment_membership/test_unit_segment_membership_services.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
from datetime import datetime
12
from unittest.mock import MagicMock
23

34
import pytest
45
from common.test_tools import RunTasksFixture
6+
from django.db import connections
57
from flag_engine.segments.constants import EQUAL
68
from pytest_django.fixtures import SettingsWrapper
79
from pytest_mock import MockerFixture
@@ -186,6 +188,96 @@ def matching_segment(segment: Segment) -> Segment:
186188
return segment
187189

188190

191+
@pytest.mark.clickhouse
192+
def test_get_segment_members_page__deleted_identity__excluded(
193+
segment_membership_identities: None,
194+
matching_segment: Segment,
195+
environment: Environment,
196+
environment_api_key_str: str,
197+
) -> None:
198+
# Given
199+
with connections["clickhouse"].cursor() as cursor:
200+
cursor.executemany(
201+
"INSERT INTO IDENTITIES "
202+
"(environment_id, identifier, identity_key, traits, is_deleted) VALUES",
203+
[(environment_api_key_str, "aaron", "aaron_key", {"foo": "bar"}, True)], # type: ignore[list-item]
204+
)
205+
206+
# When
207+
members = get_segment_members_page(
208+
matching_segment, environment, cursor=None, limit=100
209+
)
210+
211+
# Then
212+
assert [member["identifier"] for member in members] == ["alice", "bob"]
213+
214+
215+
@pytest.mark.clickhouse
216+
def test_get_segment_members_page__duplicate_versions__returns_latest_once(
217+
segment_membership_identities: None,
218+
matching_segment: Segment,
219+
environment: Environment,
220+
environment_api_key_str: str,
221+
) -> None:
222+
# Given
223+
# a newer version of alice that still matches the segment
224+
with connections["clickhouse"].cursor() as cursor:
225+
cursor.executemany(
226+
"INSERT INTO IDENTITIES "
227+
"(environment_id, identifier, identity_key, traits, inserted_at) VALUES",
228+
[
229+
( # type: ignore[list-item]
230+
environment_api_key_str,
231+
"alice",
232+
"alice_key",
233+
{"foo": "bar", "version": "new"},
234+
datetime(2099, 1, 1),
235+
)
236+
],
237+
)
238+
239+
# When
240+
members = get_segment_members_page(
241+
matching_segment, environment, cursor=None, limit=100
242+
)
243+
244+
# Then
245+
# alice appears once, with the latest version's traits
246+
assert members == [
247+
SegmentMember(
248+
identifier="alice",
249+
identity_key="alice_key",
250+
traits={"foo": "bar", "version": "new"},
251+
),
252+
SegmentMember(identifier="bob", identity_key="bob_key", traits={"foo": "bar"}),
253+
]
254+
255+
256+
@pytest.mark.clickhouse
257+
def test_compute_segment_counts_for_project__deleted_identity__excluded_from_count(
258+
segment_membership_identities: None,
259+
matching_segment: Segment,
260+
project: Project,
261+
environment_api_key_str: str,
262+
) -> None:
263+
# Given
264+
with connections["clickhouse"].cursor() as cursor:
265+
cursor.executemany(
266+
"INSERT INTO IDENTITIES "
267+
"(environment_id, identifier, identity_key, traits, is_deleted) VALUES",
268+
[(environment_api_key_str, "aaron", "aaron_key", {"foo": "bar"}, True)], # type: ignore[list-item]
269+
)
270+
271+
# When
272+
with connections["clickhouse"].cursor() as cursor:
273+
counts = compute_segment_counts_for_project(project, cursor)
274+
275+
# Then
276+
assert len(counts) == 1
277+
assert counts[0].segment_id == matching_segment.id
278+
assert counts[0].count == 2
279+
280+
189281
@pytest.mark.clickhouse
190282
def test_get_segment_members_page__matching_identities__returns_members_ordered_by_identifier(
191283
segment_membership_identities: None,

docs/docs/deployment-self-hosting/observability/_events-catalogue.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,7 @@ Attributes:
379379
### `segment_membership.members.segment.skipped`
380380

381381
Logged at `error` from:
382-
- `api/segment_membership/services.py:182`
382+
- `api/segment_membership/services.py:183`
383383

384384
Attributes:
385385
- `reason`

0 commit comments

Comments
 (0)