Skip to content

feat(change-stream): SQLite-backed garbage collector - #653

Open
aldy505 wants to merge 9 commits into
mainfrom
aldy505/feat/change-stream/sqlite-garbage-collector
Open

aldy505 wants to merge 9 commits into
mainfrom
aldy505/feat/change-stream/sqlite-garbage-collector

Conversation

@aldy505

@aldy505 aldy505 commented Sep 26, 2026 •

Copy link
Copy Markdown
Collaborator

Introduces SqliteGarbageCollector and SqliteGarbageCollectorStream which implements ChangeStream.
This is required for filesystem & S3-compatible API backends.
Note that it's not wired to the backends yet.

Supersedes #582
Refs FS-482

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 5 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 7dbb16c. Configure here.

Comment thread objectstore-service/src/change_stream/garbage_collector_sqlite.rs
Comment thread migrations/sqlite/0001_garbage_collector.sql Outdated

// Decrement counter when done
active_tasks.fetch_sub(1, Ordering::SeqCst);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unordered tasks lose object updates

High Severity

write, update, and delete each tokio::spawn an independent task with no per-object ordering. A later update or delete can commit before the matching write, so expirations are skipped and deleted objects can be reinserted.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 7dbb16c. Configure here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we were actually talking about something related to this today. i don't think we can guarantee today that, if two requests are racing, the storage backend and the change stream will resolve the race in the same way. i signed off on that for COGS because it's not really a big issue there, but it is more meaningful for this GC use case

i don't have a great answer for what to do about that right now. just surfacing it here, i guess

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remember I talked about this with Jan and Lorenzo, AFAIK we didn't came up to a conclusion yet.

Comment thread objectstore-service/src/change_stream/garbage_collector_sqlite.rs Outdated
#[cfg(feature = "storage-cogs")]
mod cost_tracker;
mod factory;
mod garbage_collector_sqlite;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Collector is never constructed

Medium Severity

SqliteGarbageCollectorStream lives in a private module and is never re-exported or built by ChangeStreamFactory. No backend can select this collector, so the new feature is unreachable.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 7dbb16c. Configure here.

Comment thread objectstore-service/src/change_stream/garbage_collector_sqlite.rs Outdated
Comment thread objectstore-service/src/change_stream/garbage_collector_sqlite.rs Outdated
Comment thread objectstore-service/src/change_stream/garbage_collector_sqlite.rs Outdated
It's so hard to develop this on Windows. I need to grab my Linux laptop.
Comment thread objectstore-service/src/change_stream/garbage_collector_sqlite.rs Outdated
@codecov

codecov Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.44720% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.36%. Comparing base (bf02738) to head (282c346).
⚠️ Report is 8 commits behind head on main.

Files with missing lines Patch % Lines
...vice/src/change_stream/garbage_collector_sqlite.rs 98.44% 5 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #653      +/-   ##
==========================================
+ Coverage   91.27%   91.36%   +0.08%     
==========================================
  Files         116      117       +1     
  Lines       23496    24028     +532     
==========================================
+ Hits        21445    21952     +507     
- Misses       2051     2076      +25     
Components Coverage Δ
Rust Backend 94.87% <98.44%> (+0.05%) ⬆️
Rust Client 81.70% <ø> (-0.26%) ⬇️
Python Client 93.75% <ø> (ø)

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread objectstore-service/src/change_stream/garbage_collector_sqlite.rs Outdated
@linear-code

linear-code Bot commented Sep 29, 2026

Copy link
Copy Markdown

FS-482

@matt-codecov matt-codecov left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread objectstore-service/src/change_stream/garbage_collector_sqlite.rs
Comment thread objectstore-service/src/change_stream/garbage_collector_sqlite.rs Outdated

// Decrement counter when done
active_tasks.fetch_sub(1, Ordering::SeqCst);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we were actually talking about something related to this today. i don't think we can guarantee today that, if two requests are racing, the storage backend and the change stream will resolve the race in the same way. i signed off on that for COGS because it's not really a big issue there, but it is more meaningful for this GC use case

i don't have a great answer for what to do about that right now. just surfacing it here, i guess

Comment thread objectstore-service/src/change_stream/garbage_collector_sqlite.rs Outdated
@aldy505

aldy505 commented Sep 30, 2026

Copy link
Copy Markdown
Collaborator Author
  • 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

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.

@lcian lcian left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread objectstore-service/src/change_stream/garbage_collector_sqlite.rs Outdated
Comment thread objectstore-service/src/change_stream/garbage_collector_sqlite.rs
Comment thread objectstore-service/src/change_stream/garbage_collector_sqlite.rs Outdated
Comment thread objectstore-service/src/change_stream/garbage_collector_sqlite.rs Outdated
Comment thread objectstore-service/src/change_stream/garbage_collector_sqlite.rs Outdated
Comment thread objectstore-service/src/change_stream/garbage_collector_sqlite.rs Outdated
Comment thread objectstore-service/src/change_stream/garbage_collector_sqlite.rs
Comment thread migrations/sqlite/0001_garbage_collector.sql
Comment thread objectstore-service/src/change_stream/garbage_collector_sqlite.rs Outdated
Comment thread objectstore-service/src/change_stream/garbage_collector_sqlite.rs Outdated
Comment thread objectstore-service/src/change_stream/garbage_collector_sqlite.rs Outdated
@aldy505
aldy505 requested review from lcian and matt-codecov October 1, 2026 01:46

@matt-codecov matt-codecov left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#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 backend
  • begin_update(): lengthen the expiration in GC before committing in the storage backend
  • commit_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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants