Skip to content

Commit 53d6894

Browse files
jcrossleyjenkins
authored and
jenkins
committedDec 17, 2024·
finagle/finagle-core: Fix bug that causes backup requests to be disabled when configured via stack param in MethodBuilder
Problem When BackupRequestFilter is configured (turned on) via stack params, and MethodBuilder is configured with these params, backup requests will be incorrectly disabled. Solution When BackupRequestFilter is configured via stack params, use this configuration if backup requests have not been configured via MethodBuilder via `idempotent` / `nonIdempotent`. JIRA Issues: STOR-8478 Differential Revision: https://phabricator.twitter.biz/D1189436
1 parent e625ffb commit 53d6894

File tree

3 files changed

+126
-16
lines changed

3 files changed

+126
-16
lines changed
 

‎CHANGELOG.rst

+3
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ Bug Fixes
3939
* finagle-memcached: Fixed support for running memcached tests with external memcached. Added README with
4040
instructions under finagle/finagle-memcached. ``PHAB_ID=D1120240``
4141
* finagle-core: Fixed a bug in `ExponentialJitteredBackoff` where `rng` can be 0. ``PHAB_ID=D1178528``
42+
* finagle-core: When `c.t.f.client.BackupRequestFilter` is configured via stack params,
43+
use this configuration when using MethodBuilder if `idempotent`/`nonIdempotent` have
44+
not been set. ``PHAB_ID=D1189436``
4245

4346

4447
Breaking API Changes

‎finagle-core/src/main/scala/com/twitter/finagle/client/MethodBuilder.scala

+13-6
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ object MethodBuilder {
123123
retry: MethodBuilderRetry.Config,
124124
timeout: MethodBuilderTimeout.Config,
125125
filter: Filter.TypeAgnostic = Filter.typeAgnosticIdentity,
126-
backup: BackupRequestFilter.Param = BackupRequestFilter.Param.Disabled)
126+
backup: Option[BackupRequestFilter.Param] = None)
127127

128128
/** Used by the `ClientRegistry` */
129129
private[client] val RegistryKey = "methods"
@@ -396,7 +396,7 @@ final class MethodBuilder[Req, Rep] private[finagle] (
396396
dest,
397397
stack,
398398
params,
399-
config.copy(backup = brfParam)
399+
config.copy(backup = Some(brfParam))
400400
)
401401

402402
base.withRetry.forClassifier(idempotentedConfigClassifier)
@@ -436,7 +436,7 @@ final class MethodBuilder[Req, Rep] private[finagle] (
436436
dest,
437437
stack,
438438
params,
439-
config.copy(backup = BackupRequestFilter.Param.Disabled)
439+
config.copy(backup = Some(BackupRequestFilter.Param.Disabled))
440440
).withRetry.forClassifier(nonidempotentedConfigClassifier)
441441
}
442442

@@ -677,9 +677,16 @@ final class MethodBuilder[Req, Rep] private[finagle] (
677677

678678
val underlying = methodPool.get
679679

680-
val backupRequestParams = params +
681-
config.backup +
682-
param.ResponseClassifier(config.retry.responseClassifier)
680+
// If the user has not configured the backup requests via MethodBuilder (via `idempotent`,
681+
// of `nonIdempotent`), use the configuration from the params (Disabled if none available).
682+
val backupRequestParams = config.backup match {
683+
case Some(backupRequestParam) =>
684+
params +
685+
backupRequestParam +
686+
param.ResponseClassifier(config.retry.responseClassifier)
687+
case None =>
688+
params + param.ResponseClassifier(config.retry.responseClassifier)
689+
}
683690

684691
// register BackupRequestFilter under the same prefixes as other method entries
685692
val prefixes = Seq(registryEntry().addr) ++ registryKeyPrefix(name)

‎finagle-core/src/test/scala/com/twitter/finagle/client/MethodBuilderTest.scala

+110-10
Original file line numberDiff line numberDiff line change
@@ -658,16 +658,19 @@ class MethodBuilderTest
658658

659659
val stackClient = TestStackClient(stack, params)
660660
val initialMethodBuilder = MethodBuilder.from("with backups", stackClient)
661+
assert(initialMethodBuilder.config.backup == None)
662+
661663
val methodBuilder =
662-
initialMethodBuilder.withConfig(initialMethodBuilder.config.copy(backup = configuredBrfParam))
664+
initialMethodBuilder.withConfig(
665+
initialMethodBuilder.config.copy(backup = Some(configuredBrfParam)))
663666

664667
// Ensure BRF is configured before calling `nonIdempotent`
665-
assert(methodBuilder.config.backup == configuredBrfParam)
668+
assert(methodBuilder.config.backup == Some(configuredBrfParam))
666669

667670
val nonIdempotentClient = methodBuilder.nonIdempotent
668671

669672
// Ensure BRF is disabled after calling `nonIdempotent`
670-
assert(nonIdempotentClient.config.backup == BackupRequestFilter.Disabled)
673+
assert(nonIdempotentClient.config.backup == Some(BackupRequestFilter.Disabled))
671674
}
672675

