1use std::io;
5use std::pin::Pin;
6use std::sync::{Arc, OnceLock};
7use std::task::Poll;
8use std::time::Instant;
9
10use crate::object_store::ObjectStore as LanceObjectStore;
11use async_trait::async_trait;
12use bytes::Bytes;
13use futures::FutureExt;
14use futures::future::BoxFuture;
15use object_store::{MultipartUpload, ObjectStoreExt};
16use object_store::{ObjectStore, path::Path};
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, IoMetricsGuard};
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
40const MAX_UPLOAD_PART_SIZE: usize = 1024 * 1024 * 1024 * 5 - 1;
46
47fn clamp_initial_upload_size(raw: usize) -> (usize, bool) {
50 let clamped = raw.clamp(INITIAL_UPLOAD_STEP, MAX_UPLOAD_PART_SIZE);
51 (clamped, clamped != raw)
52}
53
54fn initial_upload_size() -> usize {
55 static LANCE_INITIAL_UPLOAD_SIZE: OnceLock<usize> = OnceLock::new();
56 *LANCE_INITIAL_UPLOAD_SIZE.get_or_init(|| {
57 let Some(raw) = std::env::var("LANCE_INITIAL_UPLOAD_SIZE")
58 .ok()
59 .and_then(|s| s.parse::<usize>().ok())
60 else {
61 return INITIAL_UPLOAD_STEP;
62 };
63 let (clamped, was_clamped) = clamp_initial_upload_size(raw);
64 if was_clamped {
65 tracing::warn!(
67 requested = raw,
68 clamped,
69 "LANCE_INITIAL_UPLOAD_SIZE must be between 5MB and 5GB; clamping to valid range"
70 );
71 }
72 clamped
73 })
74}
75
76pub struct ObjectWriter {
89 state: UploadState,
90 path: Arc<Path>,
91 cursor: usize,
92 buffer: Vec<u8>,
93 use_constant_size_upload_parts: bool,
95}
96
97#[derive(Debug, Clone, Default)]
98pub struct WriteResult {
99 pub size: usize,
100 pub e_tag: Option<String>,
101}
102
103#[derive(Debug)]
114struct UploadFailure {
115 context: String,
116 kind: io::ErrorKind,
120 source: Box<dyn std::error::Error + Send + Sync>,
121}
122
123impl UploadFailure {
124 fn new(context: String, source: object_store::Error) -> Self {
131 let mapped = io::Error::from(source);
132 let kind = mapped.kind();
133 let source: Box<dyn std::error::Error + Send + Sync> =
134 match mapped.downcast::<object_store::Error>() {
135 Ok(source) => Box::new(source),
136 Err(mapped) => Box::new(mapped),
137 };
138 Self {
139 context,
140 kind,
141 source,
142 }
143 }
144
145 fn from_task(context: String, source: tokio::task::JoinError) -> Self {
147 Self {
148 context,
149 kind: io::ErrorKind::Other,
150 source: Box::new(source),
151 }
152 }
153
154 fn into_io_error(self) -> io::Error {
155 let kind = self.kind;
156 io::Error::new(kind, self)
157 }
158}
159
160impl std::fmt::Display for UploadFailure {
161 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
162 write!(f, "{}: {}", self.context, self.source)
163 }
164}
165
166impl std::error::Error for UploadFailure {
167 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
168 Some(self.source.as_ref())
169 }
170}
171
172type UploadResult<T> = std::result::Result<T, UploadFailure>;
173
174struct PartUpload {
176 path: Arc<Path>,
177 part_idx: u16,
178 parts_in_flight: usize,
180 part_size: usize,
187}
188
189fn upload_settings() -> String {
196 format!(
197 "LANCE_INITIAL_UPLOAD_SIZE={} bytes, LANCE_UPLOAD_CONCURRENCY={}",
198 initial_upload_size(),
199 max_upload_parallelism()
200 )
201}
202
203enum UploadState {
204 Started(Arc<dyn ObjectStore>),
207 CreatingUpload(BoxFuture<'static, UploadResult<Box<dyn MultipartUpload>>>),
209 InProgress {
211 part_idx: u16,
212 upload: Box<dyn MultipartUpload>,
213 futures: JoinSet<UploadResult<()>>,
214 },
215 PuttingSingle(BoxFuture<'static, UploadResult<WriteResult>>),
218 Completing(BoxFuture<'static, UploadResult<WriteResult>>),
220 Done(WriteResult),
222}
223
224impl UploadState {
226 fn started_to_putting_single(&mut self, path: Arc<Path>, buffer: Vec<u8>) {
227 let this = std::mem::replace(self, Self::Done(WriteResult::default()));
229 *self = match this {
230 Self::Started(store) => {
231 let started_at = Instant::now();
232 let fut = async move {
233 let size = buffer.len();
234 let res = store.put(&path, buffer.into()).await.map_err(|source| {
235 UploadFailure::new(
236 format!(
237 "single PUT of {path} failed after {:?} ({size} bytes, {})",
238 started_at.elapsed(),
239 upload_settings()
240 ),
241 source,
242 )
243 })?;
244 Ok(WriteResult {
245 size,
246 e_tag: res.e_tag,
247 })
248 };
249 Self::PuttingSingle(Box::pin(fut))
250 }
251 _ => unreachable!(),
252 }
253 }
254
255 fn in_progress_to_completing(&mut self, path: Arc<Path>, bytes_written: usize) {
256 let this = std::mem::replace(self, Self::Done(WriteResult::default()));
258 *self = match this {
259 Self::InProgress {
260 mut upload,
261 futures,
262 part_idx,
263 } => {
264 debug_assert!(futures.is_empty());
265 let started_at = Instant::now();
266 let fut = async move {
267 let res = upload.complete().await.map_err(|source| {
268 UploadFailure::new(
269 format!(
270 "completing multipart upload of {path} failed after {:?} \
271 ({part_idx} parts, {bytes_written} bytes, {})",
272 started_at.elapsed(),
273 upload_settings()
274 ),
275 source,
276 )
277 })?;
278 Ok(WriteResult {
279 size: 0, e_tag: res.e_tag,
281 })
282 };
283 Self::Completing(Box::pin(fut))
284 }
285 _ => unreachable!(),
286 };
287 }
288}
289
290impl ObjectWriter {
291 pub async fn new(object_store: &LanceObjectStore, path: &Path) -> Result<Self> {
292 Ok(Self {
293 state: UploadState::Started(object_store.inner.clone()),
294 cursor: 0,
295 path: Arc::new(path.clone()),
296 buffer: Vec::with_capacity(initial_upload_size()),
297 use_constant_size_upload_parts: object_store.use_constant_size_upload_parts,
298 })
299 }
300
301 fn next_part_buffer(buffer: &mut Vec<u8>, part_idx: u16, constant_upload_size: bool) -> Bytes {
304 let new_capacity = if constant_upload_size {
305 initial_upload_size()
307 } else {
308 initial_upload_size().max(((part_idx / 100) as usize + 1) * INITIAL_UPLOAD_STEP)
310 };
311 let new_buffer = Vec::with_capacity(new_capacity);
312 let part = std::mem::replace(buffer, new_buffer);
313 Bytes::from(part)
314 }
315
316 fn put_part(
317 upload: &mut dyn MultipartUpload,
318 buffer: Bytes,
319 part: PartUpload,
320 ) -> BoxFuture<'static, UploadResult<()>> {
321 let body_size = buffer.len();
322 log::debug!("MultipartUpload submitting part with {} bytes", body_size);
323 let queued_at = Instant::now();
326 let fut = upload.put_part(buffer.into());
327 Box::pin(async move {
328 fut.await.map_err(|source| {
329 let PartUpload {
330 path,
331 part_idx,
332 parts_in_flight,
333 part_size,
334 } = part;
335 UploadFailure::new(
336 format!(
337 "multipart upload of part {part_idx} of {path} failed after {:?} \
338 ({body_size} bytes, part_size={part_size} bytes, \
339 parts_in_flight={parts_in_flight} at submission, {})",
340 queued_at.elapsed(),
341 upload_settings()
342 ),
343 source,
344 )
345 })
346 })
347 }
348
349 fn poll_tasks(
350 mut self: Pin<&mut Self>,
351 cx: &mut std::task::Context<'_>,
352 ) -> std::result::Result<(), io::Error> {
353 let mut_self = &mut *self;
354 loop {
355 match &mut mut_self.state {
356 UploadState::Started(_) | UploadState::Done(_) => break,
357 UploadState::CreatingUpload(fut) => match fut.poll_unpin(cx) {
358 Poll::Ready(Ok(mut upload)) => {
359 let mut futures = JoinSet::new();
360
361 let part_size = mut_self.buffer.capacity();
364 let data = Self::next_part_buffer(
365 &mut mut_self.buffer,
366 0,
367 mut_self.use_constant_size_upload_parts,
368 );
369 futures.spawn(Self::put_part(
370 upload.as_mut(),
371 data,
372 PartUpload {
373 path: mut_self.path.clone(),
374 part_idx: 0,
375 parts_in_flight: 1,
376 part_size,
377 },
378 ));
379
380 mut_self.state = UploadState::InProgress {
381 part_idx: 1, futures,
383 upload,
384 };
385 }
386 Poll::Ready(Err(err)) => return Err(err.into_io_error()),
387 Poll::Pending => break,
388 },
389 UploadState::InProgress { futures, .. } => {
390 while let Poll::Ready(Some(res)) = futures.poll_join_next(cx) {
391 match res {
392 Ok(Ok(())) => {}
393 Err(err) => {
394 return Err(UploadFailure::from_task(
395 format!(
396 "multipart upload task for {} did not complete",
397 mut_self.path
398 ),
399 err,
400 )
401 .into_io_error());
402 }
403 Ok(Err(err)) => return Err(err.into_io_error()),
404 }
405 }
406 break;
407 }
408 UploadState::PuttingSingle(fut) | UploadState::Completing(fut) => {
409 match fut.poll_unpin(cx) {
410 Poll::Ready(Ok(mut res)) => {
411 res.size = mut_self.cursor;
412 mut_self.state = UploadState::Done(res)
413 }
414 Poll::Ready(Err(err)) => return Err(err.into_io_error()),
415 Poll::Pending => break,
416 }
417 }
418 }
419 }
420 Ok(())
421 }
422
423 pub async fn abort(&mut self) {
424 let state = std::mem::replace(&mut self.state, UploadState::Done(WriteResult::default()));
425 if let UploadState::InProgress { mut upload, .. } = state {
426 let _ = upload.abort().await;
427 }
428 }
429}
430
431impl Drop for ObjectWriter {
432 fn drop(&mut self) {
433 if matches!(self.state, UploadState::InProgress { .. }) {
435 let state =
437 std::mem::replace(&mut self.state, UploadState::Done(WriteResult::default()));
438 if let UploadState::InProgress { mut upload, .. } = state
439 && let Ok(handle) = Handle::try_current()
440 {
441 handle.spawn(async move {
442 let _ = upload.abort().await;
443 });
444 }
445 }
446 }
447}
448
449impl AsyncWrite for ObjectWriter {
450 fn poll_write(
451 mut self: std::pin::Pin<&mut Self>,
452 cx: &mut std::task::Context<'_>,
453 buf: &[u8],
454 ) -> std::task::Poll<std::result::Result<usize, std::io::Error>> {
455 self.as_mut().poll_tasks(cx)?;
456
457 let remaining_capacity = self.buffer.capacity() - self.buffer.len();
459 let bytes_to_write = std::cmp::min(remaining_capacity, buf.len());
460 self.buffer.extend_from_slice(&buf[..bytes_to_write]);
461 self.cursor += bytes_to_write;
462
463 let mut_self = &mut *self;
466
467 if mut_self.buffer.capacity() == mut_self.buffer.len() {
469 match &mut mut_self.state {
470 UploadState::Started(store) => {
471 let path = mut_self.path.clone();
472 let store = store.clone();
473 let started_at = Instant::now();
474 let fut = Box::pin(async move {
475 store.put_multipart(path.as_ref()).await.map_err(|source| {
476 UploadFailure::new(
477 format!(
478 "failed to create multipart upload for {path} after {:?} ({})",
479 started_at.elapsed(),
480 upload_settings()
481 ),
482 source,
483 )
484 })
485 });
486 self.state = UploadState::CreatingUpload(fut);
487 }
488 UploadState::InProgress {
490 upload,
491 part_idx,
492 futures,
493 ..
494 } if futures.len() < max_upload_parallelism() => {
495 let part_size = mut_self.buffer.capacity();
499 let data = Self::next_part_buffer(
500 &mut mut_self.buffer,
501 *part_idx,
502 mut_self.use_constant_size_upload_parts,
503 );
504 let part = PartUpload {
505 path: mut_self.path.clone(),
506 part_idx: *part_idx,
507 parts_in_flight: futures.len() + 1,
508 part_size,
509 };
510 futures.spawn(
511 Self::put_part(upload.as_mut(), data, part)
512 .instrument(tracing::Span::current()),
513 );
514 *part_idx += 1;
515 }
516 _ => {}
517 }
518 }
519
520 self.poll_tasks(cx)?;
521
522 match bytes_to_write {
523 0 => Poll::Pending,
524 _ => Poll::Ready(Ok(bytes_to_write)),
525 }
526 }
527
528 fn poll_flush(
529 mut self: std::pin::Pin<&mut Self>,
530 cx: &mut std::task::Context<'_>,
531 ) -> std::task::Poll<std::result::Result<(), std::io::Error>> {
532 self.as_mut().poll_tasks(cx)?;
533
534 match &self.state {
535 UploadState::Started(_) | UploadState::Done(_) => Poll::Ready(Ok(())),
536 UploadState::CreatingUpload(_)
537 | UploadState::Completing(_)
538 | UploadState::PuttingSingle(_) => Poll::Pending,
539 UploadState::InProgress { .. } => Poll::Ready(Ok(())),
549 }
550 }
551
552 fn poll_shutdown(
553 mut self: std::pin::Pin<&mut Self>,
554 cx: &mut std::task::Context<'_>,
555 ) -> std::task::Poll<std::result::Result<(), std::io::Error>> {
556 loop {
557 self.as_mut().poll_tasks(cx)?;
558
559 let mut_self = &mut *self;
562 match &mut mut_self.state {
563 UploadState::Done(_) => return Poll::Ready(Ok(())),
564 UploadState::CreatingUpload(_)
565 | UploadState::PuttingSingle(_)
566 | UploadState::Completing(_) => return Poll::Pending,
567 UploadState::Started(_) => {
568 let part = std::mem::take(&mut mut_self.buffer);
570 let path = mut_self.path.clone();
571 self.state.started_to_putting_single(path, part);
572 }
573 UploadState::InProgress {
574 upload,
575 futures,
576 part_idx,
577 } => {
578 if !mut_self.buffer.is_empty() && futures.len() < max_upload_parallelism() {
580 let part_size = mut_self.buffer.capacity();
582 let data = Bytes::from(std::mem::take(&mut mut_self.buffer));
583 let part = PartUpload {
584 path: mut_self.path.clone(),
585 part_idx: *part_idx,
586 parts_in_flight: futures.len() + 1,
587 part_size,
588 };
589 *part_idx += 1;
592 futures.spawn(
593 Self::put_part(upload.as_mut(), data, part)
594 .instrument(tracing::Span::current()),
595 );
596 continue;
599 }
600
601 if futures.is_empty() {
603 let path = mut_self.path.clone();
604 let bytes_written = mut_self.cursor;
605 self.state.in_progress_to_completing(path, bytes_written);
606 } else {
607 return Poll::Pending;
608 }
609 }
610 }
611 }
612 }
613}
614
615#[async_trait]
616impl Writer for ObjectWriter {
617 async fn tell(&mut self) -> Result<usize> {
618 Ok(self.cursor)
619 }
620
621 async fn shutdown(&mut self) -> Result<WriteResult> {
622 AsyncWriteExt::shutdown(self).await?;
626 if let UploadState::Done(result) = &self.state {
627 Ok(result.clone())
628 } else {
629 unreachable!()
630 }
631 }
632}
633
634pub struct LocalWriter {
635 path: Path,
636 state: LocalWriteState,
637}
638
639#[derive(Default)]
640enum LocalWriteState {
641 Writing(Box<WritingState>),
642 Finishing {
643 size: usize,
644 future: BoxFuture<'static, Result<WriteResult>>,
645 },
646 Done(WriteResult),
647 #[default]
648 Poisoned,
649}
650
651struct WritingState {
652 writer: tokio::io::BufWriter<tokio::fs::File>,
653 cursor: usize,
654 temp_path: tempfile::TempPath,
656 io_tracker: Arc<IOTracker>,
657 metrics: IoMetricsGuard,
661}
662
663impl LocalWriter {
664 pub fn new(
665 file: tokio::fs::File,
666 path: Path,
667 temp_path: tempfile::TempPath,
668 io_tracker: Arc<IOTracker>,
669 ) -> Self {
670 Self {
671 path,
672 state: LocalWriteState::Writing(Box::new(WritingState {
673 writer: tokio::io::BufWriter::new(file),
674 cursor: 0,
675 temp_path,
676 metrics: io_tracker.begin_io("put"),
677 io_tracker,
678 })),
679 }
680 }
681
682 fn already_closed_err(path: &Path) -> io::Error {
683 io::Error::other(format!(
684 "cannot write to LocalWriter for {} after shutdown",
685 path
686 ))
687 }
688
689 fn poisoned_err(path: &Path) -> io::Error {
690 io::Error::other(format!("LocalWriter for {} is in poisoned state", path))
691 }
692
693 async fn persist(
694 temp_path: tempfile::TempPath,
695 final_path: Path,
696 size: usize,
697 io_tracker: Arc<IOTracker>,
698 metrics: IoMetricsGuard,
699 ) -> Result<WriteResult> {
700 let local_path = crate::local::to_local_path(&final_path);
701 let persisted = tokio::task::spawn_blocking(move || -> Result<String> {
702 temp_path.persist(&local_path).map_err(|e| {
703 Error::io(format!(
704 "failed to persist temp file to {}: {}",
705 local_path, e.error
706 ))
707 })?;
708
709 let metadata = std::fs::metadata(&local_path).map_err(|e| {
710 Error::io(format!("failed to read metadata for {}: {}", local_path, e))
711 })?;
712 Ok(get_etag(&metadata))
713 })
714 .await
715 .map_err(|e| Error::io(format!("spawn_blocking failed: {}", e)))
716 .and_then(|e_tag| e_tag);
717
718 metrics.record(&persisted, size as u64);
719 let e_tag = persisted?;
720
721 io_tracker.record_write("put", final_path, size as u64);
722
723 Ok(WriteResult {
724 size,
725 e_tag: Some(e_tag),
726 })
727 }
728}
729
730impl AsyncWrite for LocalWriter {
731 fn poll_write(
732 mut self: Pin<&mut Self>,
733 cx: &mut std::task::Context<'_>,
734 buf: &[u8],
735 ) -> Poll<std::result::Result<usize, std::io::Error>> {
736 if let LocalWriteState::Writing(state) = &mut self.state {
737 let poll = Pin::new(&mut state.writer).poll_write(cx, buf);
738 if let Poll::Ready(Ok(n)) = &poll {
739 state.cursor += *n;
740 }
741 poll
742 } else {
743 Poll::Ready(Err(Self::already_closed_err(&self.path)))
744 }
745 }
746
747 fn poll_flush(
748 mut self: Pin<&mut Self>,
749 cx: &mut std::task::Context<'_>,
750 ) -> Poll<std::result::Result<(), std::io::Error>> {
751 if let LocalWriteState::Writing(state) = &mut self.state {
752 Pin::new(&mut state.writer).poll_flush(cx)
753 } else {
754 Poll::Ready(Err(Self::already_closed_err(&self.path)))
755 }
756 }
757
758 fn poll_shutdown(
759 mut self: Pin<&mut Self>,
760 cx: &mut std::task::Context<'_>,
761 ) -> Poll<std::result::Result<(), std::io::Error>> {
762 let mut_self = &mut *self;
763 loop {
764 match &mut mut_self.state {
765 LocalWriteState::Writing(state) => {
766 if Pin::new(&mut state.writer).poll_shutdown(cx).is_pending() {
767 return Poll::Pending;
768 }
769
770 let LocalWriteState::Writing(state) =
772 std::mem::replace(&mut mut_self.state, LocalWriteState::Poisoned)
773 else {
774 unreachable!()
775 };
776 let size = state.cursor;
777 mut_self.state = LocalWriteState::Finishing {
778 size,
779 future: Box::pin(Self::persist(
780 state.temp_path,
781 mut_self.path.clone(),
782 size,
783 state.io_tracker,
784 state.metrics,
785 )),
786 };
787 }
788 LocalWriteState::Finishing { future, .. } => match future.poll_unpin(cx) {
789 Poll::Ready(Ok(result)) => mut_self.state = LocalWriteState::Done(result),
790 Poll::Ready(Err(e)) => {
791 return Poll::Ready(Err(io::Error::other(e)));
792 }
793 Poll::Pending => return Poll::Pending,
794 },
795 LocalWriteState::Done(_) => return Poll::Ready(Ok(())),
796 LocalWriteState::Poisoned => {
797 return Poll::Ready(Err(Self::poisoned_err(&self.path)));
798 }
799 }
800 }
801 }
802}
803
804#[async_trait]
805impl Writer for LocalWriter {
806 async fn tell(&mut self) -> Result<usize> {
807 match &mut self.state {
808 LocalWriteState::Writing(state) => Ok(state.cursor),
809 LocalWriteState::Finishing { size, .. } => Ok(*size),
810 LocalWriteState::Done(result) => Ok(result.size),
811 LocalWriteState::Poisoned => Err(Self::poisoned_err(&self.path).into()),
812 }
813 }
814
815 async fn shutdown(&mut self) -> Result<WriteResult> {
816 AsyncWriteExt::shutdown(self).await.map_err(|e| {
817 Error::io(format!(
818 "failed to shutdown local writer for {}: {}",
819 self.path, e
820 ))
821 })?;
822
823 match &self.state {
824 LocalWriteState::Done(result) => Ok(result.clone()),
825 _ => unreachable!(),
826 }
827 }
828}
829
830pub fn get_etag(metadata: &std::fs::Metadata) -> String {
832 let inode = get_inode(metadata);
833 let size = metadata.len();
834 let mtime = metadata
835 .modified()
836 .ok()
837 .and_then(|mtime| mtime.duration_since(std::time::SystemTime::UNIX_EPOCH).ok())
838 .unwrap_or_default()
839 .as_micros();
840
841 format!("{inode:x}-{mtime:x}-{size:x}")
844}
845
846#[cfg(unix)]
847fn get_inode(metadata: &std::fs::Metadata) -> u64 {
848 std::os::unix::fs::MetadataExt::ino(metadata)
849}
850
851#[cfg(not(unix))]
852fn get_inode(_metadata: &std::fs::Metadata) -> u64 {
853 0
854}
855
856#[cfg(test)]
857mod tests {
858 use futures::stream::BoxStream;
859 use object_store::{
860 CopyOptions, GetOptions, GetResult, ListResult, ObjectMeta, PutMultipartOptions,
861 PutOptions, PutPayload, PutResult, RenameOptions, Result as OSResult, UploadPart,
862 };
863 use std::sync::Mutex;
864 use std::time::Duration;
865 use tokio::io::AsyncWriteExt;
866 use tokio::sync::Semaphore;
867
868 use super::*;
869
870 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
872 enum FailAt {
873 Nothing,
874 CreateMultipart,
875 PutPart,
876 Complete,
877 SinglePut,
878 }
879
880 #[derive(Debug)]
883 struct UploadObservations {
884 started: Semaphore,
889 parts: Mutex<Vec<(usize, Vec<u8>)>>,
892 }
893
894 impl Default for UploadObservations {
895 fn default() -> Self {
896 Self {
897 started: Semaphore::new(0),
898 parts: Mutex::new(Vec::new()),
899 }
900 }
901 }
902
903 fn rejected(stage: &'static str) -> object_store::Error {
904 object_store::Error::Generic {
905 store: "FailingUploadStore",
906 source: format!("{stage} rejected by test").into(),
907 }
908 }
909
910 #[derive(Debug)]
911 struct FailingUpload {
912 fail_at: FailAt,
913 gate: Option<Arc<Semaphore>>,
916 observations: Arc<UploadObservations>,
917 next_part: usize,
918 }
919
920 #[async_trait]
921 impl MultipartUpload for FailingUpload {
922 fn put_part(&mut self, data: PutPayload) -> UploadPart {
923 let fails = self.fail_at == FailAt::PutPart;
924 let part_idx = self.next_part;
925 self.next_part += 1;
926 let gate = self.gate.clone();
927 let observations = self.observations.clone();
928 Box::pin(async move {
929 observations.started.add_permits(1);
930 if let Some(gate) = gate {
931 gate.acquire_owned().await.unwrap().forget();
934 }
935 if fails {
936 return Err(rejected("part"));
937 }
938 let body = data
939 .iter()
940 .flat_map(|chunk| chunk.iter().copied())
941 .collect();
942 observations.parts.lock().unwrap().push((part_idx, body));
943 Ok(())
944 })
945 }
946
947 async fn complete(&mut self) -> OSResult<PutResult> {
948 if self.fail_at == FailAt::Complete {
949 Err(rejected("complete"))
950 } else {
951 Ok(PutResult {
952 e_tag: None,
953 version: None,
954 })
955 }
956 }
957
958 async fn abort(&mut self) -> OSResult<()> {
959 Ok(())
960 }
961 }
962
963 #[derive(Debug)]
966 struct FailingUploadStore {
967 fail_at: FailAt,
968 gate: Option<Arc<Semaphore>>,
969 observations: Arc<UploadObservations>,
970 }
971
972 impl FailingUploadStore {
973 fn new(fail_at: FailAt) -> Self {
974 Self {
975 fail_at,
976 gate: None,
977 observations: Arc::new(UploadObservations::default()),
978 }
979 }
980
981 fn gated(fail_at: FailAt) -> (Self, Arc<Semaphore>) {
984 let gate = Arc::new(Semaphore::new(0));
985 let store = Self {
986 fail_at,
987 gate: Some(gate.clone()),
988 observations: Arc::new(UploadObservations::default()),
989 };
990 (store, gate)
991 }
992 }
993
994 impl std::fmt::Display for FailingUploadStore {
995 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
996 write!(f, "FailingUploadStore")
997 }
998 }
999
1000 #[async_trait]
1001 impl ObjectStore for FailingUploadStore {
1002 async fn put_opts(
1003 &self,
1004 _location: &Path,
1005 _bytes: PutPayload,
1006 _opts: PutOptions,
1007 ) -> OSResult<PutResult> {
1008 if self.fail_at == FailAt::SinglePut {
1009 Err(rejected("single put"))
1010 } else {
1011 Ok(PutResult {
1012 e_tag: None,
1013 version: None,
1014 })
1015 }
1016 }
1017
1018 async fn put_multipart_opts(
1019 &self,
1020 _location: &Path,
1021 _opts: PutMultipartOptions,
1022 ) -> OSResult<Box<dyn MultipartUpload>> {
1023 if self.fail_at == FailAt::CreateMultipart {
1024 Err(rejected("create multipart"))
1025 } else {
1026 Ok(Box::new(FailingUpload {
1027 fail_at: self.fail_at,
1028 gate: self.gate.clone(),
1029 observations: self.observations.clone(),
1030 next_part: 0,
1031 }))
1032 }
1033 }
1034
1035 async fn get_opts(&self, _location: &Path, _options: GetOptions) -> OSResult<GetResult> {
1036 unimplemented!()
1037 }
1038
1039 fn delete_stream(
1040 &self,
1041 _locations: BoxStream<'static, OSResult<Path>>,
1042 ) -> BoxStream<'static, OSResult<Path>> {
1043 unimplemented!()
1044 }
1045
1046 fn list(&self, _prefix: Option<&Path>) -> BoxStream<'static, OSResult<ObjectMeta>> {
1047 unimplemented!()
1048 }
1049
1050 fn list_with_offset(
1051 &self,
1052 _prefix: Option<&Path>,
1053 _offset: &Path,
1054 ) -> BoxStream<'static, OSResult<ObjectMeta>> {
1055 unimplemented!()
1056 }
1057
1058 async fn list_with_delimiter(&self, _prefix: Option<&Path>) -> OSResult<ListResult> {
1059 unimplemented!()
1060 }
1061
1062 async fn copy_opts(&self, _from: &Path, _to: &Path, _opts: CopyOptions) -> OSResult<()> {
1063 unimplemented!()
1064 }
1065
1066 async fn rename_opts(
1067 &self,
1068 _from: &Path,
1069 _to: &Path,
1070 _opts: RenameOptions,
1071 ) -> OSResult<()> {
1072 unimplemented!()
1073 }
1074 }
1075
1076 const FAILING_UPLOAD_PATH: &str = "part_7_invert.lance";
1077
1078 fn two_parts() -> usize {
1082 initial_upload_size() * 2
1083 }
1084
1085 async fn failing_upload(fail_at: FailAt, num_bytes: usize) -> io::Error {
1089 let mut store = LanceObjectStore::memory();
1090 store.inner = Arc::new(FailingUploadStore::new(fail_at));
1091
1092 let mut writer = ObjectWriter::new(&store, &Path::from(FAILING_UPLOAD_PATH))
1093 .await
1094 .unwrap();
1095 let buf = vec![0u8; num_bytes];
1096 match writer.write_all(buf.as_slice()).await {
1097 Err(err) => err,
1098 Ok(()) => AsyncWriteExt::shutdown(&mut writer)
1099 .await
1100 .expect_err("upload should have failed"),
1101 }
1102 }
1103
1104 #[tokio::test]
1105 async fn test_part_upload_failure_reports_upload_context() {
1106 let err = failing_upload(FailAt::PutPart, two_parts()).await;
1107 let message = err.to_string();
1108
1109 assert!(
1110 message.contains("multipart upload of part"),
1111 "should name the failing stage: {message}"
1112 );
1113 assert!(
1114 message.contains(FAILING_UPLOAD_PATH),
1115 "should name the object: {message}"
1116 );
1117 assert!(
1118 message.contains(&format!("{} bytes", initial_upload_size())),
1119 "should report the body size: {message}"
1120 );
1121 assert!(
1122 message.contains(&format!("part_size={} bytes", initial_upload_size())),
1123 "should report the part size in effect: {message}"
1124 );
1125 assert!(
1126 message.contains("parts_in_flight="),
1127 "should report upload concurrency in use: {message}"
1128 );
1129 assert!(
1130 message.contains("LANCE_INITIAL_UPLOAD_SIZE")
1131 && message.contains("LANCE_UPLOAD_CONCURRENCY"),
1132 "should report the knobs governing the request: {message}"
1133 );
1134 assert!(
1135 message.contains("part rejected by test"),
1136 "should keep the underlying object store error: {message}"
1137 );
1138 }
1139
1140 #[tokio::test]
1143 async fn test_part_upload_failure_reports_elapsed_time() {
1144 let err = failing_upload(FailAt::PutPart, two_parts()).await;
1145 let message = err.to_string();
1146 assert!(
1147 message.contains("failed after"),
1148 "should report how long the request took: {message}"
1149 );
1150 }
1151
1152 #[test]
1160 fn test_part_buffer_capacity_tracks_grown_part_size() {
1161 let mut buffer = Vec::<u8>::with_capacity(initial_upload_size());
1162 assert_eq!(buffer.capacity(), initial_upload_size());
1163
1164 let _ = ObjectWriter::next_part_buffer(&mut buffer, 0, false);
1165 assert_eq!(
1166 buffer.capacity(),
1167 initial_upload_size(),
1168 "early parts stay at the configured size"
1169 );
1170
1171 let _ = ObjectWriter::next_part_buffer(&mut buffer, 100, false);
1172 assert_eq!(
1173 buffer.capacity(),
1174 initial_upload_size().max(2 * INITIAL_UPLOAD_STEP),
1175 "the part size has grown past the first step"
1176 );
1177
1178 let _ = ObjectWriter::next_part_buffer(&mut buffer, 100, true);
1181 assert_eq!(buffer.capacity(), initial_upload_size());
1182 }
1183
1184 #[tokio::test]
1185 async fn test_part_upload_failure_preserves_source_chain() {
1186 let err = failing_upload(FailAt::PutPart, two_parts()).await;
1187
1188 let failure = err
1189 .get_ref()
1190 .expect("io error should carry the upload failure");
1191 let source = std::error::Error::source(failure)
1192 .expect("upload failure should expose the object store error");
1193 assert!(
1194 source.downcast_ref::<object_store::Error>().is_some(),
1195 "source should still be the object store error, got: {source}"
1196 );
1197 }
1198
1199 #[test]
1202 fn test_upload_failure_preserves_error_kind() {
1203 fn not_found() -> object_store::Error {
1204 object_store::Error::NotFound {
1205 path: FAILING_UPLOAD_PATH.to_string(),
1206 source: "not found".into(),
1207 }
1208 }
1209
1210 let unwrapped = io::Error::from(not_found());
1211 assert_eq!(unwrapped.kind(), io::ErrorKind::NotFound);
1212
1213 let wrapped =
1214 UploadFailure::new("part upload failed".to_string(), not_found()).into_io_error();
1215 assert_eq!(wrapped.kind(), unwrapped.kind());
1216 }
1217
1218 #[tokio::test]
1222 async fn test_writer_shutdown_preserves_object_store_source() {
1223 let mut store = LanceObjectStore::memory();
1224 store.inner = Arc::new(FailingUploadStore::new(FailAt::SinglePut));
1225 let mut writer = ObjectWriter::new(&store, &Path::from(FAILING_UPLOAD_PATH))
1226 .await
1227 .unwrap();
1228 writer.write_all(&[0u8; 256]).await.unwrap();
1229 let err = Writer::shutdown(&mut writer).await.unwrap_err();
1230
1231 let mut current: Option<&(dyn std::error::Error + 'static)> = Some(&err);
1232 let mut found_object_store = false;
1233 while let Some(source) = current {
1234 if source.downcast_ref::<object_store::Error>().is_some() {
1235 found_object_store = true;
1236 break;
1237 }
1238 current = source.source();
1239 }
1240 assert!(found_object_store, "source chain was flattened: {err:?}");
1241
1242 assert!(
1243 err.to_string().contains(FAILING_UPLOAD_PATH),
1244 "should still name the object: {err}"
1245 );
1246 }
1247
1248 #[tokio::test]
1249 async fn test_create_multipart_failure_reports_upload_context() {
1250 let err = failing_upload(FailAt::CreateMultipart, two_parts()).await;
1251 let message = err.to_string();
1252
1253 assert!(
1254 message.contains("failed to create multipart upload for"),
1255 "should name the failing stage: {message}"
1256 );
1257 assert!(
1258 message.contains(FAILING_UPLOAD_PATH),
1259 "should name the object: {message}"
1260 );
1261 assert!(
1262 message.contains("create multipart rejected by test"),
1263 "should keep the underlying object store error: {message}"
1264 );
1265 }
1266
1267 #[tokio::test]
1268 async fn test_complete_multipart_failure_reports_upload_context() {
1269 let num_bytes = two_parts();
1270 let err = failing_upload(FailAt::Complete, num_bytes).await;
1271 let message = err.to_string();
1272
1273 assert!(
1274 message.contains("completing multipart upload of"),
1275 "should name the failing stage: {message}"
1276 );
1277 assert!(
1278 message.contains(&format!("{num_bytes} bytes")),
1279 "should report how much had been written: {message}"
1280 );
1281 assert!(
1282 message.contains("complete rejected by test"),
1283 "should keep the underlying object store error: {message}"
1284 );
1285 }
1286
1287 #[tokio::test]
1288 async fn test_single_put_failure_reports_upload_context() {
1289 let err = failing_upload(FailAt::SinglePut, 256).await;
1291 let message = err.to_string();
1292
1293 assert!(
1294 message.contains("single PUT of"),
1295 "should name the failing stage: {message}"
1296 );
1297 assert!(
1298 message.contains(FAILING_UPLOAD_PATH),
1299 "should name the object: {message}"
1300 );
1301 assert!(
1302 message.contains("256 bytes"),
1303 "should report the body size: {message}"
1304 );
1305 assert!(
1306 message.contains("single put rejected by test"),
1307 "should keep the underlying object store error: {message}"
1308 );
1309 }
1310
1311 const GATE_RELEASE: usize = 64;
1314
1315 const FLUSH_BOUND: Duration = Duration::from_secs(10);
1319
1320 async fn await_part_in_flight(observations: &UploadObservations) {
1323 tokio::time::timeout(FLUSH_BOUND, observations.started.acquire())
1324 .await
1325 .expect("a part upload should have started")
1326 .unwrap()
1327 .forget();
1328 }
1329
1330 #[tokio::test]
1331 async fn test_flush_does_not_wait_for_in_flight_parts() {
1332 let (store, gate) = FailingUploadStore::gated(FailAt::Nothing);
1333 let observations = store.observations.clone();
1334 let mut lance_store = LanceObjectStore::memory();
1335 lance_store.inner = Arc::new(store);
1336
1337 let mut writer = ObjectWriter::new(&lance_store, &Path::from("gated.lance"))
1338 .await
1339 .unwrap();
1340 let payload = (0..two_parts()).map(|i| i as u8).collect::<Vec<_>>();
1342 writer.write_all(payload.as_slice()).await.unwrap();
1343 await_part_in_flight(&observations).await;
1344
1345 tokio::time::timeout(FLUSH_BOUND, AsyncWriteExt::flush(&mut writer))
1346 .await
1347 .expect("flush must not wait for in-flight part uploads")
1348 .unwrap();
1349
1350 assert!(
1351 observations.parts.lock().unwrap().is_empty(),
1352 "no gated part may have completed before the gate opened"
1353 );
1354
1355 gate.add_permits(GATE_RELEASE);
1356 let result = Writer::shutdown(&mut writer).await.unwrap();
1357 assert_eq!(result.size, payload.len());
1358
1359 let mut parts = observations.parts.lock().unwrap().clone();
1360 parts.sort_by_key(|(part_idx, _)| *part_idx);
1361 let assembled = parts
1362 .into_iter()
1363 .flat_map(|(_, body)| body)
1364 .collect::<Vec<_>>();
1365 assert_eq!(
1366 assembled, payload,
1367 "parts must reassemble into the original bytes"
1368 );
1369 }
1370
1371 #[tokio::test]
1372 async fn test_part_failure_after_flush_surfaces_at_shutdown() {
1373 let (store, gate) = FailingUploadStore::gated(FailAt::PutPart);
1374 let observations = store.observations.clone();
1375 let mut lance_store = LanceObjectStore::memory();
1376 lance_store.inner = Arc::new(store);
1377
1378 let mut writer = ObjectWriter::new(&lance_store, &Path::from(FAILING_UPLOAD_PATH))
1379 .await
1380 .unwrap();
1381 writer
1382 .write_all(vec![0u8; two_parts()].as_slice())
1383 .await
1384 .unwrap();
1385 await_part_in_flight(&observations).await;
1386 AsyncWriteExt::flush(&mut writer).await.unwrap();
1388
1389 gate.add_permits(GATE_RELEASE);
1392 let err = AsyncWriteExt::shutdown(&mut writer)
1393 .await
1394 .expect_err("a failed part upload must still surface");
1395 let message = err.to_string();
1396 assert!(
1397 message.contains(FAILING_UPLOAD_PATH),
1398 "should name the object being written: {message}"
1399 );
1400 }
1401
1402 #[tokio::test]
1403 async fn test_write() {
1404 let store = LanceObjectStore::memory();
1405
1406 let mut object_writer = ObjectWriter::new(&store, &Path::from("/foo"))
1407 .await
1408 .unwrap();
1409 assert_eq!(object_writer.tell().await.unwrap(), 0);
1410
1411 let buf = vec![0; 256];
1412 assert_eq!(object_writer.write(buf.as_slice()).await.unwrap(), 256);
1413 assert_eq!(object_writer.tell().await.unwrap(), 256);
1414
1415 assert_eq!(object_writer.write(buf.as_slice()).await.unwrap(), 256);
1416 assert_eq!(object_writer.tell().await.unwrap(), 512);
1417
1418 assert_eq!(object_writer.write(buf.as_slice()).await.unwrap(), 256);
1419 assert_eq!(object_writer.tell().await.unwrap(), 256 * 3);
1420
1421 let res = Writer::shutdown(&mut object_writer).await.unwrap();
1422 assert_eq!(res.size, 256 * 3);
1423
1424 let mut object_writer = ObjectWriter::new(&store, &Path::from("/bar"))
1426 .await
1427 .unwrap();
1428 let buf = vec![0; INITIAL_UPLOAD_STEP / 3 * 2];
1429 for i in 0..5 {
1430 object_writer.write_all(buf.as_slice()).await.unwrap();
1433 assert_eq!(object_writer.tell().await.unwrap(), (i + 1) * buf.len());
1435 }
1436 let res = Writer::shutdown(&mut object_writer).await.unwrap();
1437 assert_eq!(res.size, buf.len() * 5);
1438 }
1439
1440 #[tokio::test]
1441 async fn test_abort_write() {
1442 let store = LanceObjectStore::memory();
1443
1444 let mut object_writer = ObjectWriter::new(&store, &Path::from("/foo"))
1445 .await
1446 .unwrap();
1447 object_writer.abort().await;
1448 }
1449
1450 #[tokio::test]
1451 async fn test_local_writer_shutdown() {
1452 let tmp = lance_core::utils::tempfile::TempStdDir::default();
1453 let file_path = tmp.join("test_local_writer.bin");
1454 let os_path = Path::from_absolute_path(&file_path).unwrap();
1455 let io_tracker = Arc::new(IOTracker::default());
1456
1457 let named_temp = tempfile::NamedTempFile::new_in(&*tmp).unwrap();
1458 let temp_file_path = named_temp.path().to_owned();
1459 let (std_file, temp_path) = named_temp.into_parts();
1460 let file = tokio::fs::File::from_std(std_file);
1461 let mut writer = LocalWriter::new(file, os_path, temp_path, io_tracker.clone());
1462
1463 let data = b"hello local writer";
1464 writer.write_all(data).await.unwrap();
1465
1466 assert!(!file_path.exists());
1468 assert!(temp_file_path.exists());
1470
1471 let result = Writer::shutdown(&mut writer).await.unwrap();
1472 assert_eq!(result.size, data.len());
1473 assert!(result.e_tag.is_some());
1474 assert!(!result.e_tag.as_ref().unwrap().is_empty());
1475
1476 assert!(file_path.exists());
1478 assert!(!temp_file_path.exists());
1479
1480 let stats = io_tracker.stats();
1481 assert_eq!(stats.write_iops, 1);
1482 assert_eq!(stats.written_bytes, data.len() as u64);
1483 }
1484
1485 #[tokio::test]
1486 async fn test_local_writer_drop_cleans_up() {
1487 let tmp = lance_core::utils::tempfile::TempStdDir::default();
1488 let file_path = tmp.join("test_drop.bin");
1489 let os_path = Path::from_absolute_path(&file_path).unwrap();
1490 let io_tracker = Arc::new(IOTracker::default());
1491
1492 let named_temp = tempfile::NamedTempFile::new_in(&*tmp).unwrap();
1493 let temp_file_path = named_temp.path().to_owned();
1494 let (std_file, temp_path) = named_temp.into_parts();
1495 let file = tokio::fs::File::from_std(std_file);
1496 let mut writer = LocalWriter::new(file, os_path, temp_path, io_tracker);
1497
1498 writer.write_all(b"some data").await.unwrap();
1499 assert!(temp_file_path.exists());
1500
1501 drop(writer);
1503 assert!(!temp_file_path.exists());
1504 assert!(!file_path.exists());
1505 }
1506
1507 #[test]
1508 fn clamp_initial_upload_size_below_min_is_clamped_up() {
1509 assert_eq!(clamp_initial_upload_size(0), (INITIAL_UPLOAD_STEP, true));
1510 assert_eq!(
1511 clamp_initial_upload_size(INITIAL_UPLOAD_STEP - 1),
1512 (INITIAL_UPLOAD_STEP, true)
1513 );
1514 }
1515
1516 #[test]
1517 fn clamp_initial_upload_size_within_range_is_unchanged() {
1518 assert_eq!(
1519 clamp_initial_upload_size(INITIAL_UPLOAD_STEP),
1520 (INITIAL_UPLOAD_STEP, false)
1521 );
1522 assert_eq!(
1523 clamp_initial_upload_size(MAX_UPLOAD_PART_SIZE),
1524 (MAX_UPLOAD_PART_SIZE, false)
1525 );
1526 let mid = INITIAL_UPLOAD_STEP * 8; assert_eq!(clamp_initial_upload_size(mid), (mid, false));
1528 }
1529
1530 #[test]
1531 fn clamp_initial_upload_size_above_max_is_clamped_down() {
1532 assert_eq!(
1533 clamp_initial_upload_size(MAX_UPLOAD_PART_SIZE + 1),
1534 (MAX_UPLOAD_PART_SIZE, true)
1535 );
1536 assert_eq!(
1537 clamp_initial_upload_size(usize::MAX),
1538 (MAX_UPLOAD_PART_SIZE, true)
1539 );
1540 }
1541
1542 #[test]
1547 fn clamp_initial_upload_size_at_5gib_clamps_down() {
1548 let exactly_5_gib: usize = 5 * 1024 * 1024 * 1024;
1549 assert_eq!(
1550 clamp_initial_upload_size(exactly_5_gib),
1551 (MAX_UPLOAD_PART_SIZE, true)
1552 );
1553 }
1554}