Skip to content

fix(bigtable): skip metrics export if previous write was too recent - #18630

Open
abhishekacharya200 wants to merge 3 commits into
googleapis:mainfrom
abhishekacharya200:fix-bigtable-metrics-shutdown-export
Open

abhishekacharya200 wants to merge 3 commits into
googleapis:mainfrom
abhishekacharya200:fix-bigtable-metrics-shutdown-export

Conversation

@abhishekacharya200

Copy link
Copy Markdown
Contributor

Fixes #18576

When a process exits, MeterProvider.shutdown() runs one last export through BigtableMetricsExporter. If a periodic export happened less than ~30 seconds before that, Cloud Monitoring rejects the write ("points were written more frequently than the maximum sampling period configured for the metric"), and the exporter logs it as a warning with a full traceback. With the 60 second export interval this hits about half of all normal exits, and on Cloud Run it shows up as recurring error logs.

This follows the same approach as the Go fix for Spanner built-in metrics (googleapis/google-cloud-go#12266): the exporter remembers when it last wrote successfully and skips any export that comes less than 30 seconds (half the 60s sample period) after that write. Skipped exports return SUCCESS and only log at debug level. The metrics are cumulative, so a skipped periodic export loses nothing. The next write still carries the totals. Failed writes don't update the timestamp, so a failed export is never followed by a skip.

Tests:

  • TestGoogleCloudMetricsHandler::test_close_soon_after_export runs a real MeterProvider and PeriodicExportingMetricReader with a mocked MetricServiceClient: one flush, then close(). On main this sends a second write (assert 2 == 1). With this change it doesn't.
  • TestBigtableMetricsExporter::test_export_skipped_if_previous_export_too_recent checks the 30 second boundary with a patched time.monotonic (fails on main too).
  • TestBigtableMetricsExporter::test_export_not_skipped_after_failed_export checks that a failed write doesn't count as a recent export.

tests/unit passes locally on Python 3.14 (5497 passed) except two timing assertions in tests/unit/data/_cross_sync/test_cross_sync.py (test_wait_async_timeout and one test_event_wait_timeout_exceeded case). They fail the same way on unmodified main on my Windows machine and don't touch this code. ruff format, ruff check --select I and flake8 report nothing on the changed files.

Cloud Monitoring rejects points written more often than the metric's 60s sample period. The final export on shutdown often came within 30s of a periodic export, so it failed and logged a warning with a traceback on normal process exit.

Fixes googleapis#18576

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a minimum export interval of 30 seconds for the Bigtable client metrics exporter to prevent Cloud Monitoring from rejecting points written too frequently. It tracks the last successful export time and skips subsequent exports if they occur too soon. The review feedback suggests moving the update of _last_export_time outside of the project loop to prevent incorrect state tracking if a batch write fails for one of multiple projects, and ensuring that failures return MetricExportResult.FAILURE.

@parthea parthea self-assigned this Oct 17, 2026
@parthea parthea added kokoro:run Add this label to force Kokoro to re-run the tests. kokoro:force-run Add this label to force Kokoro to re-run the tests. labels Oct 17, 2026
@yoshi-kokoro yoshi-kokoro removed kokoro:run Add this label to force Kokoro to re-run the tests. kokoro:force-run Add this label to force Kokoro to re-run the tests. labels Oct 17, 2026

@parthea parthea left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM, I added 2 minor observations

…tten

A failed write for a later project left the time set by an earlier one, so an
immediate retry was skipped. Also close the handler in a finally block in
test_close_soon_after_export.
@parthea parthea added kokoro:force-run Add this label to force Kokoro to re-run the tests. kokoro:run Add this label to force Kokoro to re-run the tests. labels Oct 17, 2026
@yoshi-kokoro yoshi-kokoro removed kokoro:run Add this label to force Kokoro to re-run the tests. kokoro:force-run Add this label to force Kokoro to re-run the tests. labels Oct 17, 2026
parthea
parthea previously approved these changes Oct 17, 2026

@parthea parthea left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@parthea parthea removed their assignment Oct 20, 2026
@mutianf

mutianf commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Would the correct fix to add a shutdown hook? We had similar issue in java https://github.com/googleapis/google-cloud-java/blob/main/java-bigtable/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/exporter/BigtableCloudMonitoringExporter.java

And how we bypassed it is, mark the process as shutting down, and if export throw an error, catch it with a warning. This way partial success / per project metrics could still go through.

# Cloud Monitoring rejects points written more often than the metric's sample
# period (60s for Bigtable client metrics). Exports that come sooner than half of
# that after the previous write (e.g. the final export on shutdown) are skipped
_MIN_EXPORT_INTERVAL_SECONDS = 30

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

30 seems like a magic number, why 30? should this be 60?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Using 30 (half the 60s interval) gives plenty of headroom so normal 60s periodic ticks are never skipped due to latency/jitter

See analysis from Gemini below

Why we can't use 60 on the client: _last_export_time is recorded after the create_service_time_series RPC finishes. If periodic export 1 takes 0.4s over the network (finishing at t = 60.4s) and periodic export 2 fires on schedule at t = 120.0s, the elapsed time between them is 59.6s. If our threshold were 60, 59.6 < 60 would be true and every second periodic export would be skipped.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Following up on #18630 (comment) and #18630 (comment):

When a process exits shortly after a 60s periodic export, teardown triggers a final flush that Cloud Monitoring rejects with 400 (The same issue seen in #18576).

We have 2 potential solutions here:

  1. shutdown hook (marking the exporter as shutting down, still sending the final RPC, and catching the error with a warning)

or

  1. the 30s timer check in export() (skipping the RPC if the last export was < 30s ago)

  1. Shutdown hook Lets partial writes and other projects go through on exit, since create_service_time_series still writes any new time series in the batch even when returning 400 for points exported too recently. However, export() already catches errors and logs a warning today, so we'd still make a rejected 400 RPC and log a warning on exit—or if we suppress warnings on shutdown, we'd hide real errors for short-lived scripts (< 60s) where teardown is the only export. Also, since MeterProvider's built-in atexit hook calls exporter.export() before exporter.shutdown(), we'd have to pass shutdown_on_exit=False to MeterProvider and register our own atexit hook so the shutdown flag is set before MeterProvider.shutdown() runs.

  2. 30s timer check in export() Avoids the 400 RPC and warning log noise on normal exit, and works without the extra shutdown hook plumbing that is needed for 1.. It skips the final < 30s of points on exit for a project that just exported, though if we change _last_export_time to a per-project dict (dict[str, float]), other projects will still go through.

I'm leaning toward keeping the 30s timer check (updated to track _last_export_time per project) to avoid the warning noise, failed RPC on exit, and atexit plumbing, unless we think preserving the final < 30s of partial time series is worth those trade-offs. Thoughts?

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.

Thanks for the detailed write up, both of you. I went with the 30 second check and made it per project in 3209ffa: each project keeps its own last write time, so a project that just exported skips the final flush on exit while other projects still go through, and after a partial failure only the failed project is written again. Happy to switch to the shutdown hook instead if you'd rather keep the final partial series.

@parthea
parthea dismissed their stale review October 21, 2026 18:43

Changing approach

A recent write to one project no longer holds back exports to the
others. Each project records its own last successful write, so a
project that just exported skips the final flush while the rest still
go through, and after a partial failure only the failed project is
written again.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

4 participants