juspay/hyperswitch · error
grace > 1 => Some
Error message
grace > 1 => Some
What it means
Test-time expect() in crates/router/src/workflows/revenue_recovery.rs:2078: compute_mathmodel_retry_time(&stats, 3, grace=14, DEFAULT_RETRY_HOUR) returned None (or errored) for a case the test asserts must always produce a value when grace > 1. The function picks the next retry datetime within a grace window derived from StatsDocument slot weights; a None means the scheduler failed to find any candidate retry slot despite a non-trivial grace period.
Solutions
- Run the test and inspect why the picker returned None: add temporary logging inside compute_mathmodel_retry_time for the computed window bounds.
- Check the grace-window day arithmetic: with grace=14 the candidate window must always contain at least one future day (before+1 .. before+grace+1).
- Verify StatsDocument::default() (all-zero weights) falls back to a uniform/valid slot selection rather than early-returning None.
- Confirm DEFAULT_RETRY_HOUR is a valid hour (0-23) that survives slot filtering; if the hour is filtered, the fallback must still yield a datetime.
- Fix the regression in the window/slot logic rather than relaxing the test assertion.
Example fix
// before
let dt = compute_mathmodel_retry_time(&stats, 3, grace, DEFAULT_RETRY_HOUR)
.expect("grace > 1 => Some");
// after (in the function, guarantee the invariant)
compute_uniform_slot(&stats, max_retries, grace, retry_hour)
.or_else(|| fallback_next_day(retry_hour, grace)) // never None when grace >= 2
// before(production-shape fix): return None on empty window
// after: clamp window end to at least start + 1 day so Some is guaranteed for grace > 1 Defensive patterns
Strategy: fallback
Validate before calling
// caller-side guard before relying on the result:
let dt = compute_mathmodel_retry_time(&stats, retries, grace, retry_hour)
.unwrap_or_else(|| default_retry_datetime(grace, retry_hour)); Try / catch
match compute_mathmodel_retry_time(&stats, 3, grace, DEFAULT_RETRY_HOUR) {
Some(dt) => dt,
None => fallback_next_day_retry(DEFAULT_RETRY_HOUR, grace),
} Prevention
- Guarantee non-None for grace > 1 with a uniform/next-day fallback inside the function
- Cover StatsDocument::default() (zero weights) in unit tests whenever the picker changes
- Validate DEFAULT_RETRY_HOUR stays within 0..24 in config
- Add debug logging of computed window bounds when returning None
When it happens
Trigger: Calling compute_mathmodel_retry_time with grace > 1 on either a populated sample() StatsDocument or the all-defaults StatsDocument::default() and receiving None — e.g. the candidate-window computation excludes all days/hours so no retry slot is produced, an internal regression in the mathmodel picker.
Common situations: A code change to compute_mathmodel_retry_time (window arithmetic, slot filtering, DEFAULT_RETRY_HOUR handling) that makes it return None for zero/sparse stats; changing DEFAULT_RETRY_HOUR to an hour that gets filtered out; off-by-one in day-grace arithmetic so the window is empty.
Understand the failure class
Background: EmptyResultError / "no results found": when an API or scraper succeeds but returns zero rows — this error's family across 9 libraries.
Related errors
AI-assisted analysis of juspay/hyperswitch@afd9284352 (2026-09-18).
Data as JSON: /api/errors/faa076f77d75c53c.
Report an issue: GitHub.
Appendix: source
Thrown at crates/router/src/workflows/revenue_recovery.rs:2078
assert!(pick_hour(&doc.hod, DEFAULT_RETRY_HOUR) < 24);
}
assert_eq!(
pick_hour(&[SlotCounter::default(); 24], DEFAULT_RETRY_HOUR),
DEFAULT_RETRY_HOUR
);
}
#[test]
fn result_is_always_in_window() {
// Invariant for EVERY random outcome: result date ∈ [tomorrow, today+grace], hour ∈ 0..23,
// never panics — for both rich and empty stats. (Window starts on the NEXT day; the bounds
// carry a 1-day slack so a midnight tick between captures can't flake it.)
let grace: u32 = 14;
for stats in [sample(), StatsDocument::default()] {
for _ in 0..200 {
let before = common_utils::date_time::now().assume_utc();
let dt = compute_mathmodel_retry_time(&stats, 3, grace, DEFAULT_RETRY_HOUR)
.expect("grace > 1 => Some");
let last = (before + time::Duration::days(i64::from(grace) + 1)).date();
assert!(
dt.date() > before.date() && dt.date() <= last,
"date {} out of window",
dt.date()
);
assert!(dt.hour() < 24);
}
}
}
#[test]
fn window_starts_next_day() {
// Failure day is excluded: the earliest candidate is tomorrow. grace COUNTS today, so grace 2
// = today + 1 future day (tomorrow) — assert the pick is that next day, not the failure day.
let before = common_utils::date_time::now().assume_utc();
let dt = compute_mathmodel_retry_time(&sample(), 3, 2, DEFAULT_RETRY_HOUR)
.expect("grace 2 => Some");View on GitHub (pinned to afd9284352)