Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 16 additions & 22 deletions objectstore-service/src/backend/bigtable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,6 @@ const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
/// that attempt to use a certain channel after the server has closed it will pay the cost of the
/// reconnection, resulting in increased latency for those requests.
const MAX_CHANNEL_AGE: Option<Duration> = Some(Duration::from_mins(50));
/// Time to debounce bumping an object with configured TTI.
const TTI_DEBOUNCE: Duration = Duration::from_hours(24);
/// Permission scopes required for accessing the BigTable data API.
const TOKEN_SCOPES: &[&str] = &["https://www.googleapis.com/auth/bigtable.data"];

Expand Down Expand Up @@ -664,12 +662,13 @@ impl RowData {
self.expiration_policy().is_timeout() && self.time_expires().is_some_and(|ts| ts < time)
}

/// Returns `true` if this row's TTI deadline should be bumped.
fn needs_tti_bump(&self) -> bool {
matches!(
self.expiration_policy(),
ExpirationPolicy::TimeToIdle(tti) if self.expires_before(SystemTime::now() + tti - TTI_DEBOUNCE)
)
/// Checks whether this row's TTI deadline needs bumping.
///
/// Returns `Some(new_expire_at)` when the deadline is stale enough to
/// justify a write, `None` otherwise.
fn check_tti_bump(&self, access_time: SystemTime) -> Option<SystemTime> {
self.expiration_policy()
.check_tti_bump(self.time_expires(), access_time)
}
}

Expand Down Expand Up @@ -1019,7 +1018,7 @@ impl HighVolumeBackend for BigTableBackend {
};

