1use std::fmt;
32use std::future::Future;
33use std::sync::Arc;
34use std::time::{Duration, SystemTime};
35
36use bigtable_rs::bigtable::{BigTableConnection, Error as BigTableError, RowCell};
37use bigtable_rs::google::bigtable::v2::{self, mutation};
38use bytes::Bytes;
39use futures_util::TryStreamExt;
40use objectstore_types::metadata::{ExpirationPolicy, Metadata};
41use objectstore_types::range::{ByteRange, ContentRange};
42use serde::{Deserialize, Serialize};
43use tonic::Code;
44use tracing::Instrument;
45
46use crate::backend::common::{
47 Backend, DeleteResponse, GetResponse, HighVolumeBackend, MetadataResponse, PutResponse,
48 TieredGet, TieredMetadata, TieredWrite, Tombstone,
49};
50use crate::error::{Error, Result};
51use crate::gcp_auth::PrefetchingTokenProvider;
52use crate::id::ObjectId;
53use crate::stream::{ChunkedBytes, ClientStream};
54
55#[derive(Debug, Clone, Deserialize, Serialize)]
77pub struct BigTableConfig {
78 pub endpoint: Option<String>,
91
92 pub project_id: String,
100
101 pub instance_name: String,
107
108 pub table_name: String,
116
117 pub connections: Option<usize>,
127}
128
129const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
131const MAX_CHANNEL_AGE: Option<Duration> = Some(Duration::from_mins(50));
139const TTI_DEBOUNCE: Duration = Duration::from_hours(24);
141const TOKEN_SCOPES: &[&str] = &["https://www.googleapis.com/auth/bigtable.data"];
143
144const REQUEST_RETRY_COUNT: usize = 2;
146const CAS_RETRY_COUNT: usize = 3;
148
149const COLUMN_PAYLOAD: &[u8] = b"p";
151const COLUMN_METADATA: &[u8] = b"m";
153const COLUMN_REDIRECT: &[u8] = b"r";
155const COLUMN_TOMBSTONE_META: &[u8] = b"t";
157const FILTER_META: &[u8] = b"^[mrt]$";
159
160const FAMILY_GC: &str = "fg";
165const FAMILY_MANUAL: &str = "fm";
167
168pub struct BigTableBackend {
170 bigtable: BigTableConnection,
171
172 instance_path: String,
173 table_path: String,
174 table_name: String,
175}
176
177impl fmt::Debug for BigTableBackend {
178 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179 f.debug_struct("BigTableBackend")
180 .field("instance_path", &self.instance_path)
181 .field("table_path", &self.table_path)
182 .field("table_name", &self.table_name)
183 .finish_non_exhaustive()
184 }
185}
186
187fn column_filter(column: &[u8]) -> v2::RowFilter {
189 v2::RowFilter {
190 filter: Some(v2::row_filter::Filter::ColumnQualifierRegexFilter(
191 [b"^", column, b"$"].concat(),
192 )),
193 }
194}
195
196fn legacy_tombstone_filter() -> v2::RowFilter {
201 v2::RowFilter {
202 filter: Some(v2::row_filter::Filter::Chain(v2::row_filter::Chain {
203 filters: vec![
204 column_filter(COLUMN_METADATA),
205 v2::RowFilter {
206 filter: Some(v2::row_filter::Filter::ValueRegexFilter(
207 b"^\\{\"is_redirect_tombstone\":true[,}].*".to_vec(),
208 )),
209 },
210 ],
211 })),
212 }
213}
214
215fn live_row_filter(inner: v2::RowFilter) -> v2::RowFilter {
217 let now_micros = time_to_micros_saturating(SystemTime::now());
218
219 v2::RowFilter {
220 filter: Some(v2::row_filter::Filter::Interleave(
221 v2::row_filter::Interleave {
222 filters: vec![
223 v2::RowFilter {
225 filter: Some(v2::row_filter::Filter::Chain(v2::row_filter::Chain {
226 filters: vec![
227 v2::RowFilter {
228 filter: Some(v2::row_filter::Filter::FamilyNameRegexFilter(
229 format!("^{FAMILY_MANUAL}$"),
230 )),
231 },
232 inner.clone(),
233 ],
234 })),
235 },
236 v2::RowFilter {
238 filter: Some(v2::row_filter::Filter::Chain(v2::row_filter::Chain {
239 filters: vec![
240 v2::RowFilter {
241 filter: Some(v2::row_filter::Filter::FamilyNameRegexFilter(
242 format!("^{FAMILY_GC}$"),
243 )),
244 },
245 v2::RowFilter {
246 filter: Some(v2::row_filter::Filter::TimestampRangeFilter(
247 v2::TimestampRange {
248 start_timestamp_micros: now_micros,
249 end_timestamp_micros: 0,
250 },
251 )),
252 },
253 inner,
254 ],
255 })),
256 },
257 ],
258 },
259 )),
260 }
261}
262
263fn tombstone_filter() -> v2::RowFilter {
271 let filter = v2::RowFilter {
272 filter: Some(v2::row_filter::Filter::Interleave(
273 v2::row_filter::Interleave {
274 filters: vec![column_filter(COLUMN_REDIRECT), legacy_tombstone_filter()],
275 },
276 )),
277 };
278 live_row_filter(filter)
279}
280
281fn tombstone_predicate() -> MutatePredicate {
287 MutatePredicate::Exclude(tombstone_filter())
288}
289
290fn exact_value_regex(value: &str) -> Vec<u8> {
295 format!("^{}$", regex::escape(value)).into_bytes()
296}
297
298fn redirect_target_filter(target: &ObjectId, own_id: &ObjectId) -> v2::RowFilter {
316 let target_path = exact_value_regex(&target.as_storage_path().to_string());
317
318 let exact_match = v2::RowFilter {
319 filter: Some(v2::row_filter::Filter::Chain(v2::row_filter::Chain {
320 filters: vec![
321 column_filter(COLUMN_REDIRECT),
322 v2::RowFilter {
323 filter: Some(v2::row_filter::Filter::ValueRegexFilter(target_path)),
324 },
325 ],
326 })),
327 };
328
329 if target != own_id {
330 return live_row_filter(exact_match);
331 }
332
333 let empty_redirect_match = v2::RowFilter {
334 filter: Some(v2::row_filter::Filter::Chain(v2::row_filter::Chain {
335 filters: vec![
336 column_filter(COLUMN_REDIRECT),
337 v2::RowFilter {
338 filter: Some(v2::row_filter::Filter::ValueRegexFilter(b"^$".to_vec())),
339 },
340 ],
341 })),
342 };
343
344 let filter = v2::RowFilter {
348 filter: Some(v2::row_filter::Filter::Interleave(
349 v2::row_filter::Interleave {
350 filters: vec![exact_match, empty_redirect_match, legacy_tombstone_filter()],
351 },
352 )),
353 };
354 live_row_filter(filter)
355}
356
357fn update_predicate(old: &ObjectId, new: &ObjectId, own_id: &ObjectId) -> MutatePredicate {
364 MutatePredicate::Include(v2::RowFilter {
365 filter: Some(v2::row_filter::Filter::Interleave(
366 v2::row_filter::Interleave {
367 filters: vec![
368 redirect_target_filter(old, own_id),
369 redirect_target_filter(new, own_id),
370 ],
371 },
372 )),
373 })
374}
375
376fn optional_target_predicate(target: &ObjectId, own_id: &ObjectId) -> MutatePredicate {
388 MutatePredicate::Exclude(v2::RowFilter {
389 filter: Some(v2::row_filter::Filter::Condition(Box::new(
390 v2::row_filter::Condition {
391 predicate_filter: Some(Box::new(redirect_target_filter(target, own_id))),
392 true_filter: Some(Box::new(v2::RowFilter {
393 filter: Some(v2::row_filter::Filter::BlockAllFilter(true)),
394 })),
395 false_filter: Some(Box::new(tombstone_filter())),
396 },
397 ))),
398 })
399}
400
401#[derive(Clone, Debug)]
406enum MutatePredicate {
407 Include(v2::RowFilter),
411 Exclude(v2::RowFilter),
415}
416
417fn metadata_filter() -> v2::RowFilter {
422 v2::RowFilter {
423 filter: Some(v2::row_filter::Filter::ColumnQualifierRegexFilter(
424 FILTER_META.to_owned(),
425 )),
426 }
427}
428
429fn mutation(mutation: mutation::Mutation) -> v2::Mutation {
430 v2::Mutation {
431 mutation: Some(mutation),
432 }
433}
434
435fn delete_row_mutation() -> v2::Mutation {
437 mutation(mutation::Mutation::DeleteFromRow(
438 mutation::DeleteFromRow {},
439 ))
440}
441
442fn bumped_tti_metadata(metadata: &Metadata) -> Metadata {
447 let mut metadata = metadata.clone();
448 metadata.time_expires = metadata
449 .expiration_policy
450 .expires_in()
451 .map(|tti| SystemTime::now() + tti);
452 metadata
453}
454
455fn object_mutations(mut metadata: Metadata, payload: Vec<u8>) -> Result<[v2::Mutation; 3]> {
461 let (family, timestamp_micros) = match metadata.time_expires {
462 None => (FAMILY_MANUAL, -1),
463 Some(deadline) => (FAMILY_GC, system_time_to_micros(deadline)?),
464 };
465
466 metadata.size = Some(payload.len());
468
469 let metadata_bytes = serde_json::to_vec(&metadata)
470 .map_err(|cause| Error::serde("failed to serialize metadata", cause))?;
471
472 Ok([
473 delete_row_mutation(),
475 mutation(mutation::Mutation::SetCell(mutation::SetCell {
476 family_name: family.to_owned(),
477 column_qualifier: COLUMN_PAYLOAD.to_owned(),
478 timestamp_micros,
479 value: payload,
480 })),
481 mutation(mutation::Mutation::SetCell(mutation::SetCell {
482 family_name: family.to_owned(),
483 column_qualifier: COLUMN_METADATA.to_owned(),
484 timestamp_micros,
485 value: metadata_bytes,
486 })),
487 ])
488}
489
490#[derive(Clone, Debug, Default, Deserialize, Serialize)]
495struct TombstoneMeta {
496 #[serde(default, skip_serializing_if = "ExpirationPolicy::is_manual")]
500 expiration_policy: ExpirationPolicy,
501}
502
503fn tombstone_mutations(tombstone: &Tombstone, now: SystemTime) -> Result<[v2::Mutation; 3]> {
509 let (family, timestamp_micros) = match tombstone.expiration_policy {
510 ExpirationPolicy::Manual => (FAMILY_MANUAL, -1),
511 ExpirationPolicy::TimeToLive(ttl) => (FAMILY_GC, ttl_to_micros(ttl, now)?),
512 ExpirationPolicy::TimeToIdle(tti) => (FAMILY_GC, ttl_to_micros(tti, now)?),
513 };
514
515 let tombstone_meta = TombstoneMeta {
516 expiration_policy: tombstone.expiration_policy,
517 };
518
519 Ok([
520 delete_row_mutation(),
521 mutation(mutation::Mutation::SetCell(mutation::SetCell {
522 family_name: family.to_owned(),
523 column_qualifier: COLUMN_REDIRECT.to_owned(),
524 timestamp_micros,
525 value: tombstone.target.as_storage_path().to_string().into_bytes(),
526 })),
527 mutation(mutation::Mutation::SetCell(mutation::SetCell {
528 family_name: family.to_owned(),
529 column_qualifier: COLUMN_TOMBSTONE_META.to_owned(),
530 timestamp_micros,
531 value: serde_json::to_vec(&tombstone_meta)
532 .map_err(|cause| Error::serde("failed to serialize tombstone", cause))?,
533 })),
534 ])
535}
536
537#[derive(Debug, Deserialize)]
541struct LegacyTombstoneMeta {
542 #[serde(default)]
549 is_redirect_tombstone: bool,
550
551 #[serde(default)]
553 expiration_policy: ExpirationPolicy,
554}
555
556enum RowData {
558 Object {
560 metadata: Metadata,
561 payload: Vec<u8>,
562 },
563 Tombstone {
565 target: Vec<u8>,
566 meta: TombstoneMeta,
567 time_expires: Option<SystemTime>,
568 },
569}
570
571impl RowData {
572 fn from_cells(cells: Vec<RowCell>) -> Result<Self> {
579 let mut metadata_opt: Option<Metadata> = None;
580 let mut tombstone_meta_opt: Option<TombstoneMeta> = None;
581 let mut redirect_detected = false;
582 let mut redirect_target = Vec::new();
583 let mut expire_at = None;
584 let mut payload = Vec::new();
585
586 for cell in cells {
587 if cell.family_name == FAMILY_GC {
592 expire_at = micros_to_time(cell.timestamp_micros);
593 }
594
595 match cell.qualifier.as_slice() {
596 COLUMN_REDIRECT => {
597 redirect_detected = true;
598 redirect_target = cell.value;
599 }
600 COLUMN_PAYLOAD => {
601 payload = cell.value;
602 }
603 COLUMN_TOMBSTONE_META => {
604 tombstone_meta_opt =
605 Some(serde_json::from_slice(&cell.value).map_err(|cause| {
606 Error::serde("failed to deserialize tombstone meta", cause)
607 })?);
608 }
609 COLUMN_METADATA => {
610 if let Ok(legacy_meta) =
611 serde_json::from_slice::<LegacyTombstoneMeta>(&cell.value)
612 && legacy_meta.is_redirect_tombstone
613 {
614 redirect_detected = true;
615 objectstore_metrics::count!("bigtable.legacy_tombstone_read");
616 tombstone_meta_opt = Some(TombstoneMeta {
617 expiration_policy: legacy_meta.expiration_policy,
618 });
619 } else {
620 metadata_opt =
621 Some(serde_json::from_slice(&cell.value).map_err(|cause| {
622 Error::serde("failed to deserialize metadata", cause)
623 })?);
624 }
625 }
626 _ => {}
627 }
628 }
629
630 Ok(if redirect_detected {
631 RowData::Tombstone {
632 target: redirect_target,
633 meta: tombstone_meta_opt.unwrap_or_default(),
634 time_expires: expire_at,
635 }
636 } else {
637 let mut metadata = metadata_opt.unwrap_or_default();
639 metadata.time_expires = expire_at;
640 RowData::Object { metadata, payload }
641 })
642 }
643
644 fn expiration_policy(&self) -> ExpirationPolicy {
646 match self {
647 RowData::Object { metadata, .. } => metadata.expiration_policy,
648 RowData::Tombstone { meta, .. } => meta.expiration_policy,
649 }
650 }
651
652 fn time_expires(&self) -> Option<SystemTime> {
654 match self {
655 RowData::Object { metadata, .. } => metadata.time_expires,
656 RowData::Tombstone { time_expires, .. } => *time_expires,
657 }
658 }
659
660 fn expires_before(&self, time: SystemTime) -> bool {
664 self.expiration_policy().is_timeout() && self.time_expires().is_some_and(|ts| ts < time)
665 }
666
667 fn needs_tti_bump(&self) -> bool {
669 matches!(
670 self.expiration_policy(),
671 ExpirationPolicy::TimeToIdle(tti) if self.expires_before(SystemTime::now() + tti - TTI_DEBOUNCE)
672 )
673 }
674}
675
676fn parse_redirect_target(redirect_path: &[u8], tombstone_id: &ObjectId) -> Result<ObjectId> {
682 if redirect_path.is_empty() {
683 objectstore_metrics::count!("bigtable.empty_redirect_read");
684 Ok(tombstone_id.clone())
685 } else {
686 let redirect_str = std::str::from_utf8(redirect_path)
687 .map_err(|_| Error::generic("invalid UTF-8 in redirect path"))?;
688 ObjectId::from_storage_path(redirect_str)
689 .ok_or_else(|| Error::generic("corrupt redirect path"))
690 }
691}
692
693impl BigTableBackend {
694 pub async fn new(config: BigTableConfig) -> anyhow::Result<Self> {
699 let BigTableConfig {
700 endpoint,
701 project_id,
702 instance_name,
703 table_name,
704 connections,
705 } = config;
706
707 let bigtable = if let Some(ref endpoint) = endpoint {
708 BigTableConnection::new_with_emulator(
709 endpoint,
710 &project_id,
711 &instance_name,
712 false, Some(CONNECT_TIMEOUT),
714 )?
715 } else {
716 let token_provider = PrefetchingTokenProvider::gcp_auth(TOKEN_SCOPES).await?;
717 BigTableConnection::new_with_managed_transport(
718 &project_id,
719 &instance_name,
720 false, Some(CONNECT_TIMEOUT),
722 Arc::new(token_provider),
723 connections.unwrap_or(1),
724 true, None, MAX_CHANNEL_AGE,
727 )
728 .await?
729 };
730
731 let client = bigtable.client();
732
733 Ok(Self {
734 bigtable,
735 instance_path: format!("projects/{project_id}/instances/{instance_name}"),
736 table_path: client.get_full_table_name(&table_name),
737 table_name,
738 })
739 }
740
741 #[tracing::instrument(level = "debug", fields(action), skip_all)]
745 async fn read_row(
746 &self,
747 path: &[u8],
748 filter: Option<v2::RowFilter>,
749 action: &'static str,
750 ) -> Result<Option<RowData>> {
751 let request = v2::ReadRowsRequest {
752 table_name: self.table_path.clone(),
753 rows: Some(v2::RowSet {
754 row_keys: vec![path.to_owned()],
755 row_ranges: vec![],
756 }),
757 filter,
758 rows_limit: 1,
759 ..Default::default()
760 };
761
762 let response = retry(action, || async {
763 self.bigtable.client().read_rows(request.clone()).await
764 })
765 .await?;
766 debug_assert!(response.len() <= 1, "Expected at most one row");
767
768 let Some((_, cells)) = response.into_iter().next() else {
769 objectstore_log::debug!("Object not found");
770 return Ok(None);
771 };
772
773 let row = RowData::from_cells(cells)?;
774 Ok(if row.expires_before(SystemTime::now()) {
775 None
776 } else {
777 Some(row)
778 })
779 }
780
781 #[tracing::instrument(level = "debug", fields(action), skip_all)]
782 async fn mutate(
783 &self,
784 path: Vec<u8>,
785 mutations: impl Into<Vec<v2::Mutation>>,
786 action: &'static str,
787 ) -> Result<v2::MutateRowResponse> {
788 let request = v2::MutateRowRequest {
789 table_name: self.table_path.clone(),
790 row_key: path,
791 mutations: mutations.into(),
792 ..Default::default()
793 };
794
795 let response = retry(action, || async {
796 self.bigtable.client().mutate_row(request.clone()).await
797 })
798 .await?;
799
800 Ok(response.into_inner())
801 }
802
803 async fn put_row(
804 &self,
805 path: Vec<u8>,
806 metadata: Metadata,
807 payload: Vec<u8>,
808 action: &'static str,
809 ) -> Result<v2::MutateRowResponse> {
810 let mutations = object_mutations(metadata, payload)?;
811 self.mutate(path, mutations, action).await
812 }
813
814 async fn put_tombstone_row(
815 &self,
816 path: Vec<u8>,
817 tombstone: &Tombstone,
818 action: &'static str,
819 ) -> Result<v2::MutateRowResponse> {
820 let mutations = tombstone_mutations(tombstone, SystemTime::now())?;
821 self.mutate(path, mutations, action).await
822 }
823
824 #[tracing::instrument(level = "debug", fields(?hv_id, loaded), skip_all)]
828 async fn bump_tti(&self, path: Vec<u8>, row: &RowData, loaded: bool, hv_id: &ObjectId) {
829 let expiration_policy = row.expiration_policy();
830
831 match row {
832 RowData::Tombstone { target, .. } => {
833 let target = match parse_redirect_target(target, hv_id) {
834 Ok(target) => target,
835 Err(e) => {
836 objectstore_log::error!(!!&e, "invalid redirect target in tombstone row");
837 return;
838 }
839 };
840
841 let tombstone = Tombstone {
842 target,
843 expiration_policy,
844 };
845 let _ = self.put_tombstone_row(path, &tombstone, "tti-bump").await;
846 }
847 RowData::Object { metadata, payload } if loaded => {
848 let bumped = bumped_tti_metadata(metadata);
849 let _ = self
850 .put_row(path, bumped, payload.clone(), "tti-bump")
851 .await;
852 }
853 RowData::Object { metadata, .. } => {
854 let payload_read = self
855 .read_row(&path, Some(column_filter(COLUMN_PAYLOAD)), "tti-bump")
856 .await;
857
858 if let Ok(Some(RowData::Object { payload, .. })) = payload_read {
859 let bumped = bumped_tti_metadata(metadata);
860 let _ = self.put_row(path, bumped, payload, "tti-bump").await;
861 }
862 }
863 }
864 }
865
866 #[tracing::instrument(level = "debug", fields(action = context), skip_all)]
868 async fn check_and_mutate(
869 &self,
870 row_key: Vec<u8>,
871 predicate: MutatePredicate,
872 mutations: impl Into<Vec<v2::Mutation>>,
873 context: &'static str,
874 ) -> Result<bool> {
875 let (filter, true_mutations, false_mutations, success_on_match) = match predicate {
876 MutatePredicate::Include(f) => (f, mutations.into(), vec![], true),
877 MutatePredicate::Exclude(f) => (f, vec![], mutations.into(), false),
878 };
879
880 let request = v2::CheckAndMutateRowRequest {
881 table_name: self.table_path.clone(),
882 row_key,
883 predicate_filter: Some(filter),
884 true_mutations,
885 false_mutations,
886 ..Default::default()
887 };
888
889 let future = retry(context, || async {
890 self.bigtable
891 .client()
892 .check_and_mutate_row(request.clone())
893 .await
894 });
895
896 Ok(future.await?.predicate_matched == success_on_match)
897 }
898}
899
900#[async_trait::async_trait]
901impl Backend for BigTableBackend {
902 fn name(&self) -> &'static str {
903 "bigtable"
904 }
905
906 #[tracing::instrument(level = "debug", fields(?id), skip_all)]
907 async fn put_object(
908 &self,
909 id: &ObjectId,
910 metadata: &Metadata,
911 mut stream: ClientStream,
912 ) -> Result<PutResponse> {
913 objectstore_log::debug!("Writing to Bigtable backend");
914 let path = id.as_storage_path().to_string().into_bytes();
915
916 let mut payload = ChunkedBytes::new(0);
917 while let Some(chunk) = stream.try_next().await? {
918 payload.push(chunk);
919 }
920
921 self.put_row(path, metadata.clone(), payload.into_bytes().into(), "put")
922 .await?;
923
924 Ok(())
925 }
926
927 #[tracing::instrument(level = "debug", skip(self))]
928 async fn get_object(&self, id: &ObjectId, range: Option<ByteRange>) -> Result<GetResponse> {
929 match self.get_tiered_object(id, range).await? {
930 TieredGet::Object(metadata, content_range, payload) => {
931 Ok(Some((metadata, content_range, payload)))
932 }
933 TieredGet::Tombstone(_) => Err(Error::UnexpectedTombstone),
934 TieredGet::NotFound => Ok(None),
935 }
936 }
937
938 #[tracing::instrument(level = "debug", skip(self))]
939 async fn get_metadata(&self, id: &ObjectId) -> Result<MetadataResponse> {
940 match self.get_tiered_metadata(id).await? {
941 TieredMetadata::Object(metadata) => Ok(Some(metadata)),
942 TieredMetadata::Tombstone(_) => Err(Error::UnexpectedTombstone),
943 TieredMetadata::NotFound => Ok(None),
944 }
945 }
946
947 #[tracing::instrument(level = "debug", skip(self))]
948 async fn delete_object(&self, id: &ObjectId) -> Result<DeleteResponse> {
949 objectstore_log::debug!("Deleting from Bigtable backend");
950
951 let path = id.as_storage_path().to_string().into_bytes();
952 self.mutate(path, [delete_row_mutation()], "delete").await?;
953
954 Ok(())
955 }
956}
957
958#[async_trait::async_trait]
959impl HighVolumeBackend for BigTableBackend {
960 #[tracing::instrument(level = "debug", fields(?id), skip_all)]
961 async fn put_non_tombstone(
962 &self,
963 id: &ObjectId,
964 metadata: &Metadata,
965 payload: Bytes,
966 ) -> Result<Option<Tombstone>> {
967 objectstore_log::debug!("Conditional put to Bigtable backend");
968
969 let path = id.as_storage_path().to_string().into_bytes();
970 let mutations = object_mutations(metadata.clone(), payload.to_vec())?;
971
972 for _ in 0..CAS_RETRY_COUNT {
973 let write_succeeded = self
974 .check_and_mutate(
975 path.clone(),
976 tombstone_predicate(),
977 mutations.clone(),
978 "put_non_tombstone",
979 )
980 .await?;
981
982 if write_succeeded {
983 return Ok(None);
984 }
985
986 let row = self
988 .read_row(&path, Some(metadata_filter()), "put_non_tombstone")
989 .await?;
990
991 match row {
992 Some(RowData::Tombstone { target, meta, .. }) => {
993 return Ok(Some(Tombstone {
994 target: parse_redirect_target(&target, id)?,
995 expiration_policy: meta.expiration_policy,
996 }));
997 }
998 Some(RowData::Object { .. }) => continue,
1000 None => continue,
1002 }
1003 }
1004
1005 Err(Error::generic("BigTable: race loop in put_non_tombstone"))
1006 }
1007
1008 #[tracing::instrument(level = "debug", skip(self))]
1009 async fn get_tiered_object(
1010 &self,
1011 id: &ObjectId,
1012 range: Option<ByteRange>,
1013 ) -> Result<TieredGet> {
1014 objectstore_log::debug!("Reading from Bigtable backend");
1015 let path = id.as_storage_path().to_string().into_bytes();
1016
1017 let Some(row) = self.read_row(&path, None, "get_tiered_object").await? else {
1018 return Ok(TieredGet::NotFound);
1019 };
1020
1021 if row.needs_tti_bump() {
1023 self.bump_tti(path.clone(), &row, true, id).await;
1024 }
1025
1026 Ok(match row {
1027 RowData::Tombstone { meta, target, .. } => TieredGet::Tombstone(Tombstone {
1028 target: parse_redirect_target(&target, id)?,
1029 expiration_policy: meta.expiration_policy,
1030 }),
1031 RowData::Object { metadata, payload } => {
1032 let mut metadata = metadata;
1033 let payload = Bytes::from(payload);
1034 if metadata.size.is_none() {
1035 metadata.size = Some(payload.len());
1037 }
1038
1039 let (content_range, payload) = apply_range(payload, range)?;
1040 TieredGet::Object(metadata, content_range, crate::stream::single(payload))
1041 }
1042 })
1043 }
1044
1045 #[tracing::instrument(level = "debug", skip(self))]
1046 async fn get_tiered_metadata(&self, id: &ObjectId) -> Result<TieredMetadata> {
1047 objectstore_log::debug!("Reading metadata from Bigtable backend");
1048 let path = id.as_storage_path().to_string().into_bytes();
1049
1050 let row_opt = self
1054 .read_row(&path, Some(metadata_filter()), "get_tiered_metadata")
1055 .await?;
1056 let Some(row) = row_opt else {
1057 return Ok(TieredMetadata::NotFound);
1058 };
1059
1060 if row.needs_tti_bump() {
1062 self.bump_tti(path.clone(), &row, false, id).await;
1063 }
1064
1065 Ok(match row {
1066 RowData::Tombstone { meta, target, .. } => TieredMetadata::Tombstone(Tombstone {
1067 target: parse_redirect_target(&target, id)?,
1068 expiration_policy: meta.expiration_policy,
1069 }),
1070 RowData::Object { metadata, .. } => TieredMetadata::Object(metadata),
1071 })
1072 }
1073
1074 #[tracing::instrument(level = "debug", skip(self))]
1075 async fn delete_non_tombstone(&self, id: &ObjectId) -> Result<Option<Tombstone>> {
1076 objectstore_log::debug!("Conditional delete from Bigtable backend");
1077
1078 let path = id.as_storage_path().to_string().into_bytes();
1079
1080 for _ in 0..CAS_RETRY_COUNT {
1081 let write_succeeded = self
1082 .check_and_mutate(
1083 path.clone(),
1084 tombstone_predicate(),
1085 [delete_row_mutation()],
1086 "delete_non_tombstone",
1087 )
1088 .await?;
1089
1090 if write_succeeded {
1091 return Ok(None);
1092 }
1093
1094 let row = self
1096 .read_row(&path, Some(metadata_filter()), "delete_non_tombstone")
1097 .await?;
1098
1099 match row {
1100 Some(RowData::Tombstone { target, meta, .. }) => {
1101 return Ok(Some(Tombstone {
1102 target: parse_redirect_target(&target, id)?,
1103 expiration_policy: meta.expiration_policy,
1104 }));
1105 }
1106 Some(RowData::Object { .. }) => continue,
1108 None => return Ok(None),
1110 }
1111 }
1112
1113 Err(Error::generic(
1114 "BigTable: race loop in delete_non_tombstone",
1115 ))
1116 }
1117
1118 #[tracing::instrument(level = "debug", skip(self, write))]
1119 async fn compare_and_write(
1120 &self,
1121 id: &ObjectId,
1122 current: Option<&ObjectId>,
1123 write: TieredWrite,
1124 ) -> Result<bool> {
1125 objectstore_log::debug!("CAS put to Bigtable backend");
1126
1127 let path = id.as_storage_path().to_string().into_bytes();
1128 let now = SystemTime::now();
1129
1130 let predicate = match (current, write.target()) {
1131 (Some(old), Some(new)) => update_predicate(old, new, id),
1132 (Some(target), None) => optional_target_predicate(target, id),
1133 (None, Some(target)) => optional_target_predicate(target, id),
1134 (None, None) => tombstone_predicate(),
1135 };
1136
1137 let mutations = match write {
1138 TieredWrite::Tombstone(tombstone) => tombstone_mutations(&tombstone, now)?.into(),
1139 TieredWrite::Object(m, p) => object_mutations(m, p.to_vec())?.into(),
1140 TieredWrite::Delete => vec![delete_row_mutation()],
1141 };
1142
1143 self.check_and_mutate(path, predicate, mutations, "compare_and_write")
1144 .await
1145 }
1146}
1147
1148fn ttl_to_micros(ttl: Duration, from: SystemTime) -> Result<i64> {
1154 let deadline = from.checked_add(ttl).ok_or_else(|| Error::Generic {
1155 context: format!(
1156 "TTL duration overflow: {} plus {}s cannot be represented as SystemTime",
1157 humantime::format_rfc3339_seconds(from),
1158 ttl.as_secs()
1159 ),
1160 cause: None,
1161 })?;
1162
1163 system_time_to_micros(deadline)
1164}
1165
1166fn system_time_to_micros(deadline: SystemTime) -> Result<i64> {
1171 let millis = deadline
1172 .duration_since(SystemTime::UNIX_EPOCH)
1173 .map_err(|e| Error::Generic {
1174 context: format!(
1175 "unable to get duration since UNIX_EPOCH for SystemTime {}",
1176 humantime::format_rfc3339_seconds(deadline)
1177 ),
1178 cause: Some(Box::new(e)),
1179 })?
1180 .as_millis();
1181
1182 (millis * 1000).try_into().map_err(|e| Error::Generic {
1183 context: format!("failed to convert {millis}ms to i64 microseconds"),
1184 cause: Some(Box::new(e)),
1185 })
1186}
1187
1188fn time_to_micros_saturating(t: SystemTime) -> i64 {
1191 let millis = t
1192 .duration_since(SystemTime::UNIX_EPOCH)
1193 .unwrap_or_default()
1194 .as_millis();
1195 i64::try_from(millis * 1000).unwrap_or(i64::MAX)
1196}
1197
1198fn micros_to_time(micros: i64) -> Option<SystemTime> {
1200 let micros = u64::try_from(micros).ok()?;
1201 let duration = Duration::from_micros(micros);
1202 SystemTime::UNIX_EPOCH.checked_add(duration)
1203}
1204
1205async fn retry<T, F>(context: &'static str, f: impl Fn() -> F) -> Result<T>
1207where
1208 F: Future<Output = Result<T, BigTableError>> + Send,
1209{
1210 let mut retry_count = 0usize;
1211
1212 loop {
1213 let attempt_span = tracing::debug_span!(
1214 "bigtable.request",
1215 action = context,
1216 grpc.status = tracing::field::Empty,
1217 );
1218 let attempt = async {
1219 let result = f().await;
1220 let span = tracing::Span::current();
1221 match &result {
1222 Ok(_) => span.record("grpc.status", "ok"),
1223 Err(BigTableError::RpcError(status)) => {
1224 span.record("grpc.status", tracing::field::debug(status.code()))
1225 }
1226 Err(_) => &span,
1228 };
1229 result
1230 };
1231
1232 match attempt.instrument(attempt_span).await {
1233 Ok(res) => return Ok(res),
1234 Err(e) if retry_count >= REQUEST_RETRY_COUNT || !is_retryable(&e) => {
1235 objectstore_metrics::count!("bigtable.failures", action = context);
1236 return Err(Error::Generic {
1237 context: format!("Bigtable: `{context}` failed"),
1238 cause: Some(Box::new(e)),
1239 });
1240 }
1241 Err(e) => {
1242 retry_count += 1;
1243 objectstore_metrics::count!("bigtable.retries", action = context);
1244 objectstore_log::warn!(!!&e, retry_count, context, "Retrying request");
1245 }
1246 }
1247 }
1248}
1249
1250fn is_retryable(error: &BigTableError) -> bool {
1251 match error {
1252 BigTableError::GCPAuthError(_) => true,
1254 BigTableError::TransportError(_) => true,
1256 BigTableError::IoError(_) => true,
1258 BigTableError::TimeoutError(_) => true,
1259
1260 BigTableError::RpcError(status) => match status.code() {
1262 Code::Unavailable => true,
1264 Code::Cancelled => true,
1266 Code::DeadlineExceeded => true,
1267 Code::Unauthenticated => true,
1269 Code::Aborted => true,
1271 Code::Internal => true,
1272 Code::FailedPrecondition => true,
1273 Code::Unknown => true,
1274 _ => false,
1275 },
1276 _ => false,
1277 }
1278}
1279
1280fn apply_range(payload: Bytes, range: Option<ByteRange>) -> Result<(Option<ContentRange>, Bytes)> {
1286 let Some(byte_range) = range else {
1287 return Ok((None, payload));
1288 };
1289
1290 let total = payload.len() as u64;
1291 let content_range = byte_range
1292 .resolve(total)
1293 .ok_or(Error::RangeNotSatisfiable { total })?;
1294
1295 let sliced = payload.slice(content_range.start as usize..content_range.end as usize + 1);
1296 Ok((Some(content_range), sliced))
1297}
1298
1299#[cfg(test)]
1300mod tests {
1301 use std::collections::BTreeMap;
1302
1303 use anyhow::Result;
1304 use objectstore_types::scope::{Scope, Scopes};
1305
1306 use super::*;
1307 use crate::id::ObjectContext;
1308 use crate::stream;
1309
1310 async fn create_test_backend() -> Result<BigTableBackend> {
1316 BigTableBackend::new(BigTableConfig {
1317 endpoint: Some("localhost:8086".into()),
1318 project_id: "testing".into(),
1319 instance_name: "objectstore".into(),
1320 table_name: "objectstore".into(),
1321 connections: None,
1322 })
1323 .await
1324 }
1325
1326 fn make_id() -> ObjectId {
1327 ObjectId::random(ObjectContext {
1328 usecase: "testing".into(),
1329 scopes: Scopes::from_iter([Scope::create("testing", "value").unwrap()]),
1330 })
1331 }
1332
1333 async fn create_object(
1334 backend: &BigTableBackend,
1335 id: &ObjectId,
1336 metadata: &Metadata,
1337 payload: &[u8],
1338 now: SystemTime,
1339 ) -> Result<()> {
1340 let path = id.as_storage_path().to_string().into_bytes();
1341 let mut metadata = metadata.clone();
1344 if metadata.time_expires.is_none() {
1345 metadata.time_expires = metadata.expiration_policy.expires_in().map(|ttl| now + ttl);
1346 }
1347 let mutations = object_mutations(metadata, payload.to_vec())?;
1348 backend.mutate(path, mutations, "test-setup").await?;
1349 Ok(())
1350 }
1351
1352 async fn create_tombstone(
1353 backend: &BigTableBackend,
1354 id: &ObjectId,
1355 tombstone: &Tombstone,
1356 now: SystemTime,
1357 ) -> Result<()> {
1358 let path = id.as_storage_path().to_string().into_bytes();
1359 let mutations = tombstone_mutations(tombstone, now)?;
1360 backend.mutate(path, mutations, "test-setup").await?;
1361 Ok(())
1362 }
1363
1364 async fn write_legacy_tombstone(
1366 backend: &BigTableBackend,
1367 id: &ObjectId,
1368 expiration_policy: ExpirationPolicy,
1369 time_expires: Option<SystemTime>,
1370 ) -> Result<()> {
1371 let meta = if expiration_policy.is_manual() {
1372 r#"{"is_redirect_tombstone":true}"#.to_owned()
1373 } else {
1374 let policy_json = serde_json::to_string(&expiration_policy).unwrap();
1375 format!(r#"{{"is_redirect_tombstone":true,"expiration_policy":{policy_json}}}"#)
1376 };
1377
1378 let (family, timestamp_micros) = if expiration_policy.is_manual() {
1379 (FAMILY_MANUAL, -1)
1380 } else {
1381 let t =
1382 time_expires.unwrap_or(SystemTime::now() + expiration_policy.expires_in().unwrap());
1383 (FAMILY_GC, time_to_micros_saturating(t))
1384 };
1385
1386 let path = id.as_storage_path().to_string().into_bytes();
1387 let mutations = [mutation(mutation::Mutation::SetCell(mutation::SetCell {
1388 family_name: family.to_owned(),
1389 column_qualifier: COLUMN_METADATA.to_owned(),
1390 timestamp_micros,
1391 value: meta.into_bytes(),
1392 }))];
1393
1394 backend.mutate(path, mutations, "test-setup").await?;
1395
1396 Ok(())
1397 }
1398
1399 async fn write_empty_redirect_tombstone(
1402 backend: &BigTableBackend,
1403 id: &ObjectId,
1404 ) -> Result<()> {
1405 let path = id.as_storage_path().to_string().into_bytes();
1406 let mutations = [
1407 mutation(mutation::Mutation::SetCell(mutation::SetCell {
1408 family_name: FAMILY_MANUAL.to_owned(),
1409 column_qualifier: COLUMN_REDIRECT.to_owned(),
1410 timestamp_micros: -1,
1411 value: b"".to_vec(), })),
1413 mutation(mutation::Mutation::SetCell(mutation::SetCell {
1414 family_name: FAMILY_MANUAL.to_owned(),
1415 column_qualifier: COLUMN_TOMBSTONE_META.to_owned(),
1416 timestamp_micros: -1,
1417 value: b"{}".to_vec(),
1418 })),
1419 ];
1420
1421 backend.mutate(path, mutations, "test-setup").await?;
1422
1423 Ok(())
1424 }
1425
1426 #[tokio::test]
1430 async fn test_roundtrip() -> Result<()> {
1431 let backend = create_test_backend().await?;
1432
1433 let id = make_id();
1434 let metadata = Metadata {
1435 content_type: "text/plain".into(),
1436 time_created: Some(SystemTime::now()),
1437 custom: BTreeMap::from_iter([("hello".into(), "world".into())]),
1438 ..Default::default()
1439 };
1440
1441 backend
1442 .put_object(&id, &metadata, stream::single("hello, world"))
1443 .await?;
1444
1445 let (obj_meta, _, stream) = backend.get_object(&id, None).await?.unwrap();
1446 let payload = stream::read_to_vec(stream).await?;
1447 assert_eq!(payload, b"hello, world");
1448 assert_eq!(obj_meta.content_type, metadata.content_type);
1449 assert_eq!(obj_meta.custom, metadata.custom);
1450
1451 let head_meta = backend.get_metadata(&id).await?.unwrap();
1452 assert_eq!(head_meta.content_type, metadata.content_type);
1453 assert_eq!(head_meta.custom, metadata.custom);
1454
1455 Ok(())
1456 }
1457
1458 #[tokio::test]
1460 async fn test_time_expires_roundtrip() -> Result<()> {
1461 let backend = create_test_backend().await?;
1462
1463 let id = make_id();
1464 let ttl = Duration::from_hours(2 * 24);
1465 let expires = SystemTime::now() + ttl;
1466 let metadata = Metadata {
1467 expiration_policy: ExpirationPolicy::TimeToLive(ttl),
1468 time_expires: Some(expires),
1469 ..Default::default()
1470 };
1471 create_object(&backend, &id, &metadata, b"data", SystemTime::now()).await?;
1472
1473 let meta = backend.get_metadata(&id).await?.unwrap();
1474 let stored_ms = meta
1476 .time_expires
1477 .unwrap()
1478 .duration_since(SystemTime::UNIX_EPOCH)?
1479 .as_millis();
1480 let expected_ms = expires.duration_since(SystemTime::UNIX_EPOCH)?.as_millis();
1481 assert_eq!(stored_ms, expected_ms);
1482
1483 Ok(())
1484 }
1485
1486 #[tokio::test]
1488 async fn test_nonexistent() -> Result<()> {
1489 let backend = create_test_backend().await?;
1490
1491 let id = make_id();
1492 assert!(backend.get_object(&id, None).await?.is_none());
1493 assert!(backend.get_metadata(&id).await?.is_none());
1494 backend.delete_object(&id).await?;
1495
1496 Ok(())
1497 }
1498
1499 #[tokio::test]
1500 async fn test_overwrite() -> Result<()> {
1501 let backend = create_test_backend().await?;
1502
1503 let id = make_id();
1504 let first_metadata = Metadata {
1505 custom: BTreeMap::from_iter([("invalid".into(), "invalid".into())]),
1506 ..Default::default()
1507 };
1508 create_object(&backend, &id, &first_metadata, b"hello", SystemTime::now()).await?;
1509
1510 let second_metadata = Metadata {
1511 custom: BTreeMap::from_iter([("hello".into(), "world".into())]),
1512 ..Default::default()
1513 };
1514 backend
1515 .put_object(&id, &second_metadata, stream::single("world"))
1516 .await?;
1517
1518 let (meta, _, stream) = backend.get_object(&id, None).await?.unwrap();
1519 let payload = stream::read_to_vec(stream).await?;
1520 assert_eq!(payload, b"world");
1521 assert_eq!(meta.custom, second_metadata.custom);
1522
1523 Ok(())
1524 }
1525
1526 #[tokio::test]
1527 async fn test_read_after_delete() -> Result<()> {
1528 let backend = create_test_backend().await?;
1529
1530 let id = make_id();
1531 let metadata = Metadata::default();
1532 create_object(&backend, &id, &metadata, b"hello", SystemTime::now()).await?;
1533 backend.delete_object(&id).await?;
1534
1535 assert!(backend.get_object(&id, None).await?.is_none());
1536
1537 Ok(())
1538 }
1539
1540 #[tokio::test]
1546 async fn test_tti_bump() -> Result<()> {
1547 let backend = create_test_backend().await?;
1548 let tti = Duration::from_hours(2 * 24);
1550 let metadata = Metadata {
1551 expiration_policy: ExpirationPolicy::TimeToIdle(tti),
1552 ..Default::default()
1553 };
1554
1555 let past_now = SystemTime::now() - TTI_DEBOUNCE - Duration::from_mins(1);
1558
1559 let id1 = make_id();
1561 create_object(&backend, &id1, &metadata, b"hello, world", past_now).await?;
1562
1563 let (pre_obj_meta, _, _) = backend.get_object(&id1, None).await?.unwrap();
1565 let pre_obj_expiry = pre_obj_meta.time_expires.unwrap();
1566
1567 let post_obj_meta = backend.get_metadata(&id1).await?.unwrap();
1569 let post_obj_expiry = post_obj_meta.time_expires.unwrap();
1570 assert!(
1571 post_obj_expiry > pre_obj_expiry,
1572 "bump should extend expiry"
1573 );
1574
1575 let id2 = make_id();
1577 create_object(&backend, &id2, &metadata, b"hello, world", past_now).await?;
1578
1579 let pre_meta = backend.get_metadata(&id2).await?.unwrap();
1581 let pre_expiry = pre_meta.time_expires.unwrap();
1582
1583 let post_meta = backend.get_metadata(&id2).await?.unwrap();
1585 let post_expiry = post_meta.time_expires.unwrap();
1586 assert!(post_expiry > pre_expiry, "bump should extend expiry");
1587
1588 let (_, _, stream) = backend.get_object(&id2, None).await?.unwrap();
1590 let payload = stream::read_to_vec(stream).await?;
1591 assert_eq!(payload, b"hello, world");
1592
1593 Ok(())
1594 }
1595
1596 #[tokio::test]
1597 async fn test_tti_no_bump_when_fresh() -> Result<()> {
1598 let backend = create_test_backend().await?;
1599
1600 let id = make_id();
1601 let tti = Duration::from_hours(2 * 24);
1603 let metadata = Metadata {
1604 expiration_policy: ExpirationPolicy::TimeToIdle(tti),
1605 ..Default::default()
1606 };
1607 create_object(&backend, &id, &metadata, b"hello, world", SystemTime::now()).await?;
1608
1609 let first = backend.get_metadata(&id).await?.unwrap();
1612 let second = backend.get_metadata(&id).await?.unwrap();
1613
1614 assert_eq!(
1615 first.time_expires.unwrap(),
1616 second.time_expires.unwrap(),
1617 "fresh TTI object must not be bumped"
1618 );
1619
1620 Ok(())
1621 }
1622
1623 #[tokio::test]
1626 async fn test_ttl_immediate() -> Result<()> {
1627 let backend = create_test_backend().await?;
1631
1632 let id = make_id();
1633 let metadata = Metadata {
1634 expiration_policy: ExpirationPolicy::TimeToLive(Duration::from_secs(0)),
1635 ..Default::default()
1636 };
1637 create_object(&backend, &id, &metadata, b"hello, world", SystemTime::now()).await?;
1638
1639 assert!(backend.get_object(&id, None).await?.is_none());
1640
1641 Ok(())
1642 }
1643
1644 #[tokio::test]
1645 async fn test_tti_immediate() -> Result<()> {
1646 let backend = create_test_backend().await?;
1650
1651 let id = make_id();
1652 let metadata = Metadata {
1653 expiration_policy: ExpirationPolicy::TimeToIdle(Duration::from_secs(0)),
1654 ..Default::default()
1655 };
1656 create_object(&backend, &id, &metadata, b"hello, world", SystemTime::now()).await?;
1657
1658 assert!(backend.get_object(&id, None).await?.is_none());
1659
1660 Ok(())
1661 }
1662
1663 #[tokio::test]
1672 async fn test_tiered_get() -> Result<()> {
1673 let backend = create_test_backend().await?;
1674
1675 let id = make_id();
1677 assert!(matches!(
1678 backend.get_tiered_object(&id, None).await?,
1679 TieredGet::NotFound
1680 ));
1681 assert!(matches!(
1682 backend.get_tiered_metadata(&id).await?,
1683 TieredMetadata::NotFound
1684 ));
1685
1686 let id = make_id();
1688 let put_meta = Metadata {
1689 content_type: "text/plain".into(),
1690 custom: BTreeMap::from_iter([("k".into(), "v".into())]),
1691 ..Default::default()
1692 };
1693 create_object(&backend, &id, &put_meta, b"payload", SystemTime::now()).await?;
1694
1695 let TieredGet::Object(obj_meta, _, obj_stream) =
1696 backend.get_tiered_object(&id, None).await?
1697 else {
1698 panic!("expected TieredGet::Object");
1699 };
1700 let obj_payload = stream::read_to_vec(obj_stream).await?;
1701 assert_eq!(obj_payload, b"payload");
1702 assert_eq!(obj_meta.content_type, put_meta.content_type);
1703 assert_eq!(obj_meta.custom, put_meta.custom);
1704
1705 let TieredMetadata::Object(head_meta) = backend.get_tiered_metadata(&id).await? else {
1706 panic!("expected TieredMetadata::Object");
1707 };
1708 assert_eq!(head_meta.content_type, put_meta.content_type);
1709 assert_eq!(head_meta.custom, put_meta.custom);
1710
1711 let hv_id = make_id();
1713 let lt_id = ObjectId::random(hv_id.context().clone());
1714 let tombstone = Tombstone {
1715 target: lt_id.clone(),
1716 expiration_policy: ExpirationPolicy::Manual,
1717 };
1718 create_tombstone(&backend, &hv_id, &tombstone, SystemTime::now()).await?;
1719
1720 match backend.get_tiered_object(&hv_id, None).await? {
1721 TieredGet::Tombstone(get_t) => assert_eq!(get_t.target, lt_id),
1722 other => panic!("expected TieredGet::Tombstone, got {other:?}"),
1723 }
1724 match backend.get_tiered_metadata(&hv_id).await? {
1725 TieredMetadata::Tombstone(meta_t) => assert_eq!(meta_t.target, lt_id,),
1726 other => panic!("expected TieredMetadata::Tombstone, got {other:?}"),
1727 }
1728
1729 Ok(())
1730 }
1731
1732 #[tokio::test]
1738 async fn test_put_non_tombstone() -> Result<()> {
1739 let backend = create_test_backend().await?;
1740
1741 let id = make_id();
1743 let metadata = Metadata::default();
1744 let result = backend
1745 .put_non_tombstone(&id, &metadata, Bytes::from_static(b"first"))
1746 .await?;
1747 assert_eq!(result, None, "expected None on empty row");
1748 let (_, _, stream) = backend.get_object(&id, None).await?.unwrap();
1749 assert_eq!(&stream::read_to_vec(stream).await?, b"first");
1750
1751 let id = make_id();
1753 create_object(&backend, &id, &metadata, b"old", SystemTime::now()).await?;
1754 let result = backend
1755 .put_non_tombstone(&id, &metadata, Bytes::from_static(b"new"))
1756 .await?;
1757 assert_eq!(result, None, "expected None when overwriting object");
1758 let (_, _, stream) = backend.get_object(&id, None).await?.unwrap();
1759 assert_eq!(&stream::read_to_vec(stream).await?, b"new");
1760
1761 let hv_id = make_id();
1763 let lt_id = ObjectId::random(hv_id.context().clone());
1764 let tombstone = Tombstone {
1765 target: lt_id.clone(),
1766 expiration_policy: ExpirationPolicy::Manual,
1767 };
1768 create_tombstone(&backend, &hv_id, &tombstone, SystemTime::now()).await?;
1769 let result = backend
1770 .put_non_tombstone(&hv_id, &metadata, Bytes::new())
1771 .await?;
1772 let returned = result.expect("expected Some(Tombstone) when row is a tombstone");
1773 assert_eq!(returned.target, lt_id);
1774 assert!(
1775 matches!(
1776 backend.get_tiered_metadata(&hv_id).await?,
1777 TieredMetadata::Tombstone(_)
1778 ),
1779 "tombstone must still exist after put_non_tombstone"
1780 );
1781
1782 Ok(())
1783 }
1784
1785 #[tokio::test]
1794 async fn test_delete_non_tombstone() -> Result<()> {
1795 let backend = create_test_backend().await?;
1796
1797 let id = make_id();
1799 assert_eq!(backend.delete_non_tombstone(&id).await?, None);
1800
1801 let id = make_id();
1803 let metadata = Metadata::default();
1804 create_object(&backend, &id, &metadata, b"hello, world", SystemTime::now()).await?;
1805 assert_eq!(backend.delete_non_tombstone(&id).await?, None);
1806 assert!(backend.get_object(&id, None).await?.is_none());
1807
1808 let id = make_id();
1810 let tombstone = Tombstone {
1811 target: id.clone(),
1812 expiration_policy: ExpirationPolicy::Manual,
1813 };
1814 create_tombstone(&backend, &id, &tombstone, SystemTime::now()).await?;
1815 let tombstone = backend
1816 .delete_non_tombstone(&id)
1817 .await?
1818 .expect("expected Some(tombstone)");
1819 assert_eq!(tombstone.target, id, "tombstone target must be returned");
1820 assert!(
1821 matches!(
1822 backend.get_tiered_metadata(&id).await?,
1823 TieredMetadata::Tombstone(_)
1824 ),
1825 "tombstone must still exist after delete_non_tombstone"
1826 );
1827
1828 Ok(())
1829 }
1830
1831 #[tokio::test]
1837 async fn test_cas_create_tombstone() -> Result<()> {
1838 let backend = create_test_backend().await?;
1839
1840 let hv_id = make_id();
1841 let lt_id = ObjectId::random(hv_id.context().clone());
1842 let expiration_policy = ExpirationPolicy::TimeToLive(Duration::from_hours(1));
1843 let tombstone = Tombstone {
1844 target: lt_id.clone(),
1845 expiration_policy,
1846 };
1847
1848 let committed = backend
1850 .compare_and_write(&hv_id, None, TieredWrite::Tombstone(tombstone.clone()))
1851 .await?;
1852 assert!(committed, "expected CAS success on empty row");
1853
1854 let TieredMetadata::Tombstone(t) = backend.get_tiered_metadata(&hv_id).await? else {
1856 panic!("expected TieredMetadata::Tombstone");
1857 };
1858 assert_eq!(t.target, lt_id, "target must round-trip via r column");
1859 assert_eq!(t.expiration_policy, expiration_policy);
1860 match backend.get_tiered_object(&hv_id, None).await? {
1861 TieredGet::Tombstone(t) => assert_eq!(t.target, lt_id, "round-trip via r column"),
1862 other => panic!("expected TieredGet::Tombstone, got {other:?}"),
1863 }
1864
1865 assert!(matches!(
1867 backend.get_object(&hv_id, None).await,
1868 Err(Error::UnexpectedTombstone)
1869 ));
1870 assert!(matches!(
1871 backend.get_metadata(&hv_id).await,
1872 Err(Error::UnexpectedTombstone)
1873 ));
1874
1875 let second = backend
1877 .compare_and_write(&hv_id, None, TieredWrite::Tombstone(tombstone))
1878 .await?;
1879 assert!(second, "idempotent retry");
1880
1881 Ok(())
1882 }
1883
1884 #[tokio::test]
1886 async fn test_cas_swap_tombstone() -> Result<()> {
1887 let backend = create_test_backend().await?;
1888
1889 let hv_id = make_id();
1890 let old_lt_id = ObjectId::random(hv_id.context().clone());
1891 let wrong_lt_id = ObjectId::random(hv_id.context().clone());
1892 let new_lt_id = ObjectId::random(hv_id.context().clone());
1893
1894 let tombstone = Tombstone {
1895 target: old_lt_id.clone(),
1896 expiration_policy: ExpirationPolicy::Manual,
1897 };
1898 create_tombstone(&backend, &hv_id, &tombstone, SystemTime::now()).await?;
1899
1900 let write = TieredWrite::Tombstone(Tombstone {
1902 target: new_lt_id.clone(),
1903 expiration_policy: ExpirationPolicy::Manual,
1904 });
1905 let swapped = backend
1906 .compare_and_write(&hv_id, Some(&wrong_lt_id), write.clone())
1907 .await?;
1908 assert!(!swapped, "expected CAS failure due to wrong target");
1909 match backend.get_tiered_metadata(&hv_id).await? {
1910 TieredMetadata::Tombstone(t) => assert_eq!(t.target, old_lt_id),
1911 other => panic!("expected tombstone, got {other:?}"),
1912 }
1913
1914 let swapped = backend
1916 .compare_and_write(&hv_id, Some(&old_lt_id), write.clone())
1917 .await?;
1918 assert!(swapped, "expected CAS success with correct target");
1919 match backend.get_tiered_metadata(&hv_id).await? {
1920 TieredMetadata::Tombstone(t) => assert_eq!(t.target, new_lt_id),
1921 other => panic!("expected tombstone, got {other:?}"),
1922 }
1923
1924 let retry = backend
1926 .compare_and_write(&hv_id, Some(&old_lt_id), write)
1927 .await?;
1928 assert!(retry, "idempotent retry");
1929
1930 Ok(())
1931 }
1932
1933 #[tokio::test]
1935 async fn test_cas_swap_inline() -> Result<()> {
1936 let backend = create_test_backend().await?;
1937
1938 let id = make_id();
1939 let lt_id = ObjectId::random(id.context().clone());
1940 let wrong_id = ObjectId::random(id.context().clone());
1941
1942 let tombstone = Tombstone {
1943 target: lt_id.clone(),
1944 expiration_policy: ExpirationPolicy::Manual,
1945 };
1946 create_tombstone(&backend, &id, &tombstone, SystemTime::now()).await?;
1947
1948 let write = TieredWrite::Object(Metadata::default(), Bytes::new());
1950 let swapped = backend
1951 .compare_and_write(&id, Some(&wrong_id), write)
1952 .await?;
1953 assert!(!swapped, "expected CAS failure with wrong target");
1954 assert!(matches!(
1955 backend.get_tiered_metadata(&id).await?,
1956 TieredMetadata::Tombstone(_)
1957 ));
1958
1959 let payload = Bytes::from_static(b"hello inline");
1961 let write = TieredWrite::Object(Metadata::default(), payload.clone());
1962 let swapped = backend
1963 .compare_and_write(&id, Some(<_id), write.clone())
1964 .await?;
1965 assert!(swapped, "expected CAS success with correct target");
1966 let TieredGet::Object(_, _, stream) = backend.get_tiered_object(&id, None).await? else {
1967 panic!("expected inline object after swap");
1968 };
1969 assert_eq!(&stream::read_to_vec(stream).await?, payload.as_ref());
1970
1971 let retry = backend.compare_and_write(&id, Some(<_id), write).await?;
1973 assert!(retry, "idempotent retry");
1974
1975 Ok(())
1976 }
1977
1978 #[tokio::test]
1980 async fn test_cas_create_object_on_empty_row() -> Result<()> {
1981 let backend = create_test_backend().await?;
1982
1983 let id = make_id();
1984 let payload = Bytes::from_static(b"cas object");
1985 let write = TieredWrite::Object(Metadata::default(), payload.clone());
1986 let committed = backend.compare_and_write(&id, None, write).await?;
1987 assert!(committed, "expected CAS success on empty row");
1988
1989 let TieredGet::Object(_, _, stream) = backend.get_tiered_object(&id, None).await? else {
1990 panic!("expected Object after CAS-create");
1991 };
1992 assert_eq!(&stream::read_to_vec(stream).await?, payload.as_ref());
1993
1994 Ok(())
1995 }
1996
1997 #[tokio::test]
1999 async fn test_cas_delete() -> Result<()> {
2000 let backend = create_test_backend().await?;
2001
2002 let id = make_id();
2003 let lt_id = ObjectId::random(id.context().clone());
2004 let wrong_id = ObjectId::random(id.context().clone());
2005
2006 let tombstone = Tombstone {
2007 target: lt_id.clone(),
2008 expiration_policy: ExpirationPolicy::Manual,
2009 };
2010 create_tombstone(&backend, &id, &tombstone, SystemTime::now()).await?;
2011
2012 let deleted = backend
2014 .compare_and_write(&id, Some(&wrong_id), TieredWrite::Delete)
2015 .await?;
2016 assert!(!deleted, "expected CAS failure with wrong target");
2017 assert!(matches!(
2018 backend.get_tiered_metadata(&id).await?,
2019 TieredMetadata::Tombstone(_)
2020 ));
2021
2022 let deleted = backend
2024 .compare_and_write(&id, Some(<_id), TieredWrite::Delete)
2025 .await?;
2026 assert!(deleted, "expected CAS delete success");
2027 assert!(matches!(
2028 backend.get_tiered_metadata(&id).await?,
2029 TieredMetadata::NotFound
2030 ));
2031
2032 let retry = backend
2034 .compare_and_write(&id, Some(<_id), TieredWrite::Delete)
2035 .await?;
2036 assert!(retry, "idempotent retry");
2037
2038 let id2 = make_id();
2040 let fake_lt_id = ObjectId::random(id2.context().clone());
2041 let metadata = Metadata::default();
2042 create_object(&backend, &id2, &metadata, b"data", SystemTime::now()).await?;
2043 let deleted = backend
2044 .compare_and_write(&id2, Some(&fake_lt_id), TieredWrite::Delete)
2045 .await?;
2046 assert!(deleted, "expected idempotent deletion");
2047
2048 Ok(())
2049 }
2050
2051 #[tokio::test]
2058 async fn test_legacy_tombstone_reads() -> Result<()> {
2059 let backend = create_test_backend().await?;
2060
2061 let id = make_id();
2063 write_legacy_tombstone(&backend, &id, ExpirationPolicy::Manual, None).await?;
2064
2065 let TieredMetadata::Tombstone(t) = backend.get_tiered_metadata(&id).await? else {
2066 panic!("expected tombstone");
2067 };
2068 assert_eq!(t.expiration_policy, ExpirationPolicy::Manual);
2069 assert!(matches!(
2070 backend.get_tiered_object(&id, None).await?,
2071 TieredGet::Tombstone(_)
2072 ));
2073
2074 let id = make_id();
2079 let ttl = Duration::from_hours(2 * 24);
2080 write_legacy_tombstone(&backend, &id, ExpirationPolicy::TimeToLive(ttl), None).await?;
2081
2082 let TieredMetadata::Tombstone(t) = backend.get_tiered_metadata(&id).await? else {
2083 panic!("expected TieredMetadata::Tombstone");
2084 };
2085 assert_eq!(t.expiration_policy, ExpirationPolicy::TimeToLive(ttl));
2086
2087 Ok(())
2088 }
2089
2090 #[tokio::test]
2095 async fn test_legacy_tombstone_tti_upgrade() -> Result<()> {
2096 let backend = create_test_backend().await?;
2097 let id = make_id();
2098 let path = id.as_storage_path().to_string().into_bytes();
2099
2100 let tti = Duration::from_hours(2 * 24); let old_deadline = SystemTime::now() + tti - TTI_DEBOUNCE - Duration::from_mins(1);
2105 write_legacy_tombstone(
2106 &backend,
2107 &id,
2108 ExpirationPolicy::TimeToIdle(tti),
2109 Some(old_deadline),
2110 )
2111 .await?;
2112
2113 let TieredMetadata::Tombstone(_) = backend.get_tiered_metadata(&id).await? else {
2115 panic!("expected tombstone");
2116 };
2117
2118 let new_deadline = match backend.read_row(&path, None, "test-verify").await? {
2120 Some(RowData::Tombstone { time_expires, .. }) => time_expires.unwrap(),
2121 _ => panic!("expected tombstone row after bump"),
2122 };
2123
2124 assert!(
2125 new_deadline > old_deadline,
2126 "TTI bump should extend tombstone expiry: {old_deadline:?} -> {new_deadline:?}"
2127 );
2128
2129 Ok(())
2130 }
2131
2132 #[tokio::test]
2137 async fn test_legacy_tombstone_conditional_ops() -> Result<()> {
2138 let backend = create_test_backend().await?;
2139
2140 let id = make_id();
2142 write_legacy_tombstone(&backend, &id, ExpirationPolicy::Manual, None).await?;
2143 let t_opt = backend
2144 .put_non_tombstone(&id, &Metadata::default(), Bytes::new())
2145 .await?;
2146 assert_eq!(t_opt.map(|t| t.target).as_ref(), Some(&id));
2147
2148 let id = make_id();
2150 write_legacy_tombstone(&backend, &id, ExpirationPolicy::Manual, None).await?;
2151 let t_opt = backend.delete_non_tombstone(&id).await?;
2152 assert_eq!(t_opt.map(|t| t.target).as_ref(), Some(&id));
2153
2154 let id = make_id();
2156 write_legacy_tombstone(&backend, &id, ExpirationPolicy::Manual, None).await?;
2157 let deleted = backend
2158 .compare_and_write(&id, Some(&id), TieredWrite::Delete)
2159 .await?;
2160 assert!(
2161 deleted,
2162 "CAS-delete must succeed on legacy-metadata tombstone"
2163 );
2164 assert!(matches!(
2165 backend.get_tiered_metadata(&id).await?,
2166 TieredMetadata::NotFound
2167 ));
2168
2169 let id = make_id();
2171 write_empty_redirect_tombstone(&backend, &id).await?;
2172 let deleted = backend
2173 .compare_and_write(&id, Some(&id), TieredWrite::Delete)
2174 .await?;
2175 assert!(
2176 deleted,
2177 "CAS-delete must succeed on empty-redirect tombstone"
2178 );
2179 assert!(matches!(
2180 backend.get_tiered_metadata(&id).await?,
2181 TieredMetadata::NotFound
2182 ));
2183
2184 Ok(())
2185 }
2186
2187 #[tokio::test]
2189 async fn test_empty_redirect_falls_back_to_hv_id() -> Result<()> {
2190 let backend = create_test_backend().await?;
2191 let id = make_id();
2192
2193 write_empty_redirect_tombstone(&backend, &id).await?;
2194 match backend.get_tiered_metadata(&id).await? {
2195 TieredMetadata::Tombstone(t) => assert_eq!(t.target, id, "must fall back to hv_id"),
2196 other => panic!("expected tombstone, got {other:?}"),
2197 }
2198
2199 Ok(())
2200 }
2201
2202 #[tokio::test]
2207 async fn test_cas_create_tombstone_over_expired() -> Result<()> {
2208 let backend = create_test_backend().await?;
2209
2210 let id = make_id();
2211 let old_lt_id = ObjectId::random(id.context().clone());
2212 let old_tombstone = Tombstone {
2213 target: old_lt_id,
2214 expiration_policy: ExpirationPolicy::TimeToLive(Duration::from_secs(0)),
2215 };
2216 create_tombstone(&backend, &id, &old_tombstone, SystemTime::now()).await?;
2217
2218 let new_lt_id = ObjectId::random(id.context().clone());
2219 let new_tombstone = Tombstone {
2220 target: new_lt_id.clone(),
2221 expiration_policy: ExpirationPolicy::TimeToLive(Duration::from_hours(1)),
2222 };
2223 let committed = backend
2224 .compare_and_write(&id, None, TieredWrite::Tombstone(new_tombstone))
2225 .await?;
2226 assert!(
2227 committed,
2228 "CAS with current=None must succeed over an expired tombstone"
2229 );
2230
2231 let TieredMetadata::Tombstone(t) = backend.get_tiered_metadata(&id).await? else {
2232 panic!("expected new tombstone to be readable");
2233 };
2234 assert_eq!(t.target, new_lt_id);
2235
2236 Ok(())
2237 }
2238
2239 #[tokio::test]
2242 async fn test_put_non_tombstone_over_expired() -> Result<()> {
2243 let backend = create_test_backend().await?;
2244
2245 let id = make_id();
2246 let lt_id = ObjectId::random(id.context().clone());
2247 let tombstone = Tombstone {
2248 target: lt_id,
2249 expiration_policy: ExpirationPolicy::TimeToLive(Duration::from_secs(0)),
2250 };
2251 create_tombstone(&backend, &id, &tombstone, SystemTime::now()).await?;
2252
2253 let result = backend
2254 .put_non_tombstone(&id, &Metadata::default(), Bytes::from_static(b"data"))
2255 .await?;
2256 assert_eq!(
2257 result, None,
2258 "put_non_tombstone must succeed (return None) over an expired tombstone"
2259 );
2260
2261 let (_, _, stream) = backend.get_object(&id, None).await?.unwrap();
2262 assert_eq!(&stream::read_to_vec(stream).await?, b"data");
2263
2264 Ok(())
2265 }
2266
2267 async fn put_range_test_object(backend: &BigTableBackend) -> Result<ObjectId> {
2270 let id = make_id();
2271 let metadata = Metadata {
2272 content_type: "text/plain".into(),
2273 ..Default::default()
2274 };
2275 let payload = b"Hello, range requests!";
2276 backend
2277 .put_object(&id, &metadata, stream::single(payload.as_slice()))
2278 .await?;
2279 Ok(id)
2280 }
2281
2282 #[tokio::test]
2283 async fn get_object_range_bounded() -> Result<()> {
2284 let backend = create_test_backend().await?;
2285 let id = put_range_test_object(&backend).await?;
2286
2287 let (_, content_range, stream) = backend
2288 .get_object(&id, Some(ByteRange::Bounded(7, 11)))
2289 .await?
2290 .unwrap();
2291 let data = stream::read_to_vec(stream).await?;
2292 assert_eq!(&data, b"range");
2293
2294 let content_range = content_range.unwrap();
2295 assert_eq!(content_range.start, 7);
2296 assert_eq!(content_range.end, 11);
2297 assert_eq!(content_range.total, 22);
2298
2299 Ok(())
2300 }
2301
2302 #[tokio::test]
2303 async fn get_object_range_from() -> Result<()> {
2304 let backend = create_test_backend().await?;
2305 let id = put_range_test_object(&backend).await?;
2306
2307 let (_, content_range, stream) = backend
2308 .get_object(&id, Some(ByteRange::From(7)))
2309 .await?
2310 .unwrap();
2311 let data = stream::read_to_vec(stream).await?;
2312 assert_eq!(&data, b"range requests!");
2313
2314 let content_range = content_range.unwrap();
2315 assert_eq!(content_range.start, 7);
2316 assert_eq!(content_range.end, 21);
2317 assert_eq!(content_range.total, 22);
2318
2319 Ok(())
2320 }
2321
2322 #[tokio::test]
2323 async fn get_object_range_last() -> Result<()> {
2324 let backend = create_test_backend().await?;
2325 let id = put_range_test_object(&backend).await?;
2326
2327 let (_, content_range, stream) = backend
2328 .get_object(&id, Some(ByteRange::Last(9)))
2329 .await?
2330 .unwrap();
2331 let data = stream::read_to_vec(stream).await?;
2332 assert_eq!(&data, b"requests!");
2333
2334 let content_range = content_range.unwrap();
2335 assert_eq!(content_range.start, 13);
2336 assert_eq!(content_range.end, 21);
2337 assert_eq!(content_range.total, 22);
2338
2339 Ok(())
2340 }
2341
2342 #[tokio::test]
2343 async fn get_object_range_unsatisfiable() -> Result<()> {
2344 let backend = create_test_backend().await?;
2345 let id = put_range_test_object(&backend).await?;
2346
2347 match backend.get_object(&id, Some(ByteRange::From(100))).await {
2348 Err(Error::RangeNotSatisfiable { total }) => assert_eq!(total, 22),
2349 Ok(_) => panic!("expected RangeNotSatisfiable, got Ok"),
2350 Err(e) => panic!("expected RangeNotSatisfiable, got {e:?}"),
2351 }
2352
2353 Ok(())
2354 }
2355
2356 #[tokio::test]
2357 async fn get_object_no_range_returns_full_payload() -> Result<()> {
2358 let backend = create_test_backend().await?;
2359 let id = put_range_test_object(&backend).await?;
2360
2361 let (_, content_range, stream) = backend.get_object(&id, None).await?.unwrap();
2362 let data = stream::read_to_vec(stream).await?;
2363 assert_eq!(&data, b"Hello, range requests!");
2364 assert!(content_range.is_none());
2365
2366 Ok(())
2367 }
2368}