Skip to content

feat(cogs): report backend changes to a change stream - #595

Open
matt-codecov wants to merge 1 commit into
matth/storage-inventory-tracker-2from
matth/storage-inventory-tracker-3
Open

feat(cogs): report backend changes to a change stream#595
matt-codecov wants to merge 1 commit into
matth/storage-inventory-tracker-2from
matth/storage-inventory-tracker-3

Conversation

@matt-codecov

@matt-codecov matt-codecov commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

rig up GCS and Bigtable backends to emit change stream records. nothing is actually plugging in a change stream yet, that'll be future PRs

didn't do other backends yet because i wanted feedback on the stack so far before writing that much more code haha

@matt-codecov
matt-codecov requested a review from a team as a code owner August 7, 2026 01:45
@codecov

codecov Bot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.64865% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.39%. Comparing base (a2576e8) to head (836790e).

Files with missing lines Patch % Lines
objectstore-service/src/backend/gcs.rs 97.20% 5 Missing ⚠️
Additional details and impacted files
@@                          Coverage Diff                          @@
##           matth/storage-inventory-tracker-2     #595      +/-   ##
=====================================================================
+ Coverage                              87.97%   88.39%   +0.41%     
=====================================================================
  Files                                    104      104              
  Lines                                  16776    17118     +342     
=====================================================================
+ Hits                                   14759    15131     +372     
+ Misses                                  2017     1987      -30     
Components Coverage Δ
Rust Backend 92.45% <98.64%> (+0.39%) ⬆️
Rust Client 81.97% <ø> (ø)
Python Client 93.31% <ø> (ø)

☔ 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.

@matt-codecov
matt-codecov force-pushed the matth/storage-inventory-tracker-3 branch from ab70a01 to 959eb00 Compare August 7, 2026 01:50
Comment thread objectstore-service/src/backend/gcs.rs
@matt-codecov
matt-codecov force-pushed the matth/storage-inventory-tracker-3 branch from 959eb00 to 6d8e991 Compare August 11, 2026 06:15
Comment thread objectstore-service/src/backend/gcs.rs Outdated
Comment thread objectstore-service/src/backend/bigtable.rs
@matt-codecov
matt-codecov force-pushed the matth/storage-inventory-tracker-3 branch from 6d8e991 to 28100c2 Compare August 11, 2026 21:34

@jan-auer jan-auer 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.

Preliminary review

///
/// This function does not distinguish between object rows and tombstone rows. It does not
/// include Bigtable's own overhead.
fn row_bytes(path: &[u8], mutations: &[v2::Mutation]) -> u64 {

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.

nit: Let's use length or size since bytes is commonly used to refer to the actual data.

Comment on lines +1006 to +1009
// Inline `put_row()` because we need the mutations to compute their size.
let mutations = object_mutations(metadata.clone(), payload.into_bytes().into())?;
self.mutate(path.clone(), mutations.clone(), "put").await?;
self.report_write(id, &path, &mutations, metadata.time_expires);

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.

You can make object_mutations instead return the size alongside the mutations, return that from put_row, and then use the returned value to report the write. This yields all data in the correct sequence, removes the inlining, and mutations no longer have to be cloned.

Comment thread objectstore-service/src/backend/gcs.rs Outdated
Comment thread objectstore-service/src/backend/gcs.rs Outdated
// The payload arrives as a stream with no declared length, so the stored size is
// only known once the upload has drained. Safe from double-counting because this
// request is not retried.
let (stored_size, stream) = counting_stream(stream);

@jan-auer jan-auer Aug 13, 2026

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.

Please double-check, but GCS should be returning a X-Goog-Stored-Content-Length from the upload request which we can use. Even better, this is authoritative.

Just know that this doesn't include metadata, and GCS charges for metadata as bytes, too.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

TODO also count metadata

@matt-codecov
matt-codecov force-pushed the matth/storage-inventory-tracker-3 branch from 28100c2 to 836790e Compare August 15, 2026 03:32

@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 1 potential issue.

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 836790e. Configure here.


if deleted {
self.change_stream.delete(id);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missed delete after retry 404

Medium Severity

delete_object only emits a change-stream delete when the retry closure returns true. If GCS applied the delete but the client saw a timeout or other retryable failure, the retry gets 404, returns false, and the delete is never reported. Inventory then keeps a ghost object and overstates storage COGS until expiry (permanently for manual retention).

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 836790e. Configure here.

Comment on lines 1033 to +1034
self.mutate(path, [delete_row_mutation()], "delete").await?;
self.change_stream.delete(id);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: The Bigtable delete_object function unconditionally reports a delete event to the change stream, even if the object does not exist.
Severity: MEDIUM

Suggested Fix

The delete_object function should only report a delete event if the object actually existed and was deleted. Since Bigtable's mutate with DeleteFromRow doesn't indicate if a row was affected, the implementation should first check for the object's existence before attempting the deletion. The GCS backend implementation can be used as a reference, as it checks for a successful deletion before calling self.change_stream.delete(id).

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: objectstore-service/src/backend/bigtable.rs#L1033-L1034

Potential issue: The `delete_object` function for the Bigtable backend unconditionally
reports a delete event to the change stream, even when the object being deleted does not
exist. This is because the underlying Bigtable `MutateRow` operation with
`DeleteFromRow` is idempotent and succeeds on non-existent rows, causing the subsequent
`self.change_stream.delete(id)` call to always execute. This behavior is inconsistent
with other backends like GCS, which verify that an object was actually deleted before
reporting it. This can lead to spurious delete records in the change stream inventory,
resulting in incorrect cost accounting.

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.

2 participants