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

  1. Verify the trails source is healthy and the downloaded file is complete (compare its size/hash against upstream).
  2. 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.
  3. Restart the sensor to force a full reload that bypasses the ratio check.
  4. 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

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


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)