Skip to content

fix: reduce metric cardinality and close observability gaps - #802

Merged
srstack merged 7 commits into
mainfrom
fix/metrics-cardinality-observability
Jul 27, 2026
Merged

fix: reduce metric cardinality and close observability gaps#802
srstack merged 7 commits into
mainfrom
fix/metrics-cardinality-observability

Conversation

@srstack

@srstack srstack commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • reduce high-cardinality tenant metric shapes and move per-tenant service attribution to a failure-only counter
  • delete healthy or retired tenant gauge series, including cross-pod cleanup through the tenant notify outbox
  • add bounded metrics for mutation dispatch, API key caching, notify coalescing, tenant outbox delivery, shared-pool convergence, and Vault feature state
  • implement the TiDB Cloud OpenAPI api/operation/result contract directly in the HTTP producer, including digest retry, transport/status classification, idempotent delete, and protocol failures
  • remove duplicate/dead event-bus counters and instrument the actual retention sweep path
  • reduce per-tenant DB pool connection states while preserving role-level saturation capacity

Alert consumers

The matching alert and rule-test changes were pushed to tidbcloud/runbooks#2688. They replace false-green SSE, extraction, semantic-worker, background-worker, and Vault expressions and add alerts for the new critical-path metrics.

Validation so far

  • focused metric contract, gauge lifecycle, dispatcher, cache, coalescer, outbox, shared-pool, Vault, deletion broadcast, and TiDB Cloud OpenAPI tests pass
  • runbooks promtool rule tests pass
  • full regression and lint will be run after PR creation, per requested sequencing; results will be added in a follow-up comment

Scope notes

Copilot AI review requested due to automatic review settings July 26, 2026 17:37
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@srstack, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 14 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: aba38d9c-b4e4-471a-a5a6-fc62fdf1617c

📥 Commits

Reviewing files that changed from the base of the PR and between bf995bb and 5e9af58.

📒 Files selected for processing (55)
  • pkg/backend/dat9.go
  • pkg/backend/mutation_dispatcher.go
  • pkg/backend/mutation_dispatcher_test.go
  • pkg/backend/mutation_replay.go
  • pkg/backend/mutation_replay_metrics_test.go
  • pkg/backend/quota.go
  • pkg/backend/quota_metrics_test.go
  • pkg/meta/api_key_cache.go
  • pkg/meta/api_key_cache_test.go
  • pkg/meta/db_pool.go
  • pkg/meta/db_pool_test.go
  • pkg/meta/meta.go
  • pkg/meta/meta_test.go
  • pkg/meta/quota_setting_test.go
  • pkg/meta/tenant_pool_failed_cleanup.go
  • pkg/meta/tenant_pool_membership.go
  • pkg/metrics/db.go
  • pkg/metrics/db_test.go
  • pkg/metrics/operations.go
  • pkg/metrics/operations_test.go
  • pkg/metrics/registry.go
  • pkg/metrics/workers.go
  • pkg/metrics/workers_test.go
  • pkg/server/admin_tenant_pool.go
  • pkg/server/admin_tenants.go
  • pkg/server/auth.go
  • pkg/server/auth_test.go
  • pkg/server/eventbus_test.go
  • pkg/server/fork.go
  • pkg/server/instrumentation.go
  • pkg/server/instrumentation_test.go
  • pkg/server/managed_shared_db_failed_cleanup.go
  • pkg/server/notify_coalescer.go
  • pkg/server/notify_coalescer_test.go
  • pkg/server/quota.go
  • pkg/server/server.go
  • pkg/server/server_test.go
  • pkg/server/sse.go
  • pkg/server/tenant_delete.go
  • pkg/server/tenant_delete_test.go
  • pkg/server/tenant_failed_cleanup.go
  • pkg/server/tenant_metrics.go
  • pkg/server/tenant_metrics_test.go
  • pkg/server/tenant_outbox_poller.go
  • pkg/server/tenant_outbox_poller_test.go
  • pkg/server/tenant_pool_reconcile.go
  • pkg/server/tenant_worker.go
  • pkg/server/tenant_worker_metrics_test.go
  • pkg/server/vault_read_test.go
  • pkg/server/work_mask.go
  • pkg/tenant/pool.go
  • pkg/tenant/pool_test.go
  • pkg/tenant/tidbcloudnative/http_clusters_api.go
  • pkg/tenant/tidbcloudnative/openapi_metrics.go
  • pkg/tenant/tidbcloudnative/provisioner_test.go
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/metrics-cardinality-observability

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI 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.

Pull request overview

This PR reduces Prometheus metric cardinality across tenant-scoped instrumentation while adding explicit lifecycle cleanup (including cross-pod cleanup via the tenant notify outbox) and closing observability gaps with new bounded “critical-path” metrics. It also implements a more explicit TiDB Cloud OpenAPI api/operation/result metric contract directly in the HTTP producer (including digest retry and protocol/transport classification).

Changes:

  • Add TiDB Cloud OpenAPI request classification metrics (api/operation/result) and test coverage for retry/error/protocol behaviors.
  • Broadcast tenant metrics cleanup on durable delete/deleting transitions and delete healthy/retired tenant gauge series (plus related tests).
  • Add bounded operational metrics for mutation dispatch, API key cache, notify coalescing/outbox delivery, shared-pool convergence/cleanup, Vault feature state, and DB pool state aggregation.

Reviewed changes

