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