Skip to content

Commit 749289a

Browse files
Anton Ivanovjenkins
Anton Ivanov
authored and
jenkins
committed
[finagle-core] deprecate Backoff.equalJittered
# Problem `equalJittered` is a strange Backoff policy: - the first duration is not jittered; - it's actually exponential, but we already have `exponentialJittered` policy; - its' name - `equal` - does not tell much (to me); - scaladoc says it "keeps half of the exponential growth", it's not clear what's meant by "half": it multiplies the original `startDuration` by 2 each time, similar to `exponentialJittered` policy; - scaladoc says it has "jitter between 0 and that amount", which can be confusing, because the jitter is between the current and the next duration; - it's hard to explain where `equalJittered` should be used instead of `exponentialJittered` and vice versa, they are very similar; # Solution Remove `equalJittered`, fallback to `exponentialJittered`. JIRA Issues: STOR-8883 Differential Revision: https://phabricator.twitter.biz/D1182535
1 parent c838d2b commit 749289a

File tree

8 files changed

+43
-124
lines changed

8 files changed

+43
-124
lines changed

CHANGELOG.rst

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ Runtime Behavior Changes
1717
* finagle-netty4: `EventLoopGroupTracker` now collects the distribution of cpu utilization by each netty thread
1818
and all_sockets instead of active_sockets. ``PHAB_ID=D1177719``
1919
* finagle-core: `Backoff.exponentialJittered` now jitter the first duration. ``PHAB_ID=D1182252``
20-
* finalge-core: `Backoff.exponentialJittered` now uses a new range for jitters: `[dur/2; dur + dur/2]`.
20+
* finagle-core: `Backoff.exponentialJittered` now uses a new range for jitters: `[dur/2; dur + dur/2]`.
2121
Previously it was `[0, dur)`, which could result in `next.duration < duration`
2222
for arbitrary long invocation chains. ``PHAB_ID=D1182252``
23-
23+
* finagle-core: `Backoff.equalJittered` is now deprecated and falls back to `exponentialJittered`. ``PHAB_ID=D1182535``
2424

2525
New Features
2626
~~~~~~~~~~

