Skip to content

Scheduler panics on Redis timeout under CPU load (unhandled .unwrap()) — no concurrency limit on periodic task dispatch is the root trigger #94

Description

@RywJakkraphat

Describe the bug
scheduler_loop in src/utils/task_manager/scheduler.rs and the equivalent path in src/utils/task_manager/cron.rs call .unwrap() directly on Redis command results (conn.hget(...), conn_clone.zadd(...)) on the main polling loop, which runs every second. Any error returned from these calls — including a client-side command timeout — causes an unhandled panic instead of being logged/retried. Because this runs directly inside tokio::join!(ping_server(), scheduler_loop(conn)) in main.rs (not inside a tokio::spawn), the panic takes down the entire process, not just the scheduler task.

This is triggered by a second, related gap: there is no concurrency limit on periodic backup task dispatch. scheduler_loop pulls all due tasks (zrangebyscore(SCHEDULE_KEY, 0, now)) and tokio::spawns every one of them immediately, with no semaphore or max-concurrency bound. I checked the full source tree and found no concurrency-limiting construct anywhere in the codebase.

In our case: N backup jobs shared the same cron schedule (00:00) and all fired at once → host CPU hit 100% from the concurrent dump processes → Redis, starved of CPU on the same host, became slow enough to respond that the agent's Redis client calls exceeded their timeout → the next scheduler loop tick hit the .unwrap() on that timeout error and panicked → agent restarted under the container's restart policy.

To Reproduce

  1. Configure [N] backup jobs on the same host, all with a cron schedule that fires at the same timestamp (e.g. 0 0 * * *)
  2. Let the schedule tick — all due tasks are dispatched concurrently with no throttling (scheduler_loop, src/utils/task_manager/scheduler.rs)
  3. Observe host CPU saturate to 100% from simultaneous dump processes
  4. Under sustained CPU starvation, Redis becomes slow to respond and the agent's Redis client calls start timing out
  5. On the next scheduler loop iteration, conn.hget(&key, "data").await.unwrap() (scheduler.rs) / the equivalent in cron.rs receives an Err (timeout) and panics
  6. Under a process supervisor/container restart policy, the agent restarts and immediately re-enters the same panic on the next tick — crash loop until CPU pressure eases enough for Redis to respond in time

Expected behavior

  1. Redis errors on the scheduler hot path — including command timeouts — should be handled gracefully (logged + backoff/retry), not .unwrap()'d into a process panic.
  2. Periodic task dispatch should be bounded by a configurable concurrency limit so multiple jobs scheduled for the same tick don't all execute at once and starve host CPU (which in turn starves Redis if co-located).

Environment

  • Deployment: Docker
  • Agent version: reproduced through 1.17.1

Additional context
Relevant lines on main (1.18.5):

  • src/utils/task_manager/scheduler.rs: conn.hget(&key, "data").await.unwrap(), conn_clone.zadd(SCHEDULE_KEY, &key, next_ts).await.unwrap() — both unhandled, both on the loop that runs every second
  • src/utils/task_manager/cron.rs: conn.hget(&redis_key, "data").await.unwrap() — same pattern

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions