Skip to content

Commit 37a1ae1

Browse files
committed
feat(config): make backoff multiplier configurable
Add multiplier field to BackoffConfig (default 1.5x). Configurable via YAML (grpc.backoff.multiplier), CLI (--backoff-multiplier), and env var (FACT_GRPC_BACKOFF_MULTIPLIER). Must be > 1.0. Drops Eq derive from config types in favor of PartialEq to support storing multiplier as f64 directly. Assisted-by: claude-opus-4-6@default <noreply@opencode.ai>
1 parent 9f129fe commit 37a1ae1

3 files changed

Lines changed: 200 additions & 13 deletions

File tree

fact/src/config/mod.rs

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,22 @@ fn parse_positive_duration_secs(s: &str) -> anyhow::Result<Duration> {
3939
Ok(d)
4040
}
4141

42+
fn parse_multiplier(s: &str) -> anyhow::Result<f64> {
43+
let mult = s.parse::<f64>()?;
44+
if !mult.is_finite() || mult <= 1.0 {
45+
bail!("multiplier must be > 1.0, got {mult}");
46+
}
47+
Ok(mult)
48+
}
49+
4250
const CONFIG_FILES: [&str; 4] = [
4351
"/etc/stackrox/fact.yml",
4452
"/etc/stackrox/fact.yaml",
4553
"fact.yml",
4654
"fact.yaml",
4755
];
4856

49-
#[derive(Debug, Default, PartialEq, Eq, Clone)]
57+
#[derive(Debug, Default, PartialEq, Clone)]
5058
pub struct FactConfig {
5159
paths: Option<Vec<PathBuf>>,
5260
pub grpc: GrpcConfig,
@@ -342,11 +350,12 @@ impl TryFrom<&yaml::Hash> for EndpointConfig {
342350
}
343351
}
344352

345-
#[derive(Debug, Default, PartialEq, Eq, Clone)]
353+
#[derive(Debug, Default, PartialEq, Clone)]
346354
pub struct BackoffConfig {
347355
initial: Option<Duration>,
348356
max: Option<Duration>,
349357
jitter: Option<bool>,
358+
multiplier: Option<f64>,
350359
}
351360

352361
impl BackoffConfig {
@@ -360,6 +369,9 @@ impl BackoffConfig {
360369
if let Some(jitter) = from.jitter {
361370
self.jitter = Some(jitter);
362371
}
372+
if let Some(multiplier) = from.multiplier {
373+
self.multiplier = Some(multiplier);
374+
}
363375
}
364376

365377
pub fn initial(&self) -> Duration {
@@ -373,6 +385,10 @@ impl BackoffConfig {
373385
pub fn jitter(&self) -> bool {
374386
self.jitter.unwrap_or(true)
375387
}
388+
389+
pub fn multiplier(&self) -> f64 {
390+
self.multiplier.unwrap_or(1.5)
391+
}
376392
}
377393

378394
impl TryFrom<&yaml::Hash> for BackoffConfig {
@@ -405,14 +421,26 @@ impl TryFrom<&yaml::Hash> for BackoffConfig {
405421
};
406422
backoff.jitter = Some(jitter);
407423
}
424+
"multiplier" => {
425+
let multiplier = match v.as_f64().or_else(|| v.as_i64().map(|v| v as f64)) {
426+
Some(m) if !m.is_finite() || m <= 1.0 => {
427+
bail!("invalid grpc.backoff.multiplier: {v:?}")
428+
}
429+
None => {
430+
bail!("invalid grpc.backoff.multiplier: {v:?}")
431+
}
432+
Some(m) => m,
433+
};
434+
backoff.multiplier = Some(multiplier);
435+
}
408436
name => bail!("Invalid field 'grpc.backoff.{name}' with value: {v:?}"),
409437
}
410438
}
411439
Ok(backoff)
412440
}
413441
}
414442

