@@ -392,104 +392,39 @@ def test_refresh_all_segment_counts__no_live_segments__does_nothing(
392392 enqueue .assert_not_called ()
393393
394394
395- def test_refresh_all_segment_counts__live_segment_projects__delegates_to_enqueue (
395+ def test_refresh_all_segment_counts__live_segment_projects__staggers_evenly_by_org (
396396 mocker : MockerFixture ,
397397 settings : SettingsWrapper ,
398398 project : Project ,
399399 segment : Segment ,
400400) -> None :
401- # Given two projects with live segments (flag + debounce gating is the
402- # helper's job, so refresh_all just delegates one call per project)
403- project_b = Project .objects .create (
404- name = "project-b" , organisation = project .organisation
405- )
406- Segment .objects .create (name = "seg-b" , project = project_b )
407- settings .CLICKHOUSE_ENABLED = True
408- enqueue = mocker .patch .object (tasks , "enqueue_membership_refresh" )
409-
410- # When
411- refresh_all_segment_counts ()
412-
413- # Then
414- calls = enqueue .call_args_list
415- assert {call .args [0 ].id for call in calls } == {project .id , project_b .id }
416- # enqueues are staggered across the refresh window, not fired all at once
417- delays = sorted (call .kwargs ["delay_until" ] for call in calls )
418- window = timedelta (
419- hours = settings .SEGMENT_MEMBERSHIP_REFRESH_PROJECT_STAGGER_WINDOW_HOURS
420- )
421- assert delays [0 ] < delays [1 ]
422- assert delays [- 1 ] - delays [0 ] < window
423-
424-
425- def test_refresh_all_segment_counts__multiple_orgs__staggers_by_org_then_project (
426- mocker : MockerFixture ,
427- settings : SettingsWrapper ,
428- project : Project ,
429- segment : Segment ,
430- ) -> None :
431- # Given org A with two live-segment projects and org B with one
401+ # Given three live-segment projects across two organisations
432402 project_a2 = Project .objects .create (name = "a2" , organisation = project .organisation )
433403 Segment .objects .create (name = "seg-a2" , project = project_a2 )
434404 org_b = Organisation .objects .create (name = "org-b" )
435405 project_b = Project .objects .create (name = "b1" , organisation = org_b )
436406 Segment .objects .create (name = "seg-b" , project = project_b )
437407 settings .CLICKHOUSE_ENABLED = True
438- settings .SEGMENT_MEMBERSHIP_REFRESH_INTERVAL_HOURS = 6
439- settings .SEGMENT_MEMBERSHIP_REFRESH_PROJECT_STAGGER_WINDOW_HOURS = 3
440- settings .SEGMENT_MEMBERSHIP_REFRESH_PROJECT_STAGGER_SECONDS = 10
408+ settings .SEGMENT_MEMBERSHIP_REFRESH_PROJECT_STAGGER_WINDOW_HOURS = 4
441409 enqueue = mocker .patch .object (tasks , "enqueue_membership_refresh" )
442410
443411 # When
444412 refresh_all_segment_counts ()
445413
446- # Then
447- delay_by_id = {
448- call .args [0 ].id : call .kwargs ["delay_until" ] for call in enqueue .call_args_list
449- }
450- assert set (delay_by_id ) == {project .id , project_a2 .id , project_b .id }
451- # org A's projects cluster a project-stagger apart
452- org_a = sorted ([delay_by_id [project .id ], delay_by_id [project_a2 .id ]])
453- assert org_a [1 ] - org_a [0 ] == timedelta (seconds = 10 )
454- # org B sits a full window-slot (window / (orgs + 1)) away, not a project-stagger
455- org_spacing = timedelta (hours = 3 ) / 3
456- assert abs (delay_by_id [project_b .id ] - org_a [0 ]) == org_spacing
457-
458-
459- def test_refresh_all_segment_counts__org_overflows_slot__no_cross_org_collision (
460- mocker : MockerFixture ,
461- settings : SettingsWrapper ,
462- project : Project ,
463- segment : Segment ,
464- ) -> None :
465- # Given org A with three live-segment projects and org B with one, and a
466- # project stagger wide enough that A's projects overflow its window slot
467- # (org_spacing = 1h / 3 = 20min, but the projects are 1h apart).
468- project_a2 = Project .objects .create (name = "a2" , organisation = project .organisation )
469- Segment .objects .create (name = "seg-a2" , project = project_a2 )
470- project_a3 = Project .objects .create (name = "a3" , organisation = project .organisation )
471- Segment .objects .create (name = "seg-a3" , project = project_a3 )
472- org_b = Organisation .objects .create (name = "org-b" )
473- project_b = Project .objects .create (name = "b1" , organisation = org_b )
474- Segment .objects .create (name = "seg-b" , project = project_b )
475- settings .CLICKHOUSE_ENABLED = True
476- settings .SEGMENT_MEMBERSHIP_REFRESH_PROJECT_STAGGER_WINDOW_HOURS = 1
477- settings .SEGMENT_MEMBERSHIP_REFRESH_PROJECT_STAGGER_SECONDS = 3600
478- enqueue = mocker .patch .object (tasks , "enqueue_membership_refresh" )
479-
480- # When
481- refresh_all_segment_counts ()
482-
483- # Then
484- delay_by_id = {
485- call .args [0 ].id : call .kwargs ["delay_until" ] for call in enqueue .call_args_list
486- }
487- org_a = sorted (delay_by_id [p ] for p in (project .id , project_a2 .id , project_a3 .id ))
488- # org A's projects stay a project-stagger apart, overflowing the 20min slot
489- assert org_a [1 ] - org_a [0 ] == timedelta (seconds = 3600 )
490- assert org_a [2 ] - org_a [1 ] == timedelta (seconds = 3600 )
491- # org B never schedules before org A's last project, so the two don't collide
492- assert delay_by_id [project_b .id ] >= org_a [- 1 ]
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
493428
494429
495430def test_refresh_project_segment_counts__no_clickhouse_creds__skips (
0 commit comments