diff --git a/api/segment_membership/tasks.py b/api/segment_membership/tasks.py index 13c056127948..704ef7782ab6 100644 --- a/api/segment_membership/tasks.py +++ b/api/segment_membership/tasks.py @@ -161,6 +161,21 @@ def refresh_project_segment_counts(project_id: int) -> None: now = timezone.now() for m in membership_counts: m.last_synced_at = now + + new_pairs = {(m.segment_id, m.environment_id) for m in membership_counts} + stale_ids = [ + pk + for pk, segment_id, environment_id in ( + SegmentMembershipCount.objects.filter( + segment__project=project + ).values_list("id", "segment_id", "environment_id") + ) + if (segment_id, environment_id) not in new_pairs + ] + stale_deleted, _ = SegmentMembershipCount.objects.filter( + id__in=stale_ids, + ).delete() + SegmentMembershipCount.objects.bulk_create( membership_counts, update_conflicts=True, @@ -171,4 +186,5 @@ def refresh_project_segment_counts(project_id: int) -> None: "refresh.project.completed", project__id=project_id, membership_counts__count=len(membership_counts), + stale_counts__count=stale_deleted, ) diff --git a/api/tests/unit/segment_membership/test_unit_segment_membership_tasks.py b/api/tests/unit/segment_membership/test_unit_segment_membership_tasks.py index 9a57ae884f2a..3eff7ba86c38 100644 --- a/api/tests/unit/segment_membership/test_unit_segment_membership_tasks.py +++ b/api/tests/unit/segment_membership/test_unit_segment_membership_tasks.py @@ -1,5 +1,6 @@ from unittest.mock import MagicMock +from django.utils import timezone from pytest_django.fixtures import SettingsWrapper from pytest_mock import MockerFixture from pytest_structlog import StructuredLogCapture @@ -295,3 +296,63 @@ def test_refresh_project_segment_counts__counts_returned__upserts_per_env_rows( f":project_{project.id}" ) ) + + +def test_refresh_project_segment_counts__previously_matching_pair_drops_to_zero__row_deleted( + mocker: MockerFixture, + settings: SettingsWrapper, + project: Project, + environment: Environment, + segment: Segment, + enable_features: EnableFeaturesFixture, +) -> None: + # Given a prior refresh that landed a non-zero count for (segment, env) + enable_features("segment_membership_inspection") + settings.CLICKHOUSE_ENABLED = True + SegmentMembershipCount.objects.create( + segment=segment, + environment=environment, + count=15, + last_synced_at=timezone.now(), + ) + cursor = MagicMock() + open_cursor = mocker.patch.object(tasks, "open_clickhouse_cursor") + open_cursor.return_value.__enter__.return_value = cursor + # ... and a new compute that returns no matches for the same pair (the + # rule was edited, the identity set drifted, etc.). + mocker.patch.object(tasks, "compute_segment_counts_for_project", return_value=[]) + + # When + refresh_project_segment_counts(project.id) + + # Then the stale row is gone -- pairs that no longer match drop out of + # the table entirely rather than lingering at the previous count. + assert not SegmentMembershipCount.objects.filter( + segment=segment, environment=environment + ).exists() + + +def test_refresh_project_segment_counts__never_matched_pair__no_row_written( + mocker: MockerFixture, + settings: SettingsWrapper, + project: Project, + environment: Environment, + segment: Segment, + enable_features: EnableFeaturesFixture, +) -> None: + # Given a project with no prior membership rows + enable_features("segment_membership_inspection") + settings.CLICKHOUSE_ENABLED = True + cursor = MagicMock() + open_cursor = mocker.patch.object(tasks, "open_clickhouse_cursor") + open_cursor.return_value.__enter__.return_value = cursor + mocker.patch.object(tasks, "compute_segment_counts_for_project", return_value=[]) + + # When + refresh_project_segment_counts(project.id) + + # Then no row is written: refresh upserts matches, drops misses, and + # leaves never-matched pairs untouched. + assert not SegmentMembershipCount.objects.filter( + segment=segment, environment=environment + ).exists() diff --git a/docs/docs/deployment-self-hosting/observability/_events-catalogue.md b/docs/docs/deployment-self-hosting/observability/_events-catalogue.md index 2ddb52d2f39b..eb0ad2df9312 100644 --- a/docs/docs/deployment-self-hosting/observability/_events-catalogue.md +++ b/docs/docs/deployment-self-hosting/observability/_events-catalogue.md @@ -368,11 +368,12 @@ Attributes: ### `segment_membership.refresh.project.completed` Logged at `info` from: - - `api/segment_membership/tasks.py:170` + - `api/segment_membership/tasks.py:185` Attributes: - `membership_counts.count` - `project.id` + - `stale_counts.count` ### `segment_membership.refresh.project.failed`