Repository navigation
Conversation
It's so hard to develop this on Windows. I need to grab my Linux laptop.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #653 +/- ##
==========================================
+ Coverage 91.52% 91.93% +0.41%
==========================================
Files 116 120 +4
Lines 22175 23274 +1099
==========================================
+ Hits 20295 21397 +1102
+ Misses 1880 1877 -3
☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
matt-codecov
left a comment
There was a problem hiding this comment.
a couple questions:
- do we have performance goals for self-hosted deployments? i have been burned by poor single-insert sqlite performance before. if we need high throughput, we may want to introduce batched queries, but it introduces some risk of inventory corruption if we crash without flushing batches
- do we have scaling requirements for self-hosted deployments? i've seen a few different "distributed sqlite" projects (https://rqlite.io/ / https://docs.fly.io/litefs/how-it-works/) but generally consider sqlite to be node-local
If they're using k8s (the unofficial helm chart), they want to use the postgres implementation of this garbage collector. In which I will implement after SQLite is correctly implemented. This is how Taskbroker is doing as well, they provide both options, with SQLite as default.
I've contributed to rqlite before, but no thanks, let's not consider that approach. |
There was a problem hiding this comment.
The biggest point to resolve will be #653 (comment) but in the meantime we can get the rest of the things in order before we find a solution for that.
matt-codecov
left a comment
There was a problem hiding this comment.
#677 changed the ChangeStream trait to help write GC correctly. unfortunately you will need to rebase and tweak your PR to implement:
begin_write(): create the GC record before committing in the storage backendbegin_update(): lengthen the expiration in GC before committing in the storage backendcommit_delete(): delete the GC record after deleting the stored object
if begin_write() or begin_update() fail, it's not safe to allow the backend commit to happen. otherwise we can permanently leak objects or cause GC to delete them prematurely. so those functions have to .await rather than spawn and they have to surface failure to the caller
if commit_delete() fails it's kind of harmless. GC will just have a ghost record floating around until its expiry. if the ghost record has manual expiration then it'll float around forever, but that's... probably a small edge case.
…tream/sqlite-garbage-collector
There was a problem hiding this comment.
sqlx is a hard dependency of objectstore-service, and it's pulled in with features this code doesn't need. tls-rustls-ring-native-roots and chrono aren't needed for a local SQLite file, and nothing in garbage_collector_sqlite.rs uses chrono. Trimming the features to runtime-tokio + sqlite avoids compiling the TLS stack for this. It's also worth putting it behind a feature (like storage-cogs), so builds that only use GCS or Bigtable don't compile bundled SQLite.
Posted by mistake; feedback was meant to be shared privately.
Changelog PreviewThis PR lands in these workspaces:
📋 Changelog PreviewThis is how your changes will appear in the changelog. New Features
🤖 This preview updates automatically when you update the PR. |
…tream/sqlite-garbage-collector
| let mut ids = Vec::new(); | ||
| for row in rows { | ||
| let id = row.clone(); | ||
| ids.push(ObjectId::from_storage_path(&id).unwrap()); |
There was a problem hiding this comment.
This should never fail but if it does, let's please log the error at appropriate level (you can use the macros in objectstore_log to do that, please check how it's done elsewhere) and continue instead of panicking.
| let pool = self.pool.clone(); | ||
| let id = id.clone(); | ||
|
|
||
| let query_result = sqlx::query("INSERT INTO garbage_collector (object_id, expires_at) VALUES (?, ?) ON CONFLICT (object_id) DO UPDATE SET expires_at = excluded.expires_at WHERE excluded.expires_at IS NULL OR garbage_collector.expires_at IS NULL OR garbage_collector.expires_at < excluded.expires_at") |
There was a problem hiding this comment.
Due to the fact that backend writes are not synchronized with the ChangeStream, and that we decided that our invariant is gonna be to store the maximum here, unfortunately we're gonna have to preserve a NULL expires_at (infinite duration) over a finite expires_at. It seems that this query is doing the opposite.
Concretely, we need to use something like:
INSERT INTO garbage_collector (object_id, expires_at)
VALUES (?, ?)
ON CONFLICT (object_id) DO UPDATE SET expires_at = excluded.expires_at
WHERE excluded.expires_at IS NULL
OR garbage_collector.expires_at < excluded.expires_at
This means that objects written with infinite expiry are stored forever, and that's the tradeoff we accept by storing the maximum.
Fear not, because this won't be a problem anymore after we refactor the service to mandate TieredStorage, after that we can synchronize the ChangeStream too, so conflicts should never happen.
Moreover, no usecase currently uses infinite expiry.
There was a problem hiding this comment.
Is it possible to "update" the object to have null value on the expired_at field?
| let pool = self.pool.clone(); | ||
| let id = id.clone(); | ||
|
|
||
| let query_result = sqlx::query("INSERT INTO garbage_collector (object_id, expires_at) VALUES (?, ?) ON CONFLICT (object_id) DO UPDATE SET expires_at = excluded.expires_at WHERE excluded.expires_at IS NULL OR garbage_collector.expires_at IS NULL OR garbage_collector.expires_at < excluded.expires_at") |
There was a problem hiding this comment.
[nit] You could use a raw string r#"INSERT INTO ..."# here to force this to format more nicely, as shown in https://github.com/getsentry/objectstore/pull/653/changes/f95fc6a39d1e0558c24ce9b2131f3b068046bba6..1fe4d5983b3def23fda5850026141baf24ff4ac5#r4457955970 for example.
There was a problem hiding this comment.
Or we could use sqlx::query! but that introduces some requirements at build-time and I think that's overkill for our 3 small queries.
| serde_yaml = "0.9.34-deprecated" | ||
| sqlx = "0.9.0" | ||
| sketches-ddsketch = "0.3.1" | ||
| sketches-ddsketch = "0.4.2" |
There was a problem hiding this comment.
There are many changes to the Cargo.toml/.lock which should go away when you rebase the stack.
Then [nit] look also at which sqlx features we're bringing in, we might only need a subset of the default.
There was a problem hiding this comment.
I believe.. Jan, told me to only select the features on the package level instead of workspace level.
There was a problem hiding this comment.
[nit] You could add an objectstore-service/build.rs to make sure that a rebuild applies changed or new migrations, see https://docs.rs/sqlx/0.9.0/sqlx/macro.migrate.html#triggering-recompilation-on-migration-changes.
| @@ -0,0 +1,5 @@ | |||
| CREATE TABLE IF NOT EXISTS garbage_collector ( | |||
| object_id TEXT NOT NULL PRIMARY KEY, | |||
| expires_at INTEGER, | |||
There was a problem hiding this comment.
It would likely be useful to add an index on expires_at given that we're going to iterate rows using ORDER BY expires_at ....
| .await; | ||
|
|
||
| if query_result.is_err() { | ||
| let error = query_result.err(); | ||
| match error { | ||
| Some(err) => return Err(ChangeStreamError::Failure(Box::new(err))), | ||
| None => { | ||
| return Err(ChangeStreamError::Failure( | ||
| "failed to insert row into garbage_collector".into(), | ||
| )); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| Ok(()) |
There was a problem hiding this comment.
I think this can be rewritten as follows:
| .await; | |
| if query_result.is_err() { | |
| let error = query_result.err(); | |
| match error { | |
| Some(err) => return Err(ChangeStreamError::Failure(Box::new(err))), | |
| None => { | |
| return Err(ChangeStreamError::Failure( | |
| "failed to insert row into garbage_collector".into(), | |
| )); | |
| } | |
| } | |
| } | |
| Ok(()) | |
| .await | |
| .map_err(|e| ChangeStreamError::Failure(Box::new(e)))?; | |
| Ok(()) |
Same for the analogous snippet in list_expired.
| let pool = self.pool.clone(); | ||
| let id = id.clone(); |
There was a problem hiding this comment.
[nit] In some places, such as here, it seems that using clone could be avoided, for instance here you only need a borrow of the pool which you can get through self.pool, similarly id.as_storage_path() only needs &self which we already have as argument.
Based on Lorenzo's comments
| DO UPDATE | ||
| SET expires_at = excluded.expires_at | ||
| WHERE garbage_collector.expires_at IS NOT NULL | ||
| AND garbage_collector.expires_at < excluded.expires_at"#, |
There was a problem hiding this comment.
NULL expiry cannot replace a deadline
High Severity
The conflict WHERE clause requires garbage_collector.expires_at < excluded.expires_at. A comparison against SQL NULL is unknown, so an upsert with None (infinite duration) never replaces a stored finite deadline. That breaks the store-the-maximum invariant and the GcStorage contract that None marks an object as never expiring, so a later rewrite without a TTL can still be garbage-collected.
Reviewed by Cursor Bugbot for commit 7e221a8. Configure here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 5a7f734. Configure here.
| @@ -0,0 +1,3 @@ | |||
| fn main() { | |||
| println!("cargo:rerun-if-changed=migrations"); | |||
| } | |||
There was a problem hiding this comment.
Build script watches wrong migrations path
Medium Severity
cargo:rerun-if-changed watches migrations under objectstore-service, but sqlx::migrate! embeds files from ../migrations/sqlite. Added or changed SQL there will not dirty this crate, so the compiled migrator can stay stale until some other source file forces a rebuild.
Reviewed by Cursor Bugbot for commit 5a7f734. Configure here.


Introduces
GcStoragetrait, that will be implemented by various backend. Also implemented SQLite here.Supersedes #582
Refs FS-482