1818 CatalogResourceKind ,
1919)
2020from langflow .services .policy_bundle import (
21+ PolicyBundleRevisionConflictError ,
2122 apply_policy_bundle_state ,
2223 get_policy_bundle_state ,
2324 replace_policy_bundle_state ,
3233 from langflow .services .database .service import DatabaseService
3334
3435
36+ _CATALOG_POLICY_CAS_ATTEMPTS = 3
37+
38+
3539def _normalize_keys (keys : Collection [str ]) -> frozenset [str ]:
3640 """Trim and deduplicate keys while preserving their case."""
3741 normalized : set [str ] = set ()
@@ -57,6 +61,7 @@ def __init__(
5761 self ._policy_bundle_service = policy_bundle_service
5862 self ._legacy_snapshot = CatalogPolicySnapshot ()
5963 self ._legacy_hydrated = False
64+ self ._projected_snapshot : tuple [PolicyBundleSnapshot , CatalogPolicySnapshot ] | None = None
6065 self ._write_lock = asyncio .Lock ()
6166 # Direct service-class registration does not call set_ready(), unlike
6267 # factory creation. The fail-open snapshot is usable immediately.
@@ -73,10 +78,17 @@ def snapshot(self) -> CatalogPolicySnapshot:
7378 if self ._policy_bundle_service is None :
7479 return self ._legacy_snapshot
7580 bundle = self ._policy_bundle_service .snapshot
76- return CatalogPolicySnapshot (
77- blocked_component_keys = bundle .blocked_component_keys ,
78- blocked_template_keys = bundle .blocked_template_keys ,
79- )
81+ projected = self ._projected_snapshot
82+ if projected is None or projected [0 ] is not bundle :
83+ projected = (
84+ bundle ,
85+ CatalogPolicySnapshot (
86+ blocked_component_keys = bundle .blocked_component_keys ,
87+ blocked_template_keys = bundle .blocked_template_keys ,
88+ ),
89+ )
90+ self ._projected_snapshot = projected
91+ return projected [1 ]
8092
8193 @property
8294 def policy_bundle_snapshot (self ) -> PolicyBundleSnapshot :
@@ -165,25 +177,32 @@ async def _replace_blocked_keys(
165177 actor_user_id = actor_user_id ,
166178 )
167179
168- async with session_scope () as session :
169- current = await get_policy_bundle_state (session )
170- if resource_kind == CatalogResourceKind .COMPONENT :
171- current_keys = current .blocked_component_keys
172- components = desired
173- templates = current .blocked_template_keys
174- else :
175- current_keys = current .blocked_template_keys
176- components = current .blocked_component_keys
177- templates = desired
178- committed = await replace_policy_bundle_state (
179- session ,
180- expected_revision = current .revision ,
181- approved_provider_ids = current .approved_provider_ids ,
182- blocked_component_keys = components ,
183- blocked_template_keys = templates ,
184- actor_user_id = actor_user_id ,
185- reason = f"Replace blocked { resource_kind .value } catalog keys" ,
186- )
180+ for attempt in range (_CATALOG_POLICY_CAS_ATTEMPTS ):
181+ async with session_scope () as session :
182+ current = await get_policy_bundle_state (session )
183+ if resource_kind == CatalogResourceKind .COMPONENT :
184+ current_keys = current .blocked_component_keys
185+ components = desired
186+ templates = current .blocked_template_keys
187+ else :
188+ current_keys = current .blocked_template_keys
189+ components = current .blocked_component_keys
190+ templates = desired
191+ try :
192+ committed = await replace_policy_bundle_state (
193+ session ,
194+ expected_revision = current .revision ,
195+ approved_provider_ids = current .approved_provider_ids ,
196+ blocked_component_keys = components ,
197+ blocked_template_keys = templates ,
198+ actor_user_id = actor_user_id ,
199+ reason = f"Replace blocked { resource_kind .value } catalog keys" ,
200+ )
201+ except PolicyBundleRevisionConflictError :
202+ if attempt == _CATALOG_POLICY_CAS_ATTEMPTS - 1 :
203+ raise
204+ continue
205+ break
187206
188207 apply_policy_bundle_state (committed )
189208 return CatalogPolicyUpdate (
0 commit comments