Copilot reviewed 46 out of 46 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
pkg/tenant/tidbcloudnative/provisioner_test.go Adds tests that assert TiDB Cloud OpenAPI metrics classification and digest retry behavior.
pkg/tenant/tidbcloudnative/openapi_metrics.go Defines api/operation/result constants and request/response classification helpers.
pkg/tenant/tidbcloudnative/http_clusters_api.go Records TiDB Cloud OpenAPI metrics per logical request, including protocol failures and transport classification.
pkg/server/work_mask.go Adds WorkMetricsCleanup bit for outbox-driven per-pod tenant metric cleanup.
pkg/server/vault_read_test.go Updates Vault metrics assertions to reflect reduced tenant-labeling on success paths.
pkg/server/tenant_worker.go Deletes healthy semantic-worker gauges; instruments fs_events retention paths; adds ForgetTenant hook.
pkg/server/tenant_worker_metrics_test.go Tests that semantic-worker observation exports only abnormal gauge state.
pkg/server/tenant_pool_reconcile.go Records metric when stuck shared DB pools are marked failed.
pkg/server/tenant_outbox_poller.go Records poll/cursor-flush metrics; applies broadcast metrics cleanup on WorkMetricsCleanup.
pkg/server/tenant_outbox_poller_test.go Validates cleanup broadcast semantics and removal of tenant labels on success-path counters.
pkg/server/tenant_metrics.go Adds shared DB pool status age metric and deletes it when no longer applicable.
pkg/server/tenant_metrics_test.go Extends shared DB pool metrics test coverage to include status age series.
pkg/server/tenant_failed_cleanup.go Broadcasts tenant metrics cleanup after durable “deleted” transitions in failed cleanup flows.
pkg/server/tenant_delete.go Broadcasts tenant metrics cleanup during delete flows and after finalization.
pkg/server/tenant_delete_test.go Asserts metrics cleanup is enqueued via tenant notify outbox on delete.
pkg/server/sse.go Removes duplicate/dead event-bus publish-error counter in favor of unified operation failure attribution.
pkg/server/server.go Sets Vault feature-enabled metric; adds broadcastTenantMetricsCleanup helper.
pkg/server/server_test.go Tests Vault feature state vs module availability metrics across configurations.
pkg/server/quota.go Removes duplicate TiDB Cloud OpenAPI metric recording now handled in HTTP producer.
pkg/server/notify_coalescer.go Adds bounded metrics for pending size, flush outcomes, and per-row fallback results.
pkg/server/notify_coalescer_test.go Asserts notify coalescer metrics are emitted on fallback paths.
pkg/server/managed_shared_db_failed_cleanup.go Adds step/result metrics for failed shared DB pool cleanup stages.
pkg/server/instrumentation.go Aggregates tenant in-flight metrics across actions per surface to reduce cardinality.
pkg/server/instrumentation_test.go Tests in-flight aggregation behavior across actions.
pkg/server/fork.go Broadcasts tenant metrics cleanup when fork cleanup marks tenants deleted.
pkg/server/eventbus_test.go Updates expectation: old SSE in-flight series should be deleted (not zeroed).
pkg/server/auth_test.go Updates tenant request metric assertions to match new label set (no derived result label).
pkg/server/admin_tenants.go Broadcasts tenant metrics cleanup for admin delete paths.
pkg/server/admin_tenant_pool.go Adds shared-pool wave metrics; broadcasts cleanup on pool-tenant deletions/cleanup.
pkg/metrics/workers.go Introduces bounded worker/critical-path metrics (dispatcher/cache/coalescer/outbox/shared pool).
pkg/metrics/workers_test.go Validates worker metrics stay bounded (no tenant_id/db_pool_id leakage).
pkg/metrics/registry.go Adds internal helper to delete gauge series by label within a specific gauge name.
pkg/metrics/operations.go Refactors label sets to reduce cardinality; adds failure-only tenant attribution counter; adds feature_enabled gauge; adds deletion helpers.
pkg/metrics/operations_test.go Updates/extends metric contract tests for new label sets and deletion behaviors.
pkg/metrics/db.go Reduces per-tenant DB pool connection state series; adds role-level aggregates for saturation states.
pkg/metrics/db_test.go Asserts tenant pools omit idle/max_open series while retaining role-level max_open visibility.
pkg/meta/quota_setting_test.go Updates expectations for aggregated service operation counters without tenant labels on success.
pkg/meta/db_pool.go Extends shared DB pool metric snapshot to include updated_at for status age metric.
pkg/meta/api_key_cache.go Adds cache hit/miss + entry-count metrics; emits entry gauge on fill/evict.
pkg/meta/api_key_cache_test.go Adds test coverage for API key cache metrics.
pkg/backend/quota.go Ensures quota snapshot replaces prior tenant gauge series to avoid stale org/limit labels.
pkg/backend/quota_metrics_test.go Tests quota snapshot replacement semantics (org relabel + limit removal).
pkg/backend/mutation_replay.go Deletes healthy backlog gauges and deletes prior-org gauges on org relabel.
pkg/backend/mutation_replay_metrics_test.go Updates tests to require gauge deletion (not zeroing) for healthy state.
pkg/backend/mutation_dispatcher.go Adds dispatcher queue depth/capacity, enqueue-blocked, batch size, and fallback metrics.
pkg/backend/mutation_dispatcher_test.go Validates dispatcher metrics are present (capacity, fallback).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pkg/server/tenant_worker.go
@srstack

srstack commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator Author

Regression update:

  • make test exercised the full suite. All packages outside cmd/drive9/cli passed; the command exits non-zero on two existing CLI failures: TestCtxImport_RejectsWorldReadableFile and TestCpLayerLocalUploadUsesFSLayerEntry.
  • Both CLI failures reproduce unchanged on the current main checkout with the same focused command, and this PR has no cmd/drive9/cli changes.
  • go test ./... -run ^$ -count=1 passed for the full repository.
  • make lint passed with 0 issues after removing one unused bounds declaration.
  • pkg/metrics was rerun after that cleanup and passed.
  • tidbcloud/runbooks#2688: make check-drive9-alert-rules and the full make check-rules validation pass.

GitHub CI and local-e2e are still running on the latest head 265a59d.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8c11d09186

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread pkg/server/tenant_outbox_poller.go
Comment thread pkg/server/server.go Outdated
Comment thread pkg/server/notify_coalescer.go Outdated
@srstack

srstack commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator Author

Final regression update for head 265a59d3:

  • umask 022; make test completed successfully: the full Drive9 Go test suite passed.
  • go test ./... -run "^$" -count=1 passed for the full repository.
  • make lint passed with 0 issues.
  • The two failures mentioned in the earlier comment were caused by this machine running under umask 077: tests that create files with mode 0644 observed 0600. They reproduced unchanged on main, and both focused tests passed under the standard CI umask 022.
  • In tidbcloud/runbooks#2688, both make check-drive9-alert-rules and the full make check-rules passed.
  • GitHub checks are now green: Drive9 ci, drive9-server-local e2e smoke, CodeRabbit, and runbooks check-rules all passed.

