Skip to content

fix(spanner): prevent fastpath tablet routing flaps - #20417

Open
rahul2393 wants to merge 5 commits into
mainfrom
fm/spanner-routing-flap-port-p1
Open

fix(spanner): prevent fastpath tablet routing flaps#20417
rahul2393 wants to merge 5 commits into
mainfrom
fm/spanner-routing-flap-port-p1

Conversation

@rahul2393

Copy link
Copy Markdown
Contributor

Ignore whole group updates from older generations. At equal generation, retain unavailable tablet freshness baselines until a newer incarnation permits recovery.

Refuse endpoint recreation for addresses absent from active finder membership, and serialize endpoint publication reconciliation with active-address removal.

Related Java PR googleapis/google-cloud-java#13803

Ignore whole group updates from older generations. At equal generation, retain unavailable tablet freshness baselines until a newer incarnation permits recovery.

Refuse endpoint recreation for addresses absent from active finder membership, and serialize endpoint publication reconciliation with active-address removal.
@rahul2393
rahul2393 requested review from a team as code owners August 24, 2026 14:42
@product-auto-label product-auto-label Bot added the api: spanner Issues related to the Spanner API. label Aug 24, 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 active address tracking in the endpointLifecycleManager and synchronizes it with the keyRangeCache to prevent routing flaps and ensure stale or removed endpoints are not incorrectly recreated. It also refactors tablet updates within cachedGroup to preserve proven addresses or skip states until incarnation advances. The review feedback highlights a subtle bug in this flap prevention logic where a stale update with an older incarnation could erroneously clear the skip state of a tablet that is currently skipped at a newer incarnation, and provides a code suggestion to address this issue.

Comment thread spanner/key_range_cache.go
…escalation

Track overload and unavailable failures in independent capped escalation lanes, honor server RetryInfo for overload cooldowns, and repair tiers after successful routed calls.

Reserve bounded probes after server-hinted delays while preserving fallback behavior for unhinted or unavailable cooldowns.
Track cooldown entries atomically so successful routed calls bypass clock and mutex work when no endpoint needs repair. Keep the counter synchronized with every map insertion and deletion, including repair, idle cleanup, and pruning.
@rahul2393

Copy link
Copy Markdown
Contributor Author

/gemini review

@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 enhances the Spanner client's location-aware routing and endpoint cooldown tracking by introducing independent failure lanes for overload and availability issues, success-based repair mechanisms, and better active address synchronization. The review feedback is highly constructive, pointing out a sorting bug in selectCoolingDownTabletLocked that fails to compare selection costs when no failures are present, suggesting the use of status.Code(err) to simplify gRPC status checks, and recommending the removal of redundant nil-guards where non-nil invariants are guaranteed.

Comment on lines +1330 to +1343
sort.SliceStable(candidates, func(i, j int) bool {
firstFailure := cooldowns.lastOverloadFailure(candidates[i].tablet.serverAddress)
secondFailure := cooldowns.lastOverloadFailure(candidates[j].tablet.serverAddress)
if firstFailure.IsZero() {
return false
}
if secondFailure.IsZero() {
return true
}
if !firstFailure.Equal(secondFailure) {
return firstFailure.Before(secondFailure)
}
return candidates[i].selectionCost < candidates[j].selectionCost
})

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.

high

In the sort.SliceStable comparator, if both firstFailure and secondFailure are zero (meaning both replicas are unavailable but have no overload failures), the first condition if firstFailure.IsZero() will immediately return false. This prevents the comparator from falling back to comparing their selectionCost, which means their relative order will not be correctly determined by their selection costs.

To fix this, we should explicitly handle the case where both failures are zero by comparing their selection costs first.

	sort.SliceStable(candidates, func(i, j int) bool {
		firstFailure := cooldowns.lastOverloadFailure(candidates[i].tablet.serverAddress)
		secondFailure := cooldowns.lastOverloadFailure(candidates[j].tablet.serverAddress)
		if firstFailure.IsZero() && secondFailure.IsZero() {
			return candidates[i].selectionCost < candidates[j].selectionCost
		}
		if firstFailure.IsZero() {
			return false
		}
		if secondFailure.IsZero() {
			return true
		}
		if !firstFailure.Equal(secondFailure) {
			return firstFailure.Before(secondFailure)
		}
		return candidates[i].selectionCost < candidates[j].selectionCost
	})

