stamparm/maltrail · error
trail reload REJECTED
Error message
trail reload REJECTED: {} trails would replace {} (below the {:.0}% floor); keeping the current set. If this drop is real, restart the sensor or lower 'TRAIL_RELOAD_MIN_RATIO' What it means
The Rust sensor's trail hot-reload path refuses to swap in an incoming trails set whose size is below cfg.trail_reload_min_ratio of the current count (e.g. incoming 1,000 vs current 100,000 at a 0.9 floor would wipe out 99% of coverage). It increments reloads_rejected, logs this message, and keeps the current set, treating a sudden collapse as more likely a bad download than a real IOC purge.
Solutions
- Verify the trails source is healthy and the downloaded file is complete (compare its size/hash against upstream).
- If the drop is genuine (upstream legitimately shrank the IOC set), lower TRAIL_RELOAD_MIN_RATIO or set it to 0 to disable the floor.
- Restart the sensor to force a full reload that bypasses the ratio check.
- Check metrics counters (reloads_rejected) over time to distinguish transient truncation from a persistent change.
Defensive patterns
Strategy: validation
Validate before calling
// before a reload, sanity-check the incoming set against the floor
let floor = (current as f64 * cfg.trail_reload_min_ratio) as u64;
let acceptable = cfg.trail_reload_min_ratio == 0.0 || current == 0 || incoming >= floor;
assert!(acceptable, "incoming trails {incoming} below floor {floor}"); Type guard
fn passes_reload_floor(current: u64, incoming: u64, min_ratio: f64) -> bool {
min_ratio <= 0.0 || current == 0 || incoming >= (current as f64 * min_ratio) as u64
} Try / catch
// treat a rejection as a warning signal, not a crash
if !passes_reload_floor(current, incoming, ratio) {
verify_trails_source_integrity(); // re-download and compare size/hash before retrying
} Prevention
- Monitor reloads_rejected metrics for spikes
- Validate downloaded trails file size/hash against upstream before applying
- Set TRAIL_RELOAD_MIN_RATIO deliberately; set 0 only when upstream shrinkage is expected
When it happens
Trigger: Periodic trail refresh where the newly downloaded trails DB (db.len()) contains far fewer trails than the registry's current count, and incoming < floor where floor = current * trail_reload_min_ratio (only when min_ratio > 0 and current > 0).
Common situations: Upstream trails feed partially failing and returning a truncated file; network proxy stripping the download; a stale/cached small trails file being served; misconfigured TRAIL_RELOAD_MIN_RATIO so legitimate smaller updates are rejected after a real upstream cleanup.
Understand the failure class
Background: "Invalid value" and "allowed values are" config errors: what your library rejected and how to fix it — this error's family across 41 libraries.
Related errors
- invalid configuration value for 'OFFLINE_TIMESTAMPS
- must load
- config must load
- write config
- harness config must load
AI-assisted analysis of stamparm/maltrail@77cfb06d76 (2026-09-13).
Data as JSON: /api/errors/aeb16a11da7242fe.
Report an issue: GitHub.
Appendix: source
Thrown at sensor/src/main.rs:558
}
let mtime = std::fs::metadata(&cfg_reload.trails_file).and_then(|m| m.modified()).ok();
if mtime == last_mtime && !forced {
continue;
}
last_mtime = mtime;
match trails::load_with(&cfg_reload.trails_file, &wl_reload, load_options(&cfg_reload)) {
Ok((db, stats)) => {
// A reload that loses most of the trail set is far more likely to be a
// half-written or truncated file than a real change, and publishing it
// would blind the sensor without any error ever occurring. Keep the
// last known-good store: detection continues on slightly stale trails,
// which beats continuing on almost none.
let current = reg_reload.trail_count.load(Ordering::Relaxed);
let incoming = db.len() as u64;
let floor = (current as f64 * cfg_reload.trail_reload_min_ratio) as u64;
if cfg_reload.trail_reload_min_ratio > 0.0 && current > 0 && incoming < floor {
reg_reload.reloads_rejected.fetch_add(1, Ordering::Relaxed);
output::log_error(
&format!(
"trail reload REJECTED: {} trails would replace {} (below the \
{:.0}% floor); keeping the current set. If this drop is real, \
restart the sensor or lower 'TRAIL_RELOAD_MIN_RATIO'",
thousands(incoming),
thousands(current),
cfg_reload.trail_reload_min_ratio * 100.0
),
true,
);
} else {
reg_reload.trail_count.store(incoming, Ordering::Relaxed);
store_reload.publish(db);
reg_reload.trail_generation.store(store_reload.generation(), Ordering::Relaxed);
reg_reload.reloads_ok.fetch_add(1, Ordering::Relaxed);
cprintln!("[i] reloaded {} trails", thousands(stats.loaded as u64));
}
}View on GitHub (pinned to 77cfb06d76)