Skip to content

feat(bigtable): add PeakEwma continuous time-decay latency tracker - #20187

Merged
sushanb merged 2 commits into
googleapis:mainfrom
sushanb:feat/bigtable-peak-ewma
Jul 22, 2026
Merged

feat(bigtable): add PeakEwma continuous time-decay latency tracker#20187
sushanb merged 2 commits into
googleapis:mainfrom
sushanb:feat/bigtable-peak-ewma

Conversation

@sushanb

@sushanb sushanb commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds PeakEwma, a thread-safe continuous-time exponentially-weighted moving average of latency samples. The upcoming AFE picker (Session subsystem) uses one instance per bucket to score candidates; landing the type standalone lets it merge ahead of its consumers.

  • peak_ewma.goPeakEwma struct, NewPeakEwma(tau), NewPeakEwmaSeeded(tau, seed), Update(latency), Value(). Decay weight is e^(-dt/tau) computed on every Update. First Update snaps value to the sample; subsequent updates blend prior and new proportionally.
  • NewPeakEwmaSeeded — Java parity: SessionList.java seeds transport at 500µs and e2e at 1ms so a brand-new AFE doesn't win the least-latency picker by looking free-cost. The seed is authoritative only until the first Update.

No callers on main yet — follow-up PRs for the AFE picker and sessionList will consume it.

Test plan

  • go build ./bigtable/...
  • go test ./bigtable/internal/transport/ -run '^TestPeakEwma' -count=10 -race — passes (1.6s)
  • gofmt -l bigtable/internal/transport/peak_ewma*.go — clean
  • go vet ./bigtable/internal/transport/ — clean

Test coverage:

  • Unseeded / seeded initial Value()
  • First Update overrides the seed (pins the documented behavior)
  • Constant-sample invariant — w*L + (1-w)*L == L for any weight, so the EWMA is exact on a flat stream
  • Convex-combination bound — Value stays within [min, max] of samples seen
  • Value() is a pure read (no state mutation)
  • Wide-gap dominance — dt >> tau ⇒ weight → 0 ⇒ Value tracks the newest sample (skipped under -short)
  • Mutex race check with mixed reader/writer goroutines (meaningful under -race)

Adds the PeakEwma type used by the upcoming AFE picker to score
sessionList buckets. Standalone (no callers on upstream/main yet) so the
type + tests can land ahead of the consumers.

- PeakEwma — thread-safe EWMA with e^(-dt/tau) continuous time-decay
  weighting. First Update snaps value to the sample; subsequent updates
  decay the prior and blend the new sample proportionally.
- NewPeakEwmaSeeded — Java-parity seed for cold AFEs so a brand-new
  bucket doesn't win the least-latency picker by looking free-cost
  (SessionList.java: transport 500us, e2e 1ms).

Tests cover the unseeded / seeded initial value, first-update-overrides-
seed, the constant-sample invariant (w*L + (1-w)*L == L for any weight),
the convex-combination bound (Value stays within [min, max] of samples
seen), Value() purity, wide-gap dominance (dt >> tau => new sample wins),
and a mutex race check with mixed reader/writer goroutines.
@sushanb
sushanb requested review from a team as code owners July 22, 2026 17:18
@product-auto-label product-auto-label Bot added the api: bigtable Issues related to the Bigtable API. label Jul 22, 2026

@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 PeakEwma, a thread-safe continuous-time exponentially-weighted moving average of latency samples, along with comprehensive unit tests. The review feedback points out potential robustness issues in the Update method, specifically that a zero decay constant (tau) combined with a zero time difference (dt) can result in NaN, and a backward clock jump (dt < 0) can cause the weight to exceed 1.0. A defensive code suggestion is provided to clamp dt and handle tau <= 0.

Comment on lines +59 to +74
func (e *PeakEwma) Update(latency time.Duration) {
e.mu.Lock()
defer e.mu.Unlock()
now := time.Now()
if e.lastUpdate.IsZero() {
e.value = float64(latency)
e.lastUpdate = now
return
}
dt := now.Sub(e.lastUpdate)
e.lastUpdate = now
// Continuous time-decay weight: e^(-dt/tau).
weight := math.Exp(-float64(dt) / float64(e.tau))
// EWMA = EWMA * weight + latency * (1 - weight).
e.value = e.value*weight + float64(latency)*(1-weight)
}

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.

medium

If e.tau is 0 (such as when using the zero-value of PeakEwma or if initialized with 0), and Update is called twice in rapid succession such that dt is 0, the weight calculation math.Exp(-0 / 0) will produce NaN. This permanently corrupts the EWMA value with NaN for all subsequent updates.

Additionally, if the system clock jumps backwards (resulting in dt < 0), the weight will exceed 1.0, violating the convex combination bounds and causing the EWMA to blow up or go negative.

To make the implementation robust and defensive against these edge cases, we should clamp dt to 0 and handle e.tau <= 0 by instantly decaying to the new sample.