Comment on lines +179 to +185
var grpcStatus *status.Status
var spannerErr *Error
if errors.As(err, &spannerErr) {
grpcStatus = status.Convert(spannerErr.Unwrap())
} else {
grpcStatus = status.Convert(err)
}

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

Avoid implementing custom errors.As helpers to check for gRPC status codes. In Go, grpc-go's status.FromError natively walks the error chain using errors.As to inspect the GRPCStatus interface, meaning it already handles wrapped errors. We can simplify this by calling status.Code(err) directly to check the gRPC status code.

Suggested change
var grpcStatus *status.Status
var spannerErr *Error
if errors.As(err, &spannerErr) {
grpcStatus = status.Convert(spannerErr.Unwrap())
} else {
grpcStatus = status.Convert(err)
}
statusCode := status.Code(err)
References
  1. When working with gRPC in Go, use status.Code(err) directly to check for gRPC status codes instead of implementing custom errors.As helpers. grpc-go's status.FromError natively walks the error chain using errors.As to inspect the GRPCStatus interface, meaning it already handles wrapped errors (e.g., wrapped with %w).

Comment on lines +168 to +173
func (c *locationAwareSpannerClient) recordEndpointSuccess(ep channelEndpoint) {
if c == nil || ep == nil || ep.Address() == c.defaultEndpointAddress || c.endpointCooldowns == nil {
return
}
c.endpointCooldowns.recordSuccess(ep.Address())
}

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

Do not add defensive nil-guards on variables or parameters where invariants guaranteed by construction or call sites ensure they will never be nil. At all call sites of recordEndpointSuccess, the receiver c is guaranteed to be non-nil, and ep is guaranteed to be non-nil because the calls are guarded by !usedDefaultEndpoint (which is only false when ep is non-nil).

Suggested change
func (c *locationAwareSpannerClient) recordEndpointSuccess(ep channelEndpoint) {
if c == nil || ep == nil || ep.Address() == c.defaultEndpointAddress || c.endpointCooldowns == nil {
return
}
c.endpointCooldowns.recordSuccess(ep.Address())
}
func (c *locationAwareSpannerClient) recordEndpointSuccess(ep channelEndpoint) {
if c.endpointCooldowns == nil || ep.Address() == c.defaultEndpointAddress {
return
}
c.endpointCooldowns.recordSuccess(ep.Address())
}
References
  1. Do not add defensive nil-guards on variables or parameters where invariants guaranteed by construction or call sites ensure they will never be nil. Such guards result in dead code and can mask real bugs by silently no-oping instead of allowing a nil-panic to immediately surface the caller's mistake.

Comment on lines +297 to +303
func (m *endpointLifecycleManager) updateActiveAddresses(addresses map[string]struct{}) {
if m == nil {
return
}
if m.testHooks != nil && m.testHooks.beforeRemovalLock != nil {
m.testHooks.beforeRemovalLock()
}

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

Do not add defensive nil-guards on variables or parameters where invariants guaranteed by construction or call sites ensure they will never be nil. The receiver m is guaranteed to be non-nil at all call sites of updateActiveAddresses because they are explicitly guarded by if lifecycleManager != nil or are in tests where the manager is initialized.

Suggested change
func (m *endpointLifecycleManager) updateActiveAddresses(addresses map[string]struct{}) {
if m == nil {
return
}
if m.testHooks != nil && m.testHooks.beforeRemovalLock != nil {
m.testHooks.beforeRemovalLock()
}
func (m *endpointLifecycleManager) updateActiveAddresses(addresses map[string]struct{}) {
if m.testHooks != nil && m.testHooks.beforeRemovalLock != nil {
m.testHooks.beforeRemovalLock()
}
References
  1. Do not add defensive nil-guards on variables or parameters where invariants guaranteed by construction or call sites ensure they will never be nil. Such guards result in dead code and can mask real bugs by silently no-oping instead of allowing a nil-panic to immediately surface the caller's mistake.

…ards

Use selection cost when cooling candidates lack overload timestamps, and remove redundant nil guards from call paths that already guarantee initialized receivers and endpoints. Preserve explicit Spanner error unwrapping because direct gRPC status conversion drops RetryInfo details.
@rahul2393
rahul2393 force-pushed the fm/spanner-routing-flap-port-p1 branch from b7436c6 to 38d13b6 Compare August 24, 2026 15:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api: spanner Issues related to the Spanner API.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant