Skip to content

Commit 171b881

Browse files
Anton Ivanovjenkins
Anton Ivanov
authored and
jenkins
committed
[finagle-core] fix ExponentialJittered and Rng.nextLong(n)
# Problems - Rng.nextLong(n) can be < 0 - ExponentialJittered first duration is always `start` without any jitter, so all services make the first retry simultaneously, potentially overwhelming a target service even more - ExponentialJittered.duration can be > ExponentialJittered.next.duration for unlimited number of times, because it chooses a random value starting from 1 # Solution Change `ExponentialJittered` to return a random number between `start/2` and `start + start/2`. Advance `start` as `start * 2` for each `next` `ExponentialJittered`. JIRA Issues: STOR-8883 Differential Revision: https://phabricator.twitter.biz/D1182252
1 parent bc815d5 commit 171b881

File tree

5 files changed

+95
-60
lines changed

5 files changed

+95
-60
lines changed

CHANGELOG.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ Runtime Behavior Changes
1616
stats cpu_time_ms and active_sockets per netty worker thread.
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``
19+
* 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]`.
21+
Previously it was `[0, dur)`, which could result in `next.duration < duration`
22+
for arbitrary long invocation chains. ``PHAB_ID=D1182252``
1923

2024

2125
New Features

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

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -159,17 +159,22 @@ object Backoff {
159159
}
160160

161161
/**
162-
* The formula to calculate the next backoff is:
163-
*{{{
164-
* backoff = random_between(0, min({@code maximum}, {@code start} * 2 ** attempt))
165-
*}}}
162+
* Current duration is:
163+
* {{{
164+
* random_between(start/2, start + start/2)
165+
* }}}
166+
* but >= 1 and <= maximum.
167+
*
168+
* Next backoff is:
169+
* {{{
170+
* exponentialJitterred(start * 2, maximum)
171+
* }}}
172+
* and Const(maximum) after start >= maximum
166173
*
167174
* @param start must be greater than 0 and less than or equal to `maximum`.
168175
* @param maximum must be greater than 0 and greater than or equal to
169176
* `start`.
170177
*
171-
* @note This is "full jitter" via
172-
* https://www.awsarchitectureblog.com/2015/03/backoff.html
173178
* @see [[decorrelatedJittered]] and [[equalJittered]] for alternative
174179
* jittered approaches.
175180
*/
@@ -182,7 +187,7 @@ object Backoff {
182187
// compare start and maximum here to avoid one
183188
// iteration of creating a new `ExponentialJittered`.
184189
if (start == maximum) new Const(start)
185-
else new ExponentialJittered(start, maximum, 1, Rng.threadLocal)
190+
else new ExponentialJittered(start.inNanoseconds, maximum.inNanoseconds, Rng.threadLocal)
186191
}
187192

188193
/**
@@ -309,32 +314,42 @@ object Backoff {
309314
/** @see [[Backoff.exponentialJittered]] as the api to create this strategy. */
310315
// exposed for testing
311316
private[finagle] final class ExponentialJittered(
312-
start: Duration,
313-
maximum: Duration,
314-
attempt: Int,
317+
meanNanos: Long,
318+
maxNanos: Long,
315319
rng: Rng)
316320
extends Backoff {
317321

318-
// Don't shift left more than 62 bits to avoid
319-
// Long overflow of the multiplier `shift`.
320-
private[this] final val MaxBitShift = 62
322+
private[this] val backoffNanos = {
323+
// gen random around min
324+
val curMinNanos = Math.max(meanNanos >> 1, 1L)
325+
var curMaxNanos = meanNanos + curMinNanos
326+
if (curMaxNanos <= meanNanos) { // overflow
327+
curMaxNanos = Long.MaxValue
328+
}
329+
Math.min(
330+
curMinNanos + rng.nextLong(curMaxNanos - curMinNanos + 1L),
331+
maxNanos
332+
)
333+
}
321334

322-
def duration: Duration = start
335+
def duration: Duration = Duration.fromNanoseconds(backoffNanos)
323336

324337
def next: Backoff = {
325-
val shift = 1L << MaxBitShift.min(attempt)
326-
// to avoid Long overflow
327-
val maxBackoff = if (start >= maximum / shift) maximum else start * shift
328-
if (maxBackoff == maximum) {
329-
new Const(maximum)
338+
if (meanNanos >= maxNanos) {
339+
new Const(Duration.fromNanoseconds(maxNanos))
330340
} else {
331-
// to avoid the case of random being 0
332-
val random = 1 + rng.nextLong(maxBackoff.inNanoseconds)
333-
new ExponentialJittered(Duration.fromNanoseconds(random), maximum, attempt + 1, rng)
341+
var nextMeanNanos = meanNanos << 1
342+
if (nextMeanNanos <= meanNanos) { // overflow
343+
nextMeanNanos = Long.MaxValue
344+
}
345+
new ExponentialJittered(nextMeanNanos, maxNanos, rng)
334346
}
335347
}
336348

337349
def isExhausted: Boolean = false
350+
351+
override def toString: String =
352+
s"${this.getClass.getSimpleName}($meanNanos,$maxNanos)"
338353
}
339354

340355
/** @see [[Backoff.take]] as the api to create this strategy. */
@@ -415,7 +430,7 @@ object Backoff {
415430
* 1. EqualJittered
416431
* - Create backoffs that jitter between 0 and half of the exponential growth. Can be created via `Backoff.equalJittered`.
417432
* 1. ExponentialJittered
418-
* - Create backoffs that jitter randomly between 0 and a value that grows exponentially by 2. Can be created via `Backoff.exponentialJittered`.
433+
* - Create backoffs that jitter randomly between value/2 and value+value/2 that grows exponentially by 2. Can be created via `Backoff.exponentialJittered`.
419434
*
420435
* @note A new [[Backoff]] will be created only when `next` is called.
421436
* @note None of the [[Backoff]]s are memoized, for strategies that involve

finagle-core/src/main/scala/com/twitter/finagle/util/Rng.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,11 @@ object Rng {
4949
def nextLong(n: Long): Long = {
5050
require(n > 0)
5151

52-
// This is the algorithm used by Java's random number generator
53-
// internally.
52+
// This algorithm is inspired by Java's random number generator
5453
// https://docs.oracle.com/javase/6/docs/api/java/util/Random.html#nextInt(int)
55-
if ((n & -n) == n)
56-
return r.nextLong() % n
54+
val m = n - 1
55+
if ((n & m) == 0L) // power of 2
56+
return r.nextLong() & m
5757

5858
var bits = 0L
5959
var v = 0L

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

Lines changed: 39 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -139,37 +139,45 @@ class BackoffTest extends AnyFunSuite with ScalaCheckDrivenPropertyChecks {
139139

140140
test("exponentialJittered") {
141141
val exponentialGen = for {
142-
startMs <- Gen.choose(1L, 1000L)
143-
maxMs <- Gen.choose(startMs, startMs * 2)
142+
startNs <- Gen.choose(1L, Long.MaxValue)
143+
maxNs <- Gen.choose(startNs, Long.MaxValue)
144144
seed <- Gen.choose(Long.MinValue, Long.MaxValue)
145-
} yield (startMs, maxMs, seed)
145+
} yield (startNs, maxNs, seed)
146146

147147
forAll(exponentialGen) {
148-
case (startMs: Long, maxMs: Long, seed: Long) =>
149-
val rng = Rng(seed)
150-
var start = startMs.millis
151-
val max = maxMs.millis
152-
val backoff: Backoff = new ExponentialJittered(start, max, 1, Rng(seed))
153-
val result: ArrayBuffer[Duration] = new ArrayBuffer[Duration]()
154-
for (attempt <- 1 to 7) {
155-
result.append(start)
156-
start = nextStart(start, max, rng, attempt)
148+
case (startNs: Long, maxNs: Long, seed: Long) =>
149+
// I don't know why this if is needed, but it is
150+
if (startNs > 0 && maxNs >= startNs) {
151+
Array(Rng(seed), ZeroRng).foreach { rng =>
152+
var backoff: Backoff = new ExponentialJittered(startNs, maxNs, rng)
153+
var meanNs = startNs
154+
var stop = false
155+
while (!stop) {
156+
val minExpected = Math.max(meanNs / 2, 1)
157+
val maxExpected = if (meanNs + minExpected > meanNs) {
158+
Math.min(meanNs + minExpected, maxNs)
159+
} else { // overflow
160+
maxNs
161+
}
162+
val actualDurNs = backoff.duration.inNanoseconds
163+
assert(actualDurNs >= minExpected, backoff)
164+
assert(actualDurNs <= maxExpected, backoff)
165+
166+
backoff = backoff.next
167+
if (meanNs >= maxNs) {
168+
assert(backoff.duration == Duration.fromNanoseconds(maxNs), backoff)
169+
assert(backoff.next == backoff, backoff)
170+
stop = true
171+
} else {
172+
meanNs = if (meanNs * 2 < meanNs) { // overflow
173+
Long.MaxValue
174+
} else {
175+
meanNs * 2
176+
}
177+
}
178+
}
179+
}
157180
}
158-
verifyBackoff(backoff, result.toSeq, exhausted = false)
159-
160-
// Verify case where Rng returns 0.
161-
val zeroRng = getZeroRng(rng)
162-
val zeroRngBackoff: Backoff = new ExponentialJittered(start, max, 1, zeroRng)
163-
val expectedResults = Seq(start, nextStart(start, max, zeroRng, 1))
164-
verifyBackoff(zeroRngBackoff, expectedResults, exhausted = false)
165-
}
166-
167-
def nextStart(start: Duration, max: Duration, rng: Rng, attempt: Int): Duration = {
168-
val shift = 1L << attempt
169-
// to avoid Long overflow
170-
val maxBackoff = if (start >= max / shift) max else start * shift
171-
if (maxBackoff == max) max
172-
else Duration.fromNanoseconds(1 + rng.nextLong(maxBackoff.inNanoseconds))
173181
}
174182
}
175183

@@ -338,12 +346,12 @@ class BackoffTest extends AnyFunSuite with ScalaCheckDrivenPropertyChecks {
338346
assert(actualBackoff.isExhausted == exhausted)
339347
}
340348

341-
private[this] def getZeroRng(rng: Rng): Rng = new Rng {
342-
override def nextDouble(): Double = rng.nextDouble()
349+
private[this] object ZeroRng extends Rng {
350+
override def nextDouble(): Double = 0.0
343351

344-
override def nextInt(n: Int): Int = rng.nextInt(n)
352+
override def nextInt(n: Int): Int = 0
345353

346-
override def nextInt(): Int = rng.nextInt()
354+
override def nextInt(): Int = 0
347355

348356
override def nextLong(n: Long): Long = 0L
349357
}

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,9 +160,17 @@ class FixedInetResolverTest extends AnyFunSuite {
160160
// since `ExponentialJittered` generates values randomly, we use the same
161161
// seed here in order to validate the values returned from `nBackoffs`.
162162
val nBackoffs: Backoff =
163-
new ExponentialJittered(1.milliseconds, 100.milliseconds, 1, Rng(777)).take(shouldFailTimes)
163+
new ExponentialJittered(
164+
1.milliseconds.inNanoseconds,
165+
100.milliseconds.inNanoseconds,
166+
Rng(777)
167+
).take(shouldFailTimes)
164168
var actualBackoff: Backoff =
165-
new ExponentialJittered(1.milliseconds, 100.milliseconds, 1, Rng(777)).take(shouldFailTimes)
169+
new ExponentialJittered(
170+
1.milliseconds.inNanoseconds,
171+
100.milliseconds.inNanoseconds,
172+
Rng(777)
173+
).take(shouldFailTimes)
166174
val mockTimer = new MockTimer
167175
val cache = FixedInetResolver.cache(resolve, maxCacheSize, nBackoffs, mockTimer)
168176
val resolver2 = new FixedInetResolver(cache, statsReceiver)

0 commit comments

Comments
 (0)