Retry a gRPC operation with exponential backoff (capped at 4 s). Non-transient gRPC errors (e.g. `NOT_FOUND`, `INVALID_ARGUMENT`, `PERMISSION_DENIED`) are returned immediately without retrying.
(op_name: &str, f: F)
| 1317 | /// Non-transient gRPC errors (e.g. `NOT_FOUND`, `INVALID_ARGUMENT`, |
| 1318 | /// `PERMISSION_DENIED`) are returned immediately without retrying. |
| 1319 | async fn grpc_retry<T, F, Fut>(op_name: &str, f: F) -> Result<T> |
| 1320 | where |
| 1321 | F: Fn() -> Fut, |
| 1322 | Fut: Future<Output = Result<T>>, |
| 1323 | { |
| 1324 | let mut last_err = None; |
| 1325 | for attempt in 1..=5u32 { |
| 1326 | match f().await { |
| 1327 | Ok(val) => return Ok(val), |
| 1328 | Err(e) => { |
| 1329 | if !is_retryable_error(&e) { |
| 1330 | return Err(e); |
| 1331 | } |
| 1332 | if attempt < 5 { |
| 1333 | warn!( |
| 1334 | attempt, |
| 1335 | max_attempts = 5, |
| 1336 | error = %e, |
| 1337 | "{op_name} failed, retrying" |
| 1338 | ); |
| 1339 | let backoff = Duration::from_secs((1u64 << (attempt - 1)).min(4)); |
| 1340 | tokio::time::sleep(backoff).await; |
| 1341 | } |
| 1342 | last_err = Some(e); |
| 1343 | } |
| 1344 | } |
| 1345 | } |
| 1346 | Err(miette::miette!( |
| 1347 | "{op_name} failed after 5 attempts: {}", |
| 1348 | last_err.expect("loop executed at least once") |
| 1349 | )) |
| 1350 | } |
| 1351 | |
| 1352 | /// Load sandbox policy from local files or gRPC. |
| 1353 | /// |
no test coverage detected