Skip to content

Commit 07673b2

Browse files
jopemachineclaude
andcommitted
test(BA-6927): cover multi-project, multi-user, multi-kernel aggregation
One observation tick spanning two projects, three users and seven kernels, with one user running in both projects. The cross product compounded up the hierarchy rather than applying a flat factor: this workload produced 1200 instead of 600 for a user (2x), 3600 instead of 1200 for a project (3x) and 23100 instead of 3300 for the domain (7x), which is why the domain figures looked most implausible in the report. Also pins that user buckets stay keyed by (user, project) so a user active in two projects does not collapse into a single bucket. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 9901d43 commit 07673b2

1 file changed

Lines changed: 185 additions & 0 deletions

File tree

tests/unit/manager/sokovan/scheduler/fair_share/test_aggregator_bucket_aggregation.py

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@
3131

3232
RESOURCE_GROUP_ID = ResourceGroupID(uuid4())
3333

34+
_USER_A = UUID("11111111-1111-4111-8111-111111111111")
35+
_USER_B = UUID("22222222-2222-4222-8222-222222222222")
36+
_USER_C = UUID("33333333-3333-4333-8333-333333333333")
37+
_PROJECT_1 = UUID("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa")
38+
_PROJECT_2 = UUID("bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb")
39+
_TICK_DAY = date(2026, 7, 17)
40+
3441

