test(bigquery): add query benchmark - #6794
alvarowolfx wants to merge 11 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a new bigquery-benchmark-queries crate, which provides a comprehensive benchmark and endurance test suite for the Rust BigQuery client library. It supports multiple query scenarios, OpenTelemetry tracing and metrics export, and under-the-hood job retry detection. Feedback on the changes highlights two key issues: first, holding a non-Send tracing span guard across .await points in runner.rs which can cause future safety issues; second, performing blocking synchronous file I/O operations within an async context in reporter.rs, violating the repository's async guidelines.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #6794 +/- ##
==========================================
- Coverage 96.95% 96.94% -0.01%
==========================================
Files 316 316
Lines 105153 105153
==========================================
- Hits 101949 101943 -6
- Misses 3204 3210 +6 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces a comprehensive BigQuery SDK Benchmark & Endurance Test Suite (bigquery-benchmark-queries) to measure latency, throughput, and connection stability of the Rust BigQuery client library. The suite supports preset and custom SQL scenarios, OpenTelemetry trace and metric exports, and real-time CSV/JSON reporting. Feedback on the implementation highlights a critical violation of the async style guide in reporter.rs due to blocking synchronous file I/O inside async functions, which should be replaced with asynchronous operations. Additionally, reviewers pointed out a potential CPU starvation issue from periodic vector sorting in build_report during long-running tests, and a validation gap in args.rs where --iterations 0 is not properly rejected when a duration is specified.
| tokio::task::block_in_place(|| { | ||
| let _ = csv_writer.flush(); | ||
| let _ = errors_writer.flush(); | ||
| if let Ok(json_file) = File::create(&json_path_clone) { | ||
| let _ = serde_json::to_writer_pretty(json_file, ¤t_report); | ||
| } | ||
| }); |
There was a problem hiding this comment.
Using tokio::task::block_in_place to perform synchronous file writes and flushes can cause a panic if the benchmark is run within a single-threaded (current-thread) Tokio runtime. Additionally, performing synchronous blocking I/O operations (like std::fs::File::create and flush) inside an async loop blocks the executor thread, which can introduce artificial latency spikes and skew the benchmark results.
Consider using tokio::fs and tokio::io::AsyncWriteExt to perform all file operations asynchronously, eliminating the need for block_in_place entirely.
| tokio::task::block_in_place(|| { | |
| let _ = csv_writer.flush(); | |
| let _ = errors_writer.flush(); | |
| if let Ok(json_file) = File::create(&json_path_clone) { | |
| let _ = serde_json::to_writer_pretty(json_file, ¤t_report); | |
| } | |
| }); | |
| let current_report = build_report(&stats); | |
| let json_path_clone = json_path.clone(); | |
| let _ = tokio::io::AsyncWriteExt::flush(csv_writer).await; | |
| let _ = tokio::io::AsyncWriteExt::flush(errors_writer).await; | |
| if let Ok(json_str) = serde_json::to_string_pretty(¤t_report) { | |
| let _ = tokio::fs::write(&json_path_clone, json_str).await; | |
| } |
References
- No blocking I/O (e.g., std::fs, std::net) in async functions. (link)
| if last_json_write.elapsed() >= Duration::from_secs(5) { | ||
| last_json_write = Instant::now(); |
There was a problem hiding this comment.
In long-running endurance tests, the number of successful query samples can grow into the hundreds of thousands or millions. Calling build_report every 5 seconds clones and sorts four potentially massive vectors (success_total_durations, success_send_durations, etc.) to compute percentile latencies. This
To prevent this, consider only computing the percentile latencies (and writing the full JSON summary) at the end of the benchmark run, or excluding the percentile calculations from the periodic 5-second updates.
| if let Some(iterations) = self.iterations | ||
| && iterations == 0 | ||
| && self.duration.is_none() | ||
| { | ||
| anyhow::bail!("--iterations must be greater than 0"); | ||
| } |
There was a problem hiding this comment.
If --iterations 0 is passed along with a --duration (e.g., --iterations 0 --duration 1h), the validation succeeds, but the benchmark runner loop will exit immediately on the first iteration because iteration >= max_iter (0 >= 0) evaluates to true.
To prevent this, --iterations should always be validated to be greater than 0 if it is specified, regardless of whether --duration is set.
if let Some(iterations) = self.iterations
&& iterations == 0
{
anyhow::bail!("--iterations must be greater than 0");
}References
- Prefer validating input ranges (such as preventing zero values that could cause division by zero) at the configuration or input validation boundary rather than using inline filtering at the point of calculation.
Benchmark for the query aspect of the bigquery crate to check resilience to retryable errors. Has different scenario with synthetic and hitting real tables, publishes metrics via Otel, writes errors to a dedicated error file (so we can better act on them) and also writes sample data to a .csv file to be queried later.
Disclaimer: this is a tool. We are not going to run it in the CI. It is not production code. We really just want to make sure it measures what we think it is measuring.