@srstack srstack left a comment

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.

Review

Verified locally on fix/metrics-cardinality-observability (8c11d091) in an isolated worktree: go build ./... clean, go vet clean on the touched trees, and pkg/metrics, pkg/server, pkg/backend, pkg/meta, pkg/tenant/tidbcloudnative all pass with -count=1. I also cross-checked the alert side (runbooks c52b6b7b): every drive9_* metric referenced by the rules now exists in code, and the four previously false-green alerts were replaced rather than silently dropped.

Good change overall. It closes the real gaps — false-green alerts, the Set(0)-instead-of-Delete leak class, and the five uninstrumented critical paths. The cross-pod cleanup via a new outbox work bit is a better mechanism than a per-pod TTL sweep. Findings below, most severe first.


Blocking

1. WorkMetricsCleanup = 8 has no compile-time assertion and no backend counterpart.

pkg/server/work_mask.go adds bit 3, but the var _ = [1]byte{}[...] assertion block still only covers SSE/Semantic/FileGC. The file's own doc comment says those assertions "ensure the values stay in sync" — bit 3 opts out of that guarantee. backend.BackendWork* stops at 4, so a future BackendWorkQuota = 8 in pkg/backend/dat9.go would collide with metrics-cleanup and the poller would run DeleteTenantCounters on every quota kick. The naming drift is already visible: the comment at the top of work_mask.go refers to a "Quota" bit that is not defined anywhere. Please either reserve bit 3 in the backend const block (even as a commented-out placeholder) or add an assertion that it is unclaimed.

2. DeleteTenantQuotaSnapshot on the quota-check hot path is both a contention and a correctness regression.

pkg/backend/quota.go calls it at the top of recordTenantQuotaSnapshot, which runs from three hot paths — file_count_check, storage_check, and mediaLLMQuotaExceededServerTx — i.e. effectively on every write.

  • Cost: each call takes the registry write lock three times and scans every label set of all three gauges (deleteGaugeByLabel in pkg/metrics/registry.go). That is O(total tenants), not O(1). At N tenants × write QPS it is an N² sweep under a process-global mutex on the write path — the opposite of this PR's goal.
  • Correctness: the delete removes confirmed/reserved/limit, then re-adds confirmed/reserved, and re-adds limit only if cfg.Max* > 0. A /metrics scrape landing between the delete and the re-add sees the tenant's series missing entirely — a disappearing denominator for Drive9Tenant*QuotaNearLimit.
  • Call sites fight each other: storage_check reaches the snapshot with a cfg whose media/video limits may be 0, so it deletes the media limit series a media check just wrote, and vice versa.

DeleteTenantQuotaLimitsWithOrg (added in this PR, unit-tested, currently dead code — no non-test caller) is the targeted fix: delete only the three state="limit" series, and only when the corresponding limit is absent. O(1), keeps confirmed/reserved continuously present, no registry scan.


Should fix

3. drive9_db_pool_connections role-only aggregate guard is dead.

In pkg/metrics/db.go the synthetic per-role aggregate is guarded by if _, exists := poolTotals[dbMetricKey{role: role}]. That zero-value key has tenantID/dbPoolUUID/tidbCloudOrgID as "", while every real key goes through metricKey(), which normalizes them to "unknown"/"guest". The lookup can never hit. Today nothing duplicates, because OpenInstrumentedForTenant always supplies a tenantID for user roles — but the guard does not protect what its comment claims, and Drive9DBConnectionPoolSaturation (unchanged, matches bare state= with no role filter) would divide across two different series if an unscoped role pool were ever registered. Build the probe key with metricKey(), or drop the guard and state the invariant.

4. Drive9TenantOutboxBacklogAgeHigh will flap.

RecordTenantOutboxPoll only sets backlog_oldest_age_seconds when full is true (batch size exactly tenantOutboxBatchSize) and sets it to 0 otherwise. A backlog draining to batchSize - 1 resets the gauge to 0 while rows are still pending. With for: 5m on > 30 at critical, a steady near-full backlog oscillates and never holds the window. Suggest reporting the age whenever len(rows) > 0 and gating the alert on the age alone.

5. RecordAPIKeyResolveCache calls entryCount() on every auth request.

pkg/meta/api_key_cache.go takes the cache mutex per hit/miss just to read len(c.byHash) for a gauge — on the exact hot path this cache exists to relieve (per its own comment, 76M execs/12h). fill and evictTenant already publish the count via RecordAPIKeyResolveCacheEntries, and the count only changes there, so the per-request read is redundant. Use the counter-only variant on the request path.

Related: the hits/misses atomics are now superseded by drive9_api_key_resolve_cache_requests_total and are unread outside tests — worth deleting.

6. Global semantic_worker gauges still write 0 and remain unalerted.

pkg/server/tenant_worker.go sets queued, processing, dead_lettered, queue_lag_seconds to 0 at start and never updates them (only inflight/workers are maintained). The per-tenant path was correctly switched to Delete and the two dead global alerts were correctly removed — but four permanently-zero global series remain. Either wire them or drop them; a constant-zero series invites someone to alert on it later.


Minor

7. _ = param placeholders leave a misleading API surface. pkg/metrics/operations.go now carries _ = result, _ = surface, _, _ = surface, action, _ = action, and _, _ = tenantID, tidbCloudOrgID; pkg/server/instrumentation.go has _ = action in tenantInFlightKey. Callers still compute and pass values that are discarded. Acceptable as a mechanical first pass, but the signatures should be narrowed in a follow-up or the dropped args will get quietly re-plumbed.