3542
def make_datetime(
3643
year: int, month: int, day: int, hour: int, minute: int, second: int = 0
@@ -830,3 +837,181 @@ def test_full_day_of_four_kernels(self, aggregator: FairShareAggregator) -> None
830837
)
831838
delta = result.user_usage_deltas[user_key]
832839
assert delta.resource_seconds["cuda.shares"] == Decimal("345600")
840+
841+
842+
@dataclass(frozen=True)
843+
class _Workload:
844+
"""A group of identical kernels one user runs inside one project."""
845+
846+
user_uuid: UUID
847+
project_id: UUID
848+
shares: Decimal
849+
kernel_count: int
850+
851+
852+
@dataclass(frozen=True)
853+
class _UserBucketExpectation:
854+
label: str
855+
user_uuid: UUID
856+
project_id: UUID
857+
resource_seconds: Decimal
858+
duration_seconds: int
859+
860+
861+
@dataclass(frozen=True)
862+
class _ProjectBucketExpectation:
863+
label: str
864+
project_id: UUID
865+
resource_seconds: Decimal
866+
duration_seconds: int
867+
868+
869+
class TestMultiTenantTick:
870+
"""One observation tick spanning two projects, three users and seven kernels.
871+
872+
The cross product does not merely double a number, it compounds up the
873+
hierarchy: each level multiplies by the count of kernels *it* aggregates.
874+
For this workload the old code produced 1200 instead of 600 for a user
875+
(2x), 3600 instead of 1200 for a project (3x) and 23100 instead of 3300
876+
for the domain (7x), which is why domain figures looked most absurd.
877+
878+
User A runs in both projects, so the user buckets must stay keyed by
879+
(user, project) and must not collapse into one.
880+
"""
881+
882+
@pytest.fixture
883+
def multi_tenant_specs(self) -> list[KernelUsageRecordCreatorSpec]:
884+
"""One 5-minute slice per kernel, all on the same day."""
885+
workloads = [
886+
_Workload(_USER_A, _PROJECT_1, Decimal("1"), 2),
887+
_Workload(_USER_B, _PROJECT_1, Decimal("2"), 1),
888+
_Workload(_USER_A, _PROJECT_2, Decimal("1"), 3),
889+
_Workload(_USER_C, _PROJECT_2, Decimal("4"), 1),
890+
]
891+
return [
892+
make_spec(
893+
period_start=make_datetime(2026, 7, 17, 10, 0, 0),
894+
period_end=make_datetime(2026, 7, 17, 10, 5, 0),
895+
resource_usage=ResourceSlot({"cuda.shares": workload.shares * 300}),
896+
occupied_slots=ResourceSlot({"cuda.shares": workload.shares}),
897+
user_uuid=workload.user_uuid,
898+
project_id=workload.project_id,
899+
)
900+
for workload in workloads
901+
for _ in range(workload.kernel_count)
902+
]
903+
904+
@pytest.mark.parametrize(
905+
"case",
906+
[
907+
_UserBucketExpectation(
908+
label="user-a-project-1", # 2 kernels * 1 share * 300s
909+
user_uuid=_USER_A,
910+
project_id=_PROJECT_1,
911+
resource_seconds=Decimal("600"),
912+
duration_seconds=600,
913+
),
914+
_UserBucketExpectation(
915+
label="user-b-project-1", # 1 kernel * 2 shares * 300s
916+
user_uuid=_USER_B,
917+
project_id=_PROJECT_1,
918+
resource_seconds=Decimal("600"),
919+
duration_seconds=300,
920+
),
921+
_UserBucketExpectation(
922+
label="user-a-project-2", # 3 kernels * 1 share * 300s
923+
user_uuid=_USER_A,
924+
project_id=_PROJECT_2,
925+
resource_seconds=Decimal("900"),
926+
duration_seconds=900,
927+
),
928+
_UserBucketExpectation(
929+
label="user-c-project-2", # 1 kernel * 4 shares * 300s
930+
user_uuid=_USER_C,
931+
project_id=_PROJECT_2,
932+
resource_seconds=Decimal("1200"),
933+
duration_seconds=300,
934+
),
935+
],
936+
ids=lambda case: case.label,
937+
)
938+
def test_user_buckets_are_keyed_by_user_and_project(
939+
self,
940+
aggregator: FairShareAggregator,
941+
multi_tenant_specs: list[KernelUsageRecordCreatorSpec],
942+
case: _UserBucketExpectation,
943+
) -> None:
944+
result = aggregator.aggregate_kernel_usage_to_buckets(multi_tenant_specs)
945+
946+
assert len(result.user_usage_deltas) == 4
947+
delta = result.user_usage_deltas[
948+
UserUsageBucketKey(
949+
user_uuid=case.user_uuid,
950+
project_id=case.project_id,
951+
domain_name="default",
952+
resource_group="default",
953+
resource_group_id=RESOURCE_GROUP_ID,
954+
period_date=_TICK_DAY,
955+
)
956+
]
957+
assert delta.resource_seconds["cuda.shares"] == case.resource_seconds
958+
assert delta.duration_seconds == case.duration_seconds
959+
960+
@pytest.mark.parametrize(
961+
"case",
962+
[
963+
_ProjectBucketExpectation(
964+
label="project-1", # user A 600 + user B 600
965+
project_id=_PROJECT_1,
966+
resource_seconds=Decimal("1200"),
967+
duration_seconds=900,
968+
),
969+
_ProjectBucketExpectation(
970+
label="project-2", # user A 900 + user C 1200
971+
project_id=_PROJECT_2,
972+
resource_seconds=Decimal("2100"),
973+
duration_seconds=1200,
974+
),
975+
],
976+
ids=lambda case: case.label,
977+
)
978+
def test_project_buckets_sum_their_users(
979+
self,
980+
aggregator: FairShareAggregator,
981+
multi_tenant_specs: list[KernelUsageRecordCreatorSpec],
982+
case: _ProjectBucketExpectation,
983+
) -> None:
984+
result = aggregator.aggregate_kernel_usage_to_buckets(multi_tenant_specs)
985+
986+
assert len(result.project_usage_deltas) == 2
987+
delta = result.project_usage_deltas[
988+
ProjectUsageBucketKey(
989+
project_id=case.project_id,
990+
domain_name="default",
991+
resource_group="default",
992+
resource_group_id=RESOURCE_GROUP_ID,
993+
period_date=_TICK_DAY,
994+
)
995+
]
996+
assert delta.resource_seconds["cuda.shares"] == case.resource_seconds
997+
assert delta.duration_seconds == case.duration_seconds
998+
999+
def test_domain_bucket_sums_every_project(
1000+
self,
1001+
aggregator: FairShareAggregator,
1002+
multi_tenant_specs: list[KernelUsageRecordCreatorSpec],
1003+
) -> None:
1004+
"""Project 1 (1200) + project 2 (2100); the old code reported 23100."""
1005+
result = aggregator.aggregate_kernel_usage_to_buckets(multi_tenant_specs)
1006+
1007+
assert len(result.domain_usage_deltas) == 1
1008+
delta = result.domain_usage_deltas[
1009+
DomainUsageBucketKey(
1010+
domain_name="default",
1011+
resource_group="default",
1012+
resource_group_id=RESOURCE_GROUP_ID,
1013+
period_date=_TICK_DAY,
1014+
)
1015+
]
1016+
assert delta.resource_seconds["cuda.shares"] == Decimal("3300")
1017+
assert delta.duration_seconds == 2100

0 commit comments

Comments
 (0)