Skip to content

Commit 87bacf0

Browse files
modified approach
1 parent e362da8 commit 87bacf0

File tree

2 files changed

+111
-37
lines changed

2 files changed

+111
-37
lines changed

nativelink-store/src/gcs_store.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ use nativelink_util::retry::{Retrier, RetryResult};
3131
use nativelink_util::store_trait::{StoreDriver, StoreKey, UploadSizeInfo};
3232
use rand::Rng;
3333
use tokio::time::sleep;
34+
use tracing::warn;
3435

3536
use crate::cas_utils::is_zero_digest;
3637
use crate::gcs_client::client::{GcsClient, GcsOperations};
@@ -222,6 +223,18 @@ where
222223
mut reader: DropCloserReadHalf,
223224
upload_size: UploadSizeInfo,
224225
) -> Result<(), Error> {
226+
// Check if this is a zero-byte file
227+
if is_zero_digest(digest.borrow()) {
228+
warn!(
229+
"Attempted to upload zero-byte file to GCS. This is not supported. \
230+
Consider using a size partitioning store to restrict zero-byte files."
231+
);
232+
// Consume any data in the reader (should be empty) and return success
233+
// to maintain compatibility with existing behavior
234+
drop(reader.consume(None).await?);
235+
return Ok(());
236+
}
237+
225238
let object_path = self.make_object_path(&digest);
226239

227240
reader.set_max_recent_data_size(

nativelink-store/tests/gcs_store_test.rs

Lines changed: 98 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -346,43 +346,44 @@ async fn get_part_with_range() -> Result<(), Error> {
346346
Ok(())
347347
}
348348

349-
#[nativelink_test]
350-
async fn get_part_zero_digest() -> Result<(), Error> {
351-
// Create mock GCS operations
352-
let mock_ops = Arc::new(MockGcsOperations::new());
353-
let ops_as_trait: Arc<dyn GcsOperations> = mock_ops.clone();
354-
let store = create_test_store(ops_as_trait).await?;
355-
356-
// Create a zero digest
357-
let digest = DigestInfo::new(Sha256::new().finalize().into(), 0);
358-
let store_key: StoreKey = to_store_key(digest);
359-
let (mut tx, mut rx) = make_buf_channel_pair();
360-
361-
// Start get_part operation
362-
let store_clone = store.clone();
363-
let get_fut = nativelink_util::spawn!("get_part_full_task", async move {
364-
store_clone.get_part(store_key, &mut tx, 0, None).await
365-
});
366-
367-
// Receive the data - should be empty
368-
let received_data = rx.consume(Some(100)).await?;
369-
assert_eq!(
370-
received_data.as_ref(),
371-
b"",
372-
"Received data should be empty for zero digest"
373-
);
374-
get_fut.await??;
375-
376-
// Verify no mock operations were called (zero digest is special-cased)
377-
let call_counts = mock_ops.get_call_counts();
378-
assert_eq!(
379-
call_counts.read_calls.load(Ordering::Relaxed),
380-
0,
381-
"No read operations should have been called for zero digest"
382-
);
383-
384-
Ok(())
385-
}
349+
// I'll delete this but it did not actually test and prevent the issue we were addressing.
350+
// #[nativelink_test]
351+
// async fn get_part_zero_digest() -> Result<(), Error> {
352+
// // Create mock GCS operations
353+
// let mock_ops = Arc::new(MockGcsOperations::new());
354+
// let ops_as_trait: Arc<dyn GcsOperations> = mock_ops.clone();
355+
// let store = create_test_store(ops_as_trait).await?;
356+
357+
// // Create a zero digest
358+
// let digest = DigestInfo::new(Sha256::new().finalize().into(), 0);
359+
// let store_key: StoreKey = to_store_key(digest);
360+
// let (mut tx, mut rx) = make_buf_channel_pair();
361+
362+
// // Start get_part operation
363+
// let store_clone = store.clone();
364+
// let get_fut = nativelink_util::spawn!("get_part_full_task", async move {
365+
// store_clone.get_part(store_key, &mut tx, 0, None).await
366+
// });
367+
368+
// // Receive the data - should be empty
369+
// let received_data = rx.consume(Some(100)).await?;
370+
// assert_eq!(
371+
// received_data.as_ref(),
372+
// b"",
373+
// "Received data should be empty for zero digest"
374+
// );
375+
// get_fut.await??;
376+
377+
// // Verify no mock operations were called (zero digest is special-cased)
378+
// let call_counts = mock_ops.get_call_counts();
379+
// assert_eq!(
380+
// call_counts.read_calls.load(Ordering::Relaxed),
381+
// 0,
382+
// "No read operations should have been called for zero digest"
383+
// );
384+
385+
// Ok(())
386+
// }
386387

387388
#[nativelink_test]
388389
async fn test_expired_object() -> Result<(), Error> {
@@ -609,3 +610,63 @@ fn create_object_path(key: &StoreKey) -> ObjectPath {
609610
&format!("{}{}", KEY_PREFIX, key.as_str()),
610611
)
611612
}
613+
614+
#[nativelink_test]
615+
async fn test_zero_byte_upload_prevention() -> Result<(), Error> {
616+
// Create mock GCS operations
617+
let mock_ops = Arc::new(MockGcsOperations::new());
618+
let ops_as_trait: Arc<dyn GcsOperations> = mock_ops.clone();
619+
let store = create_test_store(ops_as_trait).await?;
620+
621+
// Create a zero-byte digest
622+
let zero_digest = DigestInfo::new(Sha256::new().finalize().into(), 0);
623+
let store_key: StoreKey = to_store_key(zero_digest);
624+
let (mut tx, rx) = make_buf_channel_pair();
625+
626+
// Start update operation for zero-byte file
627+
let store_clone = store.clone();
628+
let update_fut = nativelink_util::spawn!("update_zero_byte_task", async move {
629+
store_clone
630+
.update(store_key, rx, UploadSizeInfo::ExactSize(0))
631+
.await
632+
});
633+
634+
// Send EOF immediately (zero bytes)
635+
tx.send_eof()?;
636+
637+
// The update should complete but no upload should occur
638+
let result = update_fut.await?;
639+
assert!(result.is_ok(), "Update should complete without error");
640+
641+
// Verify that no write operations were called
642+
let requests = mock_ops.get_requests().await;
643+
let write_request_count = requests
644+
.iter()
645+
.filter(|req| {
646+
matches!(
647+
req,
648+
MockRequest::Write { .. } | MockRequest::StartResumable { .. }
649+
)
650+
})
651+
.count();
652+
653+
assert_eq!(
654+
write_request_count, 0,
655+
"No write requests should be made for zero-byte files"
656+
);
657+
658+
// Verify call counts
659+
let call_counts = mock_ops.get_call_counts();
660+
assert_eq!(
661+
call_counts.write_calls.load(Ordering::Relaxed),
662+
0,
663+
"No write operations should have been called"
664+
);
665+
assert_eq!(
666+
call_counts.start_resumable_calls.load(Ordering::Relaxed),
667+
0,
668+
"No resumable upload operations should have been called"
669+
);
670+
671+
Ok(())
672+
}

0 commit comments

Comments
 (0)