8. Drive9QuotaReservationLockConflictRetryHigh loses its attribution. The alert still groups by tenant_id, tidbcloud_org_id over drive9_service_operations_total, but that counter no longer carries those labels, so the grouping silently collapses. lock_conflict_retry is also absent from isOperationFailureResult, so it does not land in drive9_tenant_operation_failures_total either. This does resolve the guest-org bug I flagged earlier — but by removing attribution rather than fixing it. Worth deciding explicitly which behavior you want.

9. tidbcloudOperationForRequest returns "unknown" for unmatched paths. It is only reached from doDigestAuthRequest, and the per-method call sites pass explicit constants, so unknown should be unreachable. A one-line comment saying so would keep it from reading like an unbounded label source.

10. Drive9VaultUnavailable correctness depends on pod being attached to drive9_feature_enabled. SetFeatureEnabled sets no pod label; the join relies on the scrape adding it. It does for module_up, so this is very likely fine — worth confirming in one environment before relying on the and on (namespace, pod) join.


Not addressed (carried over, not necessarily for this PR)

  • k8s-level alerts still hardcode namespace=~"dat9|drive9-tidbcloud". The 7-environment namespace/container audit hasn't happened, so several cluster-level rules may be inert outside those two namespaces.
  • DeleteTenantCounters on idle pool reap (pkg/tenant/pool.go) is unchanged — pool.go isn't in this diff. The new outbox broadcast fixes cleanup on tenant delete, which is the larger win, but a 2-minute idle reap still deletes counters for live tenants and makes increase() under-count on per-tenant alerts. Now that WorkMetricsCleanup exists, scoping counter deletion to lifecycle transitions only would be a small follow-up.
  • Tenant pool ref-leak detection remains uncovered, as documented in the rules file.

@srstack

srstack commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator Author

Review follow-up on head 23a8ec6b:

Addressed:

  1. Work-mask allocation now has a single persisted source in pkg/meta; server/backend aliases include the metrics-cleanup bit and retain compile-time equality checks.
  2. Quota snapshots no longer scan every tenant series or expose a delete/re-add scrape gap. ReplaceTenantQuotaSnapshotWithOrg performs an O(1), single-registry-lock replacement, preserves known limits when config is unavailable, and removes prior-org labels exactly.
  3. The mixed scoped/unscoped DB pool guard now probes the normalized metricKey, with a duplicate-series regression test.
  4. Tenant outbox backlog age is retained across failed polls, reported for every successful non-empty batch, and cleared only by a successful empty poll.
  5. API-key cache request accounting is counter-only on hit/miss. Entry gauges update only on fill, eviction, or lazy TTL expiry; the redundant request-path entryCount lock and test-only atomics were removed.
  6. The four never-maintained global semantic queue gauges were removed. Per-tenant semantic gauges now delete the previous-org series on org relabel.
  7. Notify coalescer pending now includes in-flight tenant IDs until durable success; failed per-row fallbacks are re-queued rather than dropped.
  8. Metrics cleanup outbox delivery is transactionally coupled to every deleting/deleted MetaDB lifecycle transition; the server helper now performs local cleanup only.
  9. Added the bounded-fallback comment for the unreachable TiDB Cloud unknown operation path.

Reviewed but intentionally unchanged:

  • The discarded-parameter signature narrowing is mechanical across 90+ call sites and was explicitly identified as follow-up-safe; keeping it out avoids unrelated churn in this already broad PR.
  • Drive9QuotaReservationLockConflictRetryHigh is already aggregate-only in runbooks c52b6b7b (sum by (namespace) over service_operations_total), so the tenant-grouping issue does not exist on the current alert branch.
  • Drive9VaultUnavailable joins two metrics from the same scrape target. Prometheus attaches namespace and pod target labels to both; neither producer writes a pod label itself, so the join contract is consistent.
  • Namespace audit, idle-pool counter deletion, and tenant ref-leak detection remain the explicitly carried-over follow-ups.

Validation on the pushed head:

  • umask 022; make test passed.
  • go test ./... -run "^$" -count=1 passed.
  • make lint passed with 0 issues.
  • All four inline threads have targeted regression tests, replies, and are resolved.

GitHub CI and local E2E are running on this head.

@srstack

srstack commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator Author

Review state rechecked against the current head 23a8ec6. GitHub currently reports no unresolved review threads and no additional formal review or inline comment beyond the four threads already fixed and resolved. No code change was made for this refresh because there is no newly retrievable technical finding to evaluate. The items intentionally left unchanged, with their technical reasons, remain documented in #802 (comment). If another review is still propagating or is visible only through a specific discussion link, it will be evaluated against the current head when that comment becomes available; actionable findings will be fixed, and rejected findings will receive an explanation in their own thread.

Remove discarded metric parameters from tenant request, bytes, inflight, and fs_events APIs, and rename the local cleanup helper to reflect its behavior. Document that deleting/deleted lifecycle transitions intentionally use per-tenant transactions so outbox delivery remains atomic without holding locks across external cleanup work.
@srstack

srstack commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator Author

Follow-up corrections are pushed in c572e58.

  1. The discarded metric parameters were removed instead of retained as compatibility placeholders. Tenant request result, HTTP/file surface/action, inflight action, and fs_events tenant/org arguments were removed from the APIs and all callers. The now-dead tenantRequestResult helper was also removed.
  2. broadcastTenantMetricsCleanup was renamed to clearLocalTenantMetrics across all 14 current call sites, matching its process-local behavior after durable outbox insertion moved into MetaDB lifecycle transitions.
  3. The deleting/deleted UpdateTenantStatus and UpdateTenantStatusIf paths intentionally retain one transaction per tenant. The batch cleanup loops interleave external cluster deprovisioning, pool draining, API-key revocation, and other work; one transaction across the loop would hold DB locks across external I/O and couple independent tenant failures into a single rollback. The per-tenant transaction preserves status plus metrics-cleanup-outbox atomicity and failure isolation. This behavior and rationale are now documented on both Store methods and in the c572e58 commit message.

This supersedes the earlier decision to leave signature narrowing as follow-up work.

Per the requested submit-first order, the commit was pushed before waiting for complete regression. Before interruption, pkg/metrics, pkg/backend, and pkg/meta passed; pkg/server was still running and was deliberately interrupted, so it is not being reported as passed.

