@@ -129,6 +129,10 @@ pub async fn sandbox_provider_list(
129129 Ok ( ( ) )
130130}
131131
132+ /// Attempts for an attach or detach whose resource version went stale between
133+ /// the command's read and its write.
134+ const PROVIDER_ATTACHMENT_CAS_ATTEMPTS : u32 = 5 ;
135+
132136/// Save a provider attachment and optionally wait for its installed authority.
133137pub async fn sandbox_provider_attach (
134138 server : & str ,
@@ -141,45 +145,53 @@ pub async fn sandbox_provider_attach(
141145 readiness. validate ( ) ?;
142146 let mut client = grpc_client ( server, tls) . await ?;
143147
144- // Fetch current sandbox to get resource_version for CAS
145- let sandbox = client
146- . get_sandbox ( GetSandboxRequest {
147- name : ( name) . to_string ( ) ,
148- workspace_scope : Some ( openshell_core:: proto:: workspace_selector (
149- ( workspace) . to_string ( ) ,
150- ) ) ,
151- } )
152- . await
153- . map_err ( |status| miette ! ( "provider attachment lookup failed ({})" , status. code( ) ) ) ?
154- . into_inner ( )
155- . sandbox
156- . ok_or_else ( || miette:: miette!( "sandbox not found" ) ) ?;
148+ // The resource version only guards this command's own read, so a
149+ // conflict from a concurrent status write is retried with a fresh read.
150+ let mut attempt = 1 ;
151+ let ( sandbox, response) = loop {
152+ let sandbox = client
153+ . get_sandbox ( GetSandboxRequest {
154+ name : ( name) . to_string ( ) ,
155+ workspace_scope : Some ( openshell_core:: proto:: workspace_selector (
156+ ( workspace) . to_string ( ) ,
157+ ) ) ,
158+ } )
159+ . await
160+ . map_err ( |status| miette ! ( "provider attachment lookup failed ({})" , status. code( ) ) ) ?
161+ . into_inner ( )
162+ . sandbox
163+ . ok_or_else ( || miette:: miette!( "sandbox not found" ) ) ?;
157164
158- let resource_version = sandbox. metadata . as_ref ( ) . map_or ( 0 , |m| m. resource_version ) ;
165+ let resource_version = sandbox. metadata . as_ref ( ) . map_or ( 0 , |m| m. resource_version ) ;
159166
160- let response = match client
161- . attach_sandbox_provider ( AttachSandboxProviderRequest {
162- request_id : String :: new ( ) ,
163- sandbox : ( name) . to_string ( ) ,
164- workspace_scope : Some ( openshell_core:: proto:: workspace_selector (
165- ( workspace) . to_string ( ) ,
166- ) ) ,
167- provider : provider. to_string ( ) ,
168- expected_resource_version : resource_version,
169- } )
170- . await
171- {
172- Ok ( response) => response. into_inner ( ) ,
173- // Explicit post-save uncertainty takes precedence over a generic retry hint.
174- Err ( status)
175- if status. code ( ) == Code :: Aborted && !provider_mutation_is_uncertain ( & status) =>
167+ match client
168+ . attach_sandbox_provider ( AttachSandboxProviderRequest {
169+ request_id : String :: new ( ) ,
170+ sandbox : ( name) . to_string ( ) ,
171+ workspace_scope : Some ( openshell_core:: proto:: workspace_selector (
172+ ( workspace) . to_string ( ) ,
173+ ) ) ,
174+ provider : provider. to_string ( ) ,
175+ expected_resource_version : resource_version,
176+ } )
177+ . await
176178 {
177- return Err ( miette:: miette!(
178- "Failed to attach provider: sandbox was modified by another operation.\n \
179- Please retry the command."
180- ) ) ;
179+ Ok ( response) => break ( sandbox, response. into_inner ( ) ) ,
180+ // Explicit post-save uncertainty takes precedence over a generic retry hint.
181+ Err ( status)
182+ if status. code ( ) == Code :: Aborted && !provider_mutation_is_uncertain ( & status) =>
183+ {
184+ if attempt < PROVIDER_ATTACHMENT_CAS_ATTEMPTS {
185+ attempt += 1 ;
186+ continue ;
187+ }
188+ return Err ( miette:: miette!(
189+ "Failed to attach provider: sandbox was modified by another operation.\n \
190+ Please retry the command."
191+ ) ) ;
192+ }
193+ Err ( error) => return Err ( provider_mutation_error ( & error, "attachment" ) ) ,
181194 }
182- Err ( error) => return Err ( provider_mutation_error ( & error, "attachment" ) ) ,
183195 } ;
184196
185197 let receipt = response. receipt . ok_or_else ( || {
@@ -216,45 +228,53 @@ pub async fn sandbox_provider_detach(
216228 readiness. validate ( ) ?;
217229 let mut client = grpc_client ( server, tls) . await ?;
218230
219- // Fetch current sandbox to get resource_version for CAS
220- let sandbox = client
221- . get_sandbox ( GetSandboxRequest {
222- name : ( name) . to_string ( ) ,
223- workspace_scope : Some ( openshell_core:: proto:: workspace_selector (
224- ( workspace) . to_string ( ) ,
225- ) ) ,
226- } )
227- . await
228- . map_err ( |status| miette ! ( "provider detachment lookup failed ({})" , status. code( ) ) ) ?
229- . into_inner ( )
230- . sandbox
231- . ok_or_else ( || miette:: miette!( "sandbox not found" ) ) ?;
231+ // The resource version only guards this command's own read, so a
232+ // conflict from a concurrent status write is retried with a fresh read.
233+ let mut attempt = 1 ;
234+ let ( sandbox, response) = loop {
235+ let sandbox = client
236+ . get_sandbox ( GetSandboxRequest {
237+ name : ( name) . to_string ( ) ,
238+ workspace_scope : Some ( openshell_core:: proto:: workspace_selector (
239+ ( workspace) . to_string ( ) ,
240+ ) ) ,
241+ } )
242+ . await
243+ . map_err ( |status| miette ! ( "provider detachment lookup failed ({})" , status. code( ) ) ) ?
244+ . into_inner ( )
245+ . sandbox
246+ . ok_or_else ( || miette:: miette!( "sandbox not found" ) ) ?;
232247
233- let resource_version = sandbox. metadata . as_ref ( ) . map_or ( 0 , |m| m. resource_version ) ;
248+ let resource_version = sandbox. metadata . as_ref ( ) . map_or ( 0 , |m| m. resource_version ) ;
234249
235- let response = match client
236- . detach_sandbox_provider ( DetachSandboxProviderRequest {
237- request_id : String :: new ( ) ,
238- sandbox : ( name) . to_string ( ) ,
239- workspace_scope : Some ( openshell_core:: proto:: workspace_selector (
240- ( workspace) . to_string ( ) ,
241- ) ) ,
242- provider : provider. to_string ( ) ,
243- expected_resource_version : resource_version,
244- } )
245- . await
246- {
247- Ok ( response) => response. into_inner ( ) ,
248- // Explicit post-save uncertainty takes precedence over a generic retry hint.
249- Err ( status)
250- if status. code ( ) == Code :: Aborted && !provider_mutation_is_uncertain ( & status) =>
250+ match client
251+ . detach_sandbox_provider ( DetachSandboxProviderRequest {
252+ request_id : String :: new ( ) ,
253+ sandbox : ( name) . to_string ( ) ,
254+ workspace_scope : Some ( openshell_core:: proto:: workspace_selector (
255+ ( workspace) . to_string ( ) ,
256+ ) ) ,
257+ provider : provider. to_string ( ) ,
258+ expected_resource_version : resource_version,
259+ } )
260+ . await
251261 {
252- return Err ( miette:: miette!(
253- "Failed to detach provider: sandbox was modified by another operation.\n \
254- Please retry the command."
255- ) ) ;
262+ Ok ( response) => break ( sandbox, response. into_inner ( ) ) ,
263+ // Explicit post-save uncertainty takes precedence over a generic retry hint.
264+ Err ( status)
265+ if status. code ( ) == Code :: Aborted && !provider_mutation_is_uncertain ( & status) =>
266+ {
267+ if attempt < PROVIDER_ATTACHMENT_CAS_ATTEMPTS {
268+ attempt += 1 ;
269+ continue ;
270+ }
271+ return Err ( miette:: miette!(
272+ "Failed to detach provider: sandbox was modified by another operation.\n \
273+ Please retry the command."
274+ ) ) ;
275+ }
276+ Err ( error) => return Err ( provider_mutation_error ( & error, "detachment" ) ) ,
256277 }
257- Err ( error) => return Err ( provider_mutation_error ( & error, "detachment" ) ) ,
258278 } ;
259279
260280 let receipt = response. receipt . ok_or_else ( || miette ! ( "gateway did not return a provider receipt; saved detachment cannot establish revocation" ) ) ?;
0 commit comments