-
-
Notifications
You must be signed in to change notification settings - Fork 5
feat(keeper): integrate filesystem backend with keeper trait #583
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: aldy505/feat/ttl-keeper
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,8 @@ use crate::backend::common::{ | |
| }; | ||
| use crate::error::{Error, Result}; | ||
| use crate::id::ObjectId; | ||
| use crate::keeper::sqlite_backed::SqliteBackedKeeper; | ||
| use crate::keeper::{Keeper, KeeperBackend, KeeperConfig}; | ||
| use crate::multipart::{ | ||
| AbortMultipartResponse, CompleteMultipartResponse, CompletedPart, InitiateMultipartResponse, | ||
| ListPartsResponse, Part, PartNumber, UploadId, UploadPartResponse, | ||
|
|
@@ -34,6 +36,9 @@ use crate::stream::{self, ClientStream}; | |
| /// storage: | ||
| /// type: filesystem | ||
| /// path: /data | ||
| /// keeper: | ||
| /// backend: sqlite | ||
| /// connection_url: sqlite://data/keeper.db | ||
| /// ``` | ||
| #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] | ||
| pub struct FileSystemConfig { | ||
|
|
@@ -51,18 +56,31 @@ pub struct FileSystemConfig { | |
| /// - `OS__STORAGE__TYPE=filesystem` | ||
| /// - `OS__STORAGE__PATH=/path/to/storage` | ||
| pub path: PathBuf, | ||
|
|
||
| /// Configuration for the keeper backend. | ||
| /// See [`KeeperConfig`] for details. | ||
| pub keeper: KeeperConfig, | ||
| } | ||
|
|
||
| /// Local filesystem backend for development and testing. | ||
| #[derive(Debug)] | ||
| pub struct LocalFsBackend { | ||
| path: PathBuf, | ||
| keeper: Box<dyn Keeper>, | ||
| } | ||
|
|
||
| impl LocalFsBackend { | ||
| /// Creates a new [`LocalFsBackend`] rooted at the directory in `config`. | ||
| pub fn new(config: FileSystemConfig) -> Self { | ||
| Self { path: config.path } | ||
| pub async fn new(config: FileSystemConfig) -> Result<Self> { | ||
| let keeper = match config.keeper.backend { | ||
| KeeperBackend::Sqlite => { | ||
| Box::new(SqliteBackedKeeper::new(&config.keeper.connection_url).await?) | ||
| } | ||
| }; | ||
| Ok(Self { | ||
| path: config.path, | ||
| keeper, | ||
| }) | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -115,6 +133,8 @@ impl Backend for LocalFsBackend { | |
| file.sync_data().await?; | ||
| drop(file); | ||
|
|
||
| self.keeper.keep(id, metadata.expiration_policy).await?; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Overwrite breaks keeper retention trackingHigh Severity
Additional Locations (1)Reviewed by Cursor Bugbot for commit eba4ba5. Configure here. |
||
|
|
||
|
Comment on lines
+136
to
+137
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: Overwriting an object with a TTL/TTI policy fails due to a database UNIQUE constraint violation, causing data inconsistency despite the file being successfully updated. Suggested FixModify the Prompt for AI AgentAlso affects:
|
||
| Ok(()) | ||
| } | ||
|
|
||
|
|
@@ -131,6 +151,7 @@ impl Backend for LocalFsBackend { | |
| } | ||
| err => err?, | ||
| }; | ||
| self.keeper.mark_accessed(id).await?; | ||
|
|
||
| let mut reader = BufReader::new(file); | ||
| let mut metadata_line = String::new(); | ||
|
|
@@ -174,6 +195,7 @@ impl Backend for LocalFsBackend { | |
| { | ||
| objectstore_log::debug!("Object not found"); | ||
| } | ||
| self.keeper.remove(id).await?; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Keeper removed on failed deletesMedium Severity
Reviewed by Cursor Bugbot for commit 499ea21. Configure here. |
||
| Ok(result?) | ||
|
sentry[bot] marked this conversation as resolved.
Comment on lines
195
to
199
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: In Suggested FixHandle the result of the filesystem deletion before attempting the keeper database operation. For example, if the file is not found, return the Prompt for AI Agent |
||
| } | ||
| } | ||
|
|
@@ -432,6 +454,8 @@ impl MultipartUploadBackend for LocalFsBackend { | |
| file.sync_data().await?; | ||
| drop(file); | ||
|
|
||
| self.keeper.keep(id, metadata.expiration_policy).await?; | ||
|
|
||
| // Clean up multipart state | ||
| tokio::fs::remove_dir_all(dir).await?; | ||
|
|
||
|
|
@@ -458,7 +482,13 @@ mod tests { | |
| let tempdir = tempfile::tempdir().unwrap(); | ||
| let backend = LocalFsBackend::new(FileSystemConfig { | ||
| path: tempdir.path().to_path_buf(), | ||
| }); | ||
| keeper: KeeperConfig { | ||
| backend: KeeperBackend::Sqlite, | ||
| connection_url: "sqlite::memory:".into(), | ||
| }, | ||
| }) | ||
| .await | ||
| .unwrap(); | ||
|
|
||
| let id = ObjectId::random(ObjectContext { | ||
| usecase: "testing".into(), | ||
|
|
@@ -499,7 +529,13 @@ mod tests { | |
| let tempdir = tempfile::tempdir().unwrap(); | ||
| let backend = LocalFsBackend::new(FileSystemConfig { | ||
| path: tempdir.path().to_path_buf(), | ||
| }); | ||
| keeper: KeeperConfig { | ||
| backend: KeeperBackend::Sqlite, | ||
| connection_url: "sqlite::memory:".into(), | ||
| }, | ||
| }) | ||
| .await | ||
| .unwrap(); | ||
|
|
||
| let id = ObjectId::random(ObjectContext { | ||
| usecase: "testing".into(), | ||
|
|
@@ -533,7 +569,13 @@ mod tests { | |
| let tempdir = tempfile::tempdir().unwrap(); | ||
| let backend = LocalFsBackend::new(FileSystemConfig { | ||
| path: tempdir.path().to_path_buf(), | ||
| }); | ||
| keeper: KeeperConfig { | ||
| backend: KeeperBackend::Sqlite, | ||
| connection_url: "sqlite::memory:".into(), | ||
| }, | ||
| }) | ||
| .await | ||
| .unwrap(); | ||
|
|
||
| let id = ObjectId::random(ObjectContext { | ||
| usecase: "testing".into(), | ||
|
|
@@ -551,17 +593,22 @@ mod tests { | |
| }) | ||
| } | ||
|
|
||
| fn make_backend() -> (tempfile::TempDir, LocalFsBackend) { | ||
| async fn make_backend() -> Result<(tempfile::TempDir, LocalFsBackend)> { | ||
| let tempdir = tempfile::tempdir().unwrap(); | ||
| let backend = LocalFsBackend::new(FileSystemConfig { | ||
| path: tempdir.path().to_path_buf(), | ||
| }); | ||
| (tempdir, backend) | ||
| keeper: KeeperConfig { | ||
| backend: KeeperBackend::Sqlite, | ||
| connection_url: "sqlite::memory:".into(), | ||
| }, | ||
| }) | ||
| .await?; | ||
| Ok((tempdir, backend)) | ||
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn multipart_single_part() { | ||
| let (_tempdir, backend) = make_backend(); | ||
| let (_tempdir, backend) = make_backend().await.unwrap(); | ||
| let id = make_id(); | ||
| let metadata = Metadata { | ||
| content_type: "text/plain".into(), | ||
|
|
@@ -613,7 +660,7 @@ mod tests { | |
|
|
||
| #[tokio::test] | ||
| async fn multipart_multiple_parts() { | ||
| let (_tempdir, backend) = make_backend(); | ||
| let (_tempdir, backend) = make_backend().await.unwrap(); | ||
| let id = make_id(); | ||
| let metadata = Metadata::default(); | ||
|
|
||
|
|
@@ -687,7 +734,7 @@ mod tests { | |
|
|
||
| #[tokio::test] | ||
| async fn multipart_list_parts() { | ||
| let (_tempdir, backend) = make_backend(); | ||
| let (_tempdir, backend) = make_backend().await.unwrap(); | ||
| let id = make_id(); | ||
| let metadata = Metadata::default(); | ||
|
|
||
|
|
@@ -750,7 +797,7 @@ mod tests { | |
|
|
||
| #[tokio::test] | ||
| async fn get_object_range_bounded() { | ||
| let (_tempdir, backend) = make_backend(); | ||
| let (_tempdir, backend) = make_backend().await.unwrap(); | ||
| let id = make_id(); | ||
| let metadata = Metadata::default(); | ||
|
|
||
|
|
@@ -777,7 +824,7 @@ mod tests { | |
|
|
||
| #[tokio::test] | ||
| async fn get_object_range_from() { | ||
| let (_tempdir, backend) = make_backend(); | ||
| let (_tempdir, backend) = make_backend().await.unwrap(); | ||
| let id = make_id(); | ||
| let metadata = Metadata::default(); | ||
|
|
||
|
|
@@ -804,7 +851,7 @@ mod tests { | |
|
|
||
| #[tokio::test] | ||
| async fn get_object_range_last() { | ||
| let (_tempdir, backend) = make_backend(); | ||
| let (_tempdir, backend) = make_backend().await.unwrap(); | ||
| let id = make_id(); | ||
| let metadata = Metadata::default(); | ||
|
|
||
|
|
@@ -831,7 +878,7 @@ mod tests { | |
|
|
||
| #[tokio::test] | ||
| async fn get_object_range_unsatisfiable() { | ||
| let (_tempdir, backend) = make_backend(); | ||
| let (_tempdir, backend) = make_backend().await.unwrap(); | ||
| let id = make_id(); | ||
| let metadata = Metadata::default(); | ||
|
|
||
|
|
@@ -849,7 +896,7 @@ mod tests { | |
|
|
||
| #[tokio::test] | ||
| async fn multipart_abort() { | ||
| let (_tempdir, backend) = make_backend(); | ||
| let (_tempdir, backend) = make_backend().await.unwrap(); | ||
| let id = make_id(); | ||
| let metadata = Metadata::default(); | ||
|
|
||
|
|
@@ -875,7 +922,7 @@ mod tests { | |
|
|
||
| #[tokio::test] | ||
| async fn multipart_invalid_etag() { | ||
| let (_tempdir, backend) = make_backend(); | ||
| let (_tempdir, backend) = make_backend().await.unwrap(); | ||
| let id = make_id(); | ||
| let metadata = Metadata::default(); | ||
|
|
||
|
|
@@ -924,7 +971,7 @@ mod tests { | |
|
|
||
| #[tokio::test] | ||
| async fn multipart_missing_part() { | ||
| let (_tempdir, backend) = make_backend(); | ||
| let (_tempdir, backend) = make_backend().await.unwrap(); | ||
| let id = make_id(); | ||
| let metadata = Metadata::default(); | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,12 @@ | ||
| //! Keeper defines a trait to support extending an object retention for backends | ||
| //! that does not support them natively (e.g., S3-compatible backend and filesystem). | ||
|
|
||
| use std::str::FromStr; | ||
|
|
||
| use objectstore_types::metadata::ExpirationPolicy; | ||
| use serde::{Deserialize, Serialize}; | ||
|
|
||
| use crate::error::Result; | ||
| use crate::error::{Error, Result}; | ||
| use crate::id::ObjectId; | ||
|
|
||
| /// SQLite-backed implementation of the [`Keeper`] trait. | ||
|
|
@@ -18,7 +21,7 @@ pub struct ObjectExpiry<'a> { | |
|
|
||
| /// Object retention keeper trait. | ||
| #[async_trait::async_trait] | ||
| pub trait Keeper: Send + Sync { | ||
| pub trait Keeper: Send + Sync + std::fmt::Debug { | ||
| /// Keep is the first step in the object retention lifecycle. | ||
| /// Practically speaking, it would not be kept if the `expiration_policy` is `Manual`. | ||
| /// The `expiration_policy` is set by the client at upload time via the supplied Metadata. | ||
|
|
@@ -32,3 +35,30 @@ pub trait Keeper: Send + Sync { | |
| /// extend the object retention. | ||
| async fn mark_accessed(&self, id: &ObjectId) -> Result<()>; | ||
| } | ||
|
|
||
| /// Keeper backend of choice. | ||
| #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] | ||
| pub enum KeeperBackend { | ||
| /// SQLite-backed keeper. | ||
| Sqlite, | ||
| } | ||
|
|
||
| impl FromStr for KeeperBackend { | ||
| type Err = Error; | ||
| fn from_str(s: &str) -> Result<Self, Self::Err> { | ||
| match s { | ||
| "sqlite" => Ok(KeeperBackend::Sqlite), | ||
| _ => Err(Error::generic(format!("unknown backend {}", s))), | ||
| } | ||
| } | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Keeper backend config case mismatchMedium Severity
Additional Locations (1)Reviewed by Cursor Bugbot for commit 499ea21. Configure here. |
||
|
|
||
| /// Configuration for the keeper backend. | ||
| #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] | ||
| pub struct KeeperConfig { | ||
| /// Specifies the backend to use. | ||
| pub backend: KeeperBackend, | ||
| /// Connection URL for the backend. | ||
| /// Refer to each backend's documentation for details. | ||
| pub connection_url: String, | ||
| } | ||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Broken default keeper database path
High Severity
The default
connection_urlpoints at/opt/objectstore/keeper.db, a path that does not exist elsewhere in this project. Local runs, Docker (/data), README env setup, and e2e tests all rely on this default whenkeeperis unset, so filesystem backend startup will fail creating the SQLite database.Reviewed by Cursor Bugbot for commit 499ea21. Configure here.