1use std::sync::Arc;
9
10use async_trait::async_trait;
11use bytes::Bytes;
12use futures::stream::BoxStream;
13use futures::{StreamExt, TryStreamExt};
14use lance_core::utils::tracing::{
15 AUDIT_MODE_CREATE, AUDIT_MODE_DELETE, AUDIT_TYPE_MANIFEST, TRACE_FILE_AUDIT,
16};
17use lance_core::{Error, Result};
18use lance_io::object_store::ObjectStore;
19use log::warn;
20use object_store::ObjectMeta;
21use object_store::ObjectStoreExt;
22use object_store::{Error as ObjectStoreError, ObjectStore as OSObjectStore, path::Path};
23use tracing::info;
24
25use super::{
26 MANIFEST_EXTENSION, ManifestLocation, ManifestNamingScheme, current_manifest_path,
27 default_resolve_version, make_staging_manifest_path, write_version_hint,
28};
29use crate::format::{IndexMetadata, Manifest, Transaction};
30use crate::io::commit::{
31 CommitError, CommitHandler, PredecessorIdentity, default_list_manifest_locations,
32 default_list_manifest_locations_since,
33};
34
35#[allow(clippy::too_many_arguments)]
38pub async fn finalize_staged<S: ExternalManifestStore + ?Sized>(
39 store: &S,
40 base_path: &Path,
41 version: u64,
42 staging_path: &Path,
43 size: u64,
44 object_store: &dyn OSObjectStore,
45 naming_scheme: ManifestNamingScheme,
46) -> Result<ManifestLocation> {
47 let final_path = naming_scheme.manifest_path(base_path, version);
49 let final_e_tag =
50 copy_or_verify_final_manifest(object_store, staging_path, &final_path, version, size)
51 .await?;
52
53 let location = ManifestLocation {
54 version,
55 path: final_path.clone(),
56 size: Some(size),
57 naming_scheme,
58 e_tag: final_e_tag,
59 identity: None,
60 };
61
62 let published = store
69 .put_if_exists(base_path.as_ref(), version, final_path.as_ref(), size, None)
70 .await;
71
72 if let Err(error) = published {
73 warn!(
78 "Final manifest '{}' is committed, but the external manifest index could not be updated; retaining staging manifest '{}' for repair: {}",
79 final_path, staging_path, error
80 );
81 return Ok(location);
82 }
83
84 match object_store.delete(staging_path).await {
86 Ok(_) => {}
87 Err(ObjectStoreError::NotFound { .. }) => {}
88 Err(error) => {
89 warn!(
93 "Failed to delete finalized staging manifest '{}': {}",
94 staging_path, error
95 );
96 return Ok(location);
97 }
98 }
99 info!(target: TRACE_FILE_AUDIT, mode=AUDIT_MODE_DELETE, r#type=AUDIT_TYPE_MANIFEST, path = staging_path.as_ref());
100
101 Ok(location)
102}
103
104#[derive(Debug, Clone, PartialEq, Eq)]
106pub enum Reservation {
107 Reserved { identity: String },
110 Taken,
112 PredecessorChanged,
115}
116
117#[async_trait]
163pub trait ExternalManifestStore: std::fmt::Debug + Send + Sync {
164 async fn get(&self, base_uri: &str, version: u64) -> Result<String>;
166
167 async fn get_manifest_location(
168 &self,
169 base_uri: &str,
170 version: u64,
171 ) -> Result<ManifestLocation> {
172 let path = self.get(base_uri, version).await?;
173 let path = Path::parse(&path).map_err(|e| Error::invalid_input(e.to_string()))?;
174 let naming_scheme = detect_naming_scheme_from_path(&path)?;
175 Ok(ManifestLocation {
176 version,
177 path,
178 size: None,
179 naming_scheme,
180 e_tag: None,
181 identity: None,
182 })
183 }
184
185 async fn get_latest_version(&self, base_uri: &str) -> Result<Option<(u64, String)>>;
189
190 async fn get_latest_manifest_location(
196 &self,
197 base_uri: &str,
198 ) -> Result<Option<ManifestLocation>> {
199 self.get_latest_version(base_uri).await.and_then(|res| {
200 res.map(|(version, uri)| {
201 let path = Path::parse(&uri).map_err(|e| Error::invalid_input(e.to_string()))?;
202 let naming_scheme = detect_naming_scheme_from_path(&path)?;
203 Ok(ManifestLocation {
204 version,
205 path,
206 size: None,
207 naming_scheme,
208 e_tag: None,
209 identity: None,
210 })
211 })
212 .transpose()
213 })
214 }
215
216 #[allow(clippy::too_many_arguments)]
225 async fn put(
226 &self,
227 base_path: &Path,
228 version: u64,
229 staging_path: &Path,
230 size: u64,
231 _e_tag: Option<String>,
232 object_store: &dyn OSObjectStore,
233 naming_scheme: ManifestNamingScheme,
234 ) -> Result<ManifestLocation> {
235 self.put_if_not_exists(
244 base_path.as_ref(),
245 version,
246 staging_path.as_ref(),
247 size,
248 None,
249 )
250 .await?;
251
252 self.finalize(
253 base_path,
254 version,
255 staging_path,
256 size,
257 object_store,
258 naming_scheme,
259 )
260 .await
261 }
262
263 async fn finalize(
266 &self,
267 base_path: &Path,
268 version: u64,
269 staging_path: &Path,
270 size: u64,
271 object_store: &dyn OSObjectStore,
272 naming_scheme: ManifestNamingScheme,
273 ) -> Result<ManifestLocation> {
274 finalize_staged(
275 self,
276 base_path,
277 version,
278 staging_path,
279 size,
280 object_store,
281 naming_scheme,
282 )
283 .await
284 }
285
286 fn supports_predecessor_condition(&self) -> bool {
289 false
290 }
291
292 async fn get_identity(&self, _base_uri: &str, _version: u64) -> Result<Option<String>> {
296 Ok(None)
297 }
298
299 async fn list_versions(
304 &self,
305 _base_uri: &str,
306 _since: Option<u64>,
307 ) -> Result<Option<Vec<ManifestLocation>>> {
308 Ok(None)
309 }
310
311 async fn forget_version(&self, _base_uri: &str, _version: u64, _identity: &str) -> Result<()> {
316 Err(Error::not_supported(
317 "this external manifest store cannot retire a version record",
318 ))
319 }
320
321 async fn put_if_predecessor(
325 &self,
326 _base_uri: &str,
327 _version: u64,
328 _path: &str,
329 _size: u64,
330 _predecessor: &PredecessorIdentity,
331 ) -> Result<Reservation> {
332 Err(Error::not_supported(
333 "this external manifest store cannot condition a reservation on its predecessor",
334 ))
335 }
336
337 async fn put_if_not_exists(
344 &self,
345 base_uri: &str,
346 version: u64,
347 path: &str,
348 size: u64,
349 e_tag: Option<String>,
350 ) -> Result<()>;
351
352 async fn put_if_exists(
356 &self,
357 base_uri: &str,
358 version: u64,
359 path: &str,
360 size: u64,
361 e_tag: Option<String>,
362 ) -> Result<()>;
363
364 async fn delete(&self, _base_uri: &str) -> Result<()> {
366 Ok(())
367 }
368}
369
370pub(crate) fn detect_naming_scheme_from_path(path: &Path) -> Result<ManifestNamingScheme> {
371 path.filename()
372 .and_then(|name| {
373 ManifestNamingScheme::detect_scheme(name)
374 .or_else(|| Some(ManifestNamingScheme::detect_scheme_staging(name)))
375 })
376 .ok_or_else(|| {
377 Error::corrupt_file(
378 path.clone(),
379 "Path does not follow known manifest naming convention.",
380 )
381 })
382}
383
384const MAX_SERVER_SIDE_COPY_BYTES: u64 = 5 * 1024 * 1024 * 1024;
393
394const COPY_REWRITE_PART_SIZE: usize = 100 * 1024 * 1024;
400
401async fn copy_size_aware(
422 store: &dyn OSObjectStore,
423 from: &Path,
424 to: &Path,
425 size: u64,
426) -> std::result::Result<(), ObjectStoreError> {
427 if size < MAX_SERVER_SIDE_COPY_BYTES {
428 store.copy(from, to).await
429 } else {
430 copy_via_read_rewrite(store, from, to).await
431 }
432}
433
434async fn copy_or_verify_final_manifest(
450 object_store: &dyn OSObjectStore,
451 staging_path: &Path,
452 final_path: &Path,
453 version: u64,
454 selected_size: u64,
455) -> Result<Option<String>> {
456 match copy_size_aware(object_store, staging_path, final_path, selected_size).await {
457 Ok(()) => {
458 info!(target: TRACE_FILE_AUDIT, mode=AUDIT_MODE_CREATE, r#type=AUDIT_TYPE_MANIFEST, path = final_path.as_ref());
459 let final_meta = object_store.head(final_path).await?;
460 if final_meta.size != selected_size {
461 return Err(Error::corrupt_file(
462 final_path.clone(),
463 format!(
464 "Manifest size mismatch for version {}: selected staging manifest had {}, object store returned {}",
465 version, selected_size, final_meta.size
466 ),
467 ));
468 }
469 Ok(final_meta.e_tag)
470 }
471 Err(ObjectStoreError::NotFound { .. }) => match object_store.head(final_path).await {
472 Ok(final_meta) if final_meta.size == selected_size => Ok(final_meta.e_tag),
473 Ok(final_meta) => Err(Error::corrupt_file(
474 final_path.clone(),
475 format!(
476 "Manifest size mismatch for version {}: selected staging manifest had {}, object store returned {}",
477 version, selected_size, final_meta.size
478 ),
479 )),
480 Err(error) => Err(error.into()),
481 },
482 Err(error) => Err(error.into()),
483 }
484}
485
486async fn copy_via_read_rewrite(
493 store: &dyn OSObjectStore,
494 from: &Path,
495 to: &Path,
496) -> std::result::Result<(), ObjectStoreError> {
497 let mut stream = store.get(from).await?.into_stream();
499
500 let mut upload = store.put_multipart(to).await?;
510 let mut part_buf: Vec<u8> = Vec::with_capacity(COPY_REWRITE_PART_SIZE);
511
512 while let Some(chunk) = stream.next().await {
513 let chunk = match chunk {
514 Ok(b) => b,
515 Err(e) => {
516 let _ = upload.abort().await;
517 return Err(e);
518 }
519 };
520 let mut offset = 0;
526 while offset < chunk.len() {
527 let want = COPY_REWRITE_PART_SIZE - part_buf.len();
528 let take = want.min(chunk.len() - offset);
529 part_buf.extend_from_slice(&chunk[offset..offset + take]);
530 offset += take;
531
532 if part_buf.len() >= COPY_REWRITE_PART_SIZE {
533 let payload =
534 std::mem::replace(&mut part_buf, Vec::with_capacity(COPY_REWRITE_PART_SIZE));
535 if let Err(e) = upload.put_part(Bytes::from(payload).into()).await {
536 let _ = upload.abort().await;
537 return Err(e);
538 }
539 }
540 }
541 }
542
543 if !part_buf.is_empty()
546 && let Err(e) = upload.put_part(Bytes::from(part_buf).into()).await
547 {
548 let _ = upload.abort().await;
549 return Err(e);
550 }
551
552 if let Err(e) = upload.complete().await {
553 let _ = upload.abort().await;
554 return Err(e);
555 }
556 Ok(())
557}
558
559#[derive(Debug)]
563pub struct ExternalManifestCommitHandler {
564 pub external_manifest_store: Arc<dyn ExternalManifestStore>,
565}
566
567impl ExternalManifestCommitHandler {
568 async fn verify_finalized_manifest_location(
569 &self,
570 base_path: &Path,
571 location: ManifestLocation,
572 object_store: &dyn OSObjectStore,
573 ) -> std::result::Result<ManifestLocation, Error> {
574 match object_store.head(&location.path).await {
575 Ok(ObjectMeta { size, e_tag, .. }) => {
576 let ManifestLocation {
577 version,
578 path,
579 size: expected_size,
580 naming_scheme,
581 e_tag: _,
582 identity,
583 } = location;
584
585 let size = match expected_size {
586 Some(expected_size) if expected_size != size => {
587 return Err(Error::corrupt_file(
588 path,
589 format!(
590 "Manifest size mismatch for version {}: external store expected {}, object store returned {}",
591 version, expected_size, size
592 ),
593 ));
594 }
595 Some(expected_size) => Some(expected_size),
596 None => Some(size),
597 };
598
599 Ok(ManifestLocation {
606 version,
607 path,
608 size,
609 naming_scheme,
610 e_tag,
611 identity,
612 })
613 }
614 Err(ObjectStoreError::NotFound { .. }) => {
615 default_resolve_version(base_path, location.version, object_store).await
618 }
619 Err(e) => Err(e.into()),
620 }
621 }
622
623 #[allow(clippy::too_many_arguments)]
631 async fn finalize_manifest(
632 &self,
633 base_path: &Path,
634 staging_manifest_path: &Path,
635 version: u64,
636 size: u64,
637 store: &dyn OSObjectStore,
638 naming_scheme: ManifestNamingScheme,
639 ) -> std::result::Result<ManifestLocation, Error> {
640 let final_manifest_path = naming_scheme.manifest_path(base_path, version);
642
643 let final_e_tag = copy_or_verify_final_manifest(
644 store,
645 staging_manifest_path,
646 &final_manifest_path,
647 version,
648 size,
649 )
650 .await?;
651
652 let location = ManifestLocation {
653 version,
654 path: final_manifest_path,
655 size: Some(size),
656 naming_scheme,
657 e_tag: final_e_tag,
658 identity: None,
659 };
660
661 let published = self
668 .external_manifest_store
669 .put_if_exists(
670 base_path.as_ref(),
671 version,
672 location.path.as_ref(),
673 size,
674 None,
675 )
676 .await;
677
678 if let Err(error) = published {
679 warn!(
683 "Final manifest '{}' is committed, but the external manifest index could not be updated; retaining staging manifest '{}' for repair: {}",
684 location.path, staging_manifest_path, error
685 );
686 return Ok(location);
687 }
688
689 match store.delete(staging_manifest_path).await {
691 Ok(_) => {}
692 Err(ObjectStoreError::NotFound { .. }) => {}
693 Err(error) => {
694 warn!(
695 "Failed to delete finalized staging manifest '{}': {}",
696 staging_manifest_path, error
697 );
698 return Ok(location);
699 }
700 }
701 info!(target: TRACE_FILE_AUDIT, mode=AUDIT_MODE_DELETE, r#type=AUDIT_TYPE_MANIFEST, path = staging_manifest_path.as_ref());
702
703 Ok(location)
704 }
705}
706
707#[async_trait]
708impl CommitHandler for ExternalManifestCommitHandler {
709 async fn version_exists(
710 &self,
711 base_path: &Path,
712 version: u64,
713 object_store: &dyn OSObjectStore,
714 naming_scheme: ManifestNamingScheme,
715 ) -> Result<bool> {
716 match self
717 .external_manifest_store
718 .get_manifest_location(base_path.as_ref(), version)
719 .await
720 {
721 Ok(_) => Ok(true),
722 Err(Error::NotFound { .. }) => {
723 let path = naming_scheme.manifest_path(base_path, version);
724 match object_store.head(&path).await {
725 Ok(_) => Ok(true),
726 Err(ObjectStoreError::NotFound { .. }) => Ok(false),
727 Err(e) => Err(e.into()),
728 }
729 }
730 Err(e) => Err(e),
731 }
732 }
733
734 async fn resolve_latest_location(
735 &self,
736 base_path: &Path,
737 object_store: &ObjectStore,
738 ) -> std::result::Result<ManifestLocation, Error> {
739 let location = self
740 .external_manifest_store
741 .get_latest_manifest_location(base_path.as_ref())
742 .await?;
743
744 match location {
745 Some(location) => {
746 if location.identity.is_some() {
747 return recorded_as_final(location, object_store.inner.as_ref()).await;
748 }
749 if location.path.extension() == Some(MANIFEST_EXTENSION) {
750 return self
751 .verify_finalized_manifest_location(
752 base_path,
753 location,
754 object_store.inner.as_ref(),
755 )
756 .await;
757 }
758
759 let ManifestLocation {
760 version,
761 path,
762 size,
763 naming_scheme,
764 e_tag: _,
765 identity,
766 } = location;
767
768 let size = if let Some(size) = size {
769 size
770 } else {
771 match object_store.inner.head(&path).await {
772 Ok(meta) => meta.size,
773 Err(ObjectStoreError::NotFound { .. }) => {
774 let new_location = self
776 .external_manifest_store
777 .get_manifest_location(base_path.as_ref(), version)
778 .await?;
779 return Ok(new_location);
780 }
781 Err(e) => return Err(e.into()),
782 }
783 };
784
785 let mut final_location = self
786 .finalize_manifest(
787 base_path,
788 &path,
789 version,
790 size,
791 &object_store.inner,
792 naming_scheme,
793 )
794 .await?;
795 final_location.identity = identity;
796 Ok(final_location)
797 }
798 None => current_manifest_path(object_store, base_path).await,
801 }
802 }
803
804 async fn resolve_version_location(
805 &self,
806 base_path: &Path,
807 version: u64,
808 object_store: &dyn OSObjectStore,
809 ) -> std::result::Result<ManifestLocation, Error> {
810 let location_res = self
811 .external_manifest_store
812 .get_manifest_location(base_path.as_ref(), version)
813 .await;
814
815 let location = match location_res {
816 Ok(p) => p,
817 Err(Error::NotFound { .. }) => {
819 let path = default_resolve_version(base_path, version, object_store)
820 .await
821 .map_err(|_| Error::not_found(format!("{}@{}", base_path, version)))?
822 .path;
823 match object_store.head(&path).await {
824 Ok(ObjectMeta { size, e_tag, .. }) => {
825 let res = self
826 .external_manifest_store
827 .put_if_not_exists(
828 base_path.as_ref(),
829 version,
830 path.as_ref(),
831 size,
832 None,
833 )
834 .await;
835 if let Err(e) = res {
836 warn!(
837 "could not update external manifest store during load, with error: {}",
838 e
839 );
840 }
841 let naming_scheme =
842 ManifestNamingScheme::detect_scheme_staging(path.filename().unwrap());
843 return Ok(ManifestLocation {
844 version,
845 path,
846 size: Some(size),
847 naming_scheme,
848 e_tag,
849 identity: None,
850 });
851 }
852 Err(ObjectStoreError::NotFound { .. }) => {
853 return Err(Error::not_found(path.to_string()));
854 }
855 Err(e) => return Err(e.into()),
856 }
857 }
858 Err(e) => return Err(e),
859 };
860
861 if location.identity.is_some() {
862 return recorded_as_final(location, object_store).await;
863 }
864 if location.path.extension() == Some(MANIFEST_EXTENSION) {
865 return self
866 .verify_finalized_manifest_location(base_path, location, object_store)
867 .await;
868 }
869
870 let naming_scheme =
871 ManifestNamingScheme::detect_scheme_staging(location.path.filename().unwrap());
872
873 let size = if let Some(size) = location.size {
874 size
875 } else {
876 let meta = object_store.head(&location.path).await?;
877 meta.size
878 };
879
880 let mut final_location = self
881 .finalize_manifest(
882 base_path,
883 &location.path,
884 version,
885 size,
886 object_store,
887 naming_scheme,
888 )
889 .await?;
890 final_location.identity = location.identity;
891 Ok(final_location)
892 }
893
894 async fn resolve_identity(
895 &self,
896 base_path: &Path,
897 _object_store: &ObjectStore,
898 version: u64,
899 ) -> Result<Option<PredecessorIdentity>> {
900 Ok(self
901 .external_manifest_store
902 .get_identity(base_path.as_ref(), version)
903 .await?
904 .map(|identity| PredecessorIdentity { version, identity }))
905 }
906
907 fn list_manifest_locations<'a>(
908 &self,
909 base_path: &Path,
910 object_store: &'a ObjectStore,
911 sorted_descending: bool,
912 ) -> BoxStream<'a, Result<ManifestLocation>> {
913 let store = self.external_manifest_store.clone();
914 let base_path = base_path.clone();
915 futures::stream::once(async move {
916 match store.list_versions(base_path.as_ref(), None).await? {
917 Some(mut locations) => {
918 if sorted_descending {
919 locations.sort_by_key(|l| std::cmp::Reverse(l.version));
920 }
921 Ok::<_, Error>(futures::stream::iter(locations.into_iter().map(Ok)).boxed())
922 }
923 None => Ok(default_list_manifest_locations(
924 &base_path,
925 object_store,
926 sorted_descending,
927 )),
928 }
929 })
930 .try_flatten()
931 .boxed()
932 }
933
934 fn list_manifest_locations_since<'a>(
935 &self,
936 base_path: &Path,
937 object_store: &'a ObjectStore,
938 since_version: u64,
939 ) -> BoxStream<'a, Result<ManifestLocation>> {
940 let store = self.external_manifest_store.clone();
941 let base_path = base_path.clone();
942 futures::stream::once(async move {
943 match store
944 .list_versions(base_path.as_ref(), Some(since_version))
945 .await?
946 {
947 Some(mut locations) => {
948 locations.retain(|l| l.version > since_version);
949 locations.sort_by_key(|l| std::cmp::Reverse(l.version));
950 Ok::<_, Error>(futures::stream::iter(locations.into_iter().map(Ok)).boxed())
951 }
952 None => Ok(default_list_manifest_locations_since(
953 &base_path,
954 object_store,
955 since_version,
956 )),
957 }
958 })
959 .try_flatten()
960 .boxed()
961 }
962
963 async fn commit(
964 &self,
965 manifest: &mut Manifest,
966 indices: Option<Vec<IndexMetadata>>,
967 base_path: &Path,
968 object_store: &ObjectStore,
969 manifest_writer: super::ManifestWriter,
970 naming_scheme: ManifestNamingScheme,
971 transaction: Option<Transaction>,
972 ) -> std::result::Result<ManifestLocation, CommitError> {
973 let path = naming_scheme.manifest_path(base_path, manifest.version);
978 let staging_path = make_staging_manifest_path(&path)?;
979 let write_res =
980 manifest_writer(object_store, manifest, indices, &staging_path, transaction).await?;
981
982 let result = self
984 .external_manifest_store
985 .put(
986 base_path,
987 manifest.version,
988 &staging_path,
989 write_res.size as u64,
990 write_res.e_tag,
991 &object_store.inner,
992 naming_scheme,
993 )
994 .await;
995
996 match result {
997 Ok(location) => {
998 write_version_hint(object_store, base_path, manifest.version).await;
999 Ok(location)
1000 }
1001 Err(error) => Err(self
1002 .lose_or_retain(
1003 base_path,
1004 manifest.version,
1005 &staging_path,
1006 object_store,
1007 error,
1008 )
1009 .await),
1010 }
1011 }
1012
1013 async fn delete(&self, base_path: &Path) -> Result<()> {
1014 self.external_manifest_store
1015 .delete(base_path.as_ref())
1016 .await
1017 }
1018
1019 async fn forget_version(&self, base_path: &Path, version: u64, identity: &str) -> Result<()> {
1020 self.external_manifest_store
1021 .forget_version(base_path.as_ref(), version, identity)
1022 .await
1023 }
1024
1025 fn supports_predecessor_condition(&self) -> bool {
1026 self.external_manifest_store
1027 .supports_predecessor_condition()
1028 }
1029
1030 async fn resolve_latest_identity(
1031 &self,
1032 base_path: &Path,
1033 _object_store: &ObjectStore,
1034 ) -> Result<Option<PredecessorIdentity>> {
1035 let Some((version, _)) = self
1036 .external_manifest_store
1037 .get_latest_version(base_path.as_ref())
1038 .await?
1039 else {
1040 return Ok(None);
1041 };
1042 Ok(self
1043 .external_manifest_store
1044 .get_identity(base_path.as_ref(), version)
1045 .await?
1046 .map(|identity| PredecessorIdentity { version, identity }))
1047 }
1048
1049 async fn commit_after(
1050 &self,
1051 manifest: &mut Manifest,
1052 indices: Option<Vec<IndexMetadata>>,
1053 base_path: &Path,
1054 object_store: &ObjectStore,
1055 manifest_writer: super::ManifestWriter,
1056 naming_scheme: ManifestNamingScheme,
1057 transaction: Option<Transaction>,
1058 predecessor: &PredecessorIdentity,
1059 ) -> std::result::Result<ManifestLocation, CommitError> {
1060 let path =
1064 make_staging_manifest_path(&naming_scheme.manifest_path(base_path, manifest.version))?;
1065 let write_res =
1066 manifest_writer(object_store, manifest, indices, &path, transaction).await?;
1067 let size = write_res.size as u64;
1068
1069 let reserved = self
1070 .external_manifest_store
1071 .put_if_predecessor(
1072 base_path.as_ref(),
1073 manifest.version,
1074 path.as_ref(),
1075 size,
1076 predecessor,
1077 )
1078 .await;
1079 match reserved {
1080 Ok(Reservation::Reserved { identity }) => {
1081 write_version_hint(object_store, base_path, manifest.version).await;
1082 Ok(ManifestLocation {
1083 version: manifest.version,
1084 path,
1085 size: Some(size),
1086 naming_scheme,
1087 e_tag: write_res.e_tag,
1088 identity: Some(identity),
1089 })
1090 }
1091 Ok(Reservation::PredecessorChanged) => {
1092 delete_staging(object_store, &path, "refused").await;
1094 Err(CommitError::OtherError(
1095 lance_core::error::PrerequisiteFailedSnafu {
1096 message: format!(
1097 "manifest {} is no longer the predecessor this commit was judged against",
1098 predecessor.version
1099 ),
1100 }
1101 .build(),
1102 ))
1103 }
1104 Ok(Reservation::Taken) => Err(self
1105 .lose_or_retain(
1106 base_path,
1107 manifest.version,
1108 &path,
1109 object_store,
1110 Error::commit_conflict_source(
1111 manifest.version,
1112 "manifest already exists".into(),
1113 ),
1114 )
1115 .await),
1116 Err(error) => Err(self
1117 .lose_or_retain(base_path, manifest.version, &path, object_store, error)
1118 .await),
1119 }
1120 }
1121}
1122
1123impl ExternalManifestCommitHandler {
1124 async fn lose_or_retain(
1127 &self,
1128 base_path: &Path,
1129 version: u64,
1130 staging_path: &Path,
1131 object_store: &ObjectStore,
1132 error: Error,
1133 ) -> CommitError {
1134 let recorded_location = self
1135 .external_manifest_store
1136 .get_manifest_location(base_path.as_ref(), version)
1137 .await;
1138 if matches!(&recorded_location, Ok(location) if location.path != *staging_path) {
1139 delete_staging(object_store, staging_path, "losing").await;
1140 return CommitError::CommitConflict;
1141 }
1142 warn!(
1143 "External manifest commit for version {} failed; retaining staging manifest \
1144 '{}' until the commit outcome is resolved: {}",
1145 version, staging_path, error
1146 );
1147 CommitError::CommitConflict
1148 }
1149}
1150
1151async fn recorded_as_final(
1154 mut location: ManifestLocation,
1155 object_store: &dyn OSObjectStore,
1156) -> Result<ManifestLocation> {
1157 if location.size.is_none() {
1158 location.size = Some(object_store.head(&location.path).await?.size);
1159 }
1160 Ok(location)
1161}
1162
1163async fn delete_staging(object_store: &ObjectStore, staging_path: &Path, why: &str) {
1164 match object_store.inner.delete(staging_path).await {
1165 Ok(()) => {
1166 info!(target: TRACE_FILE_AUDIT, mode=AUDIT_MODE_DELETE, r#type=AUDIT_TYPE_MANIFEST, path = staging_path.as_ref());
1167 }
1168 Err(ObjectStoreError::NotFound { .. }) => {}
1169 Err(delete_error) => {
1170 warn!(
1171 "Failed to delete {} staging manifest '{}': {}",
1172 why, staging_path, delete_error
1173 );
1174 }
1175 }
1176}
1177
1178#[cfg(test)]
1179mod tests {
1180 use std::collections::HashMap;
1181 use std::sync::Mutex;
1182 use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
1183
1184 use arrow_schema::{DataType, Field as ArrowField, Schema as ArrowSchema};
1185 use lance_core::datatypes::Schema;
1186 use lance_core::utils::testing::{ProxyObjectStore, ProxyObjectStorePolicy};
1187 use lance_file::version::LanceFileVersion;
1188 use tokio::sync::Notify;
1189
1190 use super::*;
1191 use crate::format::DataStorageFormat;
1192 use crate::io::commit::{VERSIONS_DIR, write_manifest_file_to_path};
1193 use futures::TryStreamExt;
1194
1195 #[derive(Debug, Clone)]
1196 struct StoredManifest {
1197 path: String,
1198 size: u64,
1199 e_tag: Option<String>,
1200 }
1201
1202 #[derive(Debug)]
1203 struct TestExternalManifestStore {
1204 manifests: Mutex<HashMap<(String, u64), StoredManifest>>,
1205 fail_next_put_response: AtomicBool,
1206 fail_next_final_publish: AtomicBool,
1207 block_first_final_publish: bool,
1208 final_publish_calls: AtomicUsize,
1209 first_final_publish_started: Notify,
1210 release_first_final_publish: Notify,
1211 }
1212
1213 impl TestExternalManifestStore {
1214 fn new(fail_next_put_response: bool) -> Self {
1215 Self {
1216 manifests: Mutex::new(HashMap::new()),
1217 fail_next_put_response: AtomicBool::new(fail_next_put_response),
1218 fail_next_final_publish: AtomicBool::new(false),
1219 block_first_final_publish: false,
1220 final_publish_calls: AtomicUsize::new(0),
1221 first_final_publish_started: Notify::new(),
1222 release_first_final_publish: Notify::new(),
1223 }
1224 }
1225
1226 fn failing_final_publish_once() -> Self {
1227 Self {
1228 fail_next_final_publish: AtomicBool::new(true),
1229 ..Self::new(false)
1230 }
1231 }
1232
1233 fn blocking_first_final_publish() -> Self {
1234 Self {
1235 block_first_final_publish: true,
1236 ..Self::new(false)
1237 }
1238 }
1239 }
1240
1241 #[async_trait]
1242 impl ExternalManifestStore for TestExternalManifestStore {
1243 async fn get(&self, base_uri: &str, version: u64) -> Result<String> {
1244 self.manifests
1245 .lock()
1246 .unwrap()
1247 .get(&(base_uri.to_string(), version))
1248 .map(|manifest| manifest.path.clone())
1249 .ok_or_else(|| Error::not_found(format!("{base_uri}@{version}")))
1250 }
1251
1252 async fn get_manifest_location(
1253 &self,
1254 base_uri: &str,
1255 version: u64,
1256 ) -> Result<ManifestLocation> {
1257 let stored = self
1258 .manifests
1259 .lock()
1260 .unwrap()
1261 .get(&(base_uri.to_string(), version))
1262 .cloned()
1263 .ok_or_else(|| Error::not_found(format!("{base_uri}@{version}")))?;
1264 let path = Path::from(stored.path);
1265 Ok(ManifestLocation {
1266 version,
1267 naming_scheme: detect_naming_scheme_from_path(&path)?,
1268 path,
1269 size: Some(stored.size),
1270 e_tag: stored.e_tag,
1271 identity: None,
1272 })
1273 }
1274
1275 async fn get_latest_version(&self, base_uri: &str) -> Result<Option<(u64, String)>> {
1276 Ok(self
1277 .manifests
1278 .lock()
1279 .unwrap()
1280 .iter()
1281 .filter(|((stored_base, _), _)| stored_base == base_uri)
1282 .max_by_key(|((_, version), _)| *version)
1283 .map(|((_, version), manifest)| (*version, manifest.path.clone())))
1284 }
1285
1286 async fn put_if_not_exists(
1287 &self,
1288 base_uri: &str,
1289 version: u64,
1290 path: &str,
1291 size: u64,
1292 e_tag: Option<String>,
1293 ) -> Result<()> {
1294 let key = (base_uri.to_string(), version);
1295 let mut manifests = self.manifests.lock().unwrap();
1296 if manifests.contains_key(&key) {
1297 return Err(Error::commit_conflict_source(
1298 version,
1299 "manifest already exists".to_string().into(),
1300 ));
1301 }
1302 manifests.insert(
1303 key,
1304 StoredManifest {
1305 path: path.to_string(),
1306 size,
1307 e_tag,
1308 },
1309 );
1310 drop(manifests);
1311 if self.fail_next_put_response.swap(false, Ordering::SeqCst) {
1312 Err(Error::io("simulated lost external-store response"))
1313 } else {
1314 Ok(())
1315 }
1316 }
1317
1318 async fn put_if_exists(
1319 &self,
1320 base_uri: &str,
1321 version: u64,
1322 path: &str,
1323 size: u64,
1324 e_tag: Option<String>,
1325 ) -> Result<()> {
1326 if self.block_first_final_publish
1327 && self.final_publish_calls.fetch_add(1, Ordering::SeqCst) == 0
1328 {
1329 self.first_final_publish_started.notify_one();
1330 self.release_first_final_publish.notified().await;
1331 }
1332 if self.fail_next_final_publish.swap(false, Ordering::SeqCst) {
1333 return Err(Error::io("simulated final index update failure"));
1334 }
1335 let key = (base_uri.to_string(), version);
1336 let mut manifests = self.manifests.lock().unwrap();
1337 let manifest = manifests
1338 .get_mut(&key)
1339 .ok_or_else(|| Error::not_found(format!("{base_uri}@{version}")))?;
1340 *manifest = StoredManifest {
1341 path: path.to_string(),
1342 size,
1343 e_tag,
1344 };
1345 Ok(())
1346 }
1347 }
1348
1349 fn test_manifest() -> Manifest {
1350 let arrow_schema = ArrowSchema::new(vec![ArrowField::new("id", DataType::Int32, false)]);
1351 Manifest::new(
1352 Schema::try_from(&arrow_schema).unwrap(),
1353 Arc::new(vec![]),
1354 DataStorageFormat::new(LanceFileVersion::Stable.resolve()),
1355 HashMap::new(),
1356 )
1357 }
1358
1359 #[tokio::test]
1360 async fn test_finalized_manifest_ignores_legacy_external_store_etag() {
1361 let external_store = Arc::new(TestExternalManifestStore::new(false));
1362 let handler = ExternalManifestCommitHandler {
1363 external_manifest_store: external_store.clone(),
1364 };
1365 let object_store = ObjectStore::memory();
1366 let base_path = Path::from("dataset");
1367 let final_path = ManifestNamingScheme::V2.manifest_path(&base_path, 1);
1368
1369 object_store
1370 .inner
1371 .put(
1372 &final_path,
1373 object_store::PutPayload::from_static(b"manifest"),
1374 )
1375 .await
1376 .unwrap();
1377 let final_meta = object_store.inner.head(&final_path).await.unwrap();
1378
1379 external_store
1380 .put_if_not_exists(
1381 base_path.as_ref(),
1382 1,
1383 final_path.as_ref(),
1384 final_meta.size,
1385 Some("expected-generation".to_string()),
1386 )
1387 .await
1388 .unwrap();
1389
1390 let resolved = handler
1391 .resolve_version_location(&base_path, 1, object_store.inner.as_ref())
1392 .await
1393 .expect("a legacy external-store ETag must not override object storage");
1394 assert_eq!(resolved.path, final_path);
1395 assert_eq!(resolved.size, Some(final_meta.size));
1396 assert_eq!(resolved.e_tag, final_meta.e_tag);
1397 }
1398
1399 #[tokio::test]
1400 async fn test_finalized_manifest_without_external_store_etag_uses_current_etag() {
1401 let external_store = Arc::new(TestExternalManifestStore::new(false));
1402 let handler = ExternalManifestCommitHandler {
1403 external_manifest_store: external_store.clone(),
1404 };
1405 let object_store = ObjectStore::memory();
1406 let base_path = Path::from("dataset");
1407 let final_path = ManifestNamingScheme::V2.manifest_path(&base_path, 1);
1408
1409 object_store
1410 .inner
1411 .put(
1412 &final_path,
1413 object_store::PutPayload::from_static(b"manifest"),
1414 )
1415 .await
1416 .unwrap();
1417 let final_meta = object_store.inner.head(&final_path).await.unwrap();
1418 external_store
1419 .put_if_not_exists(
1420 base_path.as_ref(),
1421 1,
1422 final_path.as_ref(),
1423 final_meta.size,
1424 None,
1425 )
1426 .await
1427 .unwrap();
1428
1429 let resolved = handler
1430 .resolve_version_location(&base_path, 1, object_store.inner.as_ref())
1431 .await
1432 .expect("an absent external-store ETag must opt out of comparison");
1433 assert_eq!(resolved.path, final_path);
1434 assert_eq!(resolved.size, Some(final_meta.size));
1435 assert_eq!(resolved.e_tag, final_meta.e_tag);
1436 }
1437
1438 #[tokio::test]
1439 async fn test_default_store_returns_but_does_not_persist_etag() {
1440 let external_store = Arc::new(TestExternalManifestStore::new(false));
1441 let handler = ExternalManifestCommitHandler {
1442 external_manifest_store: external_store.clone(),
1443 };
1444 let object_store = ObjectStore::memory();
1445 let base_path = Path::from("dataset");
1446 let mut manifest = test_manifest();
1447
1448 let committed = handler
1449 .commit(
1450 &mut manifest,
1451 None,
1452 &base_path,
1453 &object_store,
1454 write_manifest_file_to_path,
1455 ManifestNamingScheme::V2,
1456 None,
1457 )
1458 .await
1459 .expect("the default store should finalize the selected manifest");
1460 let original = object_store.inner.head(&committed.path).await.unwrap();
1461 assert_eq!(committed.e_tag, original.e_tag);
1462
1463 let indexed = external_store
1464 .get_manifest_location(base_path.as_ref(), committed.version)
1465 .await
1466 .unwrap();
1467 assert_eq!(indexed.e_tag, None);
1468
1469 object_store
1470 .inner
1471 .put(
1472 &committed.path,
1473 object_store::PutPayload::from(vec![0_u8; original.size as usize]),
1474 )
1475 .await
1476 .unwrap();
1477
1478 let replacement = object_store.inner.head(&committed.path).await.unwrap();
1479 assert_ne!(replacement.e_tag, original.e_tag);
1480
1481 let resolved = handler
1482 .resolve_version_location(&base_path, committed.version, object_store.inner.as_ref())
1483 .await
1484 .expect("the external index must not reject a new physical generation");
1485 assert_eq!(resolved.e_tag, replacement.e_tag);
1486 }
1487
1488 #[tokio::test]
1489 async fn test_helping_finalizer_returns_but_does_not_persist_etag() {
1490 let external_store = Arc::new(TestExternalManifestStore::new(false));
1491 let handler = ExternalManifestCommitHandler {
1492 external_manifest_store: external_store.clone(),
1493 };
1494 let object_store = ObjectStore::memory();
1495 let base_path = Path::from("dataset");
1496 let version = 1;
1497 let final_path = ManifestNamingScheme::V2.manifest_path(&base_path, version);
1498 let staging_path = make_staging_manifest_path(&final_path).unwrap();
1499 let manifest_bytes = Bytes::from_static(b"immutable manifest bytes");
1500
1501 object_store
1502 .inner
1503 .put(&staging_path, manifest_bytes.clone().into())
1504 .await
1505 .unwrap();
1506 let staging_meta = object_store.inner.head(&staging_path).await.unwrap();
1507 external_store
1508 .put_if_not_exists(
1509 base_path.as_ref(),
1510 version,
1511 staging_path.as_ref(),
1512 staging_meta.size,
1513 staging_meta.e_tag,
1514 )
1515 .await
1516 .unwrap();
1517
1518 let finalized = handler
1519 .resolve_version_location(&base_path, version, object_store.inner.as_ref())
1520 .await
1521 .expect("a reader should finalize the selected staging manifest");
1522 let final_meta = object_store.inner.head(&final_path).await.unwrap();
1523 assert_eq!(finalized.e_tag, final_meta.e_tag);
1524
1525 let indexed = external_store
1526 .get_manifest_location(base_path.as_ref(), version)
1527 .await
1528 .unwrap();
1529 assert_eq!(indexed.e_tag, None);
1530 }
1531
1532 #[tokio::test]
1533 async fn test_onboarding_returns_but_does_not_persist_etag() {
1534 let external_store = Arc::new(TestExternalManifestStore::new(false));
1535 let handler = ExternalManifestCommitHandler {
1536 external_manifest_store: external_store.clone(),
1537 };
1538 let object_store = ObjectStore::memory();
1539 let base_path = Path::from("dataset");
1540 let version = 1;
1541 let final_path = ManifestNamingScheme::V2.manifest_path(&base_path, version);
1542
1543 object_store
1544 .inner
1545 .put(
1546 &final_path,
1547 object_store::PutPayload::from_static(b"manifest"),
1548 )
1549 .await
1550 .unwrap();
1551 let final_meta = object_store.inner.head(&final_path).await.unwrap();
1552
1553 let resolved = handler
1554 .resolve_version_location(&base_path, version, object_store.inner.as_ref())
1555 .await
1556 .expect("an existing manifest should be indexed during onboarding");
1557 assert_eq!(resolved.e_tag, final_meta.e_tag);
1558
1559 let indexed = external_store
1560 .get_manifest_location(base_path.as_ref(), version)
1561 .await
1562 .unwrap();
1563 assert_eq!(indexed.e_tag, None);
1564 }
1565
1566 #[tokio::test]
1567 async fn test_finalized_manifest_size_mismatch_remains_corruption() {
1568 let external_store = Arc::new(TestExternalManifestStore::new(false));
1569 let handler = ExternalManifestCommitHandler {
1570 external_manifest_store: external_store.clone(),
1571 };
1572 let object_store = ObjectStore::memory();
1573 let base_path = Path::from("dataset");
1574 let final_path = ManifestNamingScheme::V2.manifest_path(&base_path, 1);
1575
1576 object_store
1577 .inner
1578 .put(
1579 &final_path,
1580 object_store::PutPayload::from_static(b"manifest"),
1581 )
1582 .await
1583 .unwrap();
1584 let final_meta = object_store.inner.head(&final_path).await.unwrap();
1585 external_store
1586 .put_if_not_exists(
1587 base_path.as_ref(),
1588 1,
1589 final_path.as_ref(),
1590 final_meta.size + 1,
1591 None,
1592 )
1593 .await
1594 .unwrap();
1595
1596 let error = handler
1597 .resolve_version_location(&base_path, 1, object_store.inner.as_ref())
1598 .await
1599 .expect_err("copies of the selected staging object must preserve its size");
1600 assert!(matches!(error, Error::CorruptFile { .. }));
1601 assert!(error.to_string().contains("Manifest size mismatch"));
1602 }
1603
1604 #[tokio::test]
1605 async fn test_canonical_manifest_commits_before_index_repair() {
1606 let external_store = Arc::new(TestExternalManifestStore::failing_final_publish_once());
1607 let handler = ExternalManifestCommitHandler {
1608 external_manifest_store: external_store.clone(),
1609 };
1610 let object_store = ObjectStore::memory();
1611 let base_path = Path::from("dataset");
1612 let mut manifest = test_manifest();
1613 let version = manifest.version;
1614 let final_path = ManifestNamingScheme::V2.manifest_path(&base_path, version);
1615
1616 let committed = handler
1617 .commit(
1618 &mut manifest,
1619 None,
1620 &base_path,
1621 &object_store,
1622 write_manifest_file_to_path,
1623 ManifestNamingScheme::V2,
1624 None,
1625 )
1626 .await
1627 .expect("a failed index update must not overturn a canonical S3 commit");
1628 assert_eq!(committed.path, final_path);
1629 assert!(
1630 committed.e_tag.is_some(),
1631 "the caller must retain the canonical generation even when index repair fails"
1632 );
1633 object_store
1634 .inner
1635 .head(&final_path)
1636 .await
1637 .expect("the canonical manifest is the durable commit point");
1638
1639 let pending = external_store
1640 .get_manifest_location(base_path.as_ref(), version)
1641 .await
1642 .unwrap();
1643 assert_ne!(pending.path, final_path);
1644 object_store
1645 .inner
1646 .head(&pending.path)
1647 .await
1648 .expect("staging must remain until the external index is repaired");
1649
1650 let repaired = handler
1651 .resolve_version_location(&base_path, version, object_store.inner.as_ref())
1652 .await
1653 .expect("a reader must be able to repair the pending external index");
1654 assert_eq!(repaired.path, final_path);
1655 assert!(
1656 repaired.e_tag.is_some(),
1657 "a helping reader must receive the generation it observed"
1658 );
1659 let indexed = external_store
1660 .get_manifest_location(base_path.as_ref(), version)
1661 .await
1662 .unwrap();
1663 assert_eq!(indexed.path, final_path);
1664 assert_eq!(indexed.size, repaired.size);
1665 assert_eq!(
1666 indexed.e_tag, None,
1667 "the repaired index must not retain a physical object generation"
1668 );
1669 let staging_error = object_store
1670 .inner
1671 .head(&pending.path)
1672 .await
1673 .expect_err("repair should garbage-collect the retained staging object");
1674 assert!(matches!(staging_error, ObjectStoreError::NotFound { .. }));
1675 }
1676
1677 #[tokio::test]
1678 async fn test_concurrent_finalizers_return_but_do_not_persist_generations() {
1679 let external_store = Arc::new(TestExternalManifestStore::blocking_first_final_publish());
1680 let handler = ExternalManifestCommitHandler {
1681 external_manifest_store: external_store.clone(),
1682 };
1683 let object_store = ObjectStore::memory();
1684 let base_path = Path::from("dataset");
1685 let version = 1;
1686 let final_path = ManifestNamingScheme::V2.manifest_path(&base_path, version);
1687 let staging_path = make_staging_manifest_path(&final_path).unwrap();
1688 let manifest_bytes = Bytes::from_static(b"immutable manifest bytes");
1689
1690 object_store
1691 .inner
1692 .put(&staging_path, manifest_bytes.clone().into())
1693 .await
1694 .unwrap();
1695 let staging_meta = object_store.inner.head(&staging_path).await.unwrap();
1696
1697 let writer_store = object_store.inner.clone();
1698 let writer_external_store = external_store.clone();
1699 let writer_base_path = base_path.clone();
1700 let writer_staging_path = staging_path.clone();
1701 let writer_e_tag = staging_meta.e_tag.clone();
1702 let writer = tokio::spawn(async move {
1703 writer_external_store
1704 .put(
1705 &writer_base_path,
1706 version,
1707 &writer_staging_path,
1708 staging_meta.size,
1709 writer_e_tag,
1710 writer_store.as_ref(),
1711 ManifestNamingScheme::V2,
1712 )
1713 .await
1714 });
1715
1716 tokio::time::timeout(
1717 std::time::Duration::from_secs(5),
1718 external_store.first_final_publish_started.notified(),
1719 )
1720 .await
1721 .expect("the direct finalizer should pause after COPY");
1722
1723 let first_generation = object_store.inner.head(&final_path).await.unwrap();
1724 let reservation = external_store
1725 .get_manifest_location(base_path.as_ref(), version)
1726 .await
1727 .unwrap();
1728 assert_eq!(reservation.path, staging_path);
1729 assert_eq!(reservation.e_tag, None);
1730
1731 let reader_location = handler
1739 .resolve_version_location(&base_path, version, object_store.inner.as_ref())
1740 .await
1741 .unwrap();
1742
1743 external_store.release_first_final_publish.notify_one();
1744 let writer_location = writer.await.unwrap().unwrap();
1745 let final_meta = object_store.inner.head(&final_path).await.unwrap();
1746 let final_bytes = object_store
1747 .inner
1748 .get(&final_path)
1749 .await
1750 .unwrap()
1751 .bytes()
1752 .await
1753 .unwrap();
1754 let indexed = external_store
1755 .get_manifest_location(base_path.as_ref(), version)
1756 .await
1757 .unwrap();
1758
1759 assert_eq!(final_bytes, manifest_bytes);
1760 assert_ne!(
1761 first_generation.e_tag, final_meta.e_tag,
1762 "the deterministic race must create a new physical generation"
1763 );
1764 assert_eq!(writer_location.e_tag, first_generation.e_tag);
1765 assert_eq!(reader_location.e_tag, final_meta.e_tag);
1766 assert_eq!(indexed.path, final_path);
1767 assert_eq!(indexed.size, Some(final_meta.size));
1768 assert_eq!(
1769 indexed.e_tag, None,
1770 "all finalizers must publish the same generation-independent tuple"
1771 );
1772
1773 let resolved = handler
1774 .resolve_version_location(&base_path, version, object_store.inner.as_ref())
1775 .await
1776 .expect("the finalized manifest must remain readable after the race");
1777 assert_eq!(resolved.e_tag, final_meta.e_tag);
1778 }
1779
1780 #[tokio::test]
1781 async fn test_lost_external_store_response_retains_staging_manifest() {
1782 let external_store = Arc::new(TestExternalManifestStore::new(true));
1783 let handler = ExternalManifestCommitHandler {
1784 external_manifest_store: external_store.clone(),
1785 };
1786 let object_store = ObjectStore::memory();
1787 let base_path = Path::from("dataset");
1788 let mut manifest = test_manifest();
1789
1790 let commit_error = handler
1791 .commit(
1792 &mut manifest,
1793 None,
1794 &base_path,
1795 &object_store,
1796 write_manifest_file_to_path,
1797 ManifestNamingScheme::V2,
1798 None,
1799 )
1800 .await
1801 .expect_err("the simulated response loss must be surfaced");
1802 assert!(matches!(commit_error, CommitError::CommitConflict));
1803
1804 let staging_path = Path::from(external_store.get("dataset", 1).await.unwrap());
1805 object_store.inner.head(&staging_path).await.unwrap();
1806
1807 let resolved = handler
1808 .resolve_version_location(&base_path, 1, object_store.inner.as_ref())
1809 .await
1810 .expect("the retained staging manifest must allow finalization");
1811 assert_eq!(
1812 resolved.path,
1813 ManifestNamingScheme::V2.manifest_path(&base_path, 1)
1814 );
1815 object_store.inner.head(&resolved.path).await.unwrap();
1816 }
1817
1818 #[tokio::test]
1819 async fn test_finalization_returns_etag_without_persisting_it() {
1820 let external_store = Arc::new(TestExternalManifestStore::new(false));
1821 let handler = ExternalManifestCommitHandler {
1822 external_manifest_store: external_store.clone(),
1823 };
1824 let object_store = ObjectStore::memory();
1825 let base_path = Path::from("dataset");
1826 let mut manifest = test_manifest();
1827 let version = manifest.version;
1828 let final_path = ManifestNamingScheme::V2.manifest_path(&base_path, version);
1829
1830 let committed = handler
1831 .commit(
1832 &mut manifest,
1833 None,
1834 &base_path,
1835 &object_store,
1836 write_manifest_file_to_path,
1837 ManifestNamingScheme::V2,
1838 None,
1839 )
1840 .await
1841 .expect("the generic workflow should commit the canonical manifest");
1842 assert_eq!(committed.path, final_path);
1843 let final_meta = object_store.inner.head(&final_path).await.unwrap();
1844 assert_eq!(
1845 committed.e_tag, final_meta.e_tag,
1846 "the freshly committed Dataset needs the observed generation for cache separation"
1847 );
1848
1849 let indexed = external_store
1850 .get_manifest_location(base_path.as_ref(), version)
1851 .await
1852 .expect("the external index must advance after the canonical copy");
1853 assert_eq!(indexed.path, final_path);
1854 assert_eq!(
1855 indexed.e_tag, None,
1856 "the external index must remain independent of physical generations"
1857 );
1858 }
1859
1860 #[tokio::test]
1861 async fn test_missing_staging_verifies_existing_final_manifest() {
1862 let object_store = ObjectStore::memory();
1863 let staging_path = Path::from("dataset/_versions/1.manifest-missing");
1864 let final_path = Path::from("dataset/_versions/1.manifest");
1865 let manifest_bytes = Bytes::from_static(b"immutable manifest bytes");
1866 object_store
1867 .inner
1868 .put(&final_path, manifest_bytes.clone().into())
1869 .await
1870 .unwrap();
1871 let final_meta = object_store.inner.head(&final_path).await.unwrap();
1872
1873 let recovered_e_tag = copy_or_verify_final_manifest(
1874 object_store.inner.as_ref(),
1875 &staging_path,
1876 &final_path,
1877 1,
1878 manifest_bytes.len() as u64,
1879 )
1880 .await
1881 .expect("an existing canonical manifest should prove another helper finalized it");
1882
1883 assert_eq!(recovered_e_tag, final_meta.e_tag);
1884 }
1885
1886 #[tokio::test]
1887 async fn test_missing_staging_rejects_missing_final_manifest() {
1888 let object_store = ObjectStore::memory();
1889 let staging_path = Path::from("dataset/_versions/1.manifest-missing");
1890 let final_path = Path::from("dataset/_versions/1.manifest");
1891
1892 let error = copy_or_verify_final_manifest(
1893 object_store.inner.as_ref(),
1894 &staging_path,
1895 &final_path,
1896 1,
1897 42,
1898 )
1899 .await
1900 .expect_err("missing staging and canonical objects cannot establish a commit");
1901
1902 assert!(matches!(error, Error::NotFound { .. }), "{error:?}");
1903 assert!(error.to_string().contains(final_path.as_ref()), "{error}");
1904 }
1905
1906 #[tokio::test]
1907 async fn test_missing_staging_rejects_wrong_final_size() {
1908 let object_store = ObjectStore::memory();
1909 let staging_path = Path::from("dataset/_versions/1.manifest-missing");
1910 let final_path = Path::from("dataset/_versions/1.manifest");
1911 object_store
1912 .inner
1913 .put(&final_path, Bytes::from_static(b"wrong size").into())
1914 .await
1915 .unwrap();
1916
1917 let error = copy_or_verify_final_manifest(
1918 object_store.inner.as_ref(),
1919 &staging_path,
1920 &final_path,
1921 1,
1922 42,
1923 )
1924 .await
1925 .expect_err("a same-path object with the wrong size is not the selected manifest");
1926
1927 assert!(matches!(error, Error::CorruptFile { .. }), "{error:?}");
1928 assert!(
1929 error.to_string().contains("Manifest size mismatch"),
1930 "{error}"
1931 );
1932 }
1933
1934 #[tokio::test]
1935 async fn test_copy_failure_after_external_store_commit_retains_staging_manifest() {
1936 let external_store = Arc::new(TestExternalManifestStore::new(false));
1937 let handler = ExternalManifestCommitHandler {
1938 external_manifest_store: external_store.clone(),
1939 };
1940
1941 let mut object_store = ObjectStore::memory();
1942 let fail_next_copy = Arc::new(AtomicBool::new(true));
1943 let failed_copy_source = Arc::new(Mutex::new(None));
1944 let mut policy = ProxyObjectStorePolicy::new();
1945 let policy_fail_next_copy = fail_next_copy.clone();
1946 let policy_failed_copy_source = failed_copy_source.clone();
1947 policy.set_before_policy(
1948 "fail-copy-once",
1949 Arc::new(move |method, location| {
1950 if method == "copy" && policy_fail_next_copy.swap(false, Ordering::SeqCst) {
1951 *policy_failed_copy_source.lock().unwrap() = Some(location.clone());
1952 return Err(Error::io("simulated copy failure"));
1953 }
1954 Ok(())
1955 }),
1956 );
1957 let policy = Arc::new(Mutex::new(policy));
1958 object_store.inner = Arc::new(ProxyObjectStore::new(
1959 object_store.inner.clone(),
1960 policy.clone(),
1961 ));
1962
1963 let base_path = Path::from("dataset");
1964 let mut manifest = test_manifest();
1965 let version = manifest.version;
1966 let canonical_path = ManifestNamingScheme::V2.manifest_path(&base_path, version);
1967
1968 let commit_error = handler
1969 .commit(
1970 &mut manifest,
1971 None,
1972 &base_path,
1973 &object_store,
1974 write_manifest_file_to_path,
1975 ManifestNamingScheme::V2,
1976 None,
1977 )
1978 .await
1979 .expect_err("the simulated copy failure must be surfaced");
1980 assert!(matches!(commit_error, CommitError::CommitConflict));
1981 assert!(
1982 !fail_next_copy.load(Ordering::SeqCst),
1983 "the one-shot copy failure must be consumed"
1984 );
1985
1986 let recorded_location = external_store
1987 .get_manifest_location(base_path.as_ref(), version)
1988 .await
1989 .expect("the external store must retain the committed staging location");
1990 let staging_path = failed_copy_source
1991 .lock()
1992 .unwrap()
1993 .clone()
1994 .expect("the failure must be injected at copy(staging, canonical)");
1995 assert_eq!(recorded_location.path, staging_path);
1996 object_store
1997 .inner
1998 .head(&staging_path)
1999 .await
2000 .expect("the winning staging manifest must be retained");
2001
2002 let canonical_error = object_store
2003 .inner
2004 .head(&canonical_path)
2005 .await
2006 .expect_err("copy failed before creating the canonical manifest");
2007 assert!(
2008 matches!(canonical_error, ObjectStoreError::NotFound { .. }),
2009 "unexpected canonical manifest error: {canonical_error}"
2010 );
2011
2012 policy.lock().unwrap().clear_before_policy("fail-copy-once");
2013 let resolved = handler
2014 .resolve_version_location(&base_path, version, object_store.inner.as_ref())
2015 .await
2016 .expect("the retained staging manifest must allow finalization");
2017 assert_eq!(resolved.path, canonical_path);
2018
2019 let finalized_location = external_store
2020 .get_manifest_location(base_path.as_ref(), version)
2021 .await
2022 .expect("the external store must publish the canonical location");
2023 assert_eq!(finalized_location.path, canonical_path);
2024 object_store
2025 .inner
2026 .head(&canonical_path)
2027 .await
2028 .expect("the canonical manifest must exist after finalization");
2029
2030 let staging_error = object_store
2031 .inner
2032 .head(&staging_path)
2033 .await
2034 .expect_err("successful finalization must clean up the staging manifest");
2035 assert!(
2036 matches!(staging_error, ObjectStoreError::NotFound { .. }),
2037 "unexpected staging manifest error: {staging_error}"
2038 );
2039 }
2040
2041 #[derive(Debug, Default)]
2044 struct IdentifiedStore {
2045 rows: Mutex<HashMap<u64, (String, u64, String)>>,
2046 next_identity: AtomicUsize,
2047 hold_next_reservation: AtomicBool,
2048 reservation_held: Notify,
2049 release_reservation: Notify,
2050 }
2051
2052 impl IdentifiedStore {
2053 fn mint(&self) -> String {
2054 format!(
2055 "identity-{}",
2056 self.next_identity.fetch_add(1, Ordering::SeqCst)
2057 )
2058 }
2059
2060 fn handler(self: &Arc<Self>) -> ExternalManifestCommitHandler {
2061 ExternalManifestCommitHandler {
2062 external_manifest_store: self.clone(),
2063 }
2064 }
2065
2066 fn recreate(&self) {
2069 let mut rows = self.rows.lock().unwrap();
2070 let versions: Vec<u64> = rows.keys().copied().collect();
2071 rows.clear();
2072 for version in versions {
2073 rows.insert(version, (v2_path(version), 1, self.mint()));
2074 }
2075 }
2076
2077 fn identity_of(&self, version: u64) -> Option<String> {
2078 self.rows
2079 .lock()
2080 .unwrap()
2081 .get(&version)
2082 .map(|row| row.2.clone())
2083 }
2084 }
2085
2086 #[async_trait]
2087 impl ExternalManifestStore for IdentifiedStore {
2088 async fn get(&self, _base_uri: &str, version: u64) -> Result<String> {
2089 self.rows
2090 .lock()
2091 .unwrap()
2092 .get(&version)
2093 .map(|row| row.0.clone())
2094 .ok_or_else(|| Error::not_found(format!("@{version}")))
2095 }
2096
2097 async fn get_manifest_location(
2098 &self,
2099 _base_uri: &str,
2100 version: u64,
2101 ) -> Result<ManifestLocation> {
2102 let row = self
2103 .rows
2104 .lock()
2105 .unwrap()
2106 .get(&version)
2107 .cloned()
2108 .ok_or_else(|| Error::not_found(format!("@{version}")))?;
2109 let path = Path::parse(&row.0).unwrap();
2110 Ok(ManifestLocation {
2111 version,
2112 naming_scheme: detect_naming_scheme_from_path(&path)?,
2113 path,
2114 size: Some(row.1),
2115 e_tag: None,
2116 identity: Some(row.2),
2117 })
2118 }
2119
2120 async fn get_latest_version(&self, _base_uri: &str) -> Result<Option<(u64, String)>> {
2121 Ok(self
2122 .rows
2123 .lock()
2124 .unwrap()
2125 .iter()
2126 .max_by_key(|(version, _)| **version)
2127 .map(|(version, row)| (*version, row.0.clone())))
2128 }
2129
2130 async fn get_latest_manifest_location(
2131 &self,
2132 base_uri: &str,
2133 ) -> Result<Option<ManifestLocation>> {
2134 match self.get_latest_version(base_uri).await? {
2135 Some((version, _)) => self
2136 .get_manifest_location(base_uri, version)
2137 .await
2138 .map(Some),
2139 None => Ok(None),
2140 }
2141 }
2142
2143 async fn put_if_not_exists(
2144 &self,
2145 _base_uri: &str,
2146 version: u64,
2147 path: &str,
2148 size: u64,
2149 _e_tag: Option<String>,
2150 ) -> Result<()> {
2151 let identity = self.mint();
2152 let mut rows = self.rows.lock().unwrap();
2153 if rows.contains_key(&version) {
2154 return Err(Error::commit_conflict_source(version, "exists".into()));
2155 }
2156 rows.insert(version, (path.to_string(), size, identity));
2157 Ok(())
2158 }
2159
2160 async fn put_if_exists(
2161 &self,
2162 _base_uri: &str,
2163 version: u64,
2164 path: &str,
2165 size: u64,
2166 _e_tag: Option<String>,
2167 ) -> Result<()> {
2168 let mut rows = self.rows.lock().unwrap();
2169 let row = rows
2170 .get_mut(&version)
2171 .ok_or_else(|| Error::not_found(format!("@{version}")))?;
2172 row.0 = path.to_string();
2173 row.1 = size;
2174 Ok(())
2175 }
2176
2177 fn supports_predecessor_condition(&self) -> bool {
2178 true
2179 }
2180
2181 async fn get_identity(&self, _base_uri: &str, version: u64) -> Result<Option<String>> {
2182 Ok(self.identity_of(version))
2183 }
2184
2185 async fn forget_version(
2186 &self,
2187 _base_uri: &str,
2188 version: u64,
2189 identity: &str,
2190 ) -> Result<()> {
2191 let mut rows = self.rows.lock().unwrap();
2192 if rows.get(&version).is_some_and(|row| row.2 == identity) {
2193 rows.remove(&version);
2194 }
2195 Ok(())
2196 }
2197
2198 async fn list_versions(
2199 &self,
2200 base_uri: &str,
2201 since: Option<u64>,
2202 ) -> Result<Option<Vec<ManifestLocation>>> {
2203 let versions: Vec<u64> = self.rows.lock().unwrap().keys().copied().collect();
2204 let mut locations = Vec::new();
2205 for version in versions {
2206 if since.is_none_or(|since| version > since) {
2207 locations.push(self.get_manifest_location(base_uri, version).await?);
2208 }
2209 }
2210 Ok(Some(locations))
2211 }
2212
2213 async fn put_if_predecessor(
2214 &self,
2215 _base_uri: &str,
2216 version: u64,
2217 path: &str,
2218 size: u64,
2219 predecessor: &PredecessorIdentity,
2220 ) -> Result<Reservation> {
2221 if self.hold_next_reservation.swap(false, Ordering::SeqCst) {
2222 self.reservation_held.notify_one();
2223 self.release_reservation.notified().await;
2224 }
2225 let identity = self.mint();
2226 let mut rows = self.rows.lock().unwrap();
2227 let held = rows
2228 .get(&predecessor.version)
2229 .is_some_and(|row| row.2 == predecessor.identity);
2230 if !held {
2231 return Ok(Reservation::PredecessorChanged);
2232 }
2233 if rows.contains_key(&version) {
2234 return Ok(Reservation::Taken);
2235 }
2236 rows.insert(version, (path.to_string(), size, identity.clone()));
2237 Ok(Reservation::Reserved { identity })
2238 }
2239 }
2240
2241 fn v2_path(version: u64) -> String {
2242 ManifestNamingScheme::V2
2243 .manifest_path(&Path::from("dataset"), version)
2244 .to_string()
2245 }
2246
2247 fn v2_names(versions: &[u64]) -> Vec<String> {
2248 let mut names: Vec<String> = versions
2249 .iter()
2250 .map(|v| Path::from(v2_path(*v)).filename().unwrap().to_string())
2251 .collect();
2252 names.sort();
2253 names
2254 }
2255
2256 async fn identified_fixture(
2259 store: &Arc<IdentifiedStore>,
2260 ) -> (
2261 ExternalManifestCommitHandler,
2262 ObjectStore,
2263 Path,
2264 PredecessorIdentity,
2265 ) {
2266 let handler = store.handler();
2267 let object_store = ObjectStore::memory();
2268 let base_path = Path::from("dataset");
2269 handler
2270 .commit(
2271 &mut test_manifest(),
2272 None,
2273 &base_path,
2274 &object_store,
2275 write_manifest_file_to_path,
2276 ManifestNamingScheme::V2,
2277 None,
2278 )
2279 .await
2280 .unwrap();
2281 let predecessor = handler
2282 .resolve_latest_identity(&base_path, &object_store)
2283 .await
2284 .unwrap()
2285 .unwrap();
2286 assert_eq!(predecessor.version, 1);
2287 (handler, object_store, base_path, predecessor)
2288 }
2289
2290 async fn commit_after_v2(
2291 handler: &ExternalManifestCommitHandler,
2292 object_store: &ObjectStore,
2293 base_path: &Path,
2294 predecessor: &PredecessorIdentity,
2295 ) -> std::result::Result<ManifestLocation, CommitError> {
2296 let mut manifest = test_manifest();
2297 manifest.version = 2;
2298 handler
2299 .commit_after(
2300 &mut manifest,
2301 None,
2302 base_path,
2303 object_store,
2304 write_manifest_file_to_path,
2305 ManifestNamingScheme::V2,
2306 None,
2307 predecessor,
2308 )
2309 .await
2310 }
2311
2312 async fn versions_dir_files(object_store: &ObjectStore, base_path: &Path) -> Vec<String> {
2313 let mut files: Vec<String> = object_store
2314 .inner
2315 .list(Some(&base_path.clone().join(VERSIONS_DIR)))
2316 .map_ok(|meta| meta.location.filename().unwrap().to_string())
2317 .try_collect()
2318 .await
2319 .unwrap();
2320 files.sort();
2321 files
2322 }
2323
2324 #[tokio::test]
2325 async fn test_a_conditioned_commit_lands_under_its_minted_identity() {
2326 let store = Arc::new(IdentifiedStore::default());
2327 let (handler, object_store, base_path, predecessor) = identified_fixture(&store).await;
2328 let location = commit_after_v2(&handler, &object_store, &base_path, &predecessor)
2329 .await
2330 .unwrap();
2331 let name = location.path.filename().unwrap();
2334 assert!(name.contains(".manifest-"), "{name}");
2335 assert_eq!(ManifestNamingScheme::detect_scheme(name), None);
2336
2337 assert!(location.identity.is_some());
2338 assert_eq!(location.identity, store.identity_of(2));
2339 let resolved = handler
2340 .resolve_latest_location(&base_path, &object_store)
2341 .await
2342 .unwrap();
2343 assert_eq!(resolved.path, location.path);
2344 assert_eq!(resolved.identity, location.identity);
2345 assert_eq!(
2347 listed_versions(&handler, &object_store, &base_path).await,
2348 vec![2, 1]
2349 );
2350 let since: Vec<u64> = handler
2351 .list_manifest_locations_since(&base_path, &object_store, 1)
2352 .map_ok(|l| l.version)
2353 .try_collect()
2354 .await
2355 .unwrap();
2356 assert_eq!(since, vec![2]);
2357 assert_eq!(versions_dir_files(&object_store, &base_path).await.len(), 2);
2358 }
2359
2360 #[tokio::test]
2361 async fn test_a_changed_predecessor_is_refused_without_publishing() {
2362 let store = Arc::new(IdentifiedStore::default());
2363 let (handler, object_store, base_path, _) = identified_fixture(&store).await;
2364 let stale = PredecessorIdentity {
2365 version: 1,
2366 identity: "identity-from-a-dropped-dataset".to_string(),
2367 };
2368 let err = commit_after_v2(&handler, &object_store, &base_path, &stale)
2369 .await
2370 .unwrap_err();
2371 assert!(
2372 matches!(
2373 err,
2374 CommitError::OtherError(Error::PrerequisiteFailed { .. })
2375 ),
2376 "{err:?}"
2377 );
2378 assert!(store.identity_of(2).is_none());
2379 assert_eq!(
2380 versions_dir_files(&object_store, &base_path).await,
2381 v2_names(&[1])
2382 );
2383 }
2384
2385 #[tokio::test]
2386 async fn test_a_taken_version_is_a_conflict() {
2387 let store = Arc::new(IdentifiedStore::default());
2388 let (handler, object_store, base_path, predecessor) = identified_fixture(&store).await;
2389 store
2390 .put_if_not_exists("dataset", 2, &v2_path(2), 1, None)
2391 .await
2392 .unwrap();
2393 let err = commit_after_v2(&handler, &object_store, &base_path, &predecessor)
2394 .await
2395 .unwrap_err();
2396 assert!(matches!(err, CommitError::CommitConflict), "{err:?}");
2397 assert_eq!(
2398 versions_dir_files(&object_store, &base_path).await,
2399 v2_names(&[1])
2400 );
2401 }
2402
2403 #[tokio::test]
2406 async fn test_a_recreation_before_publication_is_refused() {
2407 let store = Arc::new(IdentifiedStore::default());
2408 let (handler, object_store, base_path, predecessor) = identified_fixture(&store).await;
2409 store.recreate();
2410 let err = commit_after_v2(&handler, &object_store, &base_path, &predecessor)
2411 .await
2412 .unwrap_err();
2413 assert!(
2414 matches!(
2415 err,
2416 CommitError::OtherError(Error::PrerequisiteFailed { .. })
2417 ),
2418 "{err:?}"
2419 );
2420 assert_eq!(
2421 versions_dir_files(&object_store, &base_path).await,
2422 v2_names(&[1])
2423 );
2424 }
2425 async fn listed_versions(
2426 handler: &ExternalManifestCommitHandler,
2427 object_store: &ObjectStore,
2428 base_path: &Path,
2429 ) -> Vec<u64> {
2430 handler
2431 .list_manifest_locations(base_path, object_store, true)
2432 .map_ok(|l| l.version)
2433 .try_collect()
2434 .await
2435 .unwrap()
2436 }
2437
2438 #[tokio::test(flavor = "multi_thread")]
2441 async fn test_a_cancelled_reservation_publishes_nothing() {
2442 let store = Arc::new(IdentifiedStore::default());
2443 let (handler, object_store, base_path, predecessor) = identified_fixture(&store).await;
2444 store.hold_next_reservation.store(true, Ordering::SeqCst);
2445 let task = {
2446 let (handler, object_store, base_path) =
2447 (store.handler(), object_store.clone(), base_path.clone());
2448 tokio::spawn(async move {
2449 commit_after_v2(&handler, &object_store, &base_path, &predecessor).await
2450 })
2451 };
2452 tokio::time::timeout(
2453 std::time::Duration::from_secs(30),
2454 store.reservation_held.notified(),
2455 )
2456 .await
2457 .expect("the commit never reached its reservation");
2458 task.abort();
2459 assert!(task.await.unwrap_err().is_cancelled());
2460
2461 assert!(store.identity_of(2).is_none());
2462 assert_eq!(
2463 listed_versions(&handler, &object_store, &base_path).await,
2464 vec![1]
2465 );
2466 assert_eq!(versions_dir_files(&object_store, &base_path).await.len(), 2);
2468 let raw: Vec<u64> = default_list_manifest_locations(&base_path, &object_store, true)
2469 .map_ok(|l| l.version)
2470 .try_collect()
2471 .await
2472 .unwrap();
2473 assert_eq!(raw, vec![1]);
2474 }
2475 #[tokio::test]
2478 async fn test_forgetting_a_version_retires_only_that_record() {
2479 let store = Arc::new(IdentifiedStore::default());
2480 let (handler, object_store, base_path, predecessor) = identified_fixture(&store).await;
2481 commit_after_v2(&handler, &object_store, &base_path, &predecessor)
2482 .await
2483 .unwrap();
2484 handler
2485 .forget_version(&base_path, 1, "identity-from-a-dropped-dataset")
2486 .await
2487 .unwrap();
2488 assert_eq!(
2489 listed_versions(&handler, &object_store, &base_path).await,
2490 vec![2, 1]
2491 );
2492 let identity = store.identity_of(1).unwrap();
2493 handler
2494 .forget_version(&base_path, 1, &identity)
2495 .await
2496 .unwrap();
2497 handler
2498 .forget_version(&base_path, 1, &identity)
2499 .await
2500 .unwrap();
2501 assert_eq!(
2502 listed_versions(&handler, &object_store, &base_path).await,
2503 vec![2]
2504 );
2505 }
2506 #[tokio::test]
2509 async fn test_retirement_is_refused_where_the_store_cannot_forget() {
2510 #[derive(Debug)]
2511 struct NoForget(Arc<IdentifiedStore>);
2512 #[async_trait]
2513 impl ExternalManifestStore for NoForget {
2514 async fn get(&self, b: &str, v: u64) -> Result<String> {
2515 self.0.get(b, v).await
2516 }
2517 async fn get_latest_version(&self, b: &str) -> Result<Option<(u64, String)>> {
2518 self.0.get_latest_version(b).await
2519 }
2520 async fn put_if_not_exists(
2521 &self,
2522 b: &str,
2523 v: u64,
2524 p: &str,
2525 s: u64,
2526 e: Option<String>,
2527 ) -> Result<()> {
2528 self.0.put_if_not_exists(b, v, p, s, e).await
2529 }
2530 async fn put_if_exists(
2531 &self,
2532 b: &str,
2533 v: u64,
2534 p: &str,
2535 s: u64,
2536 e: Option<String>,
2537 ) -> Result<()> {
2538 self.0.put_if_exists(b, v, p, s, e).await
2539 }
2540 fn supports_predecessor_condition(&self) -> bool {
2541 true
2542 }
2543 }
2544 let handler = ExternalManifestCommitHandler {
2545 external_manifest_store: Arc::new(NoForget(Arc::new(IdentifiedStore::default()))),
2546 };
2547 let err = handler
2548 .forget_version(&Path::from("dataset"), 1, "identity-0")
2549 .await
2550 .unwrap_err();
2551 assert!(matches!(err, Error::NotSupported { .. }), "{err}");
2552 }
2553}