Skip to content

Commit c44c298

Browse files
committed
feat(config): make backoff initial/max configurable via YAML, CLI, and env vars
Add BackoffConfig to GrpcConfig with YAML (grpc.backoff.initial, grpc.backoff.max), CLI (--backoff-initial, --backoff-max), and env var (FACT_GRPC_BACKOFF_INITIAL, FACT_GRPC_BACKOFF_MAX) support. Extract yaml_to_duration_secs and parse_duration_secs helpers, reusing them for scan_interval parsing as well. Assisted-by: claude-opus-4-6@default <noreply@opencode.ai>
1 parent d523bb9 commit c44c298

3 files changed

Lines changed: 527 additions & 21 deletions

File tree

fact/src/config/mod.rs

Lines changed: 112 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,29 @@ pub mod reloader;
1616
#[cfg(test)]
1717
mod tests;
1818

19+
fn yaml_to_duration_secs(v: &Yaml) -> Option<Duration> {
20+
v.as_f64()
21+
.or_else(|| v.as_i64().map(|i| i as f64))
22+
.filter(|s| s.is_finite() && *s >= 0.0)
23+
.map(Duration::from_secs_f64)
24+
}
25+
26+
fn parse_duration_secs(s: &str) -> anyhow::Result<Duration> {
27+
let f: f64 = s.parse()?;
28+
if !f.is_finite() || f < 0.0 {
29+
bail!("value must be a non-negative finite number, got {f}");
30+
}
31+
Ok(Duration::from_secs_f64(f))
32+
}
33+
34+
fn parse_positive_duration_secs(s: &str) -> anyhow::Result<Duration> {
35+
let d = parse_duration_secs(s)?;
36+
if d.is_zero() {
37+
bail!("value must be greater than zero");
38+
}
39+
Ok(d)
40+
}
41+
1942
const CONFIG_FILES: [&str; 4] = [
2043
"/etc/stackrox/fact.yml",
2144
"/etc/stackrox/fact.yaml",
@@ -218,20 +241,11 @@ impl TryFrom<Vec<Yaml>> for FactConfig {
218241
config.hotreload = Some(hotreload);
219242
}
220243
"scan_interval" => {
221-
// scan_internal == 0 disables the scanner
222-
if let Some(scan_interval) = v.as_f64() {
223-
if scan_interval < 0.0 {
224-
bail!("invalid scan_interval: {scan_interval}");
225-
}
226-
config.scan_interval = Some(Duration::from_secs_f64(scan_interval));
227-
} else if let Some(scan_interval) = v.as_i64() {
228-
if scan_interval < 0 {
229-
bail!("invalid scan_interval: {scan_interval}");
230-
}
231-
config.scan_interval = Some(Duration::from_secs(scan_interval as u64))
232-
} else {
233-
bail!("scan_interval field has incorrect type: {v:?}");
234-
}
244+
// scan_interval == 0 disables the scanner
245+
config.scan_interval = Some(
246+
yaml_to_duration_secs(v)
247+
.with_context(|| format!("invalid scan_interval: {v:?}"))?,
248+
);
235249
}
236250
"rate_limit" => {
237251
// rate_limit == 0 means unlimited (no throttling)
@@ -328,10 +342,67 @@ impl TryFrom<&yaml::Hash> for EndpointConfig {
328342
}
329343
}
330344

345+
#[derive(Debug, Default, PartialEq, Eq, Clone)]
346+
pub struct BackoffConfig {
347+
initial: Option<Duration>,
348+
max: Option<Duration>,
349+
}
350+
351+
impl BackoffConfig {
352+
fn update(&mut self, from: &BackoffConfig) {
353+
if let Some(initial) = from.initial {
354+
self.initial = Some(initial);
355+
}
356+
if let Some(max) = from.max {
357+
self.max = Some(max);
358+
}
359+
}
360+
361+
pub fn initial(&self) -> Duration {
362+
self.initial.unwrap_or(Duration::from_secs(1))
363+
}
364+
365+
pub fn max(&self) -> Duration {
366+
self.max.unwrap_or(Duration::from_secs(60))
367+
}
368+
}
369+
370+
impl TryFrom<&yaml::Hash> for BackoffConfig {
371+
type Error = anyhow::Error;
372+
373+
fn try_from(value: &yaml::Hash) -> Result<Self, Self::Error> {
374+
let mut backoff = BackoffConfig::default();
375+
for (k, v) in value.iter() {
376+
let Some(k) = k.as_str() else {
377+
bail!("key is not string: {k:?}");
378+
};
379+
match k {
380+
"initial" => {
381+
backoff.initial = Some(
382+
yaml_to_duration_secs(v)
383+
.filter(|d| !d.is_zero())
384+
.with_context(|| format!("invalid grpc.backoff.initial: {v:?}"))?,
385+
);
386+
}
387+
"max" => {
388+
backoff.max = Some(
389+
yaml_to_duration_secs(v)
390+
.filter(|d| !d.is_zero())
391+
.with_context(|| format!("invalid grpc.backoff.max: {v:?}"))?,
392+
);
393+
}
394+
name => bail!("Invalid field 'grpc.backoff.{name}' with value: {v:?}"),
395+
}
396+
}
397+
Ok(backoff)
398+
}
399+
}
400+
331401
#[derive(Debug, Default, PartialEq, Eq, Clone)]
332402
pub struct GrpcConfig {
333403
url: Option<String>,
334404
certs: Option<PathBuf>,
405+
pub backoff: BackoffConfig,
335406
}
336407

337408
impl GrpcConfig {
@@ -343,6 +414,8 @@ impl GrpcConfig {
343414
if let Some(certs) = from.certs.as_deref() {
344415
self.certs = Some(certs.to_owned());
345416
}
417+
418+
self.backoff.update(&from.backoff);
346419
}
347420

348421
pub fn url(&self) -> Option<&str> {
@@ -377,6 +450,12 @@ impl TryFrom<&yaml::Hash> for GrpcConfig {
377450
};
378451
grpc.certs = Some(PathBuf::from(certs));
379452
}
453+
"backoff" => {
454+
let Some(backoff) = v.as_hash() else {
455+
bail!("grpc.backoff section has incorrect type: {v:?}");
456+
};
457+
grpc.backoff = BackoffConfig::try_from(backoff)?;
458+
}
380459
name => bail!("Invalid field 'grpc.{name}' with value: {v:?}"),
381460
}
382461
}
@@ -465,6 +544,18 @@ pub struct FactCli {
465544
#[arg(short, long, env = "FACT_CERTS")]
466545
certs: Option<PathBuf>,
467546

547+
/// Initial backoff delay in seconds for gRPC reconnection
548+
///
549+
/// Default value is 1 second
550+
#[arg(long, env = "FACT_GRPC_BACKOFF_INITIAL", value_parser = parse_positive_duration_secs)]
551+
backoff_initial: Option<Duration>,
552+
553+
/// Maximum backoff delay in seconds for gRPC reconnection
554+
///
555+
/// Default value is 60 seconds
556+
#[arg(long, env = "FACT_GRPC_BACKOFF_MAX", value_parser = parse_positive_duration_secs)]
557+
backoff_max: Option<Duration>,
558+
468559
/// The port to bind for all exposed endpoints
469560
#[arg(long, short, env = "FACT_ENDPOINT_ADDRESS")]
470561
address: Option<SocketAddr>,
@@ -535,8 +626,8 @@ pub struct FactCli {
535626
/// The seconds can use a decimal point for fractions of seconds.
536627
///
537628
/// Default value is 30 seconds
538-
#[arg(long, short, env = "FACT_SCAN_INTERVAL")]
539-
scan_interval: Option<f64>,
629+
#[arg(long, short, env = "FACT_SCAN_INTERVAL", value_parser = parse_duration_secs)]
630+
scan_interval: Option<Duration>,
540631

541632
/// Maximum number of file events to allow per second
542633
///
@@ -555,6 +646,10 @@ impl FactCli {
555646
grpc: GrpcConfig {
556647
url: self.url.clone(),
557648
certs: self.certs.clone(),
649+
backoff: BackoffConfig {
650+
initial: self.backoff_initial,
651+
max: self.backoff_max,
652+
},
558653
},
559654
endpoint: EndpointConfig {
560655
address: self.address,
@@ -568,7 +663,7 @@ impl FactCli {
568663
skip_pre_flight: resolve_bool_arg(self.skip_pre_flight, self.no_skip_pre_flight),
569664
json: resolve_bool_arg(self.json, self.no_json),
570665
hotreload: resolve_bool_arg(self.hotreload, self.no_hotreload),
571-
scan_interval: self.scan_interval.map(Duration::from_secs_f64),
666+
scan_interval: self.scan_interval,
572667
rate_limit: self.rate_limit,
573668
}
574669
}

0 commit comments

Comments
 (0)