Skip to content

feat(cron): add strict wall-clock schedule support - #247

Merged
jamiepine merged 6 commits into
mainfrom
feat/cron-wall-clock-schedules
Feb 27, 2026
Merged

feat(cron): add strict wall-clock schedule support#247
jamiepine merged 6 commits into
mainfrom
feat/cron-wall-clock-schedules

Conversation

@jamiepine

@jamiepine jamiepine commented Feb 27, 2026

Copy link
Copy Markdown
Member

Summary

  • add first-class cron_expr support across config, cron tool, API, scheduler, and storage so cron jobs can run on strict wall-clock schedules
  • update scheduler execution flow to compute next fire time from the resolved cron timezone when cron_expr is set, while preserving interval_secs as a backward-compatible legacy path
  • add a new migration for cron_jobs.cron_expr and update README/config/cron docs to document strict scheduling and migration-safe compatibility

Testing

  • cargo check
  • cargo fmt --all
  • cargo test --lib cron::scheduler -- --nocapture

Note

This PR introduces first-class cron expression support across the entire cron job stack. The cron crate (v0.12) dependency is added to handle standard 5-field cron syntax parsing. New jobs can specify exact wall-clock times via cron_expr (e.g., 0 9 * * * for daily at 9am), while existing interval-based jobs remain fully supported. The scheduler intelligently routes between cron-expression logic (computing next fire in the configured timezone) and interval-based logic (clock-aligned for sub-daily intervals). A non-breaking migration adds the cron_expr column to the cron_jobs table. All changes flow through config, tools, API, storage layer, and documentation.

Written by Tembo for commit 57f44ce. This will update automatically on new commits.

@coderabbitai

coderabbitai Bot commented Feb 27, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8e39cb3 and 0af83f6.

📒 Files selected for processing (2)
  • README.md
  • src/config.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/config.rs
  • README.md

Walkthrough

Adds an optional cron_expr wall‑clock scheduling field across docs, DB, API, config, tools, persistence, and scheduler; implements timezone-aware next-fire calculation and hybrid cron/interval behavior, active-hours gating at fire time, execution guard, run-once/circuit-breaker logic, and persistence of cron_expr.

Changes

Cohort / File(s) Summary
Documentation & Prompts
README.md, docs/content/docs/(configuration)/config.mdx, docs/content/docs/(features)/cron.mdx, prompts/en/tools/cron_description.md.j2
Introduce cron_expr in docs, examples, and tool prompt text; reword scheduling semantics to prefer wall‑clock cron expressions while retaining legacy interval compatibility and clarify timezone/active-hours behavior.
Database Migration
migrations/20260226000001_cron_expression.sql
Add cron_expr TEXT column to cron_jobs table (schema-only migration, no data transform).
API & Validation
src/api/cron.rs
Add cron_expr: Option<String> to request/response structs; trim and validate 5-field cron expressions (parse via cron::Schedule); include cron_expr in list responses and propagate into CronConfig.
Configuration
src/config.rs
Add pub cron_expr: Option<String> to CronDef and TOML deserialization types; carry cron_expr from config into runtime CronDef.
Scheduler Core
src/cron/scheduler.rs
Add cron_expr to CronJob/CronConfig; normalize/validate expressions; implement resolve_cron_timezone, next_fire_duration, hybrid cron vs interval loop, active-hours gating at fire time, ExecutionGuard to avoid concurrent runs, run-once and circuit-breaker disable+persist behavior, and continuous re-checking timer loop.
Persistence & Wiring
src/cron/store.rs, src/main.rs
Persist and load cron_expr in SQL queries and mappings; initialize CronConfig.cron_expr when seeding/restoring jobs in startup logic.
CLI / Tools
src/tools/cron.rs
Add cron_expr to tool args/entries and schemas; validate cron expressions on create; prefer cron_expr when describing/scheduling jobs and include it in serialized outputs.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'feat(cron): add strict wall-clock schedule support' directly and clearly summarizes the main change—adding wall-clock cron expression scheduling support to the cron job system.
Description check ✅ Passed The PR description comprehensively documents the changes, including first-class cron_expr support, scheduler flow updates, migration details, documentation updates, and testing performed.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/cron-wall-clock-schedules

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread src/cron/scheduler.rs
Comment on lines +602 to +614
let trimmed = expr.trim();
if trimmed.is_empty() {
return Ok(None);
}

