1717package com .google .cloud .spanner .spi .v1 ;
1818
1919import com .google .common .annotations .VisibleForTesting ;
20+ import io .grpc .Status ;
2021import java .time .Clock ;
2122import java .time .Duration ;
2223import java .time .Instant ;
2324import java .util .concurrent .ConcurrentHashMap ;
2425import java .util .concurrent .ThreadLocalRandom ;
26+ import java .util .concurrent .atomic .AtomicBoolean ;
2527import java .util .function .LongUnaryOperator ;
28+ import java .util .function .Predicate ;
29+ import javax .annotation .Nullable ;
2630
2731/**
28- * Tracks short-lived endpoint cooldowns after routed {@code RESOURCE_EXHAUSTED} failures.
32+ * Tracks short-lived endpoint cooldowns after routed overload or availability failures.
2933 *
30- * <p>This allows later requests to try a different replica instead of immediately routing back to
31- * the same overloaded endpoint.
34+ * <p>Server-hinted overloads and connection failures use separate escalation lanes because a short
35+ * load-shed response must not weaken protection for an unavailable endpoint.
3236 */
33- final class EndpointOverloadCooldownTracker {
37+ final class EndpointOverloadCooldownTracker implements Predicate < String > {
3438
3539 @ VisibleForTesting static final Duration DEFAULT_INITIAL_COOLDOWN = Duration .ofSeconds (10 );
3640 @ VisibleForTesting static final Duration DEFAULT_MAX_COOLDOWN = Duration .ofMinutes (1 );
3741 @ VisibleForTesting static final Duration DEFAULT_RESET_AFTER = Duration .ofMinutes (10 );
42+ @ VisibleForTesting static final int DEFAULT_SUCCESSES_PER_REPAIR = 3 ;
43+ @ VisibleForTesting static final Duration DEFAULT_MIN_HINTED_COOLDOWN = Duration .ofMillis (100 );
44+ @ VisibleForTesting static final Duration DEFAULT_MAX_HINTED_CLIENT_FLOOR = Duration .ofSeconds (2 );
45+ @ VisibleForTesting static final Duration DEFAULT_MAX_HINTED_JITTER = Duration .ofMillis (500 );
46+ @ VisibleForTesting static final int DEFAULT_MAX_TIER = 6 ;
47+
48+ private static final Duration MIN_PROBE_RESERVATION = Duration .ofMillis (250 );
49+ private static final Duration MAX_PROBE_RESERVATION = Duration .ofMillis (500 );
3850
3951 @ VisibleForTesting
4052 static final class CooldownState {
41- private final int consecutiveFailures ;
42- private final Instant cooldownUntil ;
43- private final Instant lastFailureAt ;
44-
45- private CooldownState (int consecutiveFailures , Instant cooldownUntil , Instant lastFailureAt ) {
46- this .consecutiveFailures = consecutiveFailures ;
47- this .cooldownUntil = cooldownUntil ;
48- this .lastFailureAt = lastFailureAt ;
53+ final int overloadFailures ;
54+ final int unavailableFailures ;
55+ final int successesTowardRepair ;
56+ final Instant overloadUntil ;
57+ final Instant unavailableUntil ;
58+ final Instant overloadProbeNotBefore ;
59+ final Instant probeReservedUntil ;
60+ @ Nullable final Instant lastOverloadFailureAt ;
61+ @ Nullable final Instant lastUnavailableFailureAt ;
62+
63+ private CooldownState (
64+ int overloadFailures ,
65+ int unavailableFailures ,
66+ int successesTowardRepair ,
67+ Instant overloadUntil ,
68+ Instant unavailableUntil ,
69+ Instant overloadProbeNotBefore ,
70+ Instant probeReservedUntil ,
71+ @ Nullable Instant lastOverloadFailureAt ,
72+ @ Nullable Instant lastUnavailableFailureAt ) {
73+ this .overloadFailures = overloadFailures ;
74+ this .unavailableFailures = unavailableFailures ;
75+ this .successesTowardRepair = successesTowardRepair ;
76+ this .overloadUntil = overloadUntil ;
77+ this .unavailableUntil = unavailableUntil ;
78+ this .overloadProbeNotBefore = overloadProbeNotBefore ;
79+ this .probeReservedUntil = probeReservedUntil ;
80+ this .lastOverloadFailureAt = lastOverloadFailureAt ;
81+ this .lastUnavailableFailureAt = lastUnavailableFailureAt ;
82+ }
83+ }
84+
85+ private static final class HintedCooldown {
86+ final Duration cooldown ;
87+ final Duration probeDelay ;
88+
89+ private HintedCooldown (Duration cooldown , Duration probeDelay ) {
90+ this .cooldown = cooldown ;
91+ this .probeDelay = probeDelay ;
4992 }
5093 }
5194
@@ -94,6 +137,11 @@ private CooldownState(int consecutiveFailures, Instant cooldownUntil, Instant la
94137 randomLong == null ? bound -> ThreadLocalRandom .current ().nextLong (bound ) : randomLong ;
95138 }
96139
140+ @ Override
141+ public boolean test (String address ) {
142+ return isCoolingDown (address );
143+ }
144+
97145 boolean isCoolingDown (String address ) {
98146 if (address == null || address .isEmpty ()) {
99147 return false ;
@@ -103,33 +151,198 @@ boolean isCoolingDown(String address) {
103151 if (state == null ) {
104152 return false ;
105153 }
106- if (state .cooldownUntil .isAfter (now )) {
154+ if (state .overloadUntil . isAfter ( now ) || state . unavailableUntil .isAfter (now )) {
107155 return true ;
108156 }
109- if (Duration . between (state . lastFailureAt , now ). compareTo ( resetAfter ) < 0 ) {
157+ if (! isIdle (state , now )) {
110158 return false ;
111159 }
112160 entries .remove (address , state );
113161 CooldownState current = entries .get (address );
114- return current != null && current .cooldownUntil .isAfter (now );
162+ return current != null
163+ && (current .overloadUntil .isAfter (now ) || current .unavailableUntil .isAfter (now ));
115164 }
116165
117166 void recordFailure (String address ) {
118- if (address == null || address .isEmpty ()) {
167+ recordFailure (address , Status .Code .RESOURCE_EXHAUSTED , null );
168+ }
169+
170+ void recordFailure (String address , Status .Code statusCode , @ Nullable Duration serverRetryDelay ) {
171+ if (address == null
172+ || address .isEmpty ()
173+ || (statusCode != Status .Code .RESOURCE_EXHAUSTED
174+ && statusCode != Status .Code .UNAVAILABLE )) {
119175 return ;
120176 }
121177 Instant now = clock .instant ();
122178 entries .compute (
123179 address ,
124180 (ignored , state ) -> {
125- int consecutiveFailures = 1 ;
126- if (state != null
127- && Duration .between (state .lastFailureAt , now ).compareTo (resetAfter ) < 0 ) {
128- consecutiveFailures = state .consecutiveFailures + 1 ;
181+ CooldownState current = state == null || isIdle (state , now ) ? emptyState (now ) : state ;
182+ if (statusCode == Status .Code .RESOURCE_EXHAUSTED ) {
183+ int failures =
184+ nextFailureTier (current .overloadFailures , current .lastOverloadFailureAt , now );
185+ Duration cooldown ;
186+ Duration probeDelay ;
187+ if (validRetryDelay (serverRetryDelay )) {
188+ HintedCooldown hinted = hintedOverloadCooldown (failures , serverRetryDelay );
189+ cooldown = hinted .cooldown ;
190+ probeDelay = hinted .probeDelay ;
191+ } else {
192+ cooldown = cooldownForFailures (failures );
193+ probeDelay = cooldown ;
194+ }
195+ return new CooldownState (
196+ failures ,
197+ current .unavailableFailures ,
198+ 0 ,
199+ later (current .overloadUntil , now .plus (cooldown )),
200+ current .unavailableUntil ,
201+ later (current .overloadProbeNotBefore , now .plus (probeDelay )),
202+ current .probeReservedUntil ,
203+ now ,
204+ current .lastUnavailableFailureAt );
205+ }
206+ int failures =
207+ nextFailureTier (current .unavailableFailures , current .lastUnavailableFailureAt , now );
208+ return new CooldownState (
209+ current .overloadFailures ,
210+ failures ,
211+ 0 ,
212+ current .overloadUntil ,
213+ later (current .unavailableUntil , now .plus (cooldownForFailures (failures ))),
214+ current .overloadProbeNotBefore ,
215+ current .probeReservedUntil ,
216+ current .lastOverloadFailureAt ,
217+ now );
218+ });
219+ }
220+
221+ void recordSuccess (String address ) {
222+ if (address == null || address .isEmpty ()) {
223+ return ;
224+ }
225+ Instant now = clock .instant ();
226+ entries .computeIfPresent (
227+ address ,
228+ (ignored , state ) -> {
229+ if (isIdle (state , now )) {
230+ return null ;
231+ }
232+ int successes = state .successesTowardRepair + 1 ;
233+ if (successes < DEFAULT_SUCCESSES_PER_REPAIR ) {
234+ return copyWithTiersAndSuccesses (
235+ state , state .overloadFailures , state .unavailableFailures , successes );
236+ }
237+ int overloadFailures = Math .max (0 , state .overloadFailures - 1 );
238+ int unavailableFailures = Math .max (0 , state .unavailableFailures - 1 );
239+ if (overloadFailures == 0
240+ && unavailableFailures == 0
241+ && !state .overloadUntil .isAfter (now )
242+ && !state .unavailableUntil .isAfter (now )) {
243+ return null ;
244+ }
245+ return copyWithTiersAndSuccesses (state , overloadFailures , unavailableFailures , 0 );
246+ });
247+ }
248+
249+ boolean tryReserveProbe (String address ) {
250+ if (address == null || address .isEmpty ()) {
251+ return false ;
252+ }
253+ Instant now = clock .instant ();
254+ AtomicBoolean reserved = new AtomicBoolean ();
255+ entries .computeIfPresent (
256+ address ,
257+ (ignored , state ) -> {
258+ if (!state .overloadUntil .isAfter (now )
259+ || state .unavailableUntil .isAfter (now )
260+ || state .overloadProbeNotBefore .isAfter (now )
261+ || state .probeReservedUntil .isAfter (now )) {
262+ return state ;
129263 }
130- Duration cooldown = cooldownForFailures (consecutiveFailures );
131- return new CooldownState (consecutiveFailures , now .plus (cooldown ), now );
264+ long minMillis = MIN_PROBE_RESERVATION .toMillis ();
265+ long rangeMillis =
266+ MAX_PROBE_RESERVATION .toMillis () - MIN_PROBE_RESERVATION .toMillis () + 1L ;
267+ Duration reservation = Duration .ofMillis (minMillis + randomLong .applyAsLong (rangeMillis ));
268+ reserved .set (true );
269+ return new CooldownState (
270+ state .overloadFailures ,
271+ state .unavailableFailures ,
272+ state .successesTowardRepair ,
273+ state .overloadUntil ,
274+ state .unavailableUntil ,
275+ state .overloadProbeNotBefore ,
276+ now .plus (reservation ),
277+ state .lastOverloadFailureAt ,
278+ state .lastUnavailableFailureAt );
132279 });
280+ return reserved .get ();
281+ }
282+
283+ @ Nullable
284+ Instant getLastOverloadFailureAt (String address ) {
285+ CooldownState state = entries .get (address );
286+ return state == null ? null : state .lastOverloadFailureAt ;
287+ }
288+
289+ private CooldownState emptyState (Instant now ) {
290+ return new CooldownState (0 , 0 , 0 , now , now , now , now , null , null );
291+ }
292+
293+ private CooldownState copyWithTiersAndSuccesses (
294+ CooldownState state , int overloadFailures , int unavailableFailures , int successes ) {
295+ return new CooldownState (
296+ overloadFailures ,
297+ unavailableFailures ,
298+ successes ,
299+ state .overloadUntil ,
300+ state .unavailableUntil ,
301+ state .overloadProbeNotBefore ,
302+ state .probeReservedUntil ,
303+ state .lastOverloadFailureAt ,
304+ state .lastUnavailableFailureAt );
305+ }
306+
307+ private boolean isRecent (@ Nullable Instant lastFailureAt , Instant now ) {
308+ return lastFailureAt != null && Duration .between (lastFailureAt , now ).compareTo (resetAfter ) < 0 ;
309+ }
310+
311+ private boolean isIdle (CooldownState state , Instant now ) {
312+ return !state .overloadUntil .isAfter (now )
313+ && !state .unavailableUntil .isAfter (now )
314+ && !isRecent (state .lastOverloadFailureAt , now )
315+ && !isRecent (state .lastUnavailableFailureAt , now );
316+ }
317+
318+ private int nextFailureTier (int previousFailures , @ Nullable Instant lastFailureAt , Instant now ) {
319+ if (previousFailures == 0 || !isRecent (lastFailureAt , now )) {
320+ return 1 ;
321+ }
322+ return Math .min (DEFAULT_MAX_TIER , previousFailures + 1 );
323+ }
324+
325+ private static boolean validRetryDelay (@ Nullable Duration retryDelay ) {
326+ // A present zero delay uses the 100 ms floor; an absent hint uses unhinted backoff.
327+ return retryDelay != null && !retryDelay .isNegative ();
328+ }
329+
330+ private HintedCooldown hintedOverloadCooldown (int failures , Duration serverRetryDelay ) {
331+ Duration serverFloor =
332+ serverRetryDelay .compareTo (DEFAULT_MIN_HINTED_COOLDOWN ) < 0
333+ ? DEFAULT_MIN_HINTED_COOLDOWN
334+ : serverRetryDelay ;
335+ Duration clientFloor = DEFAULT_MIN_HINTED_COOLDOWN ;
336+ for (int i = 1 ;
337+ i < failures && clientFloor .compareTo (DEFAULT_MAX_HINTED_CLIENT_FLOOR ) < 0 ;
338+ i ++) {
339+ clientFloor = min (clientFloor .multipliedBy (2 ), DEFAULT_MAX_HINTED_CLIENT_FLOOR );
340+ }
341+ Duration base = serverFloor .compareTo (clientFloor ) >= 0 ? serverFloor : clientFloor ;
342+ Duration jitterLimit = min (base .dividedBy (4 ), DEFAULT_MAX_HINTED_JITTER );
343+ long jitterRangeMillis = jitterLimit .toMillis () + 1L ;
344+ Duration jitter = Duration .ofMillis (randomLong .applyAsLong (jitterRangeMillis ));
345+ return new HintedCooldown (base .plus (jitter ), serverFloor .plus (jitter ));
133346 }
134347
135348 private Duration cooldownForFailures (int failures ) {
@@ -147,6 +360,14 @@ private Duration cooldownForFailures(int failures) {
147360 return Duration .ofMillis (floorMillis + randomLong .applyAsLong (rangeSize ));
148361 }
149362
363+ private static Duration min (Duration first , Duration second ) {
364+ return first .compareTo (second ) <= 0 ? first : second ;
365+ }
366+
367+ private static Instant later (Instant first , Instant second ) {
368+ return first .isAfter (second ) ? first : second ;
369+ }
370+
150371 @ VisibleForTesting
151372 CooldownState getState (String address ) {
152373 return entries .get (address );
0 commit comments