415-
#[derive(Debug, Default, PartialEq, Eq, Clone)]
443+
#[derive(Debug, Default, PartialEq, Clone)]
416444
pub struct GrpcConfig {
417445
url: Option<String>,
418446
certs: Option<PathBuf>,
@@ -570,6 +598,12 @@ pub struct FactCli {
570598
#[arg(long, env = "FACT_GRPC_BACKOFF_MAX", value_parser = parse_positive_duration_secs)]
571599
backoff_max: Option<Duration>,
572600

601+
/// Backoff multiplier for gRPC reconnection
602+
///
603+
/// Must be > 1.0. Default value is 1.5
604+
#[arg(long, env = "FACT_GRPC_BACKOFF_MULTIPLIER", value_parser = parse_multiplier)]
605+
backoff_multiplier: Option<f64>,
606+
573607
/// Enable jitter for gRPC reconnection backoff
574608
///
575609
/// Default value is true
@@ -676,6 +710,7 @@ impl FactCli {
676710
initial: self.backoff_initial,
677711
max: self.backoff_max,
678712
jitter: resolve_bool_arg(self.backoff_jitter, self.no_backoff_jitter),
713+
multiplier: self.backoff_multiplier,
679714
},
680715
},
681716
endpoint: EndpointConfig {

fact/src/config/tests.rs

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,20 +306,56 @@ fn parsing() {
306306
..Default::default()
307307
},
308308
),
309+
(
310+
r#"
311+
grpc:
312+
backoff:
313+
multiplier: 2
314+
"#,
315+
FactConfig {
316+
grpc: GrpcConfig {
317+
backoff: BackoffConfig {
318+
multiplier: Some(2.0),
319+
..Default::default()
320+
},
321+
..Default::default()
322+
},
323+
..Default::default()
324+
},
325+
),
326+
(
327+
r#"
328+
grpc:
329+
backoff:
330+
multiplier: 3.5
331+
"#,
332+
FactConfig {
333+
grpc: GrpcConfig {
334+
backoff: BackoffConfig {
335+
multiplier: Some(3.5),
336+
..Default::default()
337+
},
338+
..Default::default()
339+
},
340+
..Default::default()
341+
},
342+
),
309343
(
310344
r#"
311345
grpc:
312346
backoff:
313347
initial: 0.5
314348
max: 120
315349
jitter: false
350+
multiplier: 2
316351
"#,
317352
FactConfig {
318353
grpc: GrpcConfig {
319354
backoff: BackoffConfig {
320355
initial: Some(Duration::from_secs_f64(0.5)),
321356
max: Some(Duration::from_secs(120)),
322357
jitter: Some(false),
358+
multiplier: Some(2.0),
323359
},
324360
..Default::default()
325361
},
@@ -337,6 +373,7 @@ fn parsing() {
337373
initial: 0.5
338374
max: 120
339375
jitter: false
376+
multiplier: 2
340377
endpoint:
341378
address: 0.0.0.0:8080
342379
expose_metrics: true
@@ -358,6 +395,7 @@ fn parsing() {
358395
initial: Some(Duration::from_secs_f64(0.5)),
359396
max: Some(Duration::from_secs(120)),
360397
jitter: Some(false),
398+
multiplier: Some(2.0),
361399
},
362400
},
363401
endpoint: EndpointConfig {
@@ -489,6 +527,22 @@ paths:
489527
"#,
490528
"grpc.backoff.jitter field has incorrect type: Integer(4)",
491529
),
530+
(
531+
r#"
532+
grpc:
533+
backoff:
534+
multiplier: true
535+
"#,
536+
"invalid grpc.backoff.multiplier: Boolean(true)",
537+
),
538+
(
539+
r#"
540+
grpc:
541+
backoff:
542+
multiplier: 0.5
543+
"#,
544+
"invalid grpc.backoff.multiplier: Real(\"0.5\")",
545+
),
492546
(
493547
r#"
494548
grpc:
@@ -959,6 +1013,51 @@ fn update() {
9591013
..Default::default()
9601014
},
9611015
),
1016+
(
1017+
r#"
1018+
grpc:
1019+
backoff:
1020+
multiplier: 2
1021+
"#,
1022+
FactConfig::default(),
1023+
FactConfig {
1024+
grpc: GrpcConfig {
1025+
backoff: BackoffConfig {
1026+
multiplier: Some(2.0),
1027+
..Default::default()
1028+
},
1029+
..Default::default()
1030+
},
1031+
..Default::default()
1032+
},
1033+
),
1034+
(
1035+
r#"
1036+
grpc:
1037+
backoff:
1038+
multiplier: 2
1039+
"#,
1040+
FactConfig {
1041+
grpc: GrpcConfig {
1042+
backoff: BackoffConfig {
1043+
multiplier: Some(1.5),
1044+
..Default::default()
1045+
},
1046+
..Default::default()
1047+
},
1048+
..Default::default()
1049+
},
1050+
FactConfig {
1051+
grpc: GrpcConfig {
1052+
backoff: BackoffConfig {
1053+
multiplier: Some(2.0),
1054+
..Default::default()
1055+
},
1056+
..Default::default()
1057+
},
1058+
..Default::default()
1059+
},
1060+
),
9621061
(
9631062
r#"
9641063
endpoint:
@@ -1325,6 +1424,7 @@ fn update() {
13251424
initial: 0.5
13261425
max: 120
13271426
jitter: false
1427+
multiplier: 3.0
13281428
endpoint:
13291429
address: 127.0.0.1:8080
13301430
expose_metrics: true
@@ -1346,6 +1446,7 @@ fn update() {
13461446
initial: Some(Duration::from_secs(15)),
13471447
max: Some(Duration::from_secs(30)),
13481448
jitter: Some(true),
1449+
multiplier: Some(2.0),
13491450
},
13501451
},
13511452
endpoint: EndpointConfig {
@@ -1372,6 +1473,7 @@ fn update() {
13721473
initial: Some(Duration::from_secs_f64(0.5)),
13731474
max: Some(Duration::from_secs(120)),
13741475
jitter: Some(false),
1476+
multiplier: Some(3.0),
13751477
},
13761478
},
13771479
endpoint: EndpointConfig {
@@ -1422,6 +1524,7 @@ fn defaults() {
14221524
assert_eq!(config.grpc.backoff.initial(), Duration::from_secs(1));
14231525
assert_eq!(config.grpc.backoff.max(), Duration::from_secs(60));
14241526
assert!(config.grpc.backoff.jitter());
1527+
assert_eq!(config.grpc.backoff.multiplier(), 1.5);
14251528
}
14261529

14271530
static ENV_MUTEX: Mutex<()> = Mutex::new(());
@@ -1637,6 +1740,22 @@ fn env_vars() {
16371740
..Default::default()
16381741
},
16391742
),
1743+
(
1744+
EnvVar {
1745+
name: "FACT_GRPC_BACKOFF_MULTIPLIER",
1746+
value: "2.5",
1747+
},
1748+
FactConfig {
1749+
grpc: GrpcConfig {
1750+
backoff: BackoffConfig {
1751+
multiplier: Some(2.5),
1752+
..Default::default()
1753+
},
1754+
..Default::default()
1755+
},
1756+
..Default::default()
1757+
},
1758+
),
16401759
(
16411760
EnvVar {
16421761
name: "FACT_ENDPOINT_ADDRESS",
@@ -2053,6 +2172,20 @@ fn env_vars_invalid_values() {
20532172
},
20542173
"error: invalid value 'not_a_boolean' for '--no-backoff-jitter'",
20552174
),
2175+
(
2176+
EnvVar {
2177+
name: "FACT_GRPC_BACKOFF_MULTIPLIER",
2178+
value: "not_a_number",
2179+
},
2180+
"error: invalid value 'not_a_number' for '--backoff-multiplier <BACKOFF_MULTIPLIER>': invalid float literal",
2181+
),
2182+
(
2183+
EnvVar {
2184+
name: "FACT_GRPC_BACKOFF_MULTIPLIER",
2185+
value: "0.5",
2186+
},
2187+
"error: invalid value '0.5' for '--backoff-multiplier <BACKOFF_MULTIPLIER>': multiplier must be > 1.0, got 0.5",
2188+
),
20562189
];
20572190
for (env, expected) in tests {
20582191
let Err(err) = with_env_var(env) else {

0 commit comments

Comments
 (0)