1313from ai .backend .common .resilience .policies .retry import BackoffStrategy , RetryArgs , RetryPolicy
1414from ai .backend .common .resilience .resilience import Resilience
1515from ai .backend .manager .data .app_config_fragment .types import (
16+ AppConfigFragmentBulkItemError ,
17+ AppConfigFragmentBulkWriteResult ,
1618 AppConfigFragmentData ,
1719 AppConfigFragmentSearchResult ,
1820)
2123 AppConfigFragmentWriteNotAllowed ,
2224)
2325from ai .backend .manager .models .app_config_allow_list .row import AppConfigAllowListRow
24- from ai .backend .manager .models .app_config_definition .row import AppConfigDefinitionRow
2526from ai .backend .manager .models .app_config_fragment .row import AppConfigFragmentRow
2627from ai .backend .manager .models .scopes import SearchScope
2728from ai .backend .manager .repositories .app_config_fragment .creators import (
2829 AppConfigFragmentCreatorSpec ,
2930)
3031from ai .backend .manager .repositories .base import (
3132 BatchQuerier ,
33+ BulkConditionalCreator ,
34+ BulkConditionalPurger ,
35+ BulkConditionalUpdater ,
36+ Creator ,
3237 ExistsQuerier ,
3338 Purger ,
3439 Querier ,
3540 Updater ,
3641)
37- from ai .backend .manager .repositories .base .creator import NextValuePolicy
38- from ai .backend .manager .repositories .ops import DBOpsProvider
42+ from ai .backend .manager .repositories .ops import DBOpsProvider , WriteOps
3943
4044__all__ = ("AppConfigFragmentDBSource" ,)
4145
42- # Gap between successive ranks, leaving room to re-order fragments without renumbering.
43- RANK_GAP = 100
44-
4546app_config_fragment_db_source_resilience = Resilience (
4647 policies = [
4748 MetricPolicy (
@@ -70,29 +71,62 @@ class AppConfigFragmentDBSource:
7071 def __init__ (self , ops_provider : DBOpsProvider ) -> None :
7172 self ._ops = ops_provider
7273
74+ async def _update_in_tx (
75+ self ,
76+ w : WriteOps ,
77+ updater : Updater [AppConfigFragmentRow ],
78+ only_if : ExistsQuerier [AppConfigAllowListRow ],
79+ ) -> AppConfigFragmentData :
80+ """Gate + update one fragment inside the caller's write transaction.
81+
82+ A missing fragment surfaces as the update returning None below.
83+ """
84+ if not await w .exists (only_if ):
85+ raise AppConfigFragmentWriteNotAllowed (
86+ f"Writing app config fragment { updater .pk_value } is not allowed."
87+ )
88+ result = await w .update (updater )
89+ if result is None :
90+ raise AppConfigFragmentNotFound (f"App config fragment { updater .pk_value } not found" )
91+ return result .row .to_data ()
92+
93+ async def _purge_in_tx (
94+ self ,
95+ w : WriteOps ,
96+ purger : Purger [AppConfigFragmentRow ],
97+ only_if : ExistsQuerier [AppConfigAllowListRow ],
98+ ) -> AppConfigFragmentData :
99+ """Gate + purge one fragment inside the caller's write transaction.
100+
101+ A missing fragment surfaces as the purge returning None below.
102+ """
103+ if not await w .exists (only_if ):
104+ raise AppConfigFragmentWriteNotAllowed (
105+ f"Writing app config fragment { purger .pk_value } is not allowed."
106+ )
107+ result = await w .purge (purger )
108+ if result is None :
109+ raise AppConfigFragmentNotFound (f"App config fragment { purger .pk_value } not found" )
110+ return result .row .to_data ()
111+
73112 @app_config_fragment_db_source_resilience .apply ()
74113 async def create (
75114 self ,
76115 spec : AppConfigFragmentCreatorSpec ,
77116 only_if : ExistsQuerier [AppConfigAllowListRow ],
78117 ) -> AppConfigFragmentData :
79- policy = NextValuePolicy (
80- column = AppConfigFragmentRow .rank ,
81- scope_condition = lambda : AppConfigFragmentRow .config_name == spec .config_name ,
82- lock_selector = sa .select (AppConfigDefinitionRow ).where (
83- AppConfigDefinitionRow .config_name == spec .config_name
84- ),
85- gap = RANK_GAP ,
86- )
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.
118+ """Gate + create one fragment in a single write transaction.
119+
120+ The gate check and the write run in one transaction, so they commit atomically —
121+ no check-then-write race. ``rank`` is derived from the fragment's ``scope_type``.
122+ """
89123 async with self ._ops .write_ops () as w :
90124 if not await w .exists (only_if ):
91125 raise AppConfigFragmentWriteNotAllowed (
92126 f"Writing app config { spec .config_name !r} at scope "
93127 f"{ spec .scope_type .value !r} is not allowed."
94128 )
95- created = await w .create_with_next_value ( policy , spec )
129+ created = await w .create ( Creator ( spec = spec ) )
96130 return created .row .to_data ()
97131
98132 @app_config_fragment_db_source_resilience .apply ()
@@ -109,35 +143,78 @@ async def update(
109143 updater : Updater [AppConfigFragmentRow ],
110144 only_if : ExistsQuerier [AppConfigAllowListRow ],
111145 ) -> 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.
114146 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- )
119- result = await w .update (updater )
120- if result is None :
121- raise AppConfigFragmentNotFound (f"App config fragment { updater .pk_value } not found" )
122- return result .row .to_data ()
147+ return await self ._update_in_tx (w , updater , only_if )
123148
124149 @app_config_fragment_db_source_resilience .apply ()
125150 async def purge (
126151 self ,
127152 purger : Purger [AppConfigFragmentRow ],
128153 only_if : ExistsQuerier [AppConfigAllowListRow ],
129154 ) -> 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.
132155 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- )
137- result = await w .purge (purger )
138- if result is None :
139- raise AppConfigFragmentNotFound (f"App config fragment { purger .pk_value } not found" )
140- return result .row .to_data ()
156+ return await self ._purge_in_tx (w , purger , only_if )
157+
158+ @app_config_fragment_db_source_resilience .apply ()
159+ async def bulk_create (
160+ self ,
161+ bulk : BulkConditionalCreator [AppConfigFragmentRow , AppConfigAllowListRow ],
162+ ) -> AppConfigFragmentBulkWriteResult :
163+ """Create many fragments with partial success.
164+
165+ Each item is gated and inserted independently in its own savepoint: a rejected gate or a
166+ failed insert is reported in ``failed`` (with its batch index) while the rest are
167+ created. The whole batch shares one transaction, so the successful inserts commit together.
168+ """
169+ async with self ._ops .write_ops () as w :
170+ result = await w .bulk_conditional_create_partial (bulk )
171+ return AppConfigFragmentBulkWriteResult (
172+ succeeded = [row .to_data () for row in result .successes ],
173+ failed = [
174+ AppConfigFragmentBulkItemError (index = e .index , message = str (e .exception ))
175+ for e in result .errors
176+ ],
177+ )
178+
179+ @app_config_fragment_db_source_resilience .apply ()
180+ async def bulk_update (
181+ self ,
182+ bulk : BulkConditionalUpdater [AppConfigFragmentRow , AppConfigAllowListRow ],
183+ ) -> AppConfigFragmentBulkWriteResult :
184+ """Update many fragments with partial success.
185+
186+ Each item is gated and updated independently in its own savepoint: a rejected gate, a
187+ missing target, or a failed update is reported in ``failed`` while the rest are updated.
188+ """
189+ async with self ._ops .write_ops () as w :
190+ result = await w .bulk_conditional_update_partial (bulk )
191+ return AppConfigFragmentBulkWriteResult (
192+ succeeded = [row .to_data () for row in result .successes ],
193+ failed = [
194+ AppConfigFragmentBulkItemError (index = e .index , message = str (e .exception ))
195+ for e in result .errors
196+ ],
197+ )
198+
199+ @app_config_fragment_db_source_resilience .apply ()
200+ async def bulk_purge (
201+ self ,
202+ bulk : BulkConditionalPurger [AppConfigFragmentRow , AppConfigAllowListRow ],
203+ ) -> AppConfigFragmentBulkWriteResult :
204+ """Purge many fragments with partial success.
205+
206+ Each item is gated and deleted independently in its own savepoint: a rejected gate, a
207+ missing target, or a failed delete is reported in ``failed`` while the rest are purged.
208+ """
209+ async with self ._ops .write_ops () as w :
210+ result = await w .bulk_conditional_purge_partial (bulk )
211+ return AppConfigFragmentBulkWriteResult (
212+ succeeded = [row .to_data () for row in result .successes ],
213+ failed = [
214+ AppConfigFragmentBulkItemError (index = e .index , message = str (e .exception ))
215+ for e in result .errors
216+ ],
217+ )
141218
142219 @app_config_fragment_db_source_resilience .apply ()
143220 async def admin_search (self , querier : BatchQuerier ) -> AppConfigFragmentSearchResult :
0 commit comments