juspay/hyperswitch · error

grace 2 => Some

Error message

grace 2 => Some

What it means

Test-time expect() in window_starts_next_day (crates/router/src/workflows/revenue_recovery.rs:2096): compute_mathmodel_retry_time(&sample(), 3, grace=2, DEFAULT_RETRY_HOUR) returned None where the test requires Some for grace 2. The semantic contract is that the failure day is excluded and grace 2 yields exactly tomorrow, so a None means the next-day window computation failed to produce a candidate.

Solutions

  1. Check the day-window construction for grace=2: it must include exactly the next calendar day; fix any off-by-one that turns the window empty.
  2. Ensure hour-slot selection always accepts DEFAULT_RETRY_HOUR when only one day is in the window (no filtering that can drop the sole candidate).
  3. Test around midnight locally — if flaky only at boundaries, normalize the 'today' anchor date inside the function before windowing.
  4. Keep the contract documented: grace counts today, so Some must be guaranteed for grace >= 2; fix the function, not the test.

Example fix

// before
let dt = compute_mathmodel_retry_time(&sample(), 3, 2, DEFAULT_RETRY_HOUR)
    .expect("grace 2 => Some");
// after (in the function)
let start = today + 1; // failure day excluded
let end = today + grace; // inclusive: grace 2 => [tomorrow]
assert!(start <= end, "window empty for grace {grace}"); // fail loudly in dev, not None in tests
// and select from [start..=end] falling back to DEFAULT_RETRY_HOUR on the chosen day
Defensive patterns

Strategy: fallback

Validate before calling

// assert the window is non-empty before selection:
let start = today + 1;
let end = today + grace; // inclusive
assert!(start <= end, "empty retry window for grace {grace}");

Try / catch

let dt = compute_mathmodel_retry_time(&sample(), 3, 2, DEFAULT_RETRY_HOUR)
    .unwrap_or_else(|| tomorrow_at(DEFAULT_RETRY_HOUR));

Prevention

When it happens

Trigger: Calling compute_mathmodel_retry_time with grace=2 on the sample StatsDocument and getting None — the day-window [tomorrow] came back empty because the function excluded the single eligible future day (off-by-one in inclusive/exclusive grace counting or hour-slot filtering dropped the only day).

Common situations: Refactors that changed whether grace counts today (inclusive) versus starts tomorrow; DEFAULT_RETRY_HOUR hour-slot filtering removing the only candidate day/hour; timezone/date arithmetic changes shifting the 'before' date boundary (midnight ticks).

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/96499ad4949dbb47. Report an issue: GitHub.

Appendix: source

Thrown at crates/router/src/workflows/revenue_recovery.rs:2096

                    .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");
        assert!(
            dt.date() > before.date(),
            "expected next day, got {} (today {})",
            dt.date(),
            before.date()
        );
        assert!(dt.date() <= (before + time::Duration::days(2)).date());
    }

    #[test]
    fn grace_zero_and_one_return_none() {
        // grace COUNTS today; grace 0 = no grace, grace 1 = today only -> no future day -> None (v1).
        assert!(compute_mathmodel_retry_time(&sample(), 3, 0, DEFAULT_RETRY_HOUR).is_none());
        assert!(compute_mathmodel_retry_time(&sample(), 3, 1, DEFAULT_RETRY_HOUR).is_none());
    }

    #[test]
    fn zero_budget_returns_none() {

View on GitHub (pinned to afd9284352)