Skip to content

Commit 0b3351f

Browse files
jopemachineclaude
andcommitted
feat(BA-6891): add kernel scheduling-history v2 DTOs and service actions
The admin search is system-wide and unscoped, so its authorization is the SUPERADMIN role gate rather than RBAC scope resolution: it is a BaseGlobalAction run through GlobalActionProcessor, which applies the gate on the action path behind the route's superadmin_required middleware. The scoped counterpart resolves against an RBAC scope and stays on the plain action path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent ceb6096 commit 0b3351f

16 files changed

Lines changed: 404 additions & 50 deletions

File tree

changes/12869.feature.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add the v2 DTOs and service actions for kernel scheduling-history search, forming the schema shared by REST v2 and GraphQL.

src/ai/backend/common/data/permission/types.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,7 @@ class ScopeType(enum.StrEnum):
352352

353353
# === Entity-level scopes ===
354354
SESSION = "session"
355+
KERNEL = "kernel"
355356
DEPLOYMENT = "deployment"
356357
MODEL_DEPLOYMENT = "model_deployment"
357358
VFOLDER = "vfolder"
@@ -424,6 +425,7 @@ class RBACElementType(enum.StrEnum):
424425
PROJECT_RESOURCE_POLICY = "project_resource_policy"
425426
ROLE = "role"
426427
AUDIT_LOG = "audit_log"
428+
KERNEL_HISTORY = "kernel:history"
427429
EVENT_LOG = "event_log"
428430

429431
# === Admin page access control ===

src/ai/backend/common/dto/manager/v2/rbac/types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ class RBACElementTypeDTO(StrEnum):
109109
PROJECT_RESOURCE_POLICY = "project_resource_policy"
110110
ROLE = "role"
111111
AUDIT_LOG = "audit_log"
112+
KERNEL_HISTORY = "kernel:history"
112113
EVENT_LOG = "event_log"
113114

114115
# Admin page access control

src/ai/backend/common/dto/manager/v2/scheduling_history/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@
44