Schedule::from_str(trimmed).map_err(|error| {
crate::error::Error::Other(anyhow::anyhow!(
"invalid cron expression '{trimmed}': {error}"
))
})?;

Ok(Some(trimmed.to_string()))
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we intend to support only 5-field cron syntax (as docs/comments say), it might be worth enforcing that explicitly here so we don’t accidentally accept 6/7-field (seconds/year) expressions.

Suggested change
let trimmed = expr.trim();
if trimmed.is_empty() {
return Ok(None);
}
Schedule::from_str(trimmed).map_err(|error| {
crate::error::Error::Other(anyhow::anyhow!(
"invalid cron expression '{trimmed}': {error}"
))
})?;
Ok(Some(trimmed.to_string()))
}
let trimmed = expr.trim();
if trimmed.is_empty() {
return Ok(None);
}
let field_count = trimmed.split_whitespace().count();
if field_count != 5 {
return Err(crate::error::Error::Other(anyhow::anyhow!(
"cron expression must have exactly 5 fields (got {field_count}): '{trimmed}'"
)));
}
Schedule::from_str(trimmed).map_err(|error| {
crate::error::Error::Other(anyhow::anyhow!(
"invalid cron expression '{trimmed}': {error}"
))
})?;
Ok(Some(trimmed.to_string()))

Comment thread src/cron/scheduler.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/config.rs`:
- Around line 2141-2142: When loading/parsing the TOML into the struct that
contains the fields cron_expr and interval_secs, normalize cron_expr by trimming
whitespace and converting empty or all-whitespace strings to None so you never
carry an Option<String> with an empty schedule into runtime; update the
TOML-to-config conversion path (e.g. the Config::from_toml / deserialize logic
that populates cron_expr) to perform s = s.trim(); if s.is_empty() set cron_expr
= None otherwise set Some(s.to_string()); apply the same normalization where
similar schedule parsing occurs (the other occurrence around the block
referenced at lines ~3754-3758).

In `@src/cron/store.rs`:
- Line 77: The row decoding currently swallows errors by calling
row.try_get::<Option<String>, _>("cron_expr").ok().flatten() (and the same
pattern used elsewhere), which masks DB decode/schema issues; change the mapping
closure to return a Result and propagate errors instead of returning None—either
call row.try_get::<Option<String>, _>("cron_expr")? to let the error bubble up,
or extract into a helper like fn get_optional_string(row, col) ->
Result<Option<String>, E> and use that; if mapping multiple rows, use the
collect::<Result<Vec<_>, _>>() pattern so any decode error is returned to the
caller rather than silently discarded.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0d40dbf and 0feb705.

⛔ Files ignored due to path filters (2)
  • Cargo.lock is excluded by !**/*.lock, !**/*.lock
  • Cargo.toml is excluded by !**/*.toml
📒 Files selected for processing (11)
  • README.md
  • docs/content/docs/(configuration)/config.mdx
  • docs/content/docs/(features)/cron.mdx
  • migrations/20260226000001_cron_expression.sql
  • prompts/en/tools/cron_description.md.j2
  • src/api/cron.rs
  • src/config.rs
  • src/cron/scheduler.rs
  • src/cron/store.rs
  • src/main.rs
  • src/tools/cron.rs

Comment thread src/config.rs
Comment on lines +2141 to 2142
cron_expr: Option<String>,
interval_secs: Option<u64>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Normalize cron_expr when loading TOML to reject empty schedules.

Right now an empty/whitespace cron_expr is passed through unchanged. Normalizing here avoids carrying an invalid “set” schedule into runtime behavior.

🧹 Suggested fix
-                        cron_expr: h.cron_expr,
+                        cron_expr: h
+                            .cron_expr
+                            .map(|expr| expr.trim().to_string())
+                            .filter(|expr| !expr.is_empty()),

Also applies to: 3754-3758

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/config.rs` around lines 2141 - 2142, When loading/parsing the TOML into
the struct that contains the fields cron_expr and interval_secs, normalize
cron_expr by trimming whitespace and converting empty or all-whitespace strings
to None so you never carry an Option<String> with an empty schedule into
runtime; update the TOML-to-config conversion path (e.g. the Config::from_toml /
deserialize logic that populates cron_expr) to perform s = s.trim(); if
s.is_empty() set cron_expr = None otherwise set Some(s.to_string()); apply the
same normalization where similar schedule parsing occurs (the other occurrence
around the block referenced at lines ~3754-3758).