@srstack

srstack commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator Author

Remaining review follow-ups disposition:

  1. Idle pool counter deletion: fixed in e13d5e98. Pool.closeEntry no longer calls DeleteTenantCounters, so idle/capacity/shutdown eviction of a live cached backend cannot reset tenant counters and undercount increase(). Tenant-series deletion is now owned by deleting/deleted lifecycle cleanup and its durable metrics-cleanup outbox delivery. TestCloseEntryPreservesLiveTenantCounters failed against the old behavior and passes after the fix.

  2. Seven-environment namespace/deployment/container audit: no rule edit is needed. The current runbooks selectors are namespace=~"dat9|drive9-tidbcloud", deployment=~"dat9-server|drive9-server", and container=~"dat9-server|drive9-server". The current environment inventory maps as follows:

    • prod-aws-sg, dev-aws-sg, prod-aws-use1: dat9/dat9-server and drive9-tidbcloud/drive9-server
    • prod-aws-usw2, prod-ali-sg, dev-tke-bj, prod-tke-bj: drive9-tidbcloud/drive9-server (prod-ali-sg also retains dat9 configuration)
      These names are fully covered by the existing regexes, so widening or changing them would not activate any additional Drive9 workload. dev-tke-bj currently has no vmagent; that is an observability deployment gap, not a selector-contract bug, and changing alert expressions would not make that environment scrape metrics.
  3. Tenant pool ref-leak alert: intentionally not added from the existing cache metrics. drive9_shared_db_pool_cache_tenants is backed by sharedDBRefs, which counts cached tenant backends using a physical shared DB handle. It does not count outstanding Pool.Acquire leases (entry.refs). Therefore cache_tenants > 0 is normal for warm live tenants and cannot distinguish a leaked request lease; combining it with traffic absence would still conflate healthy cache residency with a leak. A correct signal needs lease-level acquire/release tracking (for example oldest outstanding lease age or bounded anomaly-only state) plus hot-path overhead validation. Adding the suggested alert on the current metrics would create a false-positive-prone contract, so this PR leaves it out with this explicit rationale rather than shipping a misleading rule.

Focused verification before push: go test ./pkg/tenant -run ^TestCloseEntryPreservesLiveTenantCounters$ -count=1 passed, and git diff --check passed. Full regression follows after comment handling, per the requested submit-first order.

@srstack srstack left a comment

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.

LGTM.

All ten items from my earlier review are resolved on e13d5e98, including the three that were initially deferred or pushed back on. Two of those pushbacks were correct and I withdraw them: Drive9QuotaReservationLockConflictRetryHigh is already aggregate-only on runbooks c52b6b7b, so the tenant-grouping concern never applied to the current alert branch; and drive9_feature_enabled / drive9_module_up come from the same scrape target, so the and on (namespace, pod) join contract holds.

What convinced me on the two blocking items

Work-mask allocation. Moving the bits to pkg/meta as 1 << iota and reducing backend.BackendWork* / server.Work* to aliases is a better fix than the assertion I asked for — it removes the possibility of divergence rather than detecting it after the fact. BackendWorkMetricsCleanup existing solely to be asserted, with an explicit "backend does not emit it" comment, is the right call. The phantom "Quota" bit references in the surrounding comments are gone too.

Quota snapshot path. ReplaceTenantQuotaSnapshotWithOrg is O(1) with a single registry lock acquisition and exact-label writes; deleteGaugeByLabel is gone. Two things beyond what I raised are also correct: caching last-known limits in Registry.tenantQuotaSnapshots so the three call sites stop clobbering each other's state="limit" series, and deleting the prior org's label set on relabel. Because it is one critical section with no intermediate delete, the scrape-window gap is closed rather than narrowed.

On the last two commits

c572e58e — the signature narrowing landed properly rather than being silenced. All seven _ = param placeholders are gone, action removal propagated correctly through tenantInFlightKey / adjustTenantInFlight / setRequestMetricTenant, and keeping state.action = action on the in-flight move path is right since action still feeds tenant_requests_total. Dropping tenantRequestResult once nothing consumed it is good hygiene. The transaction-boundary rationale on UpdateTenantStatus / UpdateTenantStatusIf is a better answer than batching — per-tenant transactions keeping failure isolation in loops that interleave external cleanup work is the correct tradeoff, and having it written down is what I wanted.

e13d5e98 — this closes the idle-reap issue I had twice marked as out of scope. With DeleteTenantCounters removed from Pool.closeEntry, the only two call sites are the outbox cleanup-bit dispatch and clearLocalTenantMetrics, both lifecycle-driven, so a 2-minute idle reap no longer deletes counters for a live tenant and the increase() under-counting on per-tenant alerts is fixed. TestCloseEntryPreservesLiveTenantCounters pins it.

Also confirmed the metrics-cleanup outbox row is written transactionally at all nine insertTenantNotifyTx sites, so cleanup delivery cannot be lost between the status transition and the signal.

Independent verification on e13d5e98

  • go build ./... clean
  • go vet ./pkg/... clean
  • pkg/metrics, pkg/server, pkg/backend, pkg/meta, pkg/tenant all pass with -count=1
  • -race clean on pkg/metrics and on the coalescer / outbox / semantic-worker tests in pkg/server
  • Alert cross-check: every drive9_* metric referenced by runbooks c52b6b7b exists in code, and the four previously false-green alerts were replaced rather than dropped
  • GitHub ci pass, drive9-server-local e2e smoke pass

Remaining follow-ups (correctly out of scope here)

  • The namespace/container audit for the k8s-level rules across all seven environments.
  • Tenant-pool ref-leak detection, still documented as uncovered in the rules file.

Nothing blocking from me.

@mornyx mornyx left a comment

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.