55
from ai.backend.common.dto.manager.v2.scheduling_history.request import (
66
AdminSearchDeploymentHistoriesInput,
7+
AdminSearchKernelHistoriesInput,
78
AdminSearchRouteHistoriesInput,
89
AdminSearchSessionHistoriesInput,
910
DeploymentHistoryFilter,
1011
DeploymentHistoryOrder,
12+
KernelHistoryFilter,
13+
KernelHistoryOrder,
1114
RouteHistoryFilter,
1215
RouteHistoryOrder,
1316
SearchDeploymentHistoryInput,
@@ -18,9 +21,11 @@
1821
)
1922
from ai.backend.common.dto.manager.v2.scheduling_history.response import (
2023
AdminSearchDeploymentHistoriesPayload,
24+
AdminSearchKernelHistoriesPayload,
2125
AdminSearchRouteHistoriesPayload,
2226
AdminSearchSessionHistoriesPayload,
2327
DeploymentHistoryNode,
28+
KernelHistoryNode,
2429
ListDeploymentHistoryPayload,
2530
ListRouteHistoryPayload,
2631
ListSessionHistoryPayload,
@@ -30,6 +35,7 @@
3035
from ai.backend.common.dto.manager.v2.scheduling_history.types import (
3136
DeploymentHistoryOrderField,
3237
DeploymentHistoryScopeDTO,
38+
KernelHistoryOrderField,
3339
OrderDirection,
3440
RouteHistoryOrderField,
3541
RouteHistoryScopeDTO,
@@ -43,6 +49,7 @@
4349
# Types
4450
"DeploymentHistoryOrderField",
4551
"DeploymentHistoryScopeDTO",
52+
"KernelHistoryOrderField",
4653
"OrderDirection",
4754
"RouteHistoryOrderField",
4855
"RouteHistoryScopeDTO",
@@ -52,10 +59,13 @@
5259
"SubStepResultInfo",
5360
# Input models (request)
5461
"AdminSearchDeploymentHistoriesInput",
62+
"AdminSearchKernelHistoriesInput",
5563
"AdminSearchRouteHistoriesInput",
5664
"AdminSearchSessionHistoriesInput",
5765
"DeploymentHistoryFilter",
5866
"DeploymentHistoryOrder",
67+
"KernelHistoryFilter",
68+
"KernelHistoryOrder",
5969
"RouteHistoryFilter",
6070
"RouteHistoryOrder",
6171
"SearchDeploymentHistoryInput",
@@ -65,9 +75,11 @@
6575
"SessionHistoryOrder",
6676
# Response models
6777
"AdminSearchDeploymentHistoriesPayload",
78+
"AdminSearchKernelHistoriesPayload",
6879
"AdminSearchRouteHistoriesPayload",
6980
"AdminSearchSessionHistoriesPayload",
7081
"DeploymentHistoryNode",
82+
"KernelHistoryNode",
7183
"ListDeploymentHistoryPayload",
7284
"ListRouteHistoryPayload",
7385
"ListSessionHistoryPayload",

src/ai/backend/common/dto/manager/v2/scheduling_history/request.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
from .types import (
1313
DeploymentHistoryOrderField,
14+
KernelHistoryOrderField,
1415
OrderDirection,
1516
RouteHistoryOrderField,
1617
SchedulingResultType,
@@ -19,10 +20,13 @@
1920

2021
__all__ = (
2122
"AdminSearchDeploymentHistoriesInput",
23+
"AdminSearchKernelHistoriesInput",
2224
"AdminSearchRouteHistoriesInput",
2325
"AdminSearchSessionHistoriesInput",
2426
"DeploymentHistoryFilter",
2527
"DeploymentHistoryOrder",
28+
"KernelHistoryFilter",
29+
"KernelHistoryOrder",
2630
"RouteHistoryFilter",
2731
"RouteHistoryOrder",
2832
"SchedulingResultFilter",
@@ -86,6 +90,37 @@ class SearchSessionHistoryInput(BaseRequestModel):
8690
offset: int = Field(default=0, ge=0, description="Number of items to skip")
8791

8892

93+
class KernelHistoryFilter(BaseRequestModel):
94+
"""Filter conditions for kernel scheduling history search."""
95+
96+
id: UUIDFilter | None = Field(default=None, description="Filter by history record ID")
97+
kernel_id: UUIDFilter | None = Field(default=None, description="Filter by kernel ID")
98+
session_id: UUIDFilter | None = Field(default=None, description="Filter by session ID")
99+
phase: StringFilter | None = Field(default=None, description="Filter by scheduling phase")
100+
from_status: list[str] | None = Field(default=None, description="Filter by from_status values")
101+
to_status: list[str] | None = Field(default=None, description="Filter by to_status values")
102+
result: SchedulingResultFilter | None = Field(
103+
default=None, description="Filter by scheduling result"
104+
)
105+
error_code: StringFilter | None = Field(default=None, description="Filter by error code")
106+
message: StringFilter | None = Field(default=None, description="Filter by message")
107+
created_at: DateTimeFilter | None = Field(default=None, description="Filter by created_at")
108+
updated_at: DateTimeFilter | None = Field(default=None, description="Filter by updated_at")
109+
AND: list[KernelHistoryFilter] | None = Field(default=None, description="AND conjunction.")
110+
OR: list[KernelHistoryFilter] | None = Field(default=None, description="OR conjunction.")
111+
NOT: list[KernelHistoryFilter] | None = Field(default=None, description="NOT negation.")
112+
113+
114+
KernelHistoryFilter.model_rebuild()
115+
116+
117+
class KernelHistoryOrder(BaseRequestModel):
118+
"""Order specification for kernel scheduling history."""
119+
120+
field: KernelHistoryOrderField = Field(description="Field to order by")
121+
direction: OrderDirection = Field(default=OrderDirection.DESC, description="Order direction")
122+
123+
89124
class DeploymentHistoryFilter(BaseRequestModel):
90125
"""Filter conditions for deployment scheduling history search."""
91126

@@ -182,6 +217,19 @@ class AdminSearchSessionHistoriesInput(BaseRequestModel):
182217
offset: int | None = Field(default=None, description="Offset pagination: number to skip")
183218

184219

220+
class AdminSearchKernelHistoriesInput(BaseRequestModel):
221+
"""Input for admin search of kernel scheduling histories."""
222+
223+
filter: KernelHistoryFilter | None = Field(default=None, description="Filter conditions")
224+
order: list[KernelHistoryOrder] | None = Field(default=None, description="Order specifications")
225+
first: int | None = Field(default=None, description="Cursor pagination: number of items")
226+
after: str | None = Field(default=None, description="Cursor pagination: after cursor")
227+
last: int | None = Field(default=None, description="Cursor pagination: last N items")
228+
before: str | None = Field(default=None, description="Cursor pagination: before cursor")
229+
limit: int | None = Field(default=None, description="Offset pagination: maximum items")
230+
offset: int | None = Field(default=None, description="Offset pagination: number to skip")
231+
232+
185233
class AdminSearchDeploymentHistoriesInput(BaseRequestModel):
186234
"""Input for admin search of deployment histories."""
187235

src/ai/backend/common/dto/manager/v2/scheduling_history/response.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@
1616

1717
__all__ = (
1818
"AdminSearchDeploymentHistoriesPayload",
19+
"AdminSearchKernelHistoriesPayload",
1920
"AdminSearchRouteHistoriesPayload",
2021
"AdminSearchSessionHistoriesPayload",
2122
"DeploymentHistoryNode",
23+
"KernelHistoryNode",
2224
"ListDeploymentHistoryPayload",
2325
"ListRouteHistoryPayload",
2426
"ListSessionHistoryPayload",
@@ -46,6 +48,26 @@ class SessionHistoryNode(BaseResponseModel):
4648
updated_at: datetime = Field(description="Timestamp when the history record was last updated")
4749

4850

51+
class KernelHistoryNode(BaseResponseModel):
52+
"""Node model representing a kernel scheduling history record.
53+
54+
Has no ``sub_steps``: the ``kernel_scheduling_history`` table carries no such column.
55+
"""
56+
57+
id: UUID = Field(description="History record ID")
58+
kernel_id: UUID = Field(description="Kernel ID this history belongs to")
59+
session_id: UUID = Field(description="Session owning the kernel")
60+
phase: str = Field(description="Scheduling phase")
61+
from_status: str | None = Field(default=None, description="Status before transition")
62+
to_status: str | None = Field(default=None, description="Status after transition")
63+
result: str = Field(description="Result of the scheduling attempt")
64+
error_code: str | None = Field(default=None, description="Error code if scheduling failed")
65+
message: str | None = Field(default=None, description="Human-readable message or error detail")
66+
attempts: int = Field(description="Number of scheduling attempts made")
67+
created_at: datetime = Field(description="Timestamp when the history record was created")
68+
updated_at: datetime = Field(description="Timestamp when the history record was last updated")
69+
70+
4971
class DeploymentHistoryNode(BaseResponseModel):
5072
"""Node model representing a deployment scheduling history record.
5173
@@ -127,6 +149,15 @@ class AdminSearchSessionHistoriesPayload(BaseResponseModel):
127149
has_previous_page: bool = Field(description="Whether there is a previous page.")
128150

129151

152+
class AdminSearchKernelHistoriesPayload(BaseResponseModel):
153+
"""Payload for admin and scoped search of kernel scheduling histories."""
154+
155+
items: list[KernelHistoryNode] = Field(description="List of kernel history nodes.")
156+
total_count: int = Field(description="Total number of records matching the filter.")
157+
has_next_page: bool = Field(description="Whether there is a next page.")
158+
has_previous_page: bool = Field(description="Whether there is a previous page.")
159+
160+
130161
class AdminSearchDeploymentHistoriesPayload(BaseResponseModel):
131162
"""Payload for admin search of deployment histories."""
132163

src/ai/backend/manager/api/gql/rbac/types/permission.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ async def scope(
239239
| RBACElementType.KEYPAIR_RESOURCE_POLICY
240240
| RBACElementType.PROJECT_RESOURCE_POLICY
241241
| RBACElementType.AUDIT_LOG
242+
| RBACElementType.KERNEL_HISTORY
242243
| RBACElementType.EVENT_LOG
243244
| RBACElementType.NOTIFICATION_RULE
244245
| RBACElementType.AGENT

src/ai/backend/manager/repositories/scheduling_history/db_source/db_source.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ async def search_kernel_scoped_history(
127127
querier: BatchQuerier,
128128
scope: KernelSchedulingHistorySearchScope,
129129
) -> KernelSchedulingHistoryListResult:
130-
"""Search kernel scheduling history within scope."""
130+
"""Search kernel history whose rows match any of ``scopes`` (OR), narrowed by ``querier``."""
131131
async with self._db.begin_readonly_session() as db_sess:
132132
query = sa.select(KernelSchedulingHistoryRow)
133133

src/ai/backend/manager/repositories/scheduling_history/repository.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ async def search_kernel_scoped_history(
100100
querier: BatchQuerier,
101101
scope: KernelSchedulingHistorySearchScope,
102102
) -> KernelSchedulingHistoryListResult:
103-
"""Search kernel scheduling history within scope."""
103+
"""Search kernel history whose rows match any of ``scopes`` (OR)."""
104104
return await self._db_source.search_kernel_scoped_history(querier, scope)
105105

106106
# ========== Deployment History (Admin) ==========

src/ai/backend/manager/repositories/scheduling_history/types.py

Lines changed: 15 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,9 @@
66
from typing import Any, override
77
from uuid import UUID
88

9-
import sqlalchemy as sa
10-
119
from ai.backend.common.data.filter_specs import UUIDEqualMatchSpec
1210
from ai.backend.common.identifier.replica import ReplicaID
13-
from ai.backend.common.types import KernelId, SessionId
11+
from ai.backend.common.types import KernelId
1412
from ai.backend.manager.errors.deployment import EndpointNotFound
1513
from ai.backend.manager.errors.kernel import (
1614
KernelNotFound,
@@ -78,60 +76,30 @@ def existence_checks(self) -> list[ExistenceCheck[Any]]:
7876
class KernelSchedulingHistorySearchScope(SearchScope):
7977
"""Scope for kernel scheduling history search.
8078
81-
Either axis may be given; when both are, they intersect. The request DTO
82-
rejects an empty scope before one is built.
79+
Used for entity-scoped queries where kernel_id is the scope parameter.
8380
"""
8481

85-
session_id: SessionId | None = None
86-
"""Restrict to the kernels of this session."""
87-
88-
kernel_id: KernelId | None = None
89-
"""Restrict to this kernel."""
82+
kernel_id: KernelId
83+
"""Required. The kernel to search history for."""
9084

9185
@override
9286
def to_condition(self) -> QueryCondition:
9387
"""Convert scope to a query condition for KernelSchedulingHistoryRow."""
94-
conditions: list[QueryCondition] = []
95-
if self.session_id is not None:
96-
conditions.append(
97-
KernelSchedulingHistoryConditions.by_session_id_filter(
98-
UUIDEqualMatchSpec(value=self.session_id, negated=False)
99-
)
100-
)
101-
if self.kernel_id is not None:
102-
conditions.append(
103-
KernelSchedulingHistoryConditions.by_kernel_id_filter(
104-
UUIDEqualMatchSpec(value=self.kernel_id, negated=False)
105-
)
106-
)
107-
108-
def inner() -> sa.sql.expression.ColumnElement[bool]:
109-
return sa.and_(*(cond() for cond in conditions))
110-
111-
return inner
88+
return KernelSchedulingHistoryConditions.by_kernel_id_filter(
89+
UUIDEqualMatchSpec(value=self.kernel_id, negated=False)
90+
)
11291

11392
@property
11493
@override
11594
def existence_checks(self) -> list[ExistenceCheck[Any]]:
116-
"""Check that each scoped entity exists."""
117-
checks: list[ExistenceCheck[Any]] = []
118-
if self.session_id is not None:
119-
checks.append(
120-
ExistenceCheck(
121-
column=SessionRow.id,
122-
value=self.session_id,
123-
error=SessionNotFound(str(self.session_id)),
124-
)
125-
)
126-
if self.kernel_id is not None:
127-
checks.append(
128-
ExistenceCheck(
129-
column=KernelRow.id,
130-
value=self.kernel_id,
131-
error=KernelNotFound(str(self.kernel_id)),
132-
)
133-
)
134-
return checks
95+
"""Check that the kernel exists."""
96+
return [
97+
ExistenceCheck(
98+
column=KernelRow.id,
99+
value=self.kernel_id,
100+
error=KernelNotFound(str(self.kernel_id)),
101+
),
102+
]
135103

136104

137105
# Deployment History Scope

0 commit comments

Comments
 (0)