1use std::io;
5use std::num::NonZeroUsize;
6use std::pin::Pin;
7use std::sync::{Arc, OnceLock};
8use std::task::Poll;
9use std::time::Instant;
10
11use crate::object_store::ObjectStore as LanceObjectStore;
12use async_trait::async_trait;
13use bytes::Bytes;
14use futures::FutureExt;
15use futures::future::BoxFuture;
16use object_store::{MultipartUpload, ObjectStoreExt};
17use object_store::{ObjectStore, path::Path};
18use tokio::io::{AsyncWrite, AsyncWriteExt};
19use tokio::task::JoinSet;
20
21use lance_core::{Error, Result};
22use tracing::Instrument;
23
24use crate::traits::Writer;
25use crate::utils::tracking_store::{IOTracker, IoMetricsGuard};
26use tokio::runtime::Handle;
27
28const INITIAL_UPLOAD_STEP: usize = 1024 * 1024 * 5;
30
31pub fn max_upload_parallelism() -> usize {
32 static MAX_UPLOAD_PARALLELISM: OnceLock<usize> = OnceLock::new();
33 *MAX_UPLOAD_PARALLELISM.get_or_init(|| {
34 std::env::var("LANCE_UPLOAD_CONCURRENCY")
35 .ok()
36 .and_then(|s| s.parse::<usize>().ok())
37 .unwrap_or(10)
38 })
39}
40
41pub fn upload_memory_bound_bytes(total_bytes: u64, constant_upload_size: bool) -> u64 {
46 let initial = initial_upload_size() as u64;
47 let step = INITIAL_UPLOAD_STEP as u64;
48 let mut part_size = initial;
49 if !constant_upload_size {
50 let mut covered = 0u64;
51 let mut tier = 0u64;
52 while covered < total_bytes {
53 part_size = initial.max((tier + 1) * step);
54 covered = covered.saturating_add(part_size.saturating_mul(100));
55 tier += 1;
56 }
57 }
58 part_size.saturating_mul(max_upload_parallelism() as u64 + 1)
59}
60
61const MAX_UPLOAD_PART_SIZE: usize = 1024 * 1024 * 1024 * 5 - 1;
67
68fn clamp_initial_upload_size(raw: usize) -> (usize, bool) {
71 let clamped = raw.clamp(INITIAL_UPLOAD_STEP, MAX_UPLOAD_PART_SIZE);
72 (clamped, clamped != raw)
73}
74
75pub fn initial_upload_size() -> usize {
76 static LANCE_INITIAL_UPLOAD_SIZE: OnceLock<usize> = OnceLock::new();
77 *LANCE_INITIAL_UPLOAD_SIZE.get_or_init(|| {
78 let Some(raw) = std::env::var("LANCE_INITIAL_UPLOAD_SIZE")
79 .ok()
80 .and_then(|s| s.parse::<usize>().ok())
81 else {
82 return INITIAL_UPLOAD_STEP;
83 };
84 let (clamped, was_clamped) = clamp_initial_upload_size(raw);
85 if was_clamped {
86 tracing::warn!(
88 requested = raw,
89 clamped,
90 "LANCE_INITIAL_UPLOAD_SIZE must be between 5MB and 5GB; clamping to valid range"
91 );
92 }
93 clamped
94 })
95}
96
97pub struct ObjectWriter {
110 state: UploadState,
111 path: Arc<Path>,
112 cursor: usize,
113 upload_parallelism: NonZeroUsize,
114 buffer: Vec<u8>,
115 use_constant_size_upload_parts: bool,
117}
118
119#[derive(Debug, Clone, Default)]
120pub struct WriteResult {
121 pub size: usize,
122 pub e_tag: Option<String>,
123}
124
125#[derive(Debug)]
136struct UploadFailure {
137 context: String,
138 kind: io::ErrorKind,
142 source: Box<dyn std::error::Error + Send + Sync>,
143}
144
145impl UploadFailure {
146 fn new(context: String, source: object_store::Error) -> Self {
153 let mapped = io::Error::from(source);
154 let kind = mapped.kind();
155 let source: Box<dyn std::error::Error + Send + Sync> =
156 match mapped.downcast::<object_store::Error>() {
157 Ok(source) => Box::new(source),
158 Err(mapped) => Box::new(mapped),
159 };
160 Self {
161 context,
162 kind,
163 source,
164 }
165 }
166
167 fn from_task(context: String, source: tokio::task::JoinError) -> Self {
169 Self {
170 context,
171 kind: io::ErrorKind::Other,
172 source: Box::new(source),
173 }
174 }
175
176 fn into_io_error(self) -> io::Error {
177 let kind = self.kind;
178 io::Error::new(kind, self)
179 }
180}
181
182impl std::fmt::Display for UploadFailure {
183 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
184 write!(f, "{}: {}", self.context, self.source)
185 }
186}
187
188impl std::error::Error for UploadFailure {
189 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
190 Some(self.source.as_ref())
191 }
192}
193
194type UploadResult<T> = std::result::Result<T, UploadFailure>;
195
196struct PartUpload {
198 path: Arc<Path>,
199 part_idx: u16,
200 parts_in_flight: usize,
202 part_size: usize,
209}
210
211fn upload_settings() -> String {
218 format!(
219 "LANCE_INITIAL_UPLOAD_SIZE={} bytes, LANCE_UPLOAD_CONCURRENCY={}",
220 initial_upload_size(),
221 max_upload_parallelism()
222 )
223}
224
225enum UploadState {
226 Started(Arc<dyn ObjectStore>),
229 CreatingUpload(BoxFuture<'static, UploadResult<Box<dyn MultipartUpload>>>),
231 InProgress {
233 part_idx: u16,
234 upload: Box<dyn MultipartUpload>,
235 futures: JoinSet<UploadResult<()>>,
236 },
237 PuttingSingle(BoxFuture<'static, UploadResult<WriteResult>>),
240 Completing(BoxFuture<'static, UploadResult<WriteResult>>),
242 Done(WriteResult),
244}
245
246impl UploadState {
248 fn started_to_putting_single(&mut self, path: Arc<Path>, buffer: Vec<u8>) {
249 let this = std::mem::replace(self, Self::Done(WriteResult::default()));
251 *self = match this {
252 Self::Started(store) => {
253 tracing::Span::current().record("part_count", 1_u64);
254 let started_at = Instant::now();
255 let fut = async move {
256 let size = buffer.len();
257 let res = store.put(&path, buffer.into()).await.map_err(|source| {
258 UploadFailure::new(
259 format!(
260 "single PUT of {path} failed after {:?} ({size} bytes, {})",
261 started_at.elapsed(),
262 upload_settings()
263 ),
264 source,
265 )
266 })?;
267 Ok(WriteResult {
268 size,
269 e_tag: res.e_tag,
270 })
271 };
272 Self::PuttingSingle(Box::pin(fut))
273 }
274 _ => unreachable!(),
275 }
276 }
277
278 fn in_progress_to_completing(&mut self, path: Arc<Path>, bytes_written: usize) {
279 let this = std::mem::replace(self, Self::Done(WriteResult::default()));
281 *self = match this {
282 Self::InProgress {
283 mut upload,
284 futures,
285 part_idx,
286 } => {
287 debug_assert!(futures.is_empty());
288 tracing::Span::current().record("part_count", part_idx as u64);
289 let started_at = Instant::now();
290 let fut = async move {
291 let res = match upload.complete().await {
292 Ok(result) => result,
293 Err(source) => {
294 let elapsed = started_at.elapsed();
295 if let Err(error) = upload.abort().await {
296 tracing::warn!(
297 path = %path,
298 error = %error,
299 "Failed to abort multipart upload after completion error"
300 );
301 }
302 return Err(UploadFailure::new(
303 format!(
304 "completing multipart upload of {path} failed after {:?} \
305 ({part_idx} parts, {bytes_written} bytes, {})",
306 elapsed,
307 upload_settings()
308 ),
309 source,
310 ));
311 }
312 };
313 Ok(WriteResult {
314 size: 0, e_tag: res.e_tag,
316 })
317 };
318 Self::Completing(Box::pin(fut))
319 }
320 _ => unreachable!(),
321 };
322 }
323}
324
325impl ObjectWriter {
326 pub async fn new(object_store: &LanceObjectStore, path: &Path) -> Result<Self> {
329 Self::with_upload_parallelism(object_store, path, max_upload_parallelism())
330 }
331
332 fn with_upload_parallelism(
333 object_store: &LanceObjectStore,
334 path: &Path,
335 upload_parallelism: usize,
336 ) -> Result<Self> {
337 let upload_parallelism = NonZeroUsize::new(upload_parallelism).ok_or_else(|| {
338 Error::invalid_input(format!(
339 "LANCE_UPLOAD_CONCURRENCY must be greater than zero, got {upload_parallelism}"
340 ))
341 })?;
342 Ok(Self {
343 state: UploadState::Started(object_store.inner.clone()),
344 cursor: 0,
345 upload_parallelism,
346 path: Arc::new(path.clone()),
347 buffer: Vec::with_capacity(initial_upload_size()),
348 use_constant_size_upload_parts: object_store.use_constant_size_upload_parts,
349 })
350 }
351
352 fn next_part_buffer(buffer: &mut Vec<u8>, part_idx: u16, constant_upload_size: bool) -> Bytes {
355 let new_capacity = if constant_upload_size {
356 initial_upload_size()
358 } else {
359 initial_upload_size().max(((part_idx / 100) as usize + 1) * INITIAL_UPLOAD_STEP)
361 };
362 let new_buffer = Vec::with_capacity(new_capacity);
363 let part = std::mem::replace(buffer, new_buffer);
364 Bytes::from(part)
365 }
366
367 fn put_part(
368 upload: &mut dyn MultipartUpload,
369 buffer: Bytes,
370 part: PartUpload,
371 ) -> BoxFuture<'static, UploadResult<()>> {
372 let body_size = buffer.len();
373 log::debug!("MultipartUpload submitting part with {} bytes", body_size);
374 let queued_at = Instant::now();
377 let fut = upload.put_part(buffer.into());
378 Box::pin(async move {
379 fut.await.map_err(|source| {
380 let PartUpload {
381 path,
382 part_idx,
383 parts_in_flight,
384 part_size,
385 } = part;
386 UploadFailure::new(
387 format!(
388 "multipart upload of part {part_idx} of {path} failed after {:?} \
389 ({body_size} bytes, part_size={part_size} bytes, \
390 parts_in_flight={parts_in_flight} at submission, {})",
391 queued_at.elapsed(),
392 upload_settings()
393 ),
394 source,
395 )
396 })
397 })
398 }
399
400 fn poll_tasks(
401 mut self: Pin<&mut Self>,
402 cx: &mut std::task::Context<'_>,
403 ) -> std::result::Result<(), io::Error> {
404 let mut_self = &mut *self;
405 loop {
406 match &mut mut_self.state {
407 UploadState::Started(_) | UploadState::Done(_) => break,
408 UploadState::CreatingUpload(fut) => match fut.poll_unpin(cx) {
409 Poll::Ready(Ok(mut upload)) => {
410 let mut futures = JoinSet::new();
411
412 let part_size = mut_self.buffer.capacity();
415 let data = Self::next_part_buffer(
416 &mut mut_self.buffer,
417 0,
418 mut_self.use_constant_size_upload_parts,
419 );
420 futures.spawn(Self::put_part(
421 upload.as_mut(),
422 data,
423 PartUpload {
424 path: mut_self.path.clone(),
425 part_idx: 0,
426 parts_in_flight: 1,
427 part_size,
428 },
429 ));
430
431 mut_self.state = UploadState::InProgress {
432 part_idx: 1, futures,
434 upload,
435 };
436 }
437 Poll::Ready(Err(err)) => return Err(err.into_io_error()),
438 Poll::Pending => break,
439 },
440 UploadState::InProgress { futures, .. } => {
441 while let Poll::Ready(Some(res)) = futures.poll_join_next(cx) {
442 match res {
443 Ok(Ok(())) => {}
444 Err(err) => {
445 return Err(UploadFailure::from_task(
446 format!(
447 "multipart upload task for {} did not complete",
448 mut_self.path
449 ),
450 err,
451 )
452 .into_io_error());
453 }
454 Ok(Err(err)) => return Err(err.into_io_error()),
455 }
456 }
457 break;
458 }
459 UploadState::PuttingSingle(fut) | UploadState::Completing(fut) => {
460 match fut.poll_unpin(cx) {
461 Poll::Ready(Ok(mut res)) => {
462 res.size = mut_self.cursor;
463 mut_self.state = UploadState::Done(res)
464 }
465 Poll::Ready(Err(err)) => return Err(err.into_io_error()),
466 Poll::Pending => break,
467 }
468 }
469 }
470 }
471 Ok(())
472 }
473
474 pub async fn abort(&mut self) {
475 let state = std::mem::replace(&mut self.state, UploadState::Done(WriteResult::default()));
476 if let UploadState::InProgress { mut upload, .. } = state {
477 let _ = upload.abort().await;
478 }
479 }
480}
481
482impl Drop for ObjectWriter {
483 fn drop(&mut self) {
484 if matches!(self.state, UploadState::InProgress { .. }) {
486 let state =
488 std::mem::replace(&mut self.state, UploadState::Done(WriteResult::default()));
489 if let UploadState::InProgress { mut upload, .. } = state
490 && let Ok(handle) = Handle::try_current()
491 {
492 handle.spawn(async move {
493 let _ = upload.abort().await;
494 });
495 }
496 }
497 }
498}
499
500impl AsyncWrite for ObjectWriter {
501 fn poll_write(
502 mut self: std::pin::Pin<&mut Self>,
503 cx: &mut std::task::Context<'_>,
504 buf: &[u8],
505 ) -> std::task::Poll<std::result::Result<usize, std::io::Error>> {
506 self.as_mut().poll_tasks(cx)?;
507
508 let remaining_capacity = self.buffer.capacity() - self.buffer.len();
510 let bytes_to_write = std::cmp::min(remaining_capacity, buf.len());
511 self.buffer.extend_from_slice(&buf[..bytes_to_write]);
512 self.cursor += bytes_to_write;
513
514 let mut_self = &mut *self;
517
518 if mut_self.buffer.capacity() == mut_self.buffer.len() {
520 match &mut mut_self.state {
521 UploadState::Started(store) => {
522 let path = mut_self.path.clone();
523 let store = store.clone();
524 let started_at = Instant::now();
525 let fut = Box::pin(async move {
526 store.put_multipart(path.as_ref()).await.map_err(|source| {
527 UploadFailure::new(
528 format!(
529 "failed to create multipart upload for {path} after {:?} ({})",
530 started_at.elapsed(),
531 upload_settings()
532 ),
533 source,
534 )
535 })
536 });
537 self.state = UploadState::CreatingUpload(fut);
538 }
539 UploadState::InProgress {
541 upload,
542 part_idx,
543 futures,
544 ..
545 } if futures.len() < mut_self.upload_parallelism.get() => {
546 let part_size = mut_self.buffer.capacity();
550 let data = Self::next_part_buffer(
551 &mut mut_self.buffer,
552 *part_idx,
553 mut_self.use_constant_size_upload_parts,
554 );
555 let part = PartUpload {
556 path: mut_self.path.clone(),
557 part_idx: *part_idx,
558 parts_in_flight: futures.len() + 1,
559 part_size,
560 };
561 futures.spawn(
562 Self::put_part(upload.as_mut(), data, part)
563 .instrument(tracing::Span::current()),
564 );
565 *part_idx += 1;
566 }
567 _ => {}
568 }
569 }
570
571 self.poll_tasks(cx)?;
572
573 match bytes_to_write {
574 0 => Poll::Pending,
575 _ => Poll::Ready(Ok(bytes_to_write)),
576 }
577 }
578
579 fn poll_flush(
580 mut self: std::pin::Pin<&mut Self>,
581 cx: &mut std::task::Context<'_>,
582 ) -> std::task::Poll<std::result::Result<(), std::io::Error>> {
583 self.as_mut().poll_tasks(cx)?;
584
585 match &self.state {
586 UploadState::Started(_) | UploadState::Done(_) => Poll::Ready(Ok(())),
587 UploadState::CreatingUpload(_)
588 | UploadState::Completing(_)
589 | UploadState::PuttingSingle(_) => Poll::Pending,
590 UploadState::InProgress { .. } => Poll::Ready(Ok(())),
600 }
601 }
602
603 fn poll_shutdown(
604 mut self: std::pin::Pin<&mut Self>,
605 cx: &mut std::task::Context<'_>,
606 ) -> std::task::Poll<std::result::Result<(), std::io::Error>> {
607 loop {
608 self.as_mut().poll_tasks(cx)?;
609
610 let mut_self = &mut *self;
613 match &mut mut_self.state {
614 UploadState::Done(_) => return Poll::Ready(Ok(())),
615 UploadState::CreatingUpload(_)
616 | UploadState::PuttingSingle(_)
617 | UploadState::Completing(_) => return Poll::Pending,
618 UploadState::Started(_) => {
619 let part = std::mem::take(&mut mut_self.buffer);
621 let path = mut_self.path.clone();
622 self.state.started_to_putting_single(path, part);
623 }
624 UploadState::InProgress {
625 upload,
626 futures,
627 part_idx,
628 } => {
629 if !mut_self.buffer.is_empty()
631 && futures.len() < mut_self.upload_parallelism.get()
632 {
633 let part_size = mut_self.buffer.capacity();
635 let data = Bytes::from(std::mem::take(&mut mut_self.buffer));
636 let part = PartUpload {
637 path: mut_self.path.clone(),
638 part_idx: *part_idx,
639 parts_in_flight: futures.len() + 1,
640 part_size,
641 };
642 *part_idx += 1;
645 futures.spawn(
646 Self::put_part(upload.as_mut(), data, part)
647 .instrument(tracing::Span::current()),
648 );
649 continue;
652 }
653
654 if futures.is_empty() {
656 if !mut_self.buffer.is_empty() {
659 return Poll::Ready(Err(io::Error::other(format!(
660 "cannot complete multipart upload of {}: {} bytes remain buffered \
661 with no outstanding parts (LANCE_UPLOAD_CONCURRENCY={})",
662 mut_self.path,
663 mut_self.buffer.len(),
664 mut_self.upload_parallelism
665 ))));
666 }
667 let path = mut_self.path.clone();
668 let bytes_written = mut_self.cursor;
669 self.state.in_progress_to_completing(path, bytes_written);
670 } else {
671 return Poll::Pending;
672 }
673 }
674 }
675 }
676 }
677}
678
679#[async_trait]
680impl Writer for ObjectWriter {
681 async fn tell(&mut self) -> Result<usize> {
682 Ok(self.cursor)
683 }
684
685 async fn shutdown(&mut self) -> Result<WriteResult> {
686 AsyncWriteExt::shutdown(self).await?;
690 if let UploadState::Done(result) = &self.state {
691 Ok(result.clone())
692 } else {
693 unreachable!()
694 }
695 }
696}
697
698pub struct LocalWriter {
699 path: Path,
700 state: LocalWriteState,
701}
702
703#[derive(Default)]
704enum LocalWriteState {
705 Writing(Box<WritingState>),
706 Finishing {
707 size: usize,
708 future: BoxFuture<'static, Result<WriteResult>>,
709 },
710 Done(WriteResult),
711 #[default]
712 Poisoned,
713}
714
715struct WritingState {
716 writer: tokio::io::BufWriter<tokio::fs::File>,
717 cursor: usize,
718 temp_path: tempfile::TempPath,
720 io_tracker: Arc<IOTracker>,
721 metrics: IoMetricsGuard,
725}
726
727impl LocalWriter {
728 pub fn new(
729 file: tokio::fs::File,
730 path: Path,
731 temp_path: tempfile::TempPath,
732 io_tracker: Arc<IOTracker>,
733 ) -> Self {
734 Self {
735 path,
736 state: LocalWriteState::Writing(Box::new(WritingState {
737 writer: tokio::io::BufWriter::new(file),
738 cursor: 0,
739 temp_path,
740 metrics: io_tracker.begin_io("put"),
741 io_tracker,
742 })),
743 }
744 }
745
746 fn already_closed_err(path: &Path) -> io::Error {
747 io::Error::other(format!(
748 "cannot write to LocalWriter for {} after shutdown",
749 path
750 ))
751 }
752
753 fn poisoned_err(path: &Path) -> io::Error {
754 io::Error::other(format!("LocalWriter for {} is in poisoned state", path))
755 }
756
757 async fn persist(
758 temp_path: tempfile::TempPath,
759 final_path: Path,
760 size: usize,
761 io_tracker: Arc<IOTracker>,
762 metrics: IoMetricsGuard,
763 ) -> Result<WriteResult> {
764 let local_path = crate::local::to_local_path(&final_path);
765 let persisted = tokio::task::spawn_blocking(move || -> Result<String> {
766 temp_path.persist(&local_path).map_err(|e| {
767 Error::io(format!(
768 "failed to persist temp file to {}: {}",
769 local_path, e.error
770 ))
771 })?;
772
773 let metadata = std::fs::metadata(&local_path).map_err(|e| {
774 Error::io(format!("failed to read metadata for {}: {}", local_path, e))
775 })?;
776 Ok(get_etag(&metadata))
777 })
778 .await
779 .map_err(|e| Error::io(format!("spawn_blocking failed: {}", e)))
780 .and_then(|e_tag| e_tag);
781
782 metrics.record(&persisted, size as u64);
783 let e_tag = persisted?;
784
785 io_tracker.record_write("put", final_path, size as u64);
786
787 Ok(WriteResult {
788 size,
789 e_tag: Some(e_tag),
790 })
791 }
792}
793
794impl AsyncWrite for LocalWriter {
795 fn poll_write(
796 mut self: Pin<&mut Self>,
797 cx: &mut std::task::Context<'_>,
798 buf: &[u8],
799 ) -> Poll<std::result::Result<usize, std::io::Error>> {
800 if let LocalWriteState::Writing(state) = &mut self.state {
801 let poll = Pin::new(&mut state.writer).poll_write(cx, buf);
802 if let Poll::Ready(Ok(n)) = &poll {
803 state.cursor += *n;
804 }
805 poll
806 } else {
807 Poll::Ready(Err(Self::already_closed_err(&self.path)))
808 }
809 }
810
811 fn poll_flush(
812 mut self: Pin<&mut Self>,
813 cx: &mut std::task::Context<'_>,
814 ) -> Poll<std::result::Result<(), std::io::Error>> {
815 if let LocalWriteState::Writing(state) = &mut self.state {
816 Pin::new(&mut state.writer).poll_flush(cx)
817 } else {
818 Poll::Ready(Err(Self::already_closed_err(&self.path)))
819 }
820 }
821
822 fn poll_shutdown(
823 mut self: Pin<&mut Self>,
824 cx: &mut std::task::Context<'_>,
825 ) -> Poll<std::result::Result<(), std::io::Error>> {
826 let mut_self = &mut *self;
827 loop {
828 match &mut mut_self.state {
829 LocalWriteState::Writing(state) => {
830 if Pin::new(&mut state.writer).poll_shutdown(cx).is_pending() {
831 return Poll::Pending;
832 }
833
834 let LocalWriteState::Writing(state) =
836 std::mem::replace(&mut mut_self.state, LocalWriteState::Poisoned)
837 else {
838 unreachable!()
839 };
840 let size = state.cursor;
841 mut_self.state = LocalWriteState::Finishing {
842 size,
843 future: Box::pin(Self::persist(
844 state.temp_path,
845 mut_self.path.clone(),
846 size,
847 state.io_tracker,
848 state.metrics,
849 )),
850 };
851 }
852 LocalWriteState::Finishing { future, .. } => match future.poll_unpin(cx) {
853 Poll::Ready(Ok(result)) => mut_self.state = LocalWriteState::Done(result),
854 Poll::Ready(Err(e)) => {
855 return Poll::Ready(Err(io::Error::other(e)));
856 }
857 Poll::Pending => return Poll::Pending,
858 },
859 LocalWriteState::Done(_) => return Poll::Ready(Ok(())),
860 LocalWriteState::Poisoned => {
861 return Poll::Ready(Err(Self::poisoned_err(&self.path)));
862 }
863 }
864 }
865 }
866}
867
868#[async_trait]
869impl Writer for LocalWriter {
870 async fn tell(&mut self) -> Result<usize> {
871 match &mut self.state {
872 LocalWriteState::Writing(state) => Ok(state.cursor),
873 LocalWriteState::Finishing { size, .. } => Ok(*size),
874 LocalWriteState::Done(result) => Ok(result.size),
875 LocalWriteState::Poisoned => Err(Self::poisoned_err(&self.path).into()),
876 }
877 }
878
879 async fn shutdown(&mut self) -> Result<WriteResult> {
880 AsyncWriteExt::shutdown(self).await.map_err(|e| {
881 Error::io(format!(
882 "failed to shutdown local writer for {}: {}",
883 self.path, e
884 ))
885 })?;
886
887 match &self.state {
888 LocalWriteState::Done(result) => Ok(result.clone()),
889 _ => unreachable!(),
890 }
891 }
892}
893
894pub fn get_etag(metadata: &std::fs::Metadata) -> String {
896 let inode = get_inode(metadata);
897 let size = metadata.len();
898 let mtime = metadata
899 .modified()
900 .ok()
901 .and_then(|mtime| mtime.duration_since(std::time::SystemTime::UNIX_EPOCH).ok())
902 .unwrap_or_default()
903 .as_micros();
904
905 format!("{inode:x}-{mtime:x}-{size:x}")
908}
909
910#[cfg(unix)]
911fn get_inode(metadata: &std::fs::Metadata) -> u64 {
912 std::os::unix::fs::MetadataExt::ino(metadata)
913}
914
915#[cfg(not(unix))]
916fn get_inode(_metadata: &std::fs::Metadata) -> u64 {
917 0
918}
919
920#[cfg(test)]
921mod tests {
922 use futures::stream::BoxStream;
923 use object_store::{
924 CopyOptions, GetOptions, GetResult, ListResult, ObjectMeta, ObjectStoreExt,
925 PutMultipartOptions, PutOptions, PutPayload, PutResult, RenameOptions, Result as OSResult,
926 UploadPart,
927 };
928 use rstest::rstest;
929 use std::sync::Mutex;
930 use std::time::Duration;
931 use tokio::io::AsyncWriteExt;
932 use tokio::sync::Semaphore;
933
934 use super::*;
935
936 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
938 enum FailAt {
939 Nothing,
940 CreateMultipart,
941 PutPart,
942 Complete,
943 SinglePut,
944 }
945
946 #[derive(Debug)]
949 struct UploadObservations {
950 started: Semaphore,
955 parts: Mutex<Vec<(usize, Vec<u8>)>>,
958 }
959
960 impl Default for UploadObservations {
961 fn default() -> Self {
962 Self {
963 started: Semaphore::new(0),
964 parts: Mutex::new(Vec::new()),
965 }
966 }
967 }
968
969 fn rejected(stage: &'static str) -> object_store::Error {
970 object_store::Error::Generic {
971 store: "FailingUploadStore",
972 source: format!("{stage} rejected by test").into(),
973 }
974 }
975
976 #[derive(Debug)]
977 struct FailingUpload {
978 fail_at: FailAt,
979 gate: Option<Arc<Semaphore>>,
982 observations: Arc<UploadObservations>,
983 next_part: usize,
984 }
985
986 #[async_trait]
987 impl MultipartUpload for FailingUpload {
988 fn put_part(&mut self, data: PutPayload) -> UploadPart {
989 let fails = self.fail_at == FailAt::PutPart;
990 let part_idx = self.next_part;
991 self.next_part += 1;
992 let gate = self.gate.clone();
993 let observations = self.observations.clone();
994 Box::pin(async move {
995 observations.started.add_permits(1);
996 if let Some(gate) = gate {
997 gate.acquire_owned().await.unwrap().forget();
1000 }
1001 if fails {
1002 return Err(rejected("part"));
1003 }
1004 let body = data
1005 .iter()
1006 .flat_map(|chunk| chunk.iter().copied())
1007 .collect();
1008 observations.parts.lock().unwrap().push((part_idx, body));
1009 Ok(())
1010 })
1011 }
1012
1013 async fn complete(&mut self) -> OSResult<PutResult> {
1014 if self.fail_at == FailAt::Complete {
1015 Err(rejected("complete"))
1016 } else {
1017 Ok(PutResult {
1018 e_tag: None,
1019 version: None,
1020 extensions: Default::default(),
1021 })
1022 }
1023 }
1024
1025 async fn abort(&mut self) -> OSResult<()> {
1026 Ok(())
1027 }
1028 }
1029
1030 #[derive(Debug)]
1033 struct FailingUploadStore {
1034 fail_at: FailAt,
1035 gate: Option<Arc<Semaphore>>,
1036 observations: Arc<UploadObservations>,
1037 }
1038
1039 impl FailingUploadStore {
1040 fn new(fail_at: FailAt) -> Self {
1041 Self {
1042 fail_at,
1043 gate: None,
1044 observations: Arc::new(UploadObservations::default()),
1045 }
1046 }
1047
1048 fn gated(fail_at: FailAt) -> (Self, Arc<Semaphore>) {
1051 let gate = Arc::new(Semaphore::new(0));
1052 let store = Self {
1053 fail_at,
1054 gate: Some(gate.clone()),
1055 observations: Arc::new(UploadObservations::default()),
1056 };
1057 (store, gate)
1058 }
1059 }
1060
1061 impl std::fmt::Display for FailingUploadStore {
1062 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1063 write!(f, "FailingUploadStore")
1064 }
1065 }
1066
1067 #[async_trait]
1068 impl ObjectStore for FailingUploadStore {
1069 async fn put_opts(
1070 &self,
1071 _location: &Path,
1072 _bytes: PutPayload,
1073 _opts: PutOptions,
1074 ) -> OSResult<PutResult> {
1075 if self.fail_at == FailAt::SinglePut {
1076 Err(rejected("single put"))
1077 } else {
1078 Ok(PutResult {
1079 e_tag: None,
1080 version: None,
1081 extensions: Default::default(),
1082 })
1083 }
1084 }
1085
1086 async fn put_multipart_opts(
1087 &self,
1088 _location: &Path,
1089 _opts: PutMultipartOptions,
1090 ) -> OSResult<Box<dyn MultipartUpload>> {
1091 if self.fail_at == FailAt::CreateMultipart {
1092 Err(rejected("create multipart"))
1093 } else {
1094 Ok(Box::new(FailingUpload {
1095 fail_at: self.fail_at,
1096 gate: self.gate.clone(),
1097 observations: self.observations.clone(),
1098 next_part: 0,
1099 }))
1100 }
1101 }
1102
1103 async fn get_opts(&self, _location: &Path, _options: GetOptions) -> OSResult<GetResult> {
1104 unimplemented!()
1105 }
1106
1107 fn delete_stream(
1108 &self,
1109 _locations: BoxStream<'static, OSResult<Path>>,
1110 ) -> BoxStream<'static, OSResult<Path>> {
1111 unimplemented!()
1112 }
1113
1114 fn list(&self, _prefix: Option<&Path>) -> BoxStream<'static, OSResult<ObjectMeta>> {
1115 unimplemented!()
1116 }
1117
1118 fn list_with_offset(
1119 &self,
1120 _prefix: Option<&Path>,
1121 _offset: &Path,
1122 ) -> BoxStream<'static, OSResult<ObjectMeta>> {
1123 unimplemented!()
1124 }
1125
1126 async fn list_with_delimiter(&self, _prefix: Option<&Path>) -> OSResult<ListResult> {
1127 unimplemented!()
1128 }
1129
1130 async fn copy_opts(&self, _from: &Path, _to: &Path, _opts: CopyOptions) -> OSResult<()> {
1131 unimplemented!()
1132 }
1133
1134 async fn rename_opts(
1135 &self,
1136 _from: &Path,
1137 _to: &Path,
1138 _opts: RenameOptions,
1139 ) -> OSResult<()> {
1140 unimplemented!()
1141 }
1142 }
1143
1144 const FAILING_UPLOAD_PATH: &str = "part_7_invert.lance";
1145
1146 fn two_parts() -> usize {
1150 initial_upload_size() * 2
1151 }
1152
1153 async fn failing_upload(fail_at: FailAt, num_bytes: usize) -> io::Error {
1157 let mut store = LanceObjectStore::memory();
1158 store.inner = Arc::new(FailingUploadStore::new(fail_at));
1159
1160 let mut writer = ObjectWriter::new(&store, &Path::from(FAILING_UPLOAD_PATH))
1161 .await
1162 .unwrap();
1163 let buf = vec![0u8; num_bytes];
1164 match writer.write_all(buf.as_slice()).await {
1165 Err(err) => err,
1166 Ok(()) => AsyncWriteExt::shutdown(&mut writer)
1167 .await
1168 .expect_err("upload should have failed"),
1169 }
1170 }
1171
1172 #[tokio::test]
1173 async fn test_part_upload_failure_reports_upload_context() {
1174 let err = failing_upload(FailAt::PutPart, two_parts()).await;
1175 let message = err.to_string();
1176
1177 assert!(
1178 message.contains("multipart upload of part"),
1179 "should name the failing stage: {message}"
1180 );
1181 assert!(
1182 message.contains(FAILING_UPLOAD_PATH),
1183 "should name the object: {message}"
1184 );
1185 assert!(
1186 message.contains(&format!("{} bytes", initial_upload_size())),
1187 "should report the body size: {message}"
1188 );
1189 assert!(
1190 message.contains(&format!("part_size={} bytes", initial_upload_size())),
1191 "should report the part size in effect: {message}"
1192 );
1193 assert!(
1194 message.contains("parts_in_flight="),
1195 "should report upload concurrency in use: {message}"
1196 );
1197 assert!(
1198 message.contains("LANCE_INITIAL_UPLOAD_SIZE")
1199 && message.contains("LANCE_UPLOAD_CONCURRENCY"),
1200 "should report the knobs governing the request: {message}"
1201 );
1202 assert!(
1203 message.contains("part rejected by test"),
1204 "should keep the underlying object store error: {message}"
1205 );
1206 }
1207
1208 #[tokio::test]
1211 async fn test_part_upload_failure_reports_elapsed_time() {
1212 let err = failing_upload(FailAt::PutPart, two_parts()).await;
1213 let message = err.to_string();
1214 assert!(
1215 message.contains("failed after"),
1216 "should report how long the request took: {message}"
1217 );
1218 }
1219
1220 #[test]
1228 fn test_part_buffer_capacity_tracks_grown_part_size() {
1229 let mut buffer = Vec::<u8>::with_capacity(initial_upload_size());
1230 assert_eq!(buffer.capacity(), initial_upload_size());
1231
1232 let _ = ObjectWriter::next_part_buffer(&mut buffer, 0, false);
1233 assert_eq!(
1234 buffer.capacity(),
1235 initial_upload_size(),
1236 "early parts stay at the configured size"
1237 );
1238
1239 let _ = ObjectWriter::next_part_buffer(&mut buffer, 100, false);
1240 assert_eq!(
1241 buffer.capacity(),
1242 initial_upload_size().max(2 * INITIAL_UPLOAD_STEP),
1243 "the part size has grown past the first step"
1244 );
1245
1246 let _ = ObjectWriter::next_part_buffer(&mut buffer, 100, true);
1249 assert_eq!(buffer.capacity(), initial_upload_size());
1250 }
1251
1252 #[tokio::test]
1253 async fn test_part_upload_failure_preserves_source_chain() {
1254 let err = failing_upload(FailAt::PutPart, two_parts()).await;
1255
1256 let failure = err
1257 .get_ref()
1258 .expect("io error should carry the upload failure");
1259 let source = std::error::Error::source(failure)
1260 .expect("upload failure should expose the object store error");
1261 assert!(
1262 source.downcast_ref::<object_store::Error>().is_some(),
1263 "source should still be the object store error, got: {source}"
1264 );
1265 }
1266
1267 #[test]
1270 fn test_upload_failure_preserves_error_kind() {
1271 fn not_found() -> object_store::Error {
1272 object_store::Error::NotFound {
1273 path: FAILING_UPLOAD_PATH.to_string(),
1274 source: "not found".into(),
1275 }
1276 }
1277
1278 let unwrapped = io::Error::from(not_found());
1279 assert_eq!(unwrapped.kind(), io::ErrorKind::NotFound);
1280
1281 let wrapped =
1282 UploadFailure::new("part upload failed".to_string(), not_found()).into_io_error();
1283 assert_eq!(wrapped.kind(), unwrapped.kind());
1284 }
1285
1286 #[tokio::test]
1290 async fn test_writer_shutdown_preserves_object_store_source() {
1291 let mut store = LanceObjectStore::memory();
1292 store.inner = Arc::new(FailingUploadStore::new(FailAt::SinglePut));
1293 let mut writer = ObjectWriter::new(&store, &Path::from(FAILING_UPLOAD_PATH))
1294 .await
1295 .unwrap();
1296 writer.write_all(&[0u8; 256]).await.unwrap();
1297 let err = Writer::shutdown(&mut writer).await.unwrap_err();
1298
1299 let mut current: Option<&(dyn std::error::Error + 'static)> = Some(&err);
1300 let mut found_object_store = false;
1301 while let Some(source) = current {
1302 if source.downcast_ref::<object_store::Error>().is_some() {
1303 found_object_store = true;
1304 break;
1305 }
1306 current = source.source();
1307 }
1308 assert!(found_object_store, "source chain was flattened: {err:?}");
1309
1310 assert!(
1311 err.to_string().contains(FAILING_UPLOAD_PATH),
1312 "should still name the object: {err}"
1313 );
1314 }
1315
1316 #[tokio::test]
1317 async fn test_create_multipart_failure_reports_upload_context() {
1318 let err = failing_upload(FailAt::CreateMultipart, two_parts()).await;
1319 let message = err.to_string();
1320
1321 assert!(
1322 message.contains("failed to create multipart upload for"),
1323 "should name the failing stage: {message}"
1324 );
1325 assert!(
1326 message.contains(FAILING_UPLOAD_PATH),
1327 "should name the object: {message}"
1328 );
1329 assert!(
1330 message.contains("create multipart rejected by test"),
1331 "should keep the underlying object store error: {message}"
1332 );
1333 }
1334
1335 #[tokio::test]
1336 async fn test_complete_multipart_failure_reports_upload_context() {
1337 let num_bytes = two_parts();
1338 let err = failing_upload(FailAt::Complete, num_bytes).await;
1339 let message = err.to_string();
1340
1341 assert!(
1342 message.contains("completing multipart upload of"),
1343 "should name the failing stage: {message}"
1344 );
1345 assert!(
1346 message.contains(&format!("{num_bytes} bytes")),
1347 "should report how much had been written: {message}"
1348 );
1349 assert!(
1350 message.contains("complete rejected by test"),
1351 "should keep the underlying object store error: {message}"
1352 );
1353 }
1354
1355 #[tokio::test]
1356 async fn test_single_put_failure_reports_upload_context() {
1357 let err = failing_upload(FailAt::SinglePut, 256).await;
1359 let message = err.to_string();
1360
1361 assert!(
1362 message.contains("single PUT of"),
1363 "should name the failing stage: {message}"
1364 );
1365 assert!(
1366 message.contains(FAILING_UPLOAD_PATH),
1367 "should name the object: {message}"
1368 );
1369 assert!(
1370 message.contains("256 bytes"),
1371 "should report the body size: {message}"
1372 );
1373 assert!(
1374 message.contains("single put rejected by test"),
1375 "should keep the underlying object store error: {message}"
1376 );
1377 }
1378
1379 const GATE_RELEASE: usize = 64;
1382
1383 const FLUSH_BOUND: Duration = Duration::from_secs(10);
1387
1388 async fn await_part_in_flight(observations: &UploadObservations) {
1391 tokio::time::timeout(FLUSH_BOUND, observations.started.acquire())
1392 .await
1393 .expect("a part upload should have started")
1394 .unwrap()
1395 .forget();
1396 }
1397
1398 #[tokio::test]
1399 async fn test_flush_does_not_wait_for_in_flight_parts() {
1400 let (store, gate) = FailingUploadStore::gated(FailAt::Nothing);
1401 let observations = store.observations.clone();
1402 let mut lance_store = LanceObjectStore::memory();
1403 lance_store.inner = Arc::new(store);
1404
1405 let mut writer = ObjectWriter::new(&lance_store, &Path::from("gated.lance"))
1406 .await
1407 .unwrap();
1408 let payload = (0..two_parts()).map(|i| i as u8).collect::<Vec<_>>();
1410 writer.write_all(payload.as_slice()).await.unwrap();
1411 await_part_in_flight(&observations).await;
1412
1413 tokio::time::timeout(FLUSH_BOUND, AsyncWriteExt::flush(&mut writer))
1414 .await
1415 .expect("flush must not wait for in-flight part uploads")
1416 .unwrap();
1417
1418 assert!(
1419 observations.parts.lock().unwrap().is_empty(),
1420 "no gated part may have completed before the gate opened"
1421 );
1422
1423 gate.add_permits(GATE_RELEASE);
1424 let result = Writer::shutdown(&mut writer).await.unwrap();
1425 assert_eq!(result.size, payload.len());
1426
1427 let mut parts = observations.parts.lock().unwrap().clone();
1428 parts.sort_by_key(|(part_idx, _)| *part_idx);
1429 let assembled = parts
1430 .into_iter()
1431 .flat_map(|(_, body)| body)
1432 .collect::<Vec<_>>();
1433 assert_eq!(
1434 assembled, payload,
1435 "parts must reassemble into the original bytes"
1436 );
1437 }
1438
1439 #[tokio::test]
1440 async fn test_part_failure_after_flush_surfaces_at_shutdown() {
1441 let (store, gate) = FailingUploadStore::gated(FailAt::PutPart);
1442 let observations = store.observations.clone();
1443 let mut lance_store = LanceObjectStore::memory();
1444 lance_store.inner = Arc::new(store);
1445
1446 let mut writer = ObjectWriter::new(&lance_store, &Path::from(FAILING_UPLOAD_PATH))
1447 .await
1448 .unwrap();
1449 writer
1450 .write_all(vec![0u8; two_parts()].as_slice())
1451 .await
1452 .unwrap();
1453 await_part_in_flight(&observations).await;
1454 AsyncWriteExt::flush(&mut writer).await.unwrap();
1456
1457 gate.add_permits(GATE_RELEASE);
1460 let err = AsyncWriteExt::shutdown(&mut writer)
1461 .await
1462 .expect_err("a failed part upload must still surface");
1463 let message = err.to_string();
1464 assert!(
1465 message.contains(FAILING_UPLOAD_PATH),
1466 "should name the object being written: {message}"
1467 );
1468 }
1469
1470 #[test]
1471 fn test_zero_upload_parallelism_rejected() {
1472 let store = LanceObjectStore::memory();
1473 let path = Path::from("zero-concurrency.lance");
1474 let error = ObjectWriter::with_upload_parallelism(&store, &path, 0)
1475 .err()
1476 .expect("zero concurrency must be rejected");
1477
1478 assert!(matches!(error, Error::InvalidInput { .. }));
1479 assert!(error.to_string().contains("LANCE_UPLOAD_CONCURRENCY"));
1480 assert!(error.to_string().contains("got 0"));
1481 }
1482
1483 #[rstest]
1484 #[case::empty(0, 0)]
1485 #[case::below_boundary(1, -1)]
1486 #[case::at_boundary(1, 0)]
1487 #[case::above_boundary(1, 16)]
1488 #[case::multipart_tail(2, 1)]
1489 #[tokio::test]
1490 async fn test_upload_parallelism_round_trip(
1491 #[case] full_parts: usize,
1492 #[case] tail: isize,
1493 #[values(Some(1), None)] upload_parallelism: Option<usize>,
1494 ) {
1495 let store = LanceObjectStore::memory();
1496 let path = Path::from("round-trip.lance");
1497 let mut writer = match upload_parallelism {
1498 Some(upload_parallelism) => {
1499 ObjectWriter::with_upload_parallelism(&store, &path, upload_parallelism).unwrap()
1500 }
1501 None => ObjectWriter::new(&store, &path).await.unwrap(),
1502 };
1503 let size = (initial_upload_size() * full_parts)
1504 .checked_add_signed(tail)
1505 .unwrap();
1506 let payload = (0..size).map(|i| i as u8).collect::<Vec<_>>();
1507
1508 let result = tokio::time::timeout(Duration::from_millis(500), async {
1509 writer.write_all(&payload).await.unwrap();
1510 Writer::shutdown(&mut writer).await.unwrap()
1511 })
1512 .await
1513 .expect("valid concurrency must not stall writes or shutdown");
1514 assert_eq!(result.size, payload.len());
1515 let persisted = store.inner.get(&path).await.unwrap().bytes().await.unwrap();
1516 assert_eq!(persisted.as_ref(), payload.as_slice());
1517 }
1518
1519 #[tokio::test]
1520 async fn test_shutdown_waits_for_tail_at_concurrency_one() {
1521 let (store, gate) = FailingUploadStore::gated(FailAt::Nothing);
1522 let observations = store.observations.clone();
1523 let mut lance_store = LanceObjectStore::memory();
1524 lance_store.inner = Arc::new(store);
1525 let mut writer =
1526 ObjectWriter::with_upload_parallelism(&lance_store, &Path::from("gated-tail.lance"), 1)
1527 .unwrap();
1528 let payload = (0..initial_upload_size() + 16)
1529 .map(|i| i as u8)
1530 .collect::<Vec<_>>();
1531 writer.write_all(&payload).await.unwrap();
1532 await_part_in_flight(&observations).await;
1533
1534 assert!(futures::poll!(Box::pin(AsyncWriteExt::shutdown(&mut writer))).is_pending());
1535 assert_eq!(writer.buffer.len(), 16);
1536 assert!(matches!(writer.state, UploadState::InProgress { .. }));
1537
1538 gate.add_permits(GATE_RELEASE);
1539 let result = Writer::shutdown(&mut writer).await.unwrap();
1540 assert_eq!(result.size, payload.len());
1541 let mut parts = observations.parts.lock().unwrap().clone();
1542 parts.sort_by_key(|(part_idx, _)| *part_idx);
1543 assert_eq!(parts.len(), 2);
1544 let assembled = parts
1545 .into_iter()
1546 .flat_map(|(_, body)| body)
1547 .collect::<Vec<_>>();
1548 assert_eq!(assembled, payload);
1549 }
1550
1551 #[tokio::test]
1552 async fn test_write() {
1553 let store = LanceObjectStore::memory();
1554
1555 let mut object_writer = ObjectWriter::new(&store, &Path::from("/foo"))
1556 .await
1557 .unwrap();
1558 assert_eq!(object_writer.tell().await.unwrap(), 0);
1559
1560 let buf = vec![0; 256];
1561 assert_eq!(object_writer.write(buf.as_slice()).await.unwrap(), 256);
1562 assert_eq!(object_writer.tell().await.unwrap(), 256);
1563
1564 assert_eq!(object_writer.write(buf.as_slice()).await.unwrap(), 256);
1565 assert_eq!(object_writer.tell().await.unwrap(), 512);
1566
1567 assert_eq!(object_writer.write(buf.as_slice()).await.unwrap(), 256);
1568 assert_eq!(object_writer.tell().await.unwrap(), 256 * 3);
1569
1570 let res = Writer::shutdown(&mut object_writer).await.unwrap();
1571 assert_eq!(res.size, 256 * 3);
1572
1573 let mut object_writer = ObjectWriter::new(&store, &Path::from("/bar"))
1575 .await
1576 .unwrap();
1577 let buf = vec![0; INITIAL_UPLOAD_STEP / 3 * 2];
1578 for i in 0..5 {
1579 object_writer.write_all(buf.as_slice()).await.unwrap();
1582 assert_eq!(object_writer.tell().await.unwrap(), (i + 1) * buf.len());
1584 }
1585 let res = Writer::shutdown(&mut object_writer).await.unwrap();
1586 assert_eq!(res.size, buf.len() * 5);
1587 }
1588
1589 #[tokio::test]
1590 async fn test_abort_write() {
1591 let store = LanceObjectStore::memory();
1592
1593 let mut object_writer = ObjectWriter::new(&store, &Path::from("/foo"))
1594 .await
1595 .unwrap();
1596 object_writer.abort().await;
1597 }
1598
1599 #[tokio::test]
1600 async fn test_local_writer_shutdown() {
1601 let tmp = lance_core::utils::tempfile::TempStdDir::default();
1602 let file_path = tmp.join("test_local_writer.bin");
1603 let os_path = Path::from_absolute_path(&file_path).unwrap();
1604 let io_tracker = Arc::new(IOTracker::default());
1605
1606 let named_temp = tempfile::NamedTempFile::new_in(&*tmp).unwrap();
1607 let temp_file_path = named_temp.path().to_owned();
1608 let (std_file, temp_path) = named_temp.into_parts();
1609 let file = tokio::fs::File::from_std(std_file);
1610 let mut writer = LocalWriter::new(file, os_path, temp_path, io_tracker.clone());
1611
1612 let data = b"hello local writer";
1613 writer.write_all(data).await.unwrap();
1614
1615 assert!(!file_path.exists());
1617 assert!(temp_file_path.exists());
1619
1620 let result = Writer::shutdown(&mut writer).await.unwrap();
1621 assert_eq!(result.size, data.len());
1622 assert!(result.e_tag.is_some());
1623 assert!(!result.e_tag.as_ref().unwrap().is_empty());
1624
1625 assert!(file_path.exists());
1627 assert!(!temp_file_path.exists());
1628
1629 let stats = io_tracker.stats();
1630 assert_eq!(stats.write_iops, 1);
1631 assert_eq!(stats.written_bytes, data.len() as u64);
1632 }
1633
1634 #[tokio::test]
1635 async fn test_local_writer_drop_cleans_up() {
1636 let tmp = lance_core::utils::tempfile::TempStdDir::default();
1637 let file_path = tmp.join("test_drop.bin");
1638 let os_path = Path::from_absolute_path(&file_path).unwrap();
1639 let io_tracker = Arc::new(IOTracker::default());
1640
1641 let named_temp = tempfile::NamedTempFile::new_in(&*tmp).unwrap();
1642 let temp_file_path = named_temp.path().to_owned();
1643 let (std_file, temp_path) = named_temp.into_parts();
1644 let file = tokio::fs::File::from_std(std_file);
1645 let mut writer = LocalWriter::new(file, os_path, temp_path, io_tracker);
1646
1647 writer.write_all(b"some data").await.unwrap();
1648 assert!(temp_file_path.exists());
1649
1650 drop(writer);
1652 assert!(!temp_file_path.exists());
1653 assert!(!file_path.exists());
1654 }
1655
1656 #[test]
1657 fn clamp_initial_upload_size_below_min_is_clamped_up() {
1658 assert_eq!(clamp_initial_upload_size(0), (INITIAL_UPLOAD_STEP, true));
1659 assert_eq!(
1660 clamp_initial_upload_size(INITIAL_UPLOAD_STEP - 1),
1661 (INITIAL_UPLOAD_STEP, true)
1662 );
1663 }
1664
1665 #[test]
1666 fn clamp_initial_upload_size_within_range_is_unchanged() {
1667 assert_eq!(
1668 clamp_initial_upload_size(INITIAL_UPLOAD_STEP),
1669 (INITIAL_UPLOAD_STEP, false)
1670 );
1671 assert_eq!(
1672 clamp_initial_upload_size(MAX_UPLOAD_PART_SIZE),
1673 (MAX_UPLOAD_PART_SIZE, false)
1674 );
1675 let mid = INITIAL_UPLOAD_STEP * 8; assert_eq!(clamp_initial_upload_size(mid), (mid, false));
1677 }
1678
1679 #[test]
1680 fn clamp_initial_upload_size_above_max_is_clamped_down() {
1681 assert_eq!(
1682 clamp_initial_upload_size(MAX_UPLOAD_PART_SIZE + 1),
1683 (MAX_UPLOAD_PART_SIZE, true)
1684 );
1685 assert_eq!(
1686 clamp_initial_upload_size(usize::MAX),
1687 (MAX_UPLOAD_PART_SIZE, true)
1688 );
1689 }
1690
1691 #[test]
1696 fn clamp_initial_upload_size_at_5gib_clamps_down() {
1697 let exactly_5_gib: usize = 5 * 1024 * 1024 * 1024;
1698 assert_eq!(
1699 clamp_initial_upload_size(exactly_5_gib),
1700 (MAX_UPLOAD_PART_SIZE, true)
1701 );
1702 }
1703}