1616 AppConfigFragmentData ,
1717 AppConfigFragmentSearchResult ,
1818)
19- from ai .backend .manager .errors .app_config import AppConfigFragmentNotFound
19+ from ai .backend .manager .errors .app_config import (
20+ AppConfigFragmentNotFound ,
21+ AppConfigFragmentWriteNotAllowed ,
22+ )
23+ from ai .backend .manager .models .app_config_allow_list .row import AppConfigAllowListRow
2024from ai .backend .manager .models .app_config_definition .row import AppConfigDefinitionRow
2125from ai .backend .manager .models .app_config_fragment .row import AppConfigFragmentRow
2226from ai .backend .manager .models .scopes import SearchScope
2327from ai .backend .manager .repositories .app_config_fragment .creators import (
2428 AppConfigFragmentCreatorSpec ,
2529)
26- from ai .backend .manager .repositories .base import BatchQuerier , Purger , Querier , Updater
30+ from ai .backend .manager .repositories .base import (
31+ BatchQuerier ,
32+ ExistsQuerier ,
33+ Purger ,
34+ Querier ,
35+ Updater ,
36+ )
2737from ai .backend .manager .repositories .base .creator import NextValuePolicy
2838from ai .backend .manager .repositories .ops import DBOpsProvider
2939
@@ -61,7 +71,11 @@ def __init__(self, ops_provider: DBOpsProvider) -> None:
6171 self ._ops = ops_provider
6272
6373 @app_config_fragment_db_source_resilience .apply ()
64- async def create (self , spec : AppConfigFragmentCreatorSpec ) -> AppConfigFragmentData :
74+ async def create (
75+ self ,
76+ spec : AppConfigFragmentCreatorSpec ,
77+ only_if : ExistsQuerier [AppConfigAllowListRow ],
78+ ) -> AppConfigFragmentData :
6579 policy = NextValuePolicy (
6680 column = AppConfigFragmentRow .rank ,
6781 scope_condition = lambda : AppConfigFragmentRow .config_name == spec .config_name ,
@@ -70,7 +84,14 @@ async def create(self, spec: AppConfigFragmentCreatorSpec) -> AppConfigFragmentD
7084 ),
7185 gap = RANK_GAP ,
7286 )
87+ # ``only_if`` (built by the caller) and the write run in one transaction, so the gate
88+ # check and the write commit atomically — no check-then-write race.
7389 async with self ._ops .write_ops () as w :
90+ if not await w .exists (only_if ):
91+ raise AppConfigFragmentWriteNotAllowed (
92+ f"Writing app config { spec .config_name !r} at scope "
93+ f"{ spec .scope_type .value !r} is not allowed."
94+ )
7495 created = await w .create_with_next_value (policy , spec )
7596 return created .row .to_data ()
7697
@@ -83,16 +104,36 @@ async def get_by_id(self, fragment_id: AppConfigFragmentID) -> AppConfigFragment
83104 return result .row .to_data ()
84105
85106 @app_config_fragment_db_source_resilience .apply ()
86- async def update (self , updater : Updater [AppConfigFragmentRow ]) -> AppConfigFragmentData :
107+ async def update (
108+ self ,
109+ updater : Updater [AppConfigFragmentRow ],
110+ only_if : ExistsQuerier [AppConfigAllowListRow ],
111+ ) -> AppConfigFragmentData :
112+ # Gate first, then write — both in one transaction so the check and the write commit
113+ # atomically. A missing fragment surfaces as the update returning None below.
87114 async with self ._ops .write_ops () as w :
115+ if not await w .exists (only_if ):
116+ raise AppConfigFragmentWriteNotAllowed (
117+ f"Writing app config fragment { updater .pk_value } is not allowed."
118+ )
88119 result = await w .update (updater )
89120 if result is None :
90121 raise AppConfigFragmentNotFound (f"App config fragment { updater .pk_value } not found" )
91122 return result .row .to_data ()
92123
93124 @app_config_fragment_db_source_resilience .apply ()
94- async def purge (self , purger : Purger [AppConfigFragmentRow ]) -> AppConfigFragmentData :
125+ async def purge (
126+ self ,
127+ purger : Purger [AppConfigFragmentRow ],
128+ only_if : ExistsQuerier [AppConfigAllowListRow ],
129+ ) -> AppConfigFragmentData :
130+ # Gate first, then write — both in one transaction so the check and the write commit
131+ # atomically. A missing fragment surfaces as the purge returning None below.
95132 async with self ._ops .write_ops () as w :
133+ if not await w .exists (only_if ):
134+ raise AppConfigFragmentWriteNotAllowed (
135+ f"Writing app config fragment { purger .pk_value } is not allowed."
136+ )
96137 result = await w .purge (purger )
97138 if result is None :
98139 raise AppConfigFragmentNotFound (f"App config fragment { purger .pk_value } not found" )
0 commit comments