Deep review across six slices (cardinality/lifecycle, new bounded metrics, OpenAPI contract, DB pool reduction, event-bus dedup, gauge deletion plumbing) with findings traced end-to-end. Approving: the failure-only per-tenant counter + whitelisted business events give a real cardinality bound; the WorkMetricsCleanup broadcast is transactional with every status transition, idempotent, and its cursor/prune semantics are correct; the OpenAPI api/operation/result contract has exactly-once, mutually-exclusive classification (verified against every return path of all 10 HTTPClustersAPI methods + the digest doer); gauge registry locking and label-key normalization are consistent. Three worth-fixing items and a handful of nits inline; also two deploy notes: the label rename (pathapi, module tidbcloud_quotatidbcloud_openapi) breaks existing dashboards keyed on the old shape, and drive9_event_bus_publish_errors_total had a live caller before this PR — any alert on it goes silent until the runbooks PR lands.

Comment thread pkg/server/tenant_metrics.go
Comment thread pkg/server/fork.go
Comment thread pkg/server/tenant_outbox_poller.go
Comment thread pkg/tenant/tidbcloudnative/http_clusters_api.go
Comment thread pkg/tenant/tidbcloudnative/openapi_metrics.go Outdated
@mornyx

mornyx commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Worth-fixing (anchored at pkg/server/auth.go:258, not in the diff so leaving it here) — late samples re-create the series the cleanup just deleted, and nothing re-cleans them.

DeleteTenantCounters runs exactly once per pod when the cleanup outbox row is consumed, but setRequestMetricTenant (auth.go:258) runs for every request that resolves a key — before the key-active and tenant-status checks — so a client that keeps calling with a deleted tenant's key (FUSE mount retry loop, SDK heartbeat) re-creates drive9_tenant_requests_total + bytes + whitelisted business-event series on every pod it hits, indefinitely. Cross-pod, the auth resolve cache additionally lets other pods accept the deleted tenant's keys as active for up to the TTL and produce full series post-cleanup. The old backstop (pool.closeEntry → delete) was deliberately removed, so there is no second sweep — these series live until pod restart, defeating the mechanism for exactly the tenants with post-delete traffic.

Consider bucketing requests that fail auth/tenant-status without tenant attribution (e.g. tenant_id="blocked"), or having the poller retain recently-cleaned IDs and suppress/re-delete.

@qiffang

qiffang commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

adversary-2 review — GREEN / APPROVE (N1/N2 tradeoffs to confirm before merge)