doc/src/sphinx/Clients.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,7 @@ The following example [#example]_ constructs an instance of ``RetryPolicy`` usin
371371
import com.twitter.conversions.DurationOps._
372372
373373
val policy: RetryPolicy[Try[Response]] =
374-
RetryPolicy.backoff(Backoff.equalJittered(10.milliseconds, 10.seconds)) {
374+
RetryPolicy.backoff(Backoff.exponentialJittered(10.milliseconds, 10.seconds)) {
375375
case Return(rep) if rep.status == Status.InternalServerError => true
376376
}
377377

finagle-benchmark/src/main/scala/com/twitter/finagle/service/BackoffBenchmark.scala

-9
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,6 @@ class BackoffBenchmark extends StdBenchAnnotations {
1818
@Benchmark
1919
def constant(state: Constant): Duration = state.next()
2020

21-
@Benchmark
22-
def equalJittered(state: EqualJittered): Duration = state.next()
23-
2421
@Benchmark
2522
def exponentialJittered(state: ExponentialJittered): Duration = state.next()
2623

@@ -50,12 +47,6 @@ object BackoffBenchmark {
5047
Backoff.const(10.seconds).concat(Backoff.const(300.seconds))
5148
)
5249

53-
@State(Scope.Thread)
54-
class EqualJittered
55-
extends BackoffState(
56-
Backoff.equalJittered(5.seconds, 300.seconds).concat(Backoff.const(300.seconds))
57-
)
58-
5950
@State(Scope.Thread)
6051
class ExponentialJittered
6152
extends BackoffState(

finagle-core/src/main/scala/com/twitter/finagle/Backoff.scala

+5-57
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,7 @@ object Backoff {
120120
* @param maximum must be greater than 0 and greater than or equal to
121121
* `start`.
122122
*
123-
* @see [[exponentialJittered]] and [[equalJittered]] for alternative
124-
* jittered approaches.
123+
* @see [[exponentialJittered]] for alternative jittered approaches.
125124
*/
126125
def decorrelatedJittered(start: Duration, maximum: Duration): Backoff = {
127126

@@ -135,27 +134,9 @@ object Backoff {
135134
else new DecorrelatedJittered(start, maximum, Rng.threadLocal)
136135
}
137136

138-
/**
139-
* Create backoffs that keep half of the exponential growth, and jitter
140-
* between 0 and that amount.
141-
*
142-
* @param start must be greater than 0 and less than or equal to `maximum`.
143-
* @param maximum must be greater than 0 and greater than or equal to
144-
* `start`.
145-
*
146-
* @see [[decorrelatedJittered]] and [[exponentialJittered]] for alternative
147-
* jittered approaches.
148-
*/
137+
@deprecated("User `.exponentialJittered(Duration, Duration)` instead", "2024-11-13")
149138
def equalJittered(start: Duration, maximum: Duration): Backoff = {
150-
151-
require(start > Duration.Zero)
152-
require(maximum > Duration.Zero)
153-
require(start <= maximum)
154-
155-
// compare start and maximum here to avoid one
156-
// iteration of creating a new `EqualJittered`.
157-
if (start == maximum) new Const(start)
158-
else new EqualJittered(start, start, maximum, 1, Rng.threadLocal)
139+
exponentialJittered(start, maximum)
159140
}
160141

161142
/**
@@ -175,8 +156,7 @@ object Backoff {
175156
* @param maximum must be greater than 0 and greater than or equal to
176157
* `start`.
177158
*
178-
* @see [[decorrelatedJittered]] and [[equalJittered]] for alternative
179-
* jittered approaches.
159+
* @see [[decorrelatedJittered]] for alternative jittered approaches.
180160
*/
181161
def exponentialJittered(start: Duration, maximum: Duration): Backoff = {
182162

@@ -281,36 +261,6 @@ object Backoff {
281261
def isExhausted: Boolean = false
282262
}
283263

284-
/** @see [[Backoff.equalJittered]] as the api to create this strategy. */
285-
// exposed for testing
286-
private[finagle] final class EqualJittered(
287-
startDuration: Duration,
288-
nextDuration: Duration,
289-
maximum: Duration,
290-
attempt: Int,
291-
rng: Rng)
292-
extends Backoff {
293-
294-
// Don't shift left more than 62 bits to avoid
295-
// Long overflow of the multiplier `shift`.
296-
private[this] final val MaxBitShift = 62
297-
298-
def duration: Duration = nextDuration
299-
300-
def next: Backoff = {
301-
val shift = 1L << MaxBitShift.min(attempt - 1)
302-
// in case of Long overflow
303-
val halfExp = if (startDuration >= maximum / shift) maximum else startDuration * shift
304-
val randomBackoff = Duration.fromNanoseconds(rng.nextLong(halfExp.inNanoseconds))
305-
306-
// in case of Long overflow
307-
if (halfExp == maximum || halfExp >= maximum - randomBackoff) new Const(maximum)
308-
else new EqualJittered(startDuration, halfExp + randomBackoff, maximum, attempt + 1, rng)
309-
}
310-
311-
def isExhausted: Boolean = false
312-
}
313-
314264
/** @see [[Backoff.exponentialJittered]] as the api to create this strategy. */
315265
// exposed for testing
316266
private[finagle] final class ExponentialJittered(
@@ -427,14 +377,12 @@ object Backoff {
427377
* - Create backoffs that grow linearly, can be created via `Backoff.linear`.
428378
* 1. DecorrelatedJittered
429379
* - Create backoffs that jitter randomly between a start value and 3 times of that value, can be created via `Backoff.decorrelatedJittered`.
430-
* 1. EqualJittered
431-
* - Create backoffs that jitter between 0 and half of the exponential growth. Can be created via `Backoff.equalJittered`.
432380
* 1. ExponentialJittered
433381
* - Create backoffs that jitter randomly between value/2 and value+value/2 that grows exponentially by 2. Can be created via `Backoff.exponentialJittered`.
434382
*
435383
* @note A new [[Backoff]] will be created only when `next` is called.
436384
* @note None of the [[Backoff]]s are memoized, for strategies that involve
437-
* randomness (`DecorrelatedJittered`, `EqualJittered` and
385+
* randomness (`DecorrelatedJittered` and
438386
* `ExponentialJittered`), there is no way to foresee the next backoff
439387
* value.
440388
* @note All [[Backoff]]s are infinite unless using [[take(Int)]] to create a

finagle-core/src/main/scala/com/twitter/finagle/liveness/FailureAccrualFactory.scala

+8-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package com.twitter.finagle.liveness
22

33
import com.twitter.conversions.DurationOps._
4-
import com.twitter.finagle.Stack.{Params, Role}
4+
import com.twitter.finagle.Stack.Params
5+
import com.twitter.finagle.Stack.Role
56
import com.twitter.finagle._
67
import com.twitter.finagle.client.Transporter
7-
import com.twitter.finagle.service.{ReqRep, ResponseClass, ResponseClassifier}
8+
import com.twitter.finagle.service.ReqRep
9+
import com.twitter.finagle.service.ResponseClass
10+
import com.twitter.finagle.service.ResponseClassifier
811
import com.twitter.finagle.stats.StatsReceiver
912
import com.twitter.logging.Logger
1013
import com.twitter.util._
@@ -20,12 +23,13 @@ object FailureAccrualFactory {
2023
private[this] val DefaultMinimumRequestThreshold =
2124
FailureAccrualPolicy.DefaultMinimumRequestThreshold
2225

23-
// Use equalJittered backoff in order to wait more time in between
26+
// Use exponentialJittered backoff in order to wait more time in between
2427
// each revival attempt on successive failures; if an endpoint has failed
2528
// previous requests, it is likely to do so again. The recent
2629
// "failure history" should influence how long to mark the endpoint
2730
// dead for.
28-
private[finagle] val jitteredBackoff: Backoff = Backoff.equalJittered(5.seconds, 300.seconds)
31+
private[finagle] val jitteredBackoff: Backoff =
32+
Backoff.exponentialJittered(5.seconds, 300.seconds)
2933

3034
private[finagle] def defaultPolicy: Function0[FailureAccrualPolicy] =
3135
new Function0[FailureAccrualPolicy] {

finagle-core/src/test/scala/com/twitter/finagle/BackoffTest.scala

-34
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package com.twitter.finagle
22

33
import com.twitter.conversions.DurationOps._
44
import com.twitter.finagle.Backoff.DecorrelatedJittered
5-
import com.twitter.finagle.Backoff.EqualJittered
65
import com.twitter.finagle.Backoff.ExponentialJittered
76
import com.twitter.finagle.util.Rng
87
import com.twitter.util.Duration
@@ -104,39 +103,6 @@ class BackoffTest extends AnyFunSuite with ScalaCheckDrivenPropertyChecks {
104103
}
105104
}
106105

107-
test("equalJittered") {
108-
val equalGen = for {
109-
startMs <- Gen.choose(1L, 1000L)
110-
maxMs <- Gen.choose(startMs, startMs * 2)
111-
seed <- Gen.choose(Long.MinValue, Long.MaxValue)
112-
} yield (startMs, maxMs, seed)
113-
114-
forAll(equalGen) {
115-
case (startMs: Long, maxMs: Long, seed: Long) =>
116-
val rng = Rng(seed)
117-
val backoff: Backoff =
118-
new EqualJittered(startMs.millis, startMs.millis, maxMs.millis, 1, Rng(seed))
119-
val result: ArrayBuffer[Duration] = new ArrayBuffer[Duration]()
120-
var start = startMs.millis
121-
for (attempt <- 1 to 7) {
122-
result.append(start)
123-
start = nextStart(startMs.millis, maxMs.millis, rng, attempt)
124-
}
125-
verifyBackoff(backoff, result.toSeq, exhausted = false)
126-
}
127-
128-
def nextStart(start: Duration, maximum: Duration, rng: Rng, attempt: Int): Duration = {
129-
val shift = 1L << (attempt - 1)
130-
// in case of Long overflow
131-
val halfExp = if (start >= maximum / shift) maximum else start * shift
132-
val randomBackoff = Duration.fromNanoseconds(rng.nextLong(halfExp.inNanoseconds))
133-
134-
// in case of Long overflow
135-
if (halfExp == maximum || halfExp >= maximum - randomBackoff) maximum
136-
else halfExp + randomBackoff
137-
}
138-
}
139-
140106
test("exponentialJittered") {
141107
val exponentialGen = for {
142108
startNs <- Gen.choose(1L, Long.MaxValue)

finagle-core/src/test/scala/com/twitter/finagle/liveness/FailureAccrualFactoryTest.scala

+13-8
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
package com.twitter.finagle.liveness
22

33
import com.twitter.conversions.DurationOps._
4-
import com.twitter.finagle.Backoff.EqualJittered
54
import com.twitter.finagle.service._
65
import com.twitter.finagle.stats.InMemoryStatsReceiver
76
import com.twitter.finagle.stats.NullStatsReceiver
87
import com.twitter.finagle.util.Rng
98
import com.twitter.finagle.Backoff
9+
import com.twitter.finagle.Backoff.ExponentialJittered
1010
import com.twitter.finagle._
1111
import com.twitter.util._
1212
import java.util.concurrent.TimeUnit
@@ -22,11 +22,10 @@ import scala.util.Random
2222
import org.scalatest.funsuite.AnyFunSuite
2323

2424
class FailureAccrualFactoryTest extends AnyFunSuite with MockitoSugar {
25-
// since `EqualJittered` generates values randomly, we pass the seed
25+
// since `ExponentialJittered` generates values randomly, we pass the seed
2626
// here in order to validate the values returned in the tests.
2727
def markDeadFor(seed: Long): Backoff =
28-
new EqualJittered(5.seconds, 5.seconds, 60.seconds, 1, Rng(seed))
29-
def markDeadForList(seed: Long) = markDeadFor(seed).take(6)
28+
new ExponentialJittered(5.seconds.inNanoseconds, 60.seconds.inNanoseconds, Rng(seed))
3029
def consecutiveFailures(seed: Long): FailureAccrualPolicy =
3130
FailureAccrualPolicy.consecutiveFailures(3, markDeadFor(seed))
3231

@@ -284,7 +283,9 @@ class FailureAccrualFactoryTest extends AnyFunSuite with MockitoSugar {
284283

285284
// Backoff to verify against from the backoff passed to create a FailureAccrual policy
286285
// Should make sure to use the same seed
287-
var backoffs = new EqualJittered(5.seconds, 5.seconds, 60.seconds, 1, Rng(8888)).take(6)
286+
var backoffs =
287+
new ExponentialJittered(5.seconds.inNanoseconds, 60.seconds.inNanoseconds, Rng(8888))
288+
.take(6)
288289
while (!backoffs.isExhausted) {
289290
assert(statsReceiver.counters.get(List("removals")) == Some(1))
290291
assert(!factory.isAvailable)
@@ -323,9 +324,11 @@ class FailureAccrualFactoryTest extends AnyFunSuite with MockitoSugar {
323324

324325
test("backoff should be 5 minutes when stream runs out") {
325326
// Backoff to pass to create a FailureAccrual policy
326-
val markDeadForFA = new EqualJittered(5.seconds, 5.seconds, 60.seconds, 1, Rng(7777)).take(3)
327+
val markDeadForFA =
328+
new ExponentialJittered(5.seconds.inNanoseconds, 60.seconds.inNanoseconds, Rng(7777)).take(3)
327329
// Backoff to verify, should use the same seed as the policy passed to FA
328-
var markDeadFor = new EqualJittered(5.seconds, 5.seconds, 60.seconds, 1, Rng(7777)).take(3)
330+
var markDeadFor =
331+
new ExponentialJittered(5.seconds.inNanoseconds, 60.seconds.inNanoseconds, Rng(7777)).take(3)
329332

330333
val statsReceiver = new InMemoryStatsReceiver()
331334
val underlyingService = mock[Service[Int, Int]]
@@ -433,7 +436,9 @@ class FailureAccrualFactoryTest extends AnyFunSuite with MockitoSugar {
433436

434437
// Backoff to verify against from the backoff passed to create a FailureAccrual policy
435438
// Should make sure to use the same seed
436-
var markDeadFor = new EqualJittered(5.seconds, 5.seconds, 60.seconds, 1, Rng(9999)).take(6)
439+
var markDeadFor =
440+
new ExponentialJittered(5.seconds.inNanoseconds, 60.seconds.inNanoseconds, Rng(9999))
441+
.take(6)
437442
for (_ <- 1 until 6) {
438443
// After another failure, the service should be unavailable
439444
intercept[Exception] {

finagle-core/src/test/scala/com/twitter/finagle/liveness/FailureAccrualPolicyTest.scala

+14-9
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package com.twitter.finagle.liveness
22

33
import com.twitter.conversions.DurationOps._
44
import com.twitter.finagle.Backoff
5-
import com.twitter.finagle.Backoff.EqualJittered
5+
import com.twitter.finagle.Backoff.ExponentialJittered
66
import com.twitter.finagle.util.Rng
77
import com.twitter.util._
88
import org.scalatestplus.mockito.MockitoSugar
@@ -11,10 +11,10 @@ import org.scalatest.funsuite.AnyFunSuite
1111
class FailureAccrualPolicyTest extends AnyFunSuite with MockitoSugar {
1212

1313
private[this] val constantBackoff = Backoff.const(5.seconds)
14-
// since `EqualJittered` generates values randomly, we pass the seed
14+
// since `ExponentialJittered` generates values randomly, we pass the seed
1515
// here in order to validate the values returned in the tests.
1616
private[this] def expBackoff(seed: Long) =
17-
new EqualJittered(5.seconds, 5.seconds, 60.seconds, 1, Rng(seed))
17+
new ExponentialJittered(5.seconds.inNanoseconds, 60.seconds.inNanoseconds, Rng(seed))
1818
private[this] def expBackoffList(seed: Long) = expBackoff(seed).take(6)
1919

2020
test("Consecutive failures policy: fail on nth attempt") {
@@ -219,14 +219,15 @@ class FailureAccrualPolicyTest extends AnyFunSuite with MockitoSugar {
219219
expBackoffList(333),
220220
5,
221221
Stopwatch.timeMillis)
222+
val backoff = expBackoffList(333)
222223

223224
timeControl.advance(30.seconds)
224225

225226
assert(policy.markDeadOnFailure() == None)
226227
assert(policy.markDeadOnFailure() == None)
227228
assert(policy.markDeadOnFailure() == None)
228229
assert(policy.markDeadOnFailure() == None)
229-
assert(policy.markDeadOnFailure() == Some(5.seconds))
230+
assert(policy.markDeadOnFailure() == Some(backoff.duration))
230231
}
231232
}
232233

@@ -245,36 +246,40 @@ class FailureAccrualPolicyTest extends AnyFunSuite with MockitoSugar {
245246

246247
test("Hybrid policy: fail on nth attempt") {
247248
val policy = hybridPolicy
249+
val backoff = expBackoff(333) // same as in hybridPolicy
248250
assert(policy.markDeadOnFailure() == None)
249251
assert(policy.markDeadOnFailure() == None)
250-
assert(policy.markDeadOnFailure() == Some(5.seconds))
252+
assert(policy.markDeadOnFailure() == Some(backoff.duration))
251253
}
252254

253255
test("Hybrid policy: failures reset to zero on revived()") {
254256
val policy = hybridPolicy
257+
val backoff = expBackoff(333) // same as in hybridPolicy
255258
assert(policy.markDeadOnFailure() == None)
256259

257260
policy.revived()
258261

259262
assert(policy.markDeadOnFailure() == None)
260263
assert(policy.markDeadOnFailure() == None)
261-
assert(policy.markDeadOnFailure() == Some(5.seconds))
264+
assert(policy.markDeadOnFailure() == Some(backoff.duration))
262265
}
263266

264267
test("Hybrid policy: failures reset to zero on success") {
265268
val policy = hybridPolicy
269+
val backoff = expBackoff(333) // same as in hybridPolicy
266270
assert(policy.markDeadOnFailure() == None)
267271

268272
policy.recordSuccess()
269273

270274
assert(policy.markDeadOnFailure() == None)
271275
assert(policy.markDeadOnFailure() == None)
272-
assert(policy.markDeadOnFailure() == Some(5.seconds))
276+
assert(policy.markDeadOnFailure() == Some(backoff.duration))
273277
}
274278

275279
test("Hybrid policy: uses windowed success rate as well as consecutive failure") {
276280
Time.withCurrentTimeFrozen { timeControl =>
277281
val policy = hybridPolicy
282+
val backoff = expBackoff(333)
278283

279284
for (i <- 0 until 15) {
280285
policy.recordSuccess()
@@ -284,13 +289,13 @@ class FailureAccrualPolicyTest extends AnyFunSuite with MockitoSugar {
284289
timeControl.advance(1.second)
285290
}
286291

287-
assert(policy.markDeadOnFailure() == Some(5.seconds))
292+
assert(policy.markDeadOnFailure() == Some(backoff.duration))
288293

289294
policy.revived()
290295

291296
assert(policy.markDeadOnFailure() == None)
292297
assert(policy.markDeadOnFailure() == None)
293-
assert(policy.markDeadOnFailure() == Some(5.seconds))
298+
assert(policy.markDeadOnFailure() == Some(backoff.duration))
294299
}
295300
}
296301

0 commit comments

Comments
 (0)