// TODO: extract into dedicated call from service
if row.needs_tti_bump() {
if row.check_tti_bump(SystemTime::now()).is_some() {
self.bump_tti(path.clone(), &row, true, id).await;
}

Expand Down Expand Up @@ -1058,7 +1057,7 @@ impl HighVolumeBackend for BigTableBackend {
};

// TODO: extract into dedicated call from service
if row.needs_tti_bump() {
if row.check_tti_bump(SystemTime::now()).is_some() {
self.bump_tti(path.clone(), &row, false, id).await;
}

Expand Down Expand Up @@ -1539,22 +1538,19 @@ mod tests {

/// Verifies TTI bump via both `get_object` (loaded=true path) and `get_metadata` (loaded=false path).
///
/// The bump condition is: `expire_at < now + tti - TTI_DEBOUNCE`. We write a stale
/// timestamp just inside the bump window (still in the future, so the row is not GC'd)
/// and confirm that a subsequent read returns a later expiry.
/// We write a stale timestamp inside the bump window (still in the future,
/// so the row is not GC'd) and confirm that a subsequent read extends the expiry.
#[tokio::test]
async fn test_tti_bump() -> Result<()> {
let backend = create_test_backend().await?;
// TTI must exceed TTI_DEBOUNCE (1 day) for the bump condition to be reachable.
let tti = Duration::from_hours(2 * 24);
let metadata = Metadata {
expiration_policy: ExpirationPolicy::TimeToIdle(tti),
..Default::default()
};

// Pass a backdated `now` so the written expiry is inside the bump window:
// expire_at = past_now + tti = now - TTI_DEBOUNCE - 60s (stale but not yet expired).
let past_now = SystemTime::now() - TTI_DEBOUNCE - Duration::from_mins(1);
// Backdate `now` so the written expiry (past_now + tti) is stale but not expired.
let past_now = SystemTime::now() - tti + Duration::from_mins(1);
Comment thread
jan-auer marked this conversation as resolved.

// Sub-sequence 1: get_object triggers bump (loaded=true path).
let id1 = make_id();
Expand Down Expand Up @@ -1598,7 +1594,6 @@ mod tests {
let backend = create_test_backend().await?;

let id = make_id();
// TTI must exceed TTI_DEBOUNCE (1 day) for the bump condition to be reachable.
let tti = Duration::from_hours(2 * 24);
let metadata = Metadata {
expiration_policy: ExpirationPolicy::TimeToIdle(tti),
Expand Down Expand Up @@ -2097,11 +2092,10 @@ mod tests {
let id = make_id();
let path = id.as_storage_path().to_string().into_bytes();

let tti = Duration::from_hours(2 * 24); // must exceed TTI_DEBOUNCE (1 day)
let tti = Duration::from_hours(2 * 24);

// Place time_expires just inside the bump window: past `now + tti - TTI_DEBOUNCE`
// but still in the future so `expires_before(now)` does not filter the row.
let old_deadline = SystemTime::now() + tti - TTI_DEBOUNCE - Duration::from_mins(1);
// Place time_expires inside the bump window but still in the future.
let old_deadline = SystemTime::now() + Duration::from_mins(1);
write_legacy_tombstone(
&backend,
&id,
Expand Down
63 changes: 47 additions & 16 deletions objectstore-service/src/backend/gcs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::future::Future;
use std::time::{Duration, SystemTime};
use std::time::SystemTime;
use std::{fmt, io};

use anyhow::Context;
Expand Down Expand Up @@ -78,8 +78,6 @@ pub struct GcsConfig {
const DEFAULT_ENDPOINT: &str = "https://storage.googleapis.com";
/// Permission scopes required for accessing GCS.
const TOKEN_SCOPES: &[&str] = &["https://www.googleapis.com/auth/devstorage.read_write"];
/// Time to debounce bumping an object with configured TTI.
const TTI_DEBOUNCE: Duration = Duration::from_hours(24);
/// How many times to retry failed operations.
const REQUEST_RETRY_COUNT: usize = 2;

Expand Down Expand Up @@ -558,25 +556,23 @@ impl GcsBackend {
return Ok(None);
};

let expire_at = gcs_metadata.custom_time;
let metadata = gcs_metadata.into_metadata()?;

// TODO: Inject the access time from the request.
let access_time = SystemTime::now();

// Filter already expired objects but leave them to garbage collection
if metadata.expiration_policy.is_timeout() && expire_at.is_some_and(|ts| ts < access_time) {
if metadata.expiration_policy.is_timeout()
&& metadata.time_expires.is_some_and(|ts| ts < access_time)
{
objectstore_log::debug!("Object found but past expiry");
return Ok(None);
}

// TODO: Schedule into background persistently so this doesn't get lost on restarts
if let ExpirationPolicy::TimeToIdle(tti) = metadata.expiration_policy {
let new_expire_at = access_time + tti;
if expire_at.is_some_and(|ts| ts < new_expire_at - TTI_DEBOUNCE) {
self.update_custom_time(object_url.clone(), new_expire_at)
.await?;
}
if let Some(new_expire_at) = metadata.check_tti_bump(access_time) {
self.update_custom_time(object_url.clone(), new_expire_at)
.await?;
}

Ok(Some(metadata))
Expand Down Expand Up @@ -1086,6 +1082,7 @@ impl MultipartUploadBackend for GcsBackend {
mod tests {
use std::collections::BTreeMap;
use std::num::NonZeroU32;
use std::time::Duration;

use anyhow::Result;
use objectstore_types::scope::{Scope, Scopes};
Expand Down Expand Up @@ -1327,7 +1324,6 @@ mod tests {
let backend = create_test_backend().await?;

let id = make_id();
// TTI must exceed TTI_DEBOUNCE (1 day) for the bump condition to be reachable.
let tti = Duration::from_hours(2 * 24);
let metadata = Metadata {
content_type: "text/plain".into(),
Expand All @@ -1340,10 +1336,9 @@ mod tests {
.put_object(&id, &metadata, stream::single("hello, world"))
.await?;

// Manually set custom_time to just inside the bump window.
// The bump condition is: expire_at < now + tti - TTI_DEBOUNCE.
// Backdate custom_time so it falls inside the bump window.
let object_url = backend.object_url(&id)?;
let old_deadline = SystemTime::now() + tti - TTI_DEBOUNCE - Duration::from_mins(1);
let old_deadline = SystemTime::now() + Duration::from_mins(1);
backend.update_custom_time(object_url, old_deadline).await?;

// First get_metadata sees the old timestamp and triggers a TTI bump.
Expand Down Expand Up @@ -1371,7 +1366,6 @@ mod tests {
let backend = create_test_backend().await?;

let id = make_id();
// TTI must exceed TTI_DEBOUNCE (1 day) for the bump condition to be reachable.
let tti = Duration::from_hours(2 * 24);
let metadata = Metadata {
content_type: "text/plain".into(),
Expand Down Expand Up @@ -1400,6 +1394,43 @@ mod tests {
Ok(())
}

#[tokio::test]
async fn test_short_tti_bumps() -> Result<()> {
let backend = create_test_backend().await?;

let id = make_id();
let tti = Duration::from_hours(2);
let metadata = Metadata {
content_type: "text/plain".into(),
expiration_policy: ExpirationPolicy::TimeToIdle(tti),
time_expires: Some(SystemTime::now() + tti),
..Default::default()
};

backend
.put_object(&id, &metadata, stream::single("hello, world"))
.await?;

// Backdate custom_time so it falls inside the bump window.
let object_url = backend.object_url(&id)?;
let old_deadline = SystemTime::now() + Duration::from_mins(1);
backend.update_custom_time(object_url, old_deadline).await?;

// First get_metadata triggers the bump.
let pre_meta = backend.get_metadata(&id).await?.unwrap();
let pre_expiry = pre_meta.time_expires.unwrap();

// Second get_metadata sees the bumped timestamp.
let post_meta = backend.get_metadata(&id).await?.unwrap();
let post_expiry = post_meta.time_expires.unwrap();
assert!(
post_expiry > pre_expiry,
"Short TTI bump should have extended the expiry: {pre_expiry:?} -> {post_expiry:?}"
);

Ok(())
}

#[tokio::test]
async fn test_compressed_payload_roundtrip() -> Result<()> {
use objectstore_types::metadata::Compression;
Expand Down
32 changes: 12 additions & 20 deletions objectstore-service/src/backend/s3_compatible.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
//! S3-compatible backend with generic protocol support.

use std::time::{Duration, SystemTime};
use std::time::SystemTime;
use std::{fmt, io};

use futures_util::{StreamExt, TryStreamExt};
use objectstore_types::metadata::{ExpirationPolicy, Metadata};
use objectstore_types::metadata::Metadata;
use objectstore_types::range::{ByteRange, ContentRange};
use reqwest::header::HeaderMap;
use reqwest::{Body, IntoUrl, Method, RequestBuilder, Response, StatusCode};
Expand Down Expand Up @@ -63,8 +63,6 @@ const GCS_CUSTOM_PREFIX: &str = "x-goog-meta-";
///
/// See: <https://cloud.google.com/storage/docs/xml-api/reference-headers#xgoogcustomtime>
const GCS_CUSTOM_TIME: &str = "x-goog-custom-time";
/// Time to debounce bumping an object with configured TTI.
const TTI_DEBOUNCE: Duration = Duration::from_hours(24);

/// An authentication token that can be passed as a bearer credential.
pub trait Token: Send + Sync {
Expand Down Expand Up @@ -230,30 +228,21 @@ where
// TODO: Inject the access time from the request.
let access_time = SystemTime::now();

let expire_at = headers
.get(GCS_CUSTOM_TIME)
.and_then(|s| s.to_str().ok())
.and_then(|s| humantime::parse_rfc3339(s).ok());

// Filter already expired objects but leave them to garbage collection
if metadata.expiration_policy.is_timeout() && expire_at.is_some_and(|ts| ts < access_time) {
if metadata.expiration_policy.is_timeout()
&& metadata.time_expires.is_some_and(|ts| ts < access_time)
{
objectstore_log::debug!("Object found but past expiry");
response.drain_body().await;
return Ok(None);
}

// TODO: extract into dedicated call from service
// TODO: Schedule into background persistently so this doesn't get lost on restarts
if let ExpirationPolicy::TimeToIdle(tti) = metadata.expiration_policy {
let expire_at = expire_at.unwrap_or(access_time);

if expire_at < access_time + tti - TTI_DEBOUNCE {
// The write helper persists `time_expires` verbatim, so refresh it to the bumped
// deadline here. The returned `metadata` keeps the pre-access value.
let mut bumped = metadata.clone();
bumped.time_expires = Some(access_time + tti);
self.update_metadata(id, &bumped).await?;
}
if let Some(new_expire_at) = metadata.check_tti_bump(access_time) {
let mut bumped = metadata.clone();
bumped.time_expires = Some(new_expire_at);
self.update_metadata(id, &bumped).await?;
}

Ok(Some((metadata, content_range, response)))
Expand Down Expand Up @@ -384,7 +373,10 @@ impl<T: TokenProvider> Backend for S3CompatibleBackend<T> {

#[cfg(test)]
mod tests {
use std::time::Duration;

use anyhow::Result;
use objectstore_types::metadata::ExpirationPolicy;
use objectstore_types::scope::{Scope, Scopes};

use super::*;
Expand Down
Loading
Loading