diff --git a/litellm-rust/crates/cache-azure-blob/src/cache.rs b/litellm-rust/crates/cache-azure-blob/src/cache.rs index f28b8c9a641..efad0bc7f82 100644 --- a/litellm-rust/crates/cache-azure-blob/src/cache.rs +++ b/litellm-rust/crates/cache-azure-blob/src/cache.rs @@ -19,7 +19,6 @@ use url::Url; use crate::credential::AzureBlobCredential; -/// Synchronous methods block on `runtime` and therefore must run outside of it pub struct AzureBlobCache { container: BlobContainerClient, codec: C, @@ -54,14 +53,15 @@ impl AzureBlobCache { codec: C, runtime: Handle, ) -> Result { - let mut url = Url::parse(account_url).map_err(|_| Error::Unavailable)?; - let account_url = url.as_str().trim_end_matches('/').to_string(); - url.path_segments_mut() - .map_err(|()| Error::Unavailable)? - .pop_if_empty() - .push(container); + let account_url = Url::parse(account_url) + .map_err(|_| Error::Unavailable)? + .as_str() + .trim_end_matches('/') + .to_string(); + let container_url = + Url::parse(&format!("{account_url}/{container}")).map_err(|_| Error::Unavailable)?; let client = BlobContainerClient::new( - url, + container_url, credential, Some(BlobContainerClientOptions { client_options, @@ -108,7 +108,7 @@ impl AzureBlobCache { .await { Ok(_) => Ok(()), - Err(error) if is_storage_error(&error, StorageErrorCode::BlobAlreadyExists) => Ok(()), + Err(error) if !overwrite && is_already_present(&error) => Ok(()), Err(_) => Err(Error::Unavailable), } } @@ -153,6 +153,11 @@ impl AzureBlobCache { } } +fn is_already_present(error: &azure_core::Error) -> bool { + is_storage_error(error, StorageErrorCode::BlobAlreadyExists) + || is_storage_error(error, StorageErrorCode::ConditionNotMet) +} + fn is_storage_error(error: &azure_core::Error, code: StorageErrorCode) -> bool { matches!( error.kind(), diff --git a/litellm-rust/crates/cache-azure-blob/src/cache/tests.rs b/litellm-rust/crates/cache-azure-blob/src/cache/tests.rs index fd116a0e28f..580674450c3 100644 --- a/litellm-rust/crates/cache-azure-blob/src/cache/tests.rs +++ b/litellm-rust/crates/cache-azure-blob/src/cache/tests.rs @@ -39,6 +39,7 @@ struct FakeState { blobs: BTreeMap>, requests: Vec, failing: bool, + precondition_conflicts: bool, } #[derive(Clone, Default)] @@ -79,6 +80,10 @@ impl FakeBlobService { self.state.lock().unwrap().failing = failing; } + fn set_precondition_conflicts(&self, enabled: bool) { + self.state.lock().unwrap().precondition_conflicts = enabled; + } + fn requests(&self) -> Vec { self.state.lock().unwrap().requests.clone() } @@ -158,7 +163,15 @@ impl HttpClient for FakeBlobService { } (Method::Put, false, Some(name)) => { if if_none_match.as_deref() == Some("*") && state.blobs.contains_key(&name) { - Self::respond(StatusCode::Conflict, Some("BlobAlreadyExists"), Vec::new()) + if state.precondition_conflicts { + Self::respond( + StatusCode::PreconditionFailed, + Some("ConditionNotMet"), + Vec::new(), + ) + } else { + Self::respond(StatusCode::Conflict, Some("BlobAlreadyExists"), Vec::new()) + } } else { let bytes = match request.body() { Body::Bytes(bytes) => bytes.to_vec(), @@ -369,6 +382,25 @@ fn sync_set_does_not_overwrite_an_existing_blob() { ); } +#[test] +fn sync_set_treats_a_precondition_conflict_as_an_existing_blob() { + let fixture = Fixture::new(FakeBlobService::default()); + fixture.service.set_precondition_conflicts(true); + fixture + .cache + .set_cache("key", entry(json!({"v": "first"})), &no_ttl()) + .unwrap(); + fixture + .cache + .set_cache("key", entry(json!({"v": "second"})), &no_ttl()) + .unwrap(); + + assert_eq!( + fixture.stored_json("key")["response"], + json!({"v": "first"}) + ); +} + #[test] fn async_set_overwrites_an_existing_blob() { let fixture = Fixture::new(FakeBlobService::default());