Skip to content

feat: add metrics for gcs payload cleanup on failed db transactions - #2528

Open
taddes wants to merge 9 commits into
masterfrom
feat/gcs-cleanup-metrics-STOR-653
Open

feat: add metrics for gcs payload cleanup on failed db transactions#2528
taddes wants to merge 9 commits into
masterfrom
feat/gcs-cleanup-metrics-STOR-653

Conversation

@taddes

@taddes taddes commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Description

STOR-623 added a best-effort GCS delete for payloads that were offloaded before a
write's database transaction failed, but it was fire and forget, so there's no way
to tell from prod whether those deletes actually land. This adds a counter around
them.

The metric is storage.gcs.payload.cleanup, tagged with:

  • handler: put_bso or post_collection
  • result: success or failure
  • reason on failures: gcs_error if GCS rejected the delete, invalid_url if the
    gs:// URL didn't parse (a code bug rather than an infra problem)

Both are emitted inside delete_payload, so any future call site gets them for free.

Tests use cadence's SpyMetricSink over the existing StorageControl stubs, one per
branch. Commits are split so each addition is reviewable on its own.

Note on local verification: cargo fmt --check is clean, clippy is clean for
syncserver, and the web::payload_offload unit tests pass. The full
make clippy_mysql can't run on arm64 here because grpcio-sys fails to build, and
it also trips two pre-existing needless_as_bytes errors in tokenserver-auth that
are already on master, so CI is the real check for those.

Review round

Rebased onto master, so the merge commit is gone and the history is linear.

  • Dropped the skipped result entirely. Both GCS clients are built together at
    startup when a payload bucket is configured, so there is no state where the control
    client is missing but the upload succeeded. The counter could only ever read zero.
    Back to if let Ok(client) with a comment recording why it holds.
  • Swapped the hand-rolled recording sink for cadence's SpyMetricSink, which needed
    crossbeam-channel as a dev-dependency to name the receiver. It was already in the
    lock via cadence.
  • Dropped the .config/nextest.toml retry commit as out of scope. Worth noting for
    whenever it gets picked up separately: nextest profiles inherit from
    [profile.default], so test-threads = 1 does apply to profile.ci, which means
    parallel tests contending on shared rows can't be the cause of that abort.

I did not take the gcs_control_client()? variant. At the cleanup site the ? would
return the GCS client error in place of the database error that actually caused the
rollback, so the real cause would be lost from both the response and Sentry.

Testing

No behaviour change beyond the metric. cargo test -p syncserver --lib web::payload_offload
covers success, GCS failure, and unparseable URL.

Issue(s)

Closes STOR-653.

@taddes taddes self-assigned this Aug 17, 2026
@taddes
taddes requested review from chenba and pjenvey August 19, 2026 14:33
Comment thread syncserver/src/web/payload_offload.rs Outdated
warn!("gcs payload cleanup failed for {gs_url}: {e}");
metrics.incr_with_tags(CLEANUP_METRIC, cleanup_failure_tags(handler, "gcs_error"));
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

You don't need to break up the chain for the side effect. You can do

      .inspect(|_| metrics.incr_with_tags(CLEANUP_METRIC, cleanup_tags(handler, "success")))
      .inspect_err(|e| {
          warn!("gcs payload cleanup failed for {gs_url}: {e}");
          metrics.incr_with_tags(CLEANUP_METRIC, cleanup_failure_tags(handler, "gcs_error"));
      })

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.

ok cool, good suggestion

Comment thread syncserver/src/web/payload_offload.rs Outdated
metrics.incr_with_tags(CLEANUP_METRIC, cleanup_failure_tags(handler, "invalid_url"));
return Err(e);
}
};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Since you are returning the Result as is (parsed -> parsed, Err(e) -> Err(e)) you can use inspect_err for the side effect.

Comment thread syncserver/src/web/payload_offload.rs Outdated
client: &StorageControl,
gs_url: &str,
metrics: &Metrics,
handler: &str,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Maybe make an enum instead of allowing any string.

@taddes
taddes force-pushed the feat/gcs-cleanup-metrics-STOR-653 branch from dafe56c to f21a62f Compare August 19, 2026 20:41
Comment thread .config/nextest.toml Outdated
# The Spanner emulator aborts a test transaction with a Conflict (503) when it
# hits lock contention on the shared test rows. That is transient, so retry in
# CI rather than failing the whole job; local runs keep the strict retries = 0.
retries = 2

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is a bit out of scope. But also, line 6 above is test-threads = 1 so I'm not sure the lock contention is even possible?

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.

It was a bug I was running into locally and that was the suggested fix 🤷 I can look a bit more closely at that part

Comment thread syncserver/src/web/handlers.rs Outdated
Comment on lines +884 to +889
Err(_) => {
metrics.incr_with_tags(
CLEANUP_METRIC,
cleanup_tags(CleanupHandler::PutBso, CleanupResult::Skipped),
);
}

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.

I'd argue we don't need these Err blocks and their metrics at all because we should never have anything offloaded in the first place if there's no gcs_control_client (there should never be a lack of gcs_control_client if there is a gcs_client).

Or at least we could do this more simpler by just propagating the error (gcs_control_client()?) -- similarly to the above state.gcs_client()? -- if we're set to offload and there's no gcs clients, just fail with an error.

Comment thread syncserver/src/web/payload_offload.rs Outdated

/// Sink that keeps every statsd line so tests can assert on emissions.
#[derive(Debug)]
struct RecordingSink(Arc<Mutex<Vec<String>>>);

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.

@taddes
taddes force-pushed the feat/gcs-cleanup-metrics-STOR-653 branch from 3a54ffd to b48e323 Compare August 24, 2026 21:04
@taddes
taddes requested review from chenba and pjenvey August 25, 2026 19:49
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