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