B1 coupling (the #1 question from #801): RESOLVED. pkg/tenant/pool.go closeEntry removes the inline metrics.DeleteTenantCounters (-6). Nothing in Pool.Acquire/Get/eviction calls the full-registry scan now — #801's 20× resident-cap raise no longer amplifies a synchronous registry-lock scan on the request path. Deletion moved to (a) lifecycle transitions writing a WorkMetricsCleanup outbox row in the same tx as the status change (meta.go:3609/3672/4173/4236, db_pool.go:1619), and (b) tenantOutboxPoller.dispatch per pod (broadcast). Verified by TestCloseEntryPreservesLiveTenantCounters.

Cross-pod cleanup correct: cleanup bit is broadcast (not sharded); every pod's poller deletes local series idempotently. Delivery is now reliable — coalescer re-queues failed rows via finishFlush (fixes the #792 silent-drop). Outbox row durable/atomic with the transition. TestTenantOutboxPollerMetricsCleanupIsBroadcast covers it.

New metrics bounded: workers.go uses shard (0–3), enum result/stage/status via cleanMetricValue; shared_db_pool_status_age bounded by pool count. TestCriticalWorkerMetricsUseBoundedLabels asserts no tenant_id/db_pool_id. append([]Attribute(nil), attrs...) copies before adding tenant labels (operations.go:375). TiDB OpenAPI metrics (api/result/transport) carry no tenant dimension.

Notes (confirm before merge; not code blockers)

  • N1 — evicted-but-active tenant series retention. Cleanup now binds only to TenantDeleting/Deleted. A healthy tenant capacity-evicted from the resident pool (cap raised to 20,480 by perf: scale shared tenant backend cache #801) keeps its series resident forever (eviction no longer prunes; only deletion does). Pod-lifetime cardinality is bounded by distinct tenants ever seen, not by 20,480. Deliberate tradeoff behind "preserve live tenant counters" — confirm against pod tenant-churn, not just concurrent residency.
  • N2 — per-tenant success-rate observability dropped. serviceOperationsTotal lost tenant_id/org_id (now aggregate); only failures are tenant-attributed. Per-tenant service-operation success ratio no longer computable (denominator gone). tenantRequestsTotal keeps per-tenant status_class, so HTTP-surface success rate survives. Confirm no dashboard/alert divides failures by the removed per-tenant total.
  • N3 — rollout-window label skew (deploy side). Multiple metrics changed label shape (tenant_requests_total drops result, tidbcloud_openapi pathapi, removed event_bus_poll_failures_total etc.). Mixed old/new binaries during rollout make fleet-sum recording rules double-count/mismatch. PR body cites runbooks#2688 but the rollout window isn't flagged in-code.
  • N4 — no explicit delete-then-reactivate race test. UUIDs aren't reused so low severity; the named race isn't directly asserted.

Verdict: GREEN. B1 genuinely resolved. #801 + #802 must land in the same merge result (plus #801's B2 quota-cache panic-path fix).

@qiffang qiffang 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.

Requesting changes on exact head e13d5e985c292d912fde8c26801fd355c0848090.

This PR does fix the #801 request-path contention class: Pool.closeEntry no longer calls metrics.DeleteTenantCounters, and the lifecycle/outbox cleanup path is transactionally tied to deleting/deleted transitions. I am not blocking on the general N1/N2 tradeoffs about live-tenant series retention or per-tenant service-operation denominators. The blockers below are concrete metric correctness failures still present on this head.

  1. Deleted/revoked tenant traffic can recreate the exact tenant series cleanup just removed. tenantOutboxPoller.dispatch deletes tenant counters once for WorkMetricsCleanup, but auth sets tenant metric scope at pkg/server/auth.go:258 before APIKeyActive and before the tenant-status switch, and /v1/status does the same at pkg/server/server.go:2066. resolveByAPIKeyHashDB returns rows by jwt_hash without filtering key or tenant status, and recordTenantHTTPRequest records tenant-scoped counters/bytes for every final response once scope is present. Result: after cleanup runs, a stale SDK/FUSE heartbeat or any client retrying a revoked/deleted key can recreate drive9_tenant_requests_total and bytes indefinitely, with no later sweep now that pool eviction cleanup was removed. Existing tests prove deletion removes current series; they do not prove cleanup remains sticky against rejected post-delete traffic. Required before merge: fail-closed behavior for inactive/deleting/deleted auth attempts, or an explicit recently-cleaned suppression/re-delete mechanism, plus a regression that cleanup followed by revoked/deleted-key traffic does not reintroduce tenant-labeled series.

  2. The reduced DB pool metric can go false-green when an unscoped user pool is open. Fork/delete flows still open tenant stores through datastore.Open (pkg/server/fork.go:647, pkg/server/fork.go:1080), which registers an unscoped role="user" pool via pkg/datastore/store.go:160-180. In pkg/metrics/db.go:361-438, tenant-scoped pools are accumulated into tenantRoleTotals, but the synthetic role aggregate is skipped whenever an unscoped same-role key exists. That means drive9_db_pool_connections{role="user",state="max_open"} can report only the transient unscoped fork/delete pool and exclude all tenant-scoped pool capacity, exactly while this PR removes per-tenant max_open series. The current mixed-pool test only asserts one role-only line exists; it does not assert the value includes tenant-scoped totals. Required before merge: either scope those fork/delete stores or make the role aggregate include both scoped and unscoped capacity, with a value-based regression.

  3. TiDB Cloud delete failures are counted as ok for unexpected 2xx responses. DeleteCluster and DeleteBranch return success only for 200/204/404, but for any other status they call recordTiDBCloudHTTPResponse(..., responseValid=true) before returning an error. tiDBCloudHTTPResult maps every 2xx with responseValid=true to ok, so a 202 Accepted or other unexpected 2xx is both a caller-visible error and an ok OpenAPI metric. That violates the new api/operation/result contract and can hide upstream protocol drift. Required before merge: classify unexpected success-class responses that still return errors as protocol/error results, with delete-cluster/delete-branch regressions.

  4. shared_db_pool_status_age_seconds is not actually age in status. tenant_metrics.go:109-114 derives non-active status age from db_pool.updated_at, but the schema defines updated_at ... ON UPDATE CURRENT_TIMESTAMP(3) and unrelated writes such as tenant-count/soft-cap/spending-limit repair also update the row. A stuck provisioning/deleting pool can therefore reset to age 0 without a status transition, making any age alert false-green. Required before merge if this metric is part of the runbook contract: store/use status-transition time or rename/redefine the metric and alerts so it is not presented as status age.

Validation I ran locally: git diff --check clean; changed Go files gofmt -l clean; go vet ./pkg/metrics ./pkg/meta ./pkg/server ./pkg/backend ./pkg/tenant ./pkg/tenant/tidbcloudnative clean; go build ./... clean; go test ./pkg/metrics ./pkg/tenant/tidbcloudnative -count=1 passed; test binaries compile for pkg/meta, pkg/server, pkg/backend, and pkg/tenant. Full local tests for the Docker-backed packages are blocked on this machine by panic: rootless Docker not found. Hosted ci and drive9-server-local e2e smoke are green.

@qiffang

qiffang commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

adversary-2 — retracting GREEN, REVISE (endorsing @adversary-1's blockers)

I verified the two most impactful of @adversary-1's four findings; both are real metric-correctness defects, harder than my earlier N1/N2 tradeoffs. My gap: I focused on the B1 request-path coupling (the headline) and cardinality bounds, not the per-metric result classification and re-introduction paths.

B3 confirmedhttp_clusters_api.go:213/217: DeleteCluster treats only 200/204/404 as success; any other status (incl. a 202 or other 2xx) calls recordTiDBCloudHTTPResponse(..., responseValid=true) then returns an error. openapi_metrics.go:53-58 maps 2xx + responseValid=true → "ok". So an unexpected 2xx delete is simultaneously a caller-visible error AND an ok OpenAPI metric — violates the new api/operation/result contract and hides upstream protocol drift. DeleteBranch:299/303 identical. Fix: pass responseValid=false for unexpected 2xx in delete paths (→ protocol-error) + delete regressions.

B1 confirmed — auth sets tenant metric scope at auth.go:258 BEFORE APIKeyActive and before the tenant-status switch; /v1/status (server.go:2066) same; resolveByAPIKeyHashDB returns by jwt_hash without filtering key/tenant status; recordTenantHTTPRequest records tenant-scoped counters for every final response. After cleanup runs, a stale SDK/FUSE heartbeat or any retry on a revoked/deleted key recreates drive9_tenant_requests_total/bytes indefinitely — and pool-eviction cleanup was removed by this PR, so no later sweep. Existing tests prove deletion removes current series, not that cleanup stays sticky against post-delete rejected traffic. Fix: fail-closed for inactive/deleting/deleted auth attempts, or a recently-cleaned suppression/re-delete mechanism + regression.

B2 / B4 — analysis paths credible (fork/delete opens an unscoped role="user" pool → role aggregate skips tenant-scoped totals → false-green DB-pool max_open; shared_db_pool_status_age derived from updated_at ON UPDATE, reset by unrelated tenant-count/soft-cap/spending writes → age alert false-green). I did not line-trace these two but concur with the direction.

Verdict: REVISE. B1 coupling is genuinely resolved (my prior finding stands), but these four metric-correctness defects block merge. #801 + #802 still need the same merge result. Aligned with @adversary-1.

@qiffang

qiffang commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

adversary-2 R2 at 5e9af58e — GREEN (all four blockers independently verified closed)

B1' RESOLVED — dual fix: (a) fail-closed — auth.go:258 no longer sets metric scope; the main middleware sets it only at :418 (after APIKeyActive + the tenant-status switch); the delete-no-acquire path uses setRequestMetricTenantForAuthStatus (instrumentation.go:230) which no-ops unless status==TenantActive. (b) cross-pod — new WorkAPIKeyCacheCleanup bit; all three revoke methods wrap UPDATE + insertTenantNotifyTx(WorkAPIKeyCacheCleanup) in one InTx (atomic, outbox only on n>0); poller dispatches InvalidateAPIKeyResolveCacheForTenant per pod. Regressions: TestSetRequestMetricTenantForAuthStatusOnlyScopesActiveTenants, TestAPIKeyRevocationsWriteCacheCleanupOutbox, TestTenantOutboxPollerAPIKeyCacheCleanupIsBroadcastWithoutDeletingMetrics.

B3' RESOLVEDDeleteCluster/DeleteBranch (http_clusters_api.go:217/303) pass responseValid=falsetiDBCloudHTTPResult(2xx,false) = protocol_error. Regression TestTiDBCloudOpenAPIDeleteUnexpectedSuccessIsProtocolError (202 → ok delta 0, protocol_error +1). Non-delete verbs keep true (those 2xx are genuinely valid).

B2' RESOLVED — root cause: fork.go:647/1080 now datastore.OpenForTenant (tenant-scoped). Backstop: db.go:371 folds unscoped saturation into the same-role aggregate; :428 suppresses the standalone unscoped line when a scoped pool exists. Value-based regression TestDBPoolConnectionsDoesNotDuplicateRoleAggregateForMixedPools (unscoped 2 + scoped 3 → aggregate max_open=5, once).

B4' RESOLVED — new db_pool.status_updated_at column (migration backfills from updated_at), set by all status-changing UPDATEs (unconditional on real transitions 505/534/622; CASE WHEN status <> ? on idempotent upserts 372/914/931); tenant_metrics.go:109 uses snapshot.StatusUpdatedAt, so unrelated ON UPDATE writes no longer reset age. Regression TestManagedSharedDBUpdatedAtTracksProgressNotNoOpPolls.

No new blockers. N1/N2/N3 tradeoff notes unchanged (evicted-live-tenant series retention, per-tenant success denominator, rollout label skew) — for qiffang's awareness, not blockers.

Verdict: GREEN. Coupling unchanged: #801 (60dfea19) + #802 (5e9af58e) must land in the same merge result.

@srstack

srstack commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator Author

Current-head review disposition for the auth/revocation finding:

  • 09debf24 moved tenant metric attribution after API-key-active and tenant-active checks, so rejected revoked/deleting/deleted traffic no longer creates tenant-labeled request/bytes/business-event series. The regression TestSetRequestMetricTenantForAuthStatusOnlyScopesActiveTenants covers cleanup followed by rejected traffic.
  • To close the cross-pod stale-cache window for live-tenant key revocation, 5e9af58e adds a separate durable TenantNotifyWorkAPIKeyCacheCleanup bit. RevokeAPIKey, RevokeTenantAPIKeys, and RevokeAPIKeysByIssuer write that bit in the same transaction as the revoke; every poller evicts only its local API-key resolve cache. It deliberately does not reuse WorkMetricsCleanup, so revoking one key cannot delete metrics for a live tenant.
  • TestTenantOutboxPollerAPIKeyCacheCleanupIsBroadcastWithoutDeletingMetrics verifies stale peer-cache eviction and live-tenant metric preservation.

The four concrete blockers in the earlier formal review were fixed in 09debf24; the current head also closes ordinary revoke propagation. The only remaining CI issue on the previous head was a global-histogram test assuming a zero baseline; 5e9af58e makes that assertion baseline-relative. Local pkg/meta, pkg/metrics, pkg/server, pkg/backend, and pkg/tenant/tidbcloudnative tests pass.

@qiffang qiffang 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.

adversary-1 R2 at 5e9af58e42779a11e9213dd1d4f0eb607723637b

Approved. I re-anchored after qiffang's latest request; there is no newer head after 5e9af58e, and both hosted code-ci and drive9-server-local e2e smoke are green on this exact commit.

The four prior metric-correctness blockers are closed:

  1. Cleanup-then-rejected traffic no longer recreates deleted tenant series. The auth paths now set tenant request metric scope only after active-tenant authorization (setRequestMetricTenantForAuthStatus), and revoked-key cleanup is broadcast through durable WorkAPIKeyCacheCleanup outbox rows so peer pods invalidate cached API-key resolutions without deleting live tenant metrics.

  2. DB role aggregate no longer false-greens when unscoped fork/delete pools coexist with tenant-scoped pools. Fork/delete now use datastore.OpenForTenant, and pkg/metrics/db.go folds unscoped saturation into the role aggregate instead of letting the unscoped row hide tenant-scoped capacity. The regression checks value, not just line count.

  3. TiDB Cloud delete unexpected 2xx responses no longer record ok. DeleteCluster and DeleteBranch now pass responseValid=false on fall-through, so a 202-style unexpected delete success records protocol_error; the regression covers both operations.

  4. shared_db_pool_status_age_seconds now uses true status-transition age. db_pool.status_updated_at is backfilled and updated only on status transitions, and metrics use StatusUpdatedAt instead of updated_at, so non-status writes cannot reset the age.

Notes: The remaining #801/#802 coupling is a merge-order/composition gate, not a blocker inside #802 itself. #801 still must not land its 20x cap unless this PR or equivalent metrics cleanup is already in the tested merge result; the current exact #801 + #802 synthetic merge still conflicts in pkg/tenant/pool_test.go, so whoever lands both must resolve and retest that combined state.

Validation performed:

  • Passed locally: git diff --check origin/main...HEAD, changed-file gofmt -l, go test ./pkg/metrics ./pkg/tenant/tidbcloudnative -count=1, test binary compilation for pkg/meta, pkg/server, pkg/backend, pkg/tenant, go vet ./pkg/metrics ./pkg/meta ./pkg/server ./pkg/backend ./pkg/tenant ./pkg/tenant/tidbcloudnative, and go build ./....
  • Hosted code-ci, drive9-server-local e2e smoke, and CodeRabbit are green on 5e9af58e.

@srstack
srstack merged commit c67c710 into main Jul 27, 2026
3 checks passed
@srstack
srstack deleted the fix/metrics-cardinality-observability branch July 27, 2026 04:03
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.

4 participants