client stats: send multiple snapshots in a single request#495
client stats: send multiple snapshots in a single request#495mattklein123 wants to merge 2 commits into
Conversation
Signed-off-by: Matt Klein <mklein@bitdrift.io>
There was a problem hiding this comment.
Pull request overview
This PR extends the bd-client-stats upload path to batch multiple on-disk aggregated snapshot files into a single StatsUploadRequest, reducing the number of separate upload requests and enabling stable retries via a deterministic batch transport UUID.
Changes:
- Batch multiple pending snapshot files into one
StatsUploadRequest(multi-snapshot upload) and track completion using the batch’s source file IDs. - Introduce a stable batch
upload_uuidderived from an ordered SHA-256 digest of source file IDs (while preserving single-file UUID behavior). - Update test helpers and add new tests covering startup batching, corruption handling, retry UUID stability, and batching behavior after successful uploads.
Reviewed changes
Copilot reviewed 5 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| Cargo.lock | Adds sha2 to the resolved dependency set. |
| bd-client-stats/Cargo.toml | Adds sha2 workspace dependency for batch UUID hashing. |
| bd-client-stats/src/file_manager.rs | Returns PendingUpload { request, source_file_ids } and batches eligible pending files into multi-snapshot requests. |
| bd-client-stats/src/stats.rs | Integrates batched pending uploads, generates stable batch transport UUIDs, and completes uploads using source file IDs. |
| bd-client-stats/src/stats_test.rs | Adds extensive test coverage for multi-snapshot batching scenarios and retry semantics. |
| bd-test-helpers/src/stats.rs | Extends StatsRequestHelper with snapshot-indexed accessors for multi-snapshot assertions. |
Comments suppressed due to low confidence (2)
bd-test-helpers/src/stats.rs:261
aggregation_window_start()no longer asserts that the request contains exactly one snapshot. This makes it easy for tests to accidentally inspect only snapshot[0] when multi-snapshot uploads are present, unlikeget_counter()/get_metric()which still enforce single-snapshot semantics. Consider restoring thesnapshot.len() == 1assertion here (and relying on the*_for_snapshotaccessor for multi-snapshot tests).
pub fn aggregation_window_start(&self) -> OffsetDateTime {
self.aggregation_window_start_for_snapshot(0)
}
bd-test-helpers/src/stats.rs:276
aggregation_window_end()no longer asserts that the request contains exactly one snapshot. For consistency withget_counter()/get_metric()and to avoid silently reading only snapshot[0] in multi-snapshot uploads, consider restoring thesnapshot.len() == 1assertion here and usingaggregation_window_end_for_snapshot()in multi-snapshot tests.
pub fn aggregation_window_end(&self) -> OffsetDateTime {
self.aggregation_window_end_for_snapshot(0)
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| pub fn overflows(&self) -> &HashMap<String, u64> { | ||
| &self.request.snapshot.first().unwrap().metric_id_overflows | ||
| } |
| let mut inner = self.inner.lock().await; | ||
| let initialized_inner = inner.get_initialized().await?; | ||
| let now = self.time_provider.now(); | ||
| let max_aggregation_window_per_file = *self.max_aggregation_window_per_file.read(); | ||
|
|
| for uuid in source_file_ids { | ||
| let found_index = | ||
| InitializedInner::find_index(&initialized_inner.index, |file| file.name == *uuid); | ||
|
|
||
| if let Some(index) = found_index { | ||
| log::debug!( | ||
| "completing pending upload: {}", | ||
| initialized_inner.index[index].name | ||
| ); | ||
| debug_assert!(initialized_inner.index[index].period_end.is_some()); | ||
| initialized_inner.delete_pending_upload(index).await?; | ||
| } else { | ||
| // There is a race condition in which we could theoretically have reached max files, but | ||
| // there is an upload in flight that comes back after we already popped the first entry. | ||
| // We could handle this by having the max file code not pop inflight uploads, but that is | ||
| // more complicated than just ignoring the response here. | ||
| log::debug!("pending upload {uuid} not found in index"); | ||
| if let Some(index) = found_index { | ||
| log::debug!( | ||
| "completing pending upload: {}", | ||
| initialized_inner.index[index].name | ||
| ); | ||
| debug_assert!(initialized_inner.index[index].period_end.is_some()); | ||
| initialized_inner.delete_pending_upload(index).await?; | ||
| } else { |
| // Retries must use the same transport UUID for the same logical batch, but the UUID also needs | ||
| // to change when the batch composition changes. A stable SHA-256 hex digest of the ordered | ||
| // source file IDs gives us both properties across retries and restarts, and `upload_uuid` is | ||
| // only treated as an opaque string in this path. |
There was a problem hiding this comment.
This seems fine given we don't really dedupe or anything, but with this I imagine we could have processed payload with snapshot a,b,c then if we retried with a,b,c,d we'd treat it as a new upload and double write? Maybe not worth dealing with now, but maybe this should have N ids in the future so we could apply them incrementally
No description provided.