{"record":{"id":"36c16053c5de3b28","repo":"tinyhumansai/openhuman","slug":"invalid-schedule-every-ms-must-be-0","errorCode":null,"errorMessage":"Invalid schedule: every_ms must be > 0","messagePattern":"Invalid schedule: every_ms must be > 0","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/openhuman/cron/schedule.rs","lineNumber":50,"sourceCode":"                        next_utc,\n                        active.start,\n                        active.end\n                    );\n                    current_from = next_utc;\n                } else {\n                    return Ok(next_utc);\n                }\n            }\n            tracing::warn!(\n                \"[cron] no occurrence found within active_hours for expr={} after 100,000 candidates\",\n                expr\n            );\n            anyhow::bail!(\"No future occurrence found within active hours after 100,000 attempts\")\n        }\n        Schedule::At { at } => Ok(*at),\n        Schedule::Every { every_ms } => {\n            if *every_ms == 0 {\n                anyhow::bail!(\"Invalid schedule: every_ms must be > 0\");\n            }\n            let ms = i64::try_from(*every_ms).context(\"every_ms is too large\")?;\n            let delta = ChronoDuration::milliseconds(ms);\n            from.checked_add_signed(delta)\n                .ok_or_else(|| anyhow::anyhow!(\"every_ms overflowed DateTime\"))\n        }\n    }\n}\n\npub fn validate_schedule(schedule: &Schedule, now: DateTime<Utc>) -> Result<()> {\n    match schedule {\n        Schedule::Cron {\n            expr,\n            tz,\n            active_hours,\n        } => {\n            let _ = normalize_expression(expr)?;\n            if let Some(active) = active_hours {","sourceCodeStart":32,"sourceCodeEnd":68,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/749120085864ce16e0f273c7b86fac7740b39c5b/src/openhuman/cron/schedule.rs#L32-L68","documentation":"In next_run_for_schedule, Schedule::Every with every_ms == 0 is rejected: a zero interval would mean 'run continuously' and spin the scheduler. validate_schedule carries an identical guard (schedule.rs:83) so the same condition also fails at validation time.","triggerScenarios":"Constructing an every-interval job with every_ms: 0 — UI frequency inputs defaulting to 0, division that computes 0, hand-written JSON, or deserialized legacy data where the field was never set — and then asking for its next run.","commonSituations":"Derived intervals (total/count landing on 0); 'run every N items' style inputs with N unset; test fixtures probing edge values.","solutions":["Set a positive interval (every_ms > 0), sized to a sane floor (e.g. >= 1000ms) for scheduler load","Clamp user-supplied frequencies at the boundary: every_ms = max(input, MIN_INTERVAL)","If 'run immediately' was intended, that is not an interval — use a one-shot Schedule::At or trigger the job manually"],"exampleFix":"// before\nSchedule::Every { every_ms: 0 }\n\n// after\nSchedule::Every { every_ms: 60_000 }","handlingStrategy":"validation","validationCode":"if let Schedule::Every { every_ms } = &schedule {\n    const MIN_INTERVAL_MS: u64 = 1_000;\n    assert!(*every_ms >= MIN_INTERVAL_MS, \"interval is zero / too small\");\n}","typeGuard":"function isValidEverySchedule(s: Schedule): boolean {\n  return s.type !== 'every' || (s.every_ms ?? 0) > 0;\n}","tryCatchPattern":null,"preventionTips":["Clamp interval inputs to a documented minimum at the UI boundary","Default frequency fields to a real value, never 0","Unit-test schedule constructors with boundary values (0, 1, u64 max)"],"tags":["cron","schedule","interval","validation"],"backgroundTag":"zero-interval-schedule","analyzedSha":"749120085864ce16e0f273c7b86fac7740b39c5b","analyzedAt":"2026-08-17T21:21:45.363Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}