func (e *PeakEwma) Update(latency time.Duration) {
	e.mu.Lock()
	defer e.mu.Unlock()
	now := time.Now()
	if e.lastUpdate.IsZero() {
		e.value = float64(latency)
		e.lastUpdate = now
		return
	}
	dt := now.Sub(e.lastUpdate)
	if dt < 0 {
		dt = 0
	}
	e.lastUpdate = now
	if e.tau <= 0 {
		e.value = float64(latency)
		return
	}
	// Continuous time-decay weight: e^(-dt/tau).
	weight := math.Exp(-float64(dt) / float64(e.tau))
	// EWMA = EWMA * weight + latency * (1 - weight).
	e.value = e.value*weight + float64(latency)*(1-weight)
}

…ive guards

Address session-reviewer + gemini-code-assist findings on PR googleapis#20187:

- Peak-snap: samples strictly higher than the current value snap up
  immediately with no blend. Without this, the tracker was a plain
  symmetric EWMA — a new AFE with one fast sample would look instantly
  cheap and steal traffic from the least-latency picker.
- Seed retained on first Update: the earlier lastUpdate.IsZero() branch
  snapped the value to the first sample, discarding the seed and
  defeating cold-start weighting. Constructors now stamp
  lastUpdate = time.Now() so the seed participates in the first
  blend normally.
- rtt <= 0 guard: zero or negative samples are dropped instead of
  corrupting the tracker.
- Backward-clock clamp: dt < 0 is clamped to 0 so a wall-clock rewind
  can't push the blend weight > 1 and shove Value outside sample range.
- tau <= 0 collapses decay to 0: guards against exp(-0/0) = NaN when
  tau and dt are both zero. Natural τ → 0 limit (new sample fully
  replaces).

Test changes:
- Drop TestPeakEwma_FirstUpdateOverridesSeed (assertion was wrong).
- Add TestPeakEwma_PeakSnapOnHigherSample.
- Add TestPeakEwma_SeedRetainedOnLowerFirstSample.
- Add TestPeakEwma_NonPositiveSampleIgnored.
- Add TestPeakEwma_ZeroTauSafe.
- Add TestPeakEwma_BackwardClockNoOvershoot.
- Rename LargeGapDominatesWithNewSample → LargeGapDominatesLowerSample
  (higher new samples now peak-snap; lower samples still blend).

12 tests pass under -race.
sushanb added a commit to sushanb/google-cloud-go that referenced this pull request Jul 22, 2026
The rationale for the seed values (500us / 1ms) stands on its own
without citing SessionList.java — the description now reads without
requiring the reader to have the Java tree handy. Peak-snap and the
other Java-parity fixes shipped on PR googleapis#20187 have not been ported to
this branch's peak_emwa.go yet; that's a separate change.
@sushanb
sushanb merged commit 9d124ef into googleapis:main Jul 22, 2026
19 checks passed
hongalex pushed a commit that referenced this pull request Jul 23, 2026
🤖 I have created a release *beep* *boop*
---


##
[1.51.0](bigtable/v1.50.0...bigtable/v1.51.0)
(2026-07-23)


### Features

* **bigtable:** Add ChainInterceptors and RetryingVRpc for vRPC pipeline
([#20185](#20185))
([c7a832a](c7a832a))
* **bigtable:** Add ClientConfigurationManager
([#19986](#19986))
([3a8f927](3a8f927))
* **bigtable:** Add debug tag counter (recordDebugTag / assertDebugTag)
([#20114](#20114))
([3c97590](3c97590))
* **bigtable:** Add lazyPool helper for on-demand session pool opening
([#20182](#20182))
([f6ae3fb](f6ae3fb))
* **bigtable:** Add PeakEwma continuous time-decay latency tracker
([#20187](#20187))
([9d124ef](9d124ef))
* **bigtable:** Add PoolSizer for server-driven session pool capacity
([#20189](#20189))
([57ebbeb](57ebbeb))
* **bigtable:** Add session package with SessionClient + SessionTableAPI
interfaces
([#20180](#20180))
([4b82fd2](4b82fd2))
* **bigtable:** Add Session primitives (AttemptOutcome, vRPC ctx,
msgtype)
([#20116](#20116))
([e1011e2](e1011e2))
* **bigtable:** Add Session state enum
([#19981](#19981))
([0748972](0748972))
* **bigtable:** Add SessionThrottler / AdaptiveSessionThrottler for
OpenSession pacing
([#20184](#20184))
([02e3c6d](02e3c6d))
* **bigtable:** Add SessionThrottler / AdaptiveSessionThrottler for
OpenSession pacing
([#20184](#20184))
([29be83e](29be83e))
* **bigtable:** Add sessionTracer for per-Session lifecycle + vRPC
metrics
([#20190](#20190))
([a466345](a466345))
* **bigtable:** Enable new auth library and JWT for instance admin
client
([#20013](#20013))
([21c4a44](21c4a44))
* **bigtable:** Modularize channel priming behind a ChannelPrimer
interface
([#20027](#20027))
([5214ab7](5214ab7))
* **bigtable:** Modularize Direct Access compatibility check
([#19987](#19987))
([a25e93d](a25e93d))
* **o11y:** Regenerate clients for LRO tracing
([#20107](#20107))
([779074e](779074e))


### Bug Fixes

* **bigtable:** Default cluster/zone in toOtelMetricAttrs to avoid
Monitoring reject
([#20178](#20178))
([14493f4](14493f4))
* **bigtable:** Eliminate stats-handler MD race in internal/metrics
tracer
([#20158](#20158))
([c387066](c387066))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api: bigtable Issues related to the Bigtable API.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants