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 99.62687% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 88.46%. Comparing base (560244f) to head (28100c2).

Files with missing lines Patch % Lines
objectstore-service/src/backend/gcs.rs 99.07% 1 Missing ⚠️
Additional details and impacted files
@@                          Coverage Diff                          @@
##           matth/storage-inventory-tracker-2     #595      +/-   ##
=====================================================================
+ Coverage                              88.24%   88.46%   +0.21%     
=====================================================================
  Files                                    104      104              
  Lines                                  16803    17055     +252     
=====================================================================
+ Hits                                   14828    15087     +259     
+ Misses                                  1975     1968       -7     
Components Coverage Δ
Rust Backend 92.46% <99.62%> (+0.17%) ⬆️
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

@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 959eb00. Configure here.

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
Ok(())
})
.await
.await?;

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: delete_object for the GCS backend reports a delete event to the change stream even when the object does not exist (receives a 404), creating spurious records.
Severity: MEDIUM

Suggested Fix

Modify the delete_object function in the GCS backend to only call self.change_stream.delete(id) if the delete operation was successful (i.e., did not result in a 404 status). This could be achieved by having the with_retry closure return a boolean indicating whether a delete occurred, and then conditionally calling the change stream method.

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/gcs.rs#L900

Potential issue: In the GCS backend's `delete_object` implementation, if a delete
operation is attempted on a non-existent object, the GCS API returns a `404 NOT FOUND`
status. The code correctly handles this status to avoid an error but then proceeds to
unconditionally call `self.change_stream.delete(id)`. This results in a delete event
being logged to the change stream for an object that was never deleted, leading to
inaccurate inventory tracking and potentially incorrect cost attribution.

Did we get this right? 👍 / 👎 to inform future reviews.

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.

this is harmless outside of the incredibly unlikely case of a racing delete/write being sequenced differently in Objectstore and Kafka. but i will fix

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.

Easy to fix by returning a boolean from the retry closure.

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.

Ok(())
})
.await
.await?;

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.

Easy to fix by returning a boolean from the retry closure.

// 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

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