fix(rust): treat ConditionNotMet as an existing blob on sync Azure Blob writes

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Yujong Lee 2026-09-21 21:16:19 +00:00
parent b5548082c6
commit 0e281d458f
2 changed files with 47 additions and 10 deletions

View file

@ -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<C> {
container: BlobContainerClient,
codec: C,
@ -54,14 +53,15 @@ impl<C: CacheCodec> AzureBlobCache<C> {
codec: C,
runtime: Handle,
) -> Result<Self, Error> {
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<C: CacheCodec> AzureBlobCache<C> {
.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<C: CacheCodec> AzureBlobCache<C> {
}
}
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(),

View file

@ -39,6 +39,7 @@ struct FakeState {
blobs: BTreeMap<String, Vec<u8>>,
requests: Vec<RecordedRequest>,
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<RecordedRequest> {
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());