|
1 | | -from datetime import datetime |
| 1 | +from datetime import datetime, timedelta |
2 | 2 | from datetime import timezone as dt_timezone |
3 | 3 | from unittest.mock import MagicMock |
4 | 4 |
|
|
13 | 13 | from task_processor.task_run_method import TaskRunMethod |
14 | 14 |
|
15 | 15 | from environments.models import Environment |
| 16 | +from organisations.models import Organisation |
16 | 17 | from projects.models import Project |
17 | 18 | from segment_membership import tasks |
18 | 19 | from segment_membership.models import SegmentMembershipCount, SegmentMembershipSeed |
@@ -375,27 +376,55 @@ def test_refresh_all_segment_counts__no_clickhouse_creds__skips( |
375 | 376 | enqueue.assert_not_called() |
376 | 377 |
|
377 | 378 |
|
378 | | -def test_refresh_all_segment_counts__live_segment_projects__delegates_to_enqueue( |
| 379 | +def test_refresh_all_segment_counts__no_live_segments__does_nothing( |
| 380 | + mocker: MockerFixture, |
| 381 | + settings: SettingsWrapper, |
| 382 | + project: Project, |
| 383 | +) -> None: |
| 384 | + # Given a project but no live segments, so there are no organisations to seed |
| 385 | + settings.CLICKHOUSE_ENABLED = True |
| 386 | + enqueue = mocker.patch.object(tasks, "enqueue_membership_refresh") |
| 387 | + |
| 388 | + # When |
| 389 | + refresh_all_segment_counts() |
| 390 | + |
| 391 | + # Then |
| 392 | + enqueue.assert_not_called() |
| 393 | + |
| 394 | + |
| 395 | +def test_refresh_all_segment_counts__live_segment_projects__staggers_evenly_by_org( |
379 | 396 | mocker: MockerFixture, |
380 | 397 | settings: SettingsWrapper, |
381 | 398 | project: Project, |
382 | 399 | segment: Segment, |
383 | 400 | ) -> None: |
384 | | - # Given two projects with live segments (flag + debounce gating is the |
385 | | - # helper's job, so refresh_all just delegates one call per project) |
386 | | - project_b = Project.objects.create( |
387 | | - name="project-b", organisation=project.organisation |
388 | | - ) |
| 401 | + # Given three live-segment projects across two organisations |
| 402 | + project_a2 = Project.objects.create(name="a2", organisation=project.organisation) |
| 403 | + Segment.objects.create(name="seg-a2", project=project_a2) |
| 404 | + org_b = Organisation.objects.create(name="org-b") |
| 405 | + project_b = Project.objects.create(name="b1", organisation=org_b) |
389 | 406 | Segment.objects.create(name="seg-b", project=project_b) |
390 | 407 | settings.CLICKHOUSE_ENABLED = True |
| 408 | + settings.SEGMENT_MEMBERSHIP_REFRESH_PROJECT_STAGGER_WINDOW_HOURS = 4 |
391 | 409 | enqueue = mocker.patch.object(tasks, "enqueue_membership_refresh") |
392 | 410 |
|
393 | 411 | # When |
394 | 412 | refresh_all_segment_counts() |
395 | 413 |
|
396 | | - # Then |
397 | | - dispatched_ids = {call.args[0].id for call in enqueue.call_args_list} |
398 | | - assert dispatched_ids == {project.id, project_b.id} |
| 414 | + # Then one call per project, ordered by (organisation_id, id) so an |
| 415 | + # organisation's projects refresh contiguously |
| 416 | + called = [ |
| 417 | + (call.args[0].id, call.kwargs["delay_until"]) for call in enqueue.call_args_list |
| 418 | + ] |
| 419 | + expected_order = sorted( |
| 420 | + (project, project_a2, project_b), key=lambda p: (p.organisation_id, p.id) |
| 421 | + ) |
| 422 | + assert [pid for pid, _ in called] == [p.id for p in expected_order] |
| 423 | + # spread evenly across the window: spacing = window / (total + 1) |
| 424 | + spacing = timedelta(hours=4) / 4 |
| 425 | + delays = [delay for _, delay in called] |
| 426 | + assert delays[1] - delays[0] == spacing |
| 427 | + assert delays[2] - delays[1] == spacing |
399 | 428 |
|
400 | 429 |
|
401 | 430 | def test_refresh_project_segment_counts__no_clickhouse_creds__skips( |
|
0 commit comments