673676
test("nonIdempotent client keeps existing ResponseClassifier in params ") {
@@ -796,8 +799,9 @@ class MethodBuilderTest
796799
.idempotent(1.percent, sendInterrupts = true, classifier)
797800

798801
mb.config.backup match {
799-
case BackupRequestFilter.Param
800-
.Configured(maxExtraLoadTunable, sendInterrupts, minSendBackupAfterMs) =>
802+
case Some(
803+
BackupRequestFilter.Param
804+
.Configured(maxExtraLoadTunable, sendInterrupts, minSendBackupAfterMs)) =>
801805
assert(
802806
maxExtraLoadTunable().get == 1.percent && sendInterrupts && minSendBackupAfterMs == 1)
803807
case _ => fail("BackupRequestFilter not configured")
@@ -862,8 +866,9 @@ class MethodBuilderTest
862866
.idempotent(tunable, sendInterrupts = true, ResponseClassifier.Default)
863867

864868
assert(
865-
mb.config.backup == BackupRequestFilter
866-
.Configured(tunable, sendInterrupts = true)
869+
mb.config.backup == Some(
870+
BackupRequestFilter
871+
.Configured(tunable, sendInterrupts = true))
867872
)
868873
}
869874

@@ -880,12 +885,13 @@ class MethodBuilderTest
880885
.idempotent(tunable, sendInterrupts = true, ResponseClassifier.Default)
881886

882887
assert(
883-
mb.config.backup == BackupRequestFilter
884-
.Configured(tunable, sendInterrupts = true)
888+
mb.config.backup == Some(
889+
BackupRequestFilter
890+
.Configured(tunable, sendInterrupts = true))
885891
)
886892

887893
val nonIdempotentMB = mb.nonIdempotent
888-
assert(nonIdempotentMB.config.backup == BackupRequestFilter.Disabled)
894+
assert(nonIdempotentMB.config.backup == Some(BackupRequestFilter.Disabled))
889895
}
890896

891897
test("idempotent combines existing classifier with new one") {
@@ -1122,6 +1128,100 @@ class MethodBuilderTest
11221128
assert(client.params[Retries.Budget].retryBudget eq retryBudget)
11231129
}
11241130

1131+
test(
1132+
"BackupRequestFilter is configured with passed-in stack params when not configured in MethodBuilder") {
1133+
val stats = new InMemoryStatsReceiver()
1134+
val timer = new MockTimer()
1135+
val params =
1136+
Stack.Params.empty +
1137+
param.Stats(stats) +
1138+
param.Timer(timer) +
1139+
BackupRequestFilter.Configured(maxExtraLoad = 0.01, sendInterrupts = true)
1140+
1141+
val svc: Service[Int, Int] = Service.mk { i =>
1142+
Future.value(i)
1143+
}
1144+
1145+
val stack = Stack.leaf(Stack.Role("test"), ServiceFactory.const(svc))
1146+
1147+
val stackClient = TestStackClient(stack, Stack.Params.empty).withParams(params)
1148+
val mb = MethodBuilder.from("mb", stackClient)
1149+
1150+
Time.withCurrentTimeFrozen { tc =>
1151+
val client = mb.newService("a_client")
1152+
awaitResult(client(1))
1153+
1154+
tc.advance(10.seconds)
1155+
timer.tick()
1156+
assert(stats.stats.contains(Seq("mb", "a_client", "backups", "send_backup_after_ms")))
1157+
}
1158+
}
1159+
1160+
test(
1161+
"BackupRequestFilter is configured with MethodBuilder idempotent configuration when also configured via stack params") {
1162+
val stats = new InMemoryStatsReceiver()
1163+
val timer = new MockTimer()
1164+
val params =
1165+
Stack.Params.empty +
1166+
param.Stats(stats) +
1167+
param.Timer(timer) +
1168+
BackupRequestFilter.Disabled
1169+
1170+
val svc: Service[Int, Int] = Service.mk { i =>
1171+
Future.value(i)
1172+
}
1173+
1174+
val stack = Stack.leaf(Stack.Role("test"), ServiceFactory.const(svc))
1175+
1176+
val stackClient = TestStackClient(stack, Stack.Params.empty).withParams(params)
1177+
val classifier: ResponseClassifier = ResponseClassifier.named("foo") {
1178+
case ReqRep(_, Throw(_: IndividualRequestTimeoutException)) =>
1179+
ResponseClass.RetryableFailure
1180+
}
1181+
1182+
// MB config should take precedence
1183+
val mb = MethodBuilder.from("mb", stackClient).idempotent(0.05, true, classifier)
1184+
1185+
Time.withCurrentTimeFrozen { tc =>
1186+
val client = mb.newService("a_client")
1187+
awaitResult(client(1))
1188+
1189+
tc.advance(10.seconds)
1190+
timer.tick()
1191+
assert(stats.stats.contains(Seq("mb", "a_client", "backups", "send_backup_after_ms")))
1192+
}
1193+
}
1194+
1195+
test(
1196+
"BackupRequestFilter is configured with MethodBuilder nonIdempotent configuration when also configured via stack params") {
1197+
val stats = new InMemoryStatsReceiver()
1198+
val timer = new MockTimer()
1199+
val params =
1200+
Stack.Params.empty +
1201+
param.Stats(stats) +
1202+
param.Timer(timer) +
1203+
BackupRequestFilter.Configured(maxExtraLoad = 0.01, sendInterrupts = true)
1204+
1205+
val svc: Service[Int, Int] = Service.mk { i =>
1206+
Future.value(i)
1207+
}
1208+
1209+
val stack = Stack.leaf(Stack.Role("test"), ServiceFactory.const(svc))
1210+
1211+
val stackClient = TestStackClient(stack, Stack.Params.empty).withParams(params)
1212+
// MB config should take precedence
1213+
val mb = MethodBuilder.from("mb", stackClient).nonIdempotent
1214+
1215+
Time.withCurrentTimeFrozen { tc =>
1216+
val client = mb.newService("a_client")
1217+
awaitResult(client(1))
1218+
1219+
tc.advance(10.seconds)
1220+
timer.tick()
1221+
assert(!stats.stats.contains(Seq("mb", "a_client", "backups", "send_backup_after_ms")))
1222+
}
1223+
}
1224+
11251225
test("shares RetryBudget between methods") {
11261226
val params = StackClient.defaultParams
11271227

0 commit comments

Comments
 (0)
Please sign in to comment.