Skip to content

Commit 6911a38

Browse files
jopemachineclaude
andcommitted
refactor(BA-5827): apply repository-layer Resilience and drop BEP refs
- Wrap AppConfigFragment repository / admin_repository methods with their own Resilience instances (mirrors the policy side from BA-5814). - Drop BEP-1052 §X references from doctrings, comments, the migration module, and the regression test — keep BEP wording only in the news fragment. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ff7426a commit 6911a38

12 files changed

Lines changed: 85 additions & 30 deletions

File tree

src/ai/backend/common/metrics/metric.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,8 @@ class DomainType(enum.StrEnum):
414414
class LayerType(enum.StrEnum):
415415
# Repository layers with _REPOSITORY suffix
416416
AGENT_REPOSITORY = "agent_repository"
417+
APP_CONFIG_FRAGMENT_ADMIN_REPOSITORY = "app_config_fragment_admin_repository"
418+
APP_CONFIG_FRAGMENT_REPOSITORY = "app_config_fragment_repository"
417419
APP_CONFIG_POLICY_ADMIN_REPOSITORY = "app_config_policy_admin_repository"
418420
APP_CONFIG_POLICY_REPOSITORY = "app_config_policy_repository"
419421
AUTH_REPOSITORY = "auth_repository"

src/ai/backend/manager/data/app_config/types.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
@dataclass(frozen=True)
1212
class AppConfigData:
13-
"""Service-layer return type for the merged AppConfig view (BEP-1052 §5).
13+
"""Service-layer return type for the merged AppConfig view.
1414
1515
`fragments` are ordered low → high merge priority (matching the
1616
policy's `scope_sources`). `config` is the deep-merged result,

src/ai/backend/manager/errors/app_config.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,9 @@ def error_code(self) -> ErrorCode:
6161
class AppConfigFragmentPolicyMissing(BackendAIError, web.HTTPConflict):
6262
"""Raised when a fragment references a `name` without a matching policy row.
6363
64-
Defense-in-depth against the required-policy invariant from
65-
BEP-1052 §1 — normally the service layer rejects earlier, but the
66-
FK violation surfaces here if the service check is bypassed.
64+
Defense-in-depth against the required-policy invariant — normally
65+
the service layer rejects earlier, but the FK violation surfaces
66+
here if the service check is bypassed.
6767
"""
6868

6969
error_type = "https://api.backend.ai/probs/app-config-fragment-policy-missing"

src/ai/backend/manager/models/alembic/versions/a662131d5603_add_app_config_fragments.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
"""add app_config_fragments table
22
3-
Lands the AppConfigFragment slice of the BEP-1052 (Scoped App Config
4-
Redesign) data-layer foundation: per-scope raw rows keyed by
3+
Adds the per-scope raw fragment table keyed by
54
`(scope_type, scope_id, name)`. `name` is a FK to
6-
`app_config_policies.config_name` with default NO ACTION (the
7-
required-policy invariant — see BEP-1052 §1).
5+
`app_config_policies.config_name` with default NO ACTION enforcing the
6+
required-policy invariant.
87
98
Stacks on top of `5df264862995_add_app_config_policies.py`; the
109
policy table must exist before the FK can be created.

src/ai/backend/manager/models/app_config_fragment/row.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ class AppConfigFragmentRow(Base): # type: ignore[misc]
4545
"name",
4646
sa.String(length=128),
4747
# FK to `app_config_policies.config_name` (default NO ACTION) —
48-
# enforces the required-policy invariant from BEP-1052 §1.
48+
# enforces the required-policy invariant.
4949
sa.ForeignKey(
5050
"app_config_policies.config_name",
5151
name="fk_app_config_fragments_name_app_config_policies_config_name",

src/ai/backend/manager/repositories/app_config_fragment/admin_repository.py

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
from collections.abc import Mapping
44
from typing import Any
55

6+
from ai.backend.common.exception import BackendAIError
7+
from ai.backend.common.metrics.metric import DomainType, LayerType
8+
from ai.backend.common.resilience.policies.metrics import MetricArgs, MetricPolicy
9+
from ai.backend.common.resilience.policies.retry import BackoffStrategy, RetryArgs, RetryPolicy
10+
from ai.backend.common.resilience.resilience import Resilience
611
from ai.backend.manager.data.app_config.types import AppConfigSearchResult
712
from ai.backend.manager.data.app_config_fragment.types import (
813
AppConfigFragmentData,
@@ -23,6 +28,25 @@
2328
from ai.backend.manager.repositories.base.creator import Creator
2429
from ai.backend.manager.repositories.base.querier import BatchQuerier
2530

31+
app_config_fragment_admin_repository_resilience = Resilience(
32+
policies=[
33+
MetricPolicy(
34+
MetricArgs(
35+
domain=DomainType.REPOSITORY,
36+
layer=LayerType.APP_CONFIG_FRAGMENT_ADMIN_REPOSITORY,
37+
)
38+
),
39+
RetryPolicy(
40+
RetryArgs(
41+
max_retries=5,
42+
retry_delay=0.1,
43+
backoff_strategy=BackoffStrategy.FIXED,
44+
non_retryable_exceptions=(BackendAIError,),
45+
)
46+
),
47+
]
48+
)
49+
2650

2751
class AppConfigFragmentAdminRepository:
2852
"""Admin-only operations on AppConfigFragment.
@@ -33,12 +57,10 @@ class AppConfigFragmentAdminRepository:
3357
`AppConfigFragmentRepository`. Authorization is enforced at the
3458
service layer before reaching either repository.
3559
36-
The required-policy invariant from BEP-1052 §1 is enforced by the
37-
service layer; the FK on `app_config_fragments.name` is the
38-
defense-in-depth backstop, translated by the creator spec into
60+
The required-policy invariant is enforced by the service layer;
61+
the FK on `app_config_fragments.name` is the defense-in-depth
62+
backstop, translated by the creator spec into
3963
:class:`AppConfigFragmentPolicyMissing`.
40-
41-
Retry + metric policies are applied at the DB-source layer.
4264
"""
4365

4466
_db_source: AppConfigFragmentDBSource
@@ -48,6 +70,7 @@ def __init__(self, db: ExtendedAsyncSAEngine) -> None:
4870

4971
# ── Mutations ─────────────────────────────────────────────────
5072

73+
@app_config_fragment_admin_repository_resilience.apply()
5174
async def create(
5275
self,
5376
key: AppConfigFragmentKey,
@@ -63,6 +86,7 @@ async def create(
6386
)
6487
return await self._db_source.create(creator)
6588

89+
@app_config_fragment_admin_repository_resilience.apply()
6690
async def update(
6791
self,
6892
key: AppConfigFragmentKey,
@@ -73,17 +97,20 @@ async def update(
7397
spec = AppConfigFragmentUpdaterSpec(extra_config=extra_config)
7498
return await self._db_source.update(key, spec)
7599

100+
@app_config_fragment_admin_repository_resilience.apply()
76101
async def purge(self, key: AppConfigFragmentKey) -> bool:
77102
return await self._db_source.purge(key)
78103

79104
# ── Cross-scope reads ────────────────────────────────────────
80105

106+
@app_config_fragment_admin_repository_resilience.apply()
81107
async def admin_search(
82108
self,
83109
querier: BatchQuerier,
84110
) -> AppConfigFragmentSearchResult:
85111
return await self._db_source.admin_search(querier)
86112

113+
@app_config_fragment_admin_repository_resilience.apply()
87114
async def admin_search_app_configs(
88115
self,
89116
querier: BatchQuerier,

src/ai/backend/manager/repositories/app_config_fragment/creators.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ class AppConfigFragmentCreatorSpec(CreatorSpec[AppConfigFragmentRow]):
2626
Maps DB constraint violations onto typed domain errors:
2727
- ``(scope_type, scope_id, name)`` UNIQUE → :class:`AppConfigFragmentConflict`
2828
- ``name`` FK to `app_config_policies.config_name` →
29-
:class:`AppConfigFragmentPolicyMissing` (BEP-1052 §1 required-policy
30-
invariant enforced as defense-in-depth).
29+
:class:`AppConfigFragmentPolicyMissing` (required-policy invariant
30+
enforced as defense-in-depth).
3131
"""
3232

3333
scope_type: str

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

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,9 @@ class AppConfigFragmentDBSource:
7575
"""Database operations for `app_config_fragments`.
7676
7777
Two roles:
78-
1. Raw CRUD on `(scope_type, scope_id, name)` rows (BEP-1052 §2).
78+
1. Raw CRUD on `(scope_type, scope_id, name)` rows.
7979
2. Merge-side reads that resolve a user's `AppConfig` view by joining
80-
`app_config_policies` to derive the chain (BEP-1052 §5).
80+
`app_config_policies` to derive the chain.
8181
"""
8282

8383
_db: ExtendedAsyncSAEngine
@@ -220,16 +220,15 @@ async def admin_search(
220220
has_previous_page=result.has_previous_page,
221221
)
222222

223-
# ── Merged-view reads (AppConfig, BEP-1052 §5) ────────────────
223+
# ── Merged-view reads (AppConfig) ─────────────────────────────
224224

225225
@staticmethod
226226
def _merge_chain(
227227
rows: Sequence[AppConfigFragmentRow],
228228
chain: Sequence[str],
229229
) -> _MergedChain:
230230
"""Order `rows` by `chain` (low → high) and deep-merge their
231-
`extra_config` in that order. Empty result projects to `None`
232-
per BEP-1052 §3 null projection.
231+
`extra_config` in that order. Empty result projects to `None`.
233232
234233
Shared by the single-doc and search merge methods so both paths
235234
produce the same shape.

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

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
import uuid
44

5+
from ai.backend.common.exception import BackendAIError
6+
from ai.backend.common.metrics.metric import DomainType, LayerType
7+
from ai.backend.common.resilience.policies.metrics import MetricArgs, MetricPolicy
8+
from ai.backend.common.resilience.policies.retry import BackoffStrategy, RetryArgs, RetryPolicy
9+
from ai.backend.common.resilience.resilience import Resilience
510
from ai.backend.manager.data.app_config.types import AppConfigData, AppConfigSearchResult
611
from ai.backend.manager.data.app_config_fragment.types import (
712
AppConfigFragmentData,
@@ -18,14 +23,32 @@
1823
)
1924
from ai.backend.manager.repositories.base.querier import BatchQuerier
2025

26+
app_config_fragment_repository_resilience = Resilience(
27+
policies=[
28+
MetricPolicy(
29+
MetricArgs(
30+
domain=DomainType.REPOSITORY,
31+
layer=LayerType.APP_CONFIG_FRAGMENT_REPOSITORY,
32+
)
33+
),
34+
RetryPolicy(
35+
RetryArgs(
36+
max_retries=5,
37+
retry_delay=0.1,
38+
backoff_strategy=BackoffStrategy.FIXED,
39+
non_retryable_exceptions=(BackendAIError,),
40+
)
41+
),
42+
]
43+
)
44+
2145

2246
class AppConfigFragmentRepository:
2347
"""Read-side repository for AppConfigFragment.
2448
2549
Scope-bound reads on raw fragments plus the per-user merged
26-
`AppConfig` view (BEP-1052 §5). Mutations and admin cross-scope
27-
reads live on `AppConfigFragmentAdminRepository`. Retry + metric
28-
policies are applied at the DB-source layer.
50+
`AppConfig` view. Mutations and admin cross-scope reads live on
51+
`AppConfigFragmentAdminRepository`.
2952
"""
3053

3154
_db_source: AppConfigFragmentDBSource
@@ -35,12 +58,15 @@ def __init__(self, db: ExtendedAsyncSAEngine) -> None:
3558

3659
# ── Raw fragment reads ────────────────────────────────────────
3760

61+
@app_config_fragment_repository_resilience.apply()
3862
async def get(self, key: AppConfigFragmentKey) -> AppConfigFragmentData | None:
3963
return await self._db_source.get(key)
4064

65+
@app_config_fragment_repository_resilience.apply()
4166
async def get_by_id(self, id: uuid.UUID) -> AppConfigFragmentData | None:
4267
return await self._db_source.get_by_id(id)
4368

69+
@app_config_fragment_repository_resilience.apply()
4470
async def search(
4571
self,
4672
scope: AppConfigFragmentSearchScope,
@@ -50,13 +76,15 @@ async def search(
5076

5177
# ── Merged view (AppConfig) ───────────────────────────────────
5278

79+
@app_config_fragment_repository_resilience.apply()
5380
async def app_config(
5481
self,
5582
user_id: uuid.UUID,
5683
config_name: str,
5784
) -> AppConfigData:
5885
return await self._db_source.get_user_app_config(user_id, config_name)
5986

87+
@app_config_fragment_repository_resilience.apply()
6088
async def search_app_configs(
6189
self,
6290
scope: UserAppConfigSearchScope,

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def existence_checks(self) -> Sequence[ExistenceCheck[str]]:
4040

4141
@dataclass(frozen=True)
4242
class UserAppConfigSearchScope(SearchScope):
43-
"""Pin merged-view search to a target `user_id` (BEP-1052 §5)."""
43+
"""Pin merged-view search to a target `user_id`."""
4444

4545
user_id: uuid.UUID
4646

0 commit comments

Comments
 (0)