Comment thread src/cron/store.rs
.map(|row| CronConfig {
id: row.try_get("id").unwrap_or_default(),
prompt: row.try_get("prompt").unwrap_or_default(),
cron_expr: row.try_get::<Option<String>, _>("cron_expr").ok().flatten(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the structure and size of the file
wc -l src/cron/store.rs

Repository: spacedriveapp/spacebot

Length of output: 87


🏁 Script executed:

# View the specific lines mentioned in the review (77, 168) with context
sed -n '70,85p' src/cron/store.rs

Repository: spacedriveapp/spacebot

Length of output: 949


🏁 Script executed:

# View line 168 with context
sed -n '160,175p' src/cron/store.rs

Repository: spacedriveapp/spacebot

Length of output: 929


🏁 Script executed:

# Search for all .ok().flatten() patterns in the file
rg -n '\.ok\(\)\.flatten\(\)' src/cron/store.rs

Repository: spacedriveapp/spacebot

Length of output: 250


🏁 Script executed:

# Also check for the specific pattern mentioned in the review
rg -n 'cron_expr:\s*row\.try_get' src/cron/store.rs

Repository: spacedriveapp/spacebot

Length of output: 250


🏁 Script executed:

# Get the full context of the first occurrence (lines 70-90)
sed -n '70,90p' src/cron/store.rs

Repository: spacedriveapp/spacebot

Length of output: 1180


🏁 Script executed:

# Get full context of second occurrence (lines 160-180)
sed -n '160,180p' src/cron/store.rs

Repository: spacedriveapp/spacebot

Length of output: 1161


🏁 Script executed:

# Check for other .ok() uses on try_get to see if pattern is consistent
rg -n '\.try_get.*\.ok\(\)' src/cron/store.rs

Repository: spacedriveapp/spacebot

Length of output: 829


🏁 Script executed:

# Check the imports to verify error handling dependencies available
head -30 src/cron/store.rs

Repository: spacedriveapp/spacebot

Length of output: 1010


Remove silent error discarding on cron_expr row decoding.

Lines 77 and 168 use .ok().flatten() to convert try_get failures into None, which silently discards database decoding errors and masks data/schema issues.

-                cron_expr: row.try_get::<Option<String>, _>("cron_expr").ok().flatten(),
+                cron_expr: row.try_get::<Option<String>, _>("cron_expr")?,

Consider using a separate helper function or the collect::<Result<Vec<_>>>() pattern to properly propagate errors from the row mapping closure instead of silently dropping them.

Per coding guidelines: "Don't silently discard errors. No let _ = on Results. Handle them, log them, or propagate them."

Also applies to line 168.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cron/store.rs` at line 77, The row decoding currently swallows errors by
calling row.try_get::<Option<String>, _>("cron_expr").ok().flatten() (and the
same pattern used elsewhere), which masks DB decode/schema issues; change the
mapping closure to return a Result and propagate errors instead of returning
None—either call row.try_get::<Option<String>, _>("cron_expr")? to let the error
bubble up, or extract into a helper like fn get_optional_string(row, col) ->
Result<Option<String>, E> and use that; if mapping multiple rows, use the
collect::<Result<Vec<_>, _>>() pattern so any decode error is returned to the
caller rather than silently discarded.

Co-authored-by: tembo[bot] <208362400+tembo[bot]@users.noreply.github.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/cron/scheduler.rs (1)

597-613: ⚠️ Potential issue | 🟠 Major

Enforce exactly 5 fields for cron_expr during normalization.

normalize_cron_expr currently checks “parseable” only. If Schedule::from_str accepts 6/7-field variants, this can violate the documented 5-field contract and change runtime behavior.

Proposed fix
 fn normalize_cron_expr(cron_expr: Option<String>) -> Result<Option<String>> {
     let Some(expr) = cron_expr else {
         return Ok(None);
     };

     let trimmed = expr.trim();
     if trimmed.is_empty() {
         return Ok(None);
     }
+
+    let field_count = trimmed.split_whitespace().count();
+    if field_count != 5 {
+        return Err(crate::error::Error::Other(anyhow::anyhow!(
+            "cron expression must have exactly 5 fields (got {field_count}): '{trimmed}'"
+        )));
+    }

     Schedule::from_str(trimmed).map_err(|error| {
         crate::error::Error::Other(anyhow::anyhow!(
             "invalid cron expression '{trimmed}': {error}"
         ))
     })?;

     Ok(Some(trimmed.to_string()))
 }
For cron crate version 0.12.0, what cron field counts does `Schedule::from_str` accept (5 vs 6/7 fields), and does it permit seconds/year fields?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cron/scheduler.rs` around lines 597 - 613, normalize_cron_expr currently
only validates parsability via Schedule::from_str but must enforce the
documented 5-field contract; update normalize_cron_expr to count
whitespace-separated fields from the trimmed expr and return an
Err(crate::error::Error::Other(...)) if the count is not exactly 5 (use a clear
message like "cron expression must have exactly 5 fields: found N"). Keep the
existing empty/None handling and still call Schedule::from_str for semantic
validation, and reference the function name normalize_cron_expr and the
Schedule::from_str call when making the change.
🧹 Nitpick comments (1)
src/cron/scheduler.rs (1)

202-203: Rename abbreviated local j variables for guideline compliance.

Please replace j with explicit names like jobs_guard / job_entry in these blocks for readability and consistency.

As per coding guidelines, "**/*.rs: Don't abbreviate variable names. Use queue not q, message not msg, channel not ch."

Also applies to: 257-259, 306-307, 319-320, 335-336

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cron/scheduler.rs` around lines 202 - 203, The local read guard variable
currently named `j` is too abbreviated; rename it to a descriptive name such as
`jobs_guard` (or `jobs_lock`) where you call `jobs.read().await` and update the
subsequent `match j.get(&job_id)` to `match jobs_guard.get(&job_id)`; likewise
rename any short `j` occurrences in the other similar blocks (e.g., the ones
surrounding `job_id`, `jobs.read().await`, and `match ... .get(...)` at the
other spots) and rename short variables used for map entries to `job_entry` for
clarity, keeping all uses consistent so the code compiles.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/cron/scheduler.rs`:
- Around line 616-623: The function interval_initial_delay should guard against
interval_secs == 0 to avoid a modulo-by-zero panic: update the condition that
currently reads `if interval_secs < 86400 && 86400 % interval_secs == 0` to
first check `interval_secs != 0` (e.g. `if interval_secs != 0 && interval_secs <
86400 && 86400 % interval_secs == 0`) or add an early return when interval_secs
== 0 (e.g. return Duration::from_secs(0)); adjust the control flow in the
function (referencing interval_initial_delay and the variable interval_secs) so
no modulo is executed with zero and a sensible Duration::from_secs(0) is
returned for the zero case.

---

Duplicate comments:
In `@src/cron/scheduler.rs`:
- Around line 597-613: normalize_cron_expr currently only validates parsability
via Schedule::from_str but must enforce the documented 5-field contract; update
normalize_cron_expr to count whitespace-separated fields from the trimmed expr
and return an Err(crate::error::Error::Other(...)) if the count is not exactly 5
(use a clear message like "cron expression must have exactly 5 fields: found
N"). Keep the existing empty/None handling and still call Schedule::from_str for
semantic validation, and reference the function name normalize_cron_expr and the
Schedule::from_str call when making the change.

---

Nitpick comments:
In `@src/cron/scheduler.rs`:
- Around line 202-203: The local read guard variable currently named `j` is too
abbreviated; rename it to a descriptive name such as `jobs_guard` (or
`jobs_lock`) where you call `jobs.read().await` and update the subsequent `match
j.get(&job_id)` to `match jobs_guard.get(&job_id)`; likewise rename any short
`j` occurrences in the other similar blocks (e.g., the ones surrounding
`job_id`, `jobs.read().await`, and `match ... .get(...)` at the other spots) and
rename short variables used for map entries to `job_entry` for clarity, keeping
all uses consistent so the code compiles.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dc9988a and b7a0623.

📒 Files selected for processing (1)
  • src/cron/scheduler.rs

Comment thread src/cron/scheduler.rs
Comment on lines +616 to +623
fn interval_initial_delay(interval_secs: u64) -> Duration {
if interval_secs < 86400 && 86400 % interval_secs == 0 {
let now_unix = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let remainder = now_unix % interval_secs;
let secs_until = if remainder == 0 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Guard interval_secs == 0 to avoid modulo-by-zero panic.

At Line 617, 86400 % interval_secs will panic when interval_secs is 0.

Proposed fix
 fn interval_initial_delay(interval_secs: u64) -> Duration {
+    if interval_secs == 0 {
+        return Duration::from_secs(1);
+    }
     if interval_secs < 86400 && 86400 % interval_secs == 0 {
         let now_unix = std::time::SystemTime::now()
             .duration_since(std::time::UNIX_EPOCH)
             .unwrap_or_default()
             .as_secs();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn interval_initial_delay(interval_secs: u64) -> Duration {
if interval_secs < 86400 && 86400 % interval_secs == 0 {
let now_unix = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let remainder = now_unix % interval_secs;
let secs_until = if remainder == 0 {
fn interval_initial_delay(interval_secs: u64) -> Duration {
if interval_secs == 0 {
return Duration::from_secs(1);
}
if interval_secs < 86400 && 86400 % interval_secs == 0 {
let now_unix = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let remainder = now_unix % interval_secs;
let secs_until = if remainder == 0 {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cron/scheduler.rs` around lines 616 - 623, The function
interval_initial_delay should guard against interval_secs == 0 to avoid a
modulo-by-zero panic: update the condition that currently reads `if
interval_secs < 86400 && 86400 % interval_secs == 0` to first check
`interval_secs != 0` (e.g. `if interval_secs != 0 && interval_secs < 86400 &&
86400 % interval_secs == 0`) or add an early return when interval_secs == 0
(e.g. return Duration::from_secs(0)); adjust the control flow in the function
(referencing interval_initial_delay and the variable interval_secs) so no modulo
is executed with zero and a sensible Duration::from_secs(0) is returned for the
zero case.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
src/cron/scheduler.rs (1)

623-625: ⚠️ Potential issue | 🔴 Critical

Guard zero interval before modulo to avoid timer-task panic.

Line 624 performs 86400 % interval_secs; if interval_secs == 0, this panics and terminates the scheduler task for that job.

🐛 Proposed fix
 fn interval_initial_delay(interval_secs: u64) -> Duration {
+    if interval_secs == 0 {
+        return Duration::from_secs(1);
+    }
     if interval_secs < 86400 && 86400 % interval_secs == 0 {
         let now_unix = std::time::SystemTime::now()
             .duration_since(std::time::UNIX_EPOCH)
             .unwrap_or_default()
             .as_secs();
#!/bin/bash
set -euo pipefail

# 1) Confirm modulo-by-zero risk is present in interval_initial_delay.
rg -n -C3 'fn interval_initial_delay|86400 % interval_secs|interval_secs == 0' src/cron/scheduler.rs

# 2) Inspect ingestion/validation paths for interval_secs to verify whether zero can still enter
#    via non-API sources (config/store/startup restore).
rg -n -C3 'interval_secs|MIN_CRON_INTERVAL_SECS|default_interval|cron_expr' \
  src/api/cron.rs src/tools/cron.rs src/cron/scheduler.rs src/config.rs
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cron/scheduler.rs` around lines 623 - 625, The function
interval_initial_delay must guard against interval_secs == 0 before performing
86400 % interval_secs to avoid a panic; update interval_initial_delay to check
interval_secs == 0 first (in the branch that currently checks interval_secs <
86400) and return an appropriate Duration (e.g., zero delay or the intended
default) or skip the modulo logic when zero, so that the modulo operation is
never executed with a zero divisor; locate and update the interval_initial_delay
function to implement this early check and ensure any callers expecting a
non-zero delay still behave correctly.
🧹 Nitpick comments (1)
src/cron/scheduler.rs (1)

200-214: Use descriptive guard variable names in the timer loop.

Line 202 uses j for the jobs map guard; rename to something explicit (e.g., jobs_guard) for readability and consistency.

As per coding guidelines: Don't abbreviate variable names. Use queue not q, message not msg, channel not ch.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cron/scheduler.rs` around lines 200 - 214, Rename the short guard
variable `j` inside the timer loop to a descriptive name (e.g., `jobs_guard`)
where the code does `let j = jobs.read().await;` and update its uses in the
match that checks `j.get(&job_id)` and the pattern `Some(j)` to avoid
abbreviation; ensure you also update the cloned variable `j.clone()` to use a
clear name (e.g., `job_entry`) so the block using `jobs.read().await`, the
`match` arms, and the `tracing::debug!` calls reference the new descriptive
identifiers (`jobs_guard`, `job_entry`, etc.) consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/cron/scheduler.rs`:
- Around line 623-625: The function interval_initial_delay must guard against
interval_secs == 0 before performing 86400 % interval_secs to avoid a panic;
update interval_initial_delay to check interval_secs == 0 first (in the branch
that currently checks interval_secs < 86400) and return an appropriate Duration
(e.g., zero delay or the intended default) or skip the modulo logic when zero,
so that the modulo operation is never executed with a zero divisor; locate and
update the interval_initial_delay function to implement this early check and
ensure any callers expecting a non-zero delay still behave correctly.

---

Nitpick comments:
In `@src/cron/scheduler.rs`:
- Around line 200-214: Rename the short guard variable `j` inside the timer loop
to a descriptive name (e.g., `jobs_guard`) where the code does `let j =
jobs.read().await;` and update its uses in the match that checks
`j.get(&job_id)` and the pattern `Some(j)` to avoid abbreviation; ensure you
also update the cloned variable `j.clone()` to use a clear name (e.g.,
`job_entry`) so the block using `jobs.read().await`, the `match` arms, and the
`tracing::debug!` calls reference the new descriptive identifiers (`jobs_guard`,
`job_entry`, etc.) consistently.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b7a0623 and 8e39cb3.

📒 Files selected for processing (3)
  • src/api/cron.rs
  • src/cron/scheduler.rs
  • src/tools/cron.rs

@jamiepine
jamiepine merged commit 085f532 into main Feb 27, 2026
4 checks passed
rktmeister pushed a commit to rktmeister/spacebot that referenced this pull request Mar 11, 2026
…l-clock-schedules

feat(cron): add strict wall-clock schedule support
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant