1use std::io;
5use std::pin::Pin;
6use std::sync::{Arc, OnceLock};
7use std::task::Poll;
8
9use crate::object_store::ObjectStore as LanceObjectStore;
10use async_trait::async_trait;
11use bytes::Bytes;
12use futures::FutureExt;
13use futures::future::BoxFuture;
14use object_store::{Error as OSError, ObjectStore, Result as OSResult, path::Path};
15use object_store::{MultipartUpload, ObjectStoreExt};
16use rand::Rng;
17use tokio::io::{AsyncWrite, AsyncWriteExt};
18use tokio::task::JoinSet;
19
20use lance_core::{Error, Result};
21use tracing::Instrument;
22
23use crate::traits::Writer;
24use crate::utils::tracking_store::IOTracker;
25use tokio::runtime::Handle;
26
27const INITIAL_UPLOAD_STEP: usize = 1024 * 1024 * 5;
29
30fn max_upload_parallelism() -> usize {
31 static MAX_UPLOAD_PARALLELISM: OnceLock<usize> = OnceLock::new();
32 *MAX_UPLOAD_PARALLELISM.get_or_init(|| {
33 std::env::var("LANCE_UPLOAD_CONCURRENCY")
34 .ok()
35 .and_then(|s| s.parse::<usize>().ok())
36 .unwrap_or(10)
37 })
38}
39
40fn max_conn_reset_retries() -> u16 {
41 static MAX_CONN_RESET_RETRIES: OnceLock<u16> = OnceLock::new();
42 *MAX_CONN_RESET_RETRIES.get_or_init(|| {
43 std::env::var("LANCE_CONN_RESET_RETRIES")
44 .ok()
45 .and_then(|s| s.parse::<u16>().ok())
46 .unwrap_or(20)
47 })
48}
49
50const MAX_UPLOAD_PART_SIZE: usize = 1024 * 1024 * 1024 * 5 - 1;
56
57fn clamp_initial_upload_size(raw: usize) -> (usize, bool) {
60 let clamped = raw.clamp(INITIAL_UPLOAD_STEP, MAX_UPLOAD_PART_SIZE);
61 (clamped, clamped != raw)
62}
63
64fn initial_upload_size() -> usize {
65 static LANCE_INITIAL_UPLOAD_SIZE: OnceLock<usize> = OnceLock::new();
66 *LANCE_INITIAL_UPLOAD_SIZE.get_or_init(|| {
67 let Some(raw) = std::env::var("LANCE_INITIAL_UPLOAD_SIZE")
68 .ok()
69 .and_then(|s| s.parse::<usize>().ok())
70 else {
71 return INITIAL_UPLOAD_STEP;
72 };
73 let (clamped, was_clamped) = clamp_initial_upload_size(raw);
74 if was_clamped {
75 tracing::warn!(
77 requested = raw,
78 clamped,
79 "LANCE_INITIAL_UPLOAD_SIZE must be between 5MB and 5GB; clamping to valid range"
80 );
81 }
82 clamped
83 })
84}
85
86pub struct ObjectWriter {
94 state: UploadState,
95 path: Arc<Path>,
96 cursor: usize,
97 connection_resets: u16,
98 buffer: Vec<u8>,
99 use_constant_size_upload_parts: bool,
101}
102
103#[derive(Debug, Clone, Default)]
104pub struct WriteResult {
105 pub size: usize,
106 pub e_tag: Option<String>,
107}
108
109enum UploadState {
110 Started(Arc<dyn ObjectStore>),
113 CreatingUpload(BoxFuture<'static, OSResult<Box<dyn MultipartUpload>>>),
115 InProgress {
117 part_idx: u16,
118 upload: Box<dyn MultipartUpload>,
119 futures: JoinSet<std::result::Result<(), UploadPutError>>,
120 },
121 PuttingSingle(BoxFuture<'static, OSResult<WriteResult>>),
124 Completing(BoxFuture<'static, OSResult<WriteResult>>),
126 Done(WriteResult),
128}
129
130impl UploadState {
132 fn started_to_putting_single(&mut self, path: Arc<Path>, buffer: Vec<u8>) {
133 let this = std::mem::replace(self, Self::Done(WriteResult::default()));
135 *self = match this {
136 Self::Started(store) => {
137 let fut = async move {
138 let size = buffer.len();
139 let res = store.put(&path, buffer.into()).await?;
140 Ok(WriteResult {
141 size,
142 e_tag: res.e_tag,
143 })
144 };
145 Self::PuttingSingle(Box::pin(fut))
146 }
147 _ => unreachable!(),
148 }
149 }
150
151 fn in_progress_to_completing(&mut self) {
152 let this = std::mem::replace(self, Self::Done(WriteResult::default()));
154 *self = match this {
155 Self::InProgress {
156 mut upload,
157 futures,
158 ..
159 } => {
160 debug_assert!(futures.is_empty());
161 let fut = async move {
162 let res = upload.complete().await?;
163 Ok(WriteResult {
164 size: 0, e_tag: res.e_tag,
166 })
167 };
168 Self::Completing(Box::pin(fut))
169 }
170 _ => unreachable!(),
171 };
172 }
173}
174
175impl ObjectWriter {
176 pub async fn new(object_store: &LanceObjectStore, path: &Path) -> Result<Self> {
177 Ok(Self {
178 state: UploadState::Started(object_store.inner.clone()),
179 cursor: 0,
180 path: Arc::new(path.clone()),
181 connection_resets: 0,
182 buffer: Vec::with_capacity(initial_upload_size()),
183 use_constant_size_upload_parts: object_store.use_constant_size_upload_parts,
184 })
185 }
186
187 fn next_part_buffer(buffer: &mut Vec<u8>, part_idx: u16, constant_upload_size: bool) -> Bytes {
190 let new_capacity = if constant_upload_size {
191 initial_upload_size()
193 } else {
194 initial_upload_size().max(((part_idx / 100) as usize + 1) * INITIAL_UPLOAD_STEP)
196 };
197 let new_buffer = Vec::with_capacity(new_capacity);
198 let part = std::mem::replace(buffer, new_buffer);
199 Bytes::from(part)
200 }
201
202 fn put_part(
203 upload: &mut dyn MultipartUpload,
204 buffer: Bytes,
205 part_idx: u16,
206 sleep: Option<std::time::Duration>,
207 ) -> BoxFuture<'static, std::result::Result<(), UploadPutError>> {
208 log::debug!(
209 "MultipartUpload submitting part with {} bytes",
210 buffer.len()
211 );
212 let fut = upload.put_part(buffer.clone().into());
213 Box::pin(async move {
214 if let Some(sleep) = sleep {
215 tokio::time::sleep(sleep).await;
216 }
217 fut.await.map_err(|source| UploadPutError {
218 part_idx,
219 buffer,
220 source,
221 })?;
222 Ok(())
223 })
224 }
225
226 fn poll_tasks(
227 mut self: Pin<&mut Self>,
228 cx: &mut std::task::Context<'_>,
229 ) -> std::result::Result<(), io::Error> {
230 let mut_self = &mut *self;
231 loop {
232 match &mut mut_self.state {
233 UploadState::Started(_) | UploadState::Done(_) => break,
234 UploadState::CreatingUpload(fut) => match fut.poll_unpin(cx) {
235 Poll::Ready(Ok(mut upload)) => {
236 let mut futures = JoinSet::new();
237
238 let data = Self::next_part_buffer(
239 &mut mut_self.buffer,
240 0,
241 mut_self.use_constant_size_upload_parts,
242 );
243 futures.spawn(Self::put_part(upload.as_mut(), data, 0, None));
244
245 mut_self.state = UploadState::InProgress {
246 part_idx: 1, futures,
248 upload,
249 };
250 }
251 Poll::Ready(Err(e)) => return Err(std::io::Error::other(e)),
252 Poll::Pending => break,
253 },
254 UploadState::InProgress {
255 upload, futures, ..
256 } => {
257 while let Poll::Ready(Some(res)) = futures.poll_join_next(cx) {
258 match res {
259 Ok(Ok(())) => {}
260 Err(err) => return Err(std::io::Error::other(err)),
261 Ok(Err(err)) if should_retry_upload_put(&err.source) => {
262 if mut_self.connection_resets < max_conn_reset_retries() {
263 mut_self.connection_resets += 1;
265
266 let sleep_time_ms = rand::rng().random_range(2_000..8_000);
268 let sleep_time =
269 std::time::Duration::from_millis(sleep_time_ms);
270
271 futures.spawn(Self::put_part(
272 upload.as_mut(),
273 err.buffer,
274 err.part_idx,
275 Some(sleep_time),
276 ));
277 } else {
278 return Err(io::Error::new(
279 io::ErrorKind::ConnectionReset,
280 Box::new(ConnectionResetError {
281 message: format!(
282 "Hit max retries ({}) for retryable upload error",
283 max_conn_reset_retries()
284 ),
285 source: Box::new(err.source),
286 }),
287 ));
288 }
289 }
290 Ok(Err(err)) => return Err(err.source.into()),
291 }
292 }
293 break;
294 }
295 UploadState::PuttingSingle(fut) | UploadState::Completing(fut) => {
296 match fut.poll_unpin(cx) {
297 Poll::Ready(Ok(mut res)) => {
298 res.size = mut_self.cursor;
299 mut_self.state = UploadState::Done(res)
300 }
301 Poll::Ready(Err(e)) => return Err(std::io::Error::other(e)),
302 Poll::Pending => break,
303 }
304 }
305 }
306 }
307 Ok(())
308 }
309
310 pub async fn abort(&mut self) {
311 let state = std::mem::replace(&mut self.state, UploadState::Done(WriteResult::default()));
312 if let UploadState::InProgress { mut upload, .. } = state {
313 let _ = upload.abort().await;
314 }
315 }
316}
317
318impl Drop for ObjectWriter {
319 fn drop(&mut self) {
320 if matches!(self.state, UploadState::InProgress { .. }) {
322 let state =
324 std::mem::replace(&mut self.state, UploadState::Done(WriteResult::default()));
325 if let UploadState::InProgress { mut upload, .. } = state
326 && let Ok(handle) = Handle::try_current()
327 {
328 handle.spawn(async move {
329 let _ = upload.abort().await;
330 });
331 }
332 }
333 }
334}
335
336struct UploadPutError {
340 part_idx: u16,
341 buffer: Bytes,
342 source: OSError,
343}
344
345fn should_retry_upload_put(source: &OSError) -> bool {
346 let OSError::Generic { source, .. } = source else {
347 return false;
348 };
349
350 let message = source.to_string().to_ascii_lowercase();
351 message.contains("connection reset by peer") || message.contains("requesttimeout")
352}
353
354#[derive(Debug)]
355struct ConnectionResetError {
356 message: String,
357 source: Box<dyn std::error::Error + Send + Sync>,
358}
359
360impl std::error::Error for ConnectionResetError {}
361
362impl std::fmt::Display for ConnectionResetError {
363 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
364 write!(f, "{}: {}", self.message, self.source)
365 }
366}
367
368impl AsyncWrite for ObjectWriter {
369 fn poll_write(
370 mut self: std::pin::Pin<&mut Self>,
371 cx: &mut std::task::Context<'_>,
372 buf: &[u8],
373 ) -> std::task::Poll<std::result::Result<usize, std::io::Error>> {
374 self.as_mut().poll_tasks(cx)?;
375
376 let remaining_capacity = self.buffer.capacity() - self.buffer.len();
378 let bytes_to_write = std::cmp::min(remaining_capacity, buf.len());
379 self.buffer.extend_from_slice(&buf[..bytes_to_write]);
380 self.cursor += bytes_to_write;
381
382 let mut_self = &mut *self;
385
386 if mut_self.buffer.capacity() == mut_self.buffer.len() {
388 match &mut mut_self.state {
389 UploadState::Started(store) => {
390 let path = mut_self.path.clone();
391 let store = store.clone();
392 let fut = Box::pin(async move { store.put_multipart(path.as_ref()).await });
393 self.state = UploadState::CreatingUpload(fut);
394 }
395 UploadState::InProgress {
397 upload,
398 part_idx,
399 futures,
400 ..
401 } if futures.len() < max_upload_parallelism() => {
402 let data = Self::next_part_buffer(
403 &mut mut_self.buffer,
404 *part_idx,
405 mut_self.use_constant_size_upload_parts,
406 );
407 futures.spawn(
408 Self::put_part(upload.as_mut(), data, *part_idx, None)
409 .instrument(tracing::Span::current()),
410 );
411 *part_idx += 1;
412 }
413 _ => {}
414 }
415 }
416
417 self.poll_tasks(cx)?;
418
419 match bytes_to_write {
420 0 => Poll::Pending,
421 _ => Poll::Ready(Ok(bytes_to_write)),
422 }
423 }
424
425 fn poll_flush(
426 mut self: std::pin::Pin<&mut Self>,
427 cx: &mut std::task::Context<'_>,
428 ) -> std::task::Poll<std::result::Result<(), std::io::Error>> {
429 self.as_mut().poll_tasks(cx)?;
430
431 match &self.state {
432 UploadState::Started(_) | UploadState::Done(_) => Poll::Ready(Ok(())),
433 UploadState::CreatingUpload(_)
434 | UploadState::Completing(_)
435 | UploadState::PuttingSingle(_) => Poll::Pending,
436 UploadState::InProgress { futures, .. } => {
437 if futures.is_empty() {
438 Poll::Ready(Ok(()))
439 } else {
440 Poll::Pending
441 }
442 }
443 }
444 }
445
446 fn poll_shutdown(
447 mut self: std::pin::Pin<&mut Self>,
448 cx: &mut std::task::Context<'_>,
449 ) -> std::task::Poll<std::result::Result<(), std::io::Error>> {
450 loop {
451 self.as_mut().poll_tasks(cx)?;
452
453 let mut_self = &mut *self;
456 match &mut mut_self.state {
457 UploadState::Done(_) => return Poll::Ready(Ok(())),
458 UploadState::CreatingUpload(_)
459 | UploadState::PuttingSingle(_)
460 | UploadState::Completing(_) => return Poll::Pending,
461 UploadState::Started(_) => {
462 let part = std::mem::take(&mut mut_self.buffer);
464 let path = mut_self.path.clone();
465 self.state.started_to_putting_single(path, part);
466 }
467 UploadState::InProgress {
468 upload,
469 futures,
470 part_idx,
471 } => {
472 if !mut_self.buffer.is_empty() && futures.len() < max_upload_parallelism() {
474 let data = Bytes::from(std::mem::take(&mut mut_self.buffer));
476 futures.spawn(
477 Self::put_part(upload.as_mut(), data, *part_idx, None)
478 .instrument(tracing::Span::current()),
479 );
480 continue;
483 }
484
485 if futures.is_empty() {
487 self.state.in_progress_to_completing();
488 } else {
489 return Poll::Pending;
490 }
491 }
492 }
493 }
494 }
495}
496
497#[async_trait]
498impl Writer for ObjectWriter {
499 async fn tell(&mut self) -> Result<usize> {
500 Ok(self.cursor)
501 }
502
503 async fn shutdown(&mut self) -> Result<WriteResult> {
504 AsyncWriteExt::shutdown(self).await.map_err(|e| {
505 Error::io(format!(
506 "failed to shutdown object writer for {}: {}",
507 self.path, e
508 ))
509 })?;
510 if let UploadState::Done(result) = &self.state {
511 Ok(result.clone())
512 } else {
513 unreachable!()
514 }
515 }
516}
517
518pub struct LocalWriter {
519 path: Path,
520 state: LocalWriteState,
521}
522
523#[derive(Default)]
524enum LocalWriteState {
525 Writing(WritingState),
526 Finishing {
527 size: usize,
528 future: BoxFuture<'static, Result<WriteResult>>,
529 },
530 Done(WriteResult),
531 #[default]
532 Poisoned,
533}
534
535struct WritingState {
536 writer: tokio::io::BufWriter<tokio::fs::File>,
537 cursor: usize,
538 temp_path: tempfile::TempPath,
540 io_tracker: Arc<IOTracker>,
541}
542
543impl LocalWriter {
544 pub fn new(
545 file: tokio::fs::File,
546 path: Path,
547 temp_path: tempfile::TempPath,
548 io_tracker: Arc<IOTracker>,
549 ) -> Self {
550 Self {
551 path,
552 state: LocalWriteState::Writing(WritingState {
553 writer: tokio::io::BufWriter::new(file),
554 cursor: 0,
555 temp_path,
556 io_tracker,
557 }),
558 }
559 }
560
561 fn already_closed_err(path: &Path) -> io::Error {
562 io::Error::other(format!(
563 "cannot write to LocalWriter for {} after shutdown",
564 path
565 ))
566 }
567
568 fn poisoned_err(path: &Path) -> io::Error {
569 io::Error::other(format!("LocalWriter for {} is in poisoned state", path))
570 }
571
572 async fn persist(
573 temp_path: tempfile::TempPath,
574 final_path: Path,
575 size: usize,
576 io_tracker: Arc<IOTracker>,
577 ) -> Result<WriteResult> {
578 let local_path = crate::local::to_local_path(&final_path);
579 let e_tag = tokio::task::spawn_blocking(move || -> Result<String> {
580 temp_path.persist(&local_path).map_err(|e| {
581 Error::io(format!(
582 "failed to persist temp file to {}: {}",
583 local_path, e.error
584 ))
585 })?;
586
587 let metadata = std::fs::metadata(&local_path).map_err(|e| {
588 Error::io(format!("failed to read metadata for {}: {}", local_path, e))
589 })?;
590 Ok(get_etag(&metadata))
591 })
592 .await
593 .map_err(|e| Error::io(format!("spawn_blocking failed: {}", e)))??;
594
595 io_tracker.record_write("put", final_path, size as u64);
596
597 Ok(WriteResult {
598 size,
599 e_tag: Some(e_tag),
600 })
601 }
602}
603
604impl AsyncWrite for LocalWriter {
605 fn poll_write(
606 mut self: Pin<&mut Self>,
607 cx: &mut std::task::Context<'_>,
608 buf: &[u8],
609 ) -> Poll<std::result::Result<usize, std::io::Error>> {
610 if let LocalWriteState::Writing(state) = &mut self.state {
611 let poll = Pin::new(&mut state.writer).poll_write(cx, buf);
612 if let Poll::Ready(Ok(n)) = &poll {
613 state.cursor += *n;
614 }
615 poll
616 } else {
617 Poll::Ready(Err(Self::already_closed_err(&self.path)))
618 }
619 }
620
621 fn poll_flush(
622 mut self: Pin<&mut Self>,
623 cx: &mut std::task::Context<'_>,
624 ) -> Poll<std::result::Result<(), std::io::Error>> {
625 if let LocalWriteState::Writing(state) = &mut self.state {
626 Pin::new(&mut state.writer).poll_flush(cx)
627 } else {
628 Poll::Ready(Err(Self::already_closed_err(&self.path)))
629 }
630 }
631
632 fn poll_shutdown(
633 mut self: Pin<&mut Self>,
634 cx: &mut std::task::Context<'_>,
635 ) -> Poll<std::result::Result<(), std::io::Error>> {
636 let mut_self = &mut *self;
637 loop {
638 match &mut mut_self.state {
639 LocalWriteState::Writing(state) => {
640 if Pin::new(&mut state.writer).poll_shutdown(cx).is_pending() {
641 return Poll::Pending;
642 }
643
644 let LocalWriteState::Writing(state) =
646 std::mem::replace(&mut mut_self.state, LocalWriteState::Poisoned)
647 else {
648 unreachable!()
649 };
650 let size = state.cursor;
651 mut_self.state = LocalWriteState::Finishing {
652 size,
653 future: Box::pin(Self::persist(
654 state.temp_path,
655 mut_self.path.clone(),
656 size,
657 state.io_tracker,
658 )),
659 };
660 }
661 LocalWriteState::Finishing { future, .. } => match future.poll_unpin(cx) {
662 Poll::Ready(Ok(result)) => mut_self.state = LocalWriteState::Done(result),
663 Poll::Ready(Err(e)) => {
664 return Poll::Ready(Err(io::Error::other(e)));
665 }
666 Poll::Pending => return Poll::Pending,
667 },
668 LocalWriteState::Done(_) => return Poll::Ready(Ok(())),
669 LocalWriteState::Poisoned => {
670 return Poll::Ready(Err(Self::poisoned_err(&self.path)));
671 }
672 }
673 }
674 }
675}
676
677#[async_trait]
678impl Writer for LocalWriter {
679 async fn tell(&mut self) -> Result<usize> {
680 match &mut self.state {
681 LocalWriteState::Writing(state) => Ok(state.cursor),
682 LocalWriteState::Finishing { size, .. } => Ok(*size),
683 LocalWriteState::Done(result) => Ok(result.size),
684 LocalWriteState::Poisoned => Err(Self::poisoned_err(&self.path).into()),
685 }
686 }
687
688 async fn shutdown(&mut self) -> Result<WriteResult> {
689 AsyncWriteExt::shutdown(self).await.map_err(|e| {
690 Error::io(format!(
691 "failed to shutdown local writer for {}: {}",
692 self.path, e
693 ))
694 })?;
695
696 match &self.state {
697 LocalWriteState::Done(result) => Ok(result.clone()),
698 _ => unreachable!(),
699 }
700 }
701}
702
703pub fn get_etag(metadata: &std::fs::Metadata) -> String {
705 let inode = get_inode(metadata);
706 let size = metadata.len();
707 let mtime = metadata
708 .modified()
709 .ok()
710 .and_then(|mtime| mtime.duration_since(std::time::SystemTime::UNIX_EPOCH).ok())
711 .unwrap_or_default()
712 .as_micros();
713
714 format!("{inode:x}-{mtime:x}-{size:x}")
717}
718
719#[cfg(unix)]
720fn get_inode(metadata: &std::fs::Metadata) -> u64 {
721 std::os::unix::fs::MetadataExt::ino(metadata)
722}
723
724#[cfg(not(unix))]
725fn get_inode(_metadata: &std::fs::Metadata) -> u64 {
726 0
727}
728
729#[cfg(test)]
730mod tests {
731 use tokio::io::AsyncWriteExt;
732
733 use super::*;
734
735 #[tokio::test]
736 async fn test_write() {
737 let store = LanceObjectStore::memory();
738
739 let mut object_writer = ObjectWriter::new(&store, &Path::from("/foo"))
740 .await
741 .unwrap();
742 assert_eq!(object_writer.tell().await.unwrap(), 0);
743
744 let buf = vec![0; 256];
745 assert_eq!(object_writer.write(buf.as_slice()).await.unwrap(), 256);
746 assert_eq!(object_writer.tell().await.unwrap(), 256);
747
748 assert_eq!(object_writer.write(buf.as_slice()).await.unwrap(), 256);
749 assert_eq!(object_writer.tell().await.unwrap(), 512);
750
751 assert_eq!(object_writer.write(buf.as_slice()).await.unwrap(), 256);
752 assert_eq!(object_writer.tell().await.unwrap(), 256 * 3);
753
754 let res = Writer::shutdown(&mut object_writer).await.unwrap();
755 assert_eq!(res.size, 256 * 3);
756
757 let mut object_writer = ObjectWriter::new(&store, &Path::from("/bar"))
759 .await
760 .unwrap();
761 let buf = vec![0; INITIAL_UPLOAD_STEP / 3 * 2];
762 for i in 0..5 {
763 object_writer.write_all(buf.as_slice()).await.unwrap();
766 assert_eq!(object_writer.tell().await.unwrap(), (i + 1) * buf.len());
768 }
769 let res = Writer::shutdown(&mut object_writer).await.unwrap();
770 assert_eq!(res.size, buf.len() * 5);
771 }
772
773 #[tokio::test]
774 async fn test_abort_write() {
775 let store = LanceObjectStore::memory();
776
777 let mut object_writer = ObjectWriter::new(&store, &Path::from("/foo"))
778 .await
779 .unwrap();
780 object_writer.abort().await;
781 }
782
783 #[tokio::test]
784 async fn test_local_writer_shutdown() {
785 let tmp = lance_core::utils::tempfile::TempStdDir::default();
786 let file_path = tmp.join("test_local_writer.bin");
787 let os_path = Path::from_absolute_path(&file_path).unwrap();
788 let io_tracker = Arc::new(IOTracker::default());
789
790 let named_temp = tempfile::NamedTempFile::new_in(&*tmp).unwrap();
791 let temp_file_path = named_temp.path().to_owned();
792 let (std_file, temp_path) = named_temp.into_parts();
793 let file = tokio::fs::File::from_std(std_file);
794 let mut writer = LocalWriter::new(file, os_path, temp_path, io_tracker.clone());
795
796 let data = b"hello local writer";
797 writer.write_all(data).await.unwrap();
798
799 assert!(!file_path.exists());
801 assert!(temp_file_path.exists());
803
804 let result = Writer::shutdown(&mut writer).await.unwrap();
805 assert_eq!(result.size, data.len());
806 assert!(result.e_tag.is_some());
807 assert!(!result.e_tag.as_ref().unwrap().is_empty());
808
809 assert!(file_path.exists());
811 assert!(!temp_file_path.exists());
812
813 let stats = io_tracker.stats();
814 assert_eq!(stats.write_iops, 1);
815 assert_eq!(stats.written_bytes, data.len() as u64);
816 }
817
818 #[tokio::test]
819 async fn test_local_writer_drop_cleans_up() {
820 let tmp = lance_core::utils::tempfile::TempStdDir::default();
821 let file_path = tmp.join("test_drop.bin");
822 let os_path = Path::from_absolute_path(&file_path).unwrap();
823 let io_tracker = Arc::new(IOTracker::default());
824
825 let named_temp = tempfile::NamedTempFile::new_in(&*tmp).unwrap();
826 let temp_file_path = named_temp.path().to_owned();
827 let (std_file, temp_path) = named_temp.into_parts();
828 let file = tokio::fs::File::from_std(std_file);
829 let mut writer = LocalWriter::new(file, os_path, temp_path, io_tracker);
830
831 writer.write_all(b"some data").await.unwrap();
832 assert!(temp_file_path.exists());
833
834 drop(writer);
836 assert!(!temp_file_path.exists());
837 assert!(!file_path.exists());
838 }
839
840 #[test]
841 fn clamp_initial_upload_size_below_min_is_clamped_up() {
842 assert_eq!(clamp_initial_upload_size(0), (INITIAL_UPLOAD_STEP, true));
843 assert_eq!(
844 clamp_initial_upload_size(INITIAL_UPLOAD_STEP - 1),
845 (INITIAL_UPLOAD_STEP, true)
846 );
847 }
848
849 #[test]
850 fn clamp_initial_upload_size_within_range_is_unchanged() {
851 assert_eq!(
852 clamp_initial_upload_size(INITIAL_UPLOAD_STEP),
853 (INITIAL_UPLOAD_STEP, false)
854 );
855 assert_eq!(
856 clamp_initial_upload_size(MAX_UPLOAD_PART_SIZE),
857 (MAX_UPLOAD_PART_SIZE, false)
858 );
859 let mid = INITIAL_UPLOAD_STEP * 8; assert_eq!(clamp_initial_upload_size(mid), (mid, false));
861 }
862
863 #[test]
864 fn should_retry_upload_put_detects_transient_errors() {
865 let request_timeout = OSError::Generic {
866 store: "S3",
867 source: Box::new(io::Error::other(
868 "Server returned non-2xx status code: 400 Bad Request: \
869 <Error><Code>RequestTimeout</Code><Message>Your socket connection to the server \
870 was not read from or written to within the timeout period. Idle connections will \
871 be closed.</Message></Error>",
872 )),
873 };
874 assert!(should_retry_upload_put(&request_timeout));
875
876 let connection_reset = OSError::Generic {
877 store: "S3",
878 source: Box::new(io::Error::new(
879 io::ErrorKind::ConnectionReset,
880 "connection reset by peer",
881 )),
882 };
883 assert!(should_retry_upload_put(&connection_reset));
884
885 let not_retryable = OSError::Generic {
886 store: "S3",
887 source: Box::new(io::Error::other("access denied")),
888 };
889 assert!(!should_retry_upload_put(¬_retryable));
890 }
891
892 #[test]
893 fn clamp_initial_upload_size_above_max_is_clamped_down() {
894 assert_eq!(
895 clamp_initial_upload_size(MAX_UPLOAD_PART_SIZE + 1),
896 (MAX_UPLOAD_PART_SIZE, true)
897 );
898 assert_eq!(
899 clamp_initial_upload_size(usize::MAX),
900 (MAX_UPLOAD_PART_SIZE, true)
901 );
902 }
903
904 #[test]
909 fn clamp_initial_upload_size_at_5gib_clamps_down() {
910 let exactly_5_gib: usize = 5 * 1024 * 1024 * 1024;
911 assert_eq!(
912 clamp_initial_upload_size(exactly_5_gib),
913 (MAX_UPLOAD_PART_SIZE, true)
914 );
915 }
916}