Change the gocron scheduler
Assume scheduler bugs can duplicate, lose, or overlap production jobs. Make the
execution semantics explicit before editing.
Compatibility invariants
- When wire behavior changes, test
N gocron with N-1 gocron-node and the
reverse. Preserve execution, output, cancellation, and terminal status.
- Evolve protobuf/gRPC additively: never renumber or reuse field numbers,
remove a supported field, or change an existing RPC's semantics. Reserve
removed identifiers.
- Keep the previous RPC/execution path during the compatibility window. Check
capability and use a deterministic fallback before invoking new behavior.
- Never retry or reroute after an ambiguous remote acceptance; that can run a
user's job twice. Preserve retry counts and status transitions on fallback.
- When distributed ownership or state changes, verify temporary
N/N-1 HA
mixtures do not duplicate dispatch, stick in Running, or falsely cancel.
Performance invariants
- Establish a baseline for materially affected hot paths with representative
task/node counts and measure applicable scheduler metrics.
- Bound goroutines, queues, retries, log buffers, streams, timers, and retained
execution state. Define backpressure and overload behavior explicitly.
- Do not hold locks during database/network/filesystem/command/notification I/O
or serialize unrelated tasks behind one global lock.
- Batch or coalesce high-frequency status/log writes when semantics permit.
Verify SQLite concurrent read/write and busy/lock behavior separately.
- Add a benchmark or deterministic load regression test for material hot-path
changes. Compare before/after under the same workload and report the result.
Define invariants
Write down which behavior the change must preserve:
- whether execution is at-most-once, at-least-once, or best-effort;
- what happens during leader loss, restart, network timeout, or agent retry;
- how scheduled and manual runs interact;
- whether the same task may overlap and how queue limits behave;
- when task counts, instance maps, logs, and final status are updated;
- who owns cancellation and how resources are released.
If the intended behavior is ambiguous and different choices change user-visible
execution, ask before implementing.
Inspect the whole execution path
Trace from route or cron callback through internal/service/task.go, scheduler
state, models, RPC/agent code, task logs, and notifications. Search every read
and write of changed global state. Identify goroutine ownership, locks, channel
capacity, blocking sends, callbacks, timers, and cleanup paths.
Do not:
- hold a mutex while performing database, network, command, or notification I/O;
- start an unbounded goroutine per event;
- close a channel from a receiver or from multiple owners;
- use check-then-act state without the same lock or atomic operation;
- treat a timeout as proof that remote execution did not start;
- silently drop queued work or overwrite the terminal task status.
Keep refactors incremental. Extracting a component is useful when it creates a
test seam; moving globals without defining ownership is not.
Test adversarial sequences
Add deterministic tests for the changed invariant, including the relevant
subset of:
- simultaneous triggers for one task;
- queue full, cancellation, timeout, and shutdown;
- disable/remove while queued or running;
- repeated callback or retry;
- leader handoff and stale leader behavior;
- remote execution accepted but response lost;
N-1 protocol doubles in both client/server directions and fallback paths;
- panic/error cleanup and counter consistency.
Avoid sleep-based assertions where a channel, fake clock, barrier, or polling
condition can make the test deterministic.
Run focused tests first, then:
go test -race ./internal/service/... ./internal/modules/... ./internal/rpc/...
go test -race -count=10 <changed-package>
Adapt package paths if the changed execution path differs. Invoke $verify
before committing. Report the promised execution semantics, race-sensitive
state touched, failure cases tested, performance evidence when relevant, and
any behavior that remains best-effort.
1---2name: scheduler-change3description: Safely implement, diagnose, or review changes to gocron scheduling and task execution. Use for cron registration, leader election, task dispatch, concurrency queues, manual runs, retries, timeouts, cancellation, task logs, agent execution, failover, or shared scheduler state.4---56# Change the gocron scheduler78Assume scheduler bugs can duplicate, lose, or overlap production jobs. Make the9execution semantics explicit before editing.1011## Compatibility invariants1213- When wire behavior changes, test `N` gocron with `N-1` gocron-node and the14 reverse. Preserve execution, output, cancellation, and terminal status.15- Evolve protobuf/gRPC additively: never renumber or reuse field numbers,16 remove a supported field, or change an existing RPC's semantics. Reserve17 removed identifiers.18- Keep the previous RPC/execution path during the compatibility window. Check19 capability and use a deterministic fallback before invoking new behavior.20- Never retry or reroute after an ambiguous remote acceptance; that can run a21 user's job twice. Preserve retry counts and status transitions on fallback.22- When distributed ownership or state changes, verify temporary `N`/`N-1` HA23 mixtures do not duplicate dispatch, stick in `Running`, or falsely cancel.2425## Performance invariants2627- Establish a baseline for materially affected hot paths with representative28 task/node counts and measure applicable scheduler metrics.29- Bound goroutines, queues, retries, log buffers, streams, timers, and retained30 execution state. Define backpressure and overload behavior explicitly.31- Do not hold locks during database/network/filesystem/command/notification I/O32 or serialize unrelated tasks behind one global lock.33- Batch or coalesce high-frequency status/log writes when semantics permit.34 Verify SQLite concurrent read/write and busy/lock behavior separately.35- Add a benchmark or deterministic load regression test for material hot-path36 changes. Compare before/after under the same workload and report the result.3738## Define invariants3940Write down which behavior the change must preserve:4142- whether execution is at-most-once, at-least-once, or best-effort;43- what happens during leader loss, restart, network timeout, or agent retry;44- how scheduled and manual runs interact;45- whether the same task may overlap and how queue limits behave;46- when task counts, instance maps, logs, and final status are updated;47- who owns cancellation and how resources are released.4849If the intended behavior is ambiguous and different choices change user-visible50execution, ask before implementing.5152## Inspect the whole execution path5354Trace from route or cron callback through `internal/service/task.go`, scheduler55state, models, RPC/agent code, task logs, and notifications. Search every read56and write of changed global state. Identify goroutine ownership, locks, channel57capacity, blocking sends, callbacks, timers, and cleanup paths.5859Do not:6061- hold a mutex while performing database, network, command, or notification I/O;62- start an unbounded goroutine per event;63- close a channel from a receiver or from multiple owners;64- use check-then-act state without the same lock or atomic operation;65- treat a timeout as proof that remote execution did not start;66- silently drop queued work or overwrite the terminal task status.6768Keep refactors incremental. Extracting a component is useful when it creates a69test seam; moving globals without defining ownership is not.7071## Test adversarial sequences7273Add deterministic tests for the changed invariant, including the relevant74subset of:7576- simultaneous triggers for one task;77- queue full, cancellation, timeout, and shutdown;78- disable/remove while queued or running;79- repeated callback or retry;80- leader handoff and stale leader behavior;81- remote execution accepted but response lost;82- `N-1` protocol doubles in both client/server directions and fallback paths;83- panic/error cleanup and counter consistency.8485Avoid sleep-based assertions where a channel, fake clock, barrier, or polling86condition can make the test deterministic.8788Run focused tests first, then:8990```bash91go test -race ./internal/service/... ./internal/modules/... ./internal/rpc/...92go test -race -count=10 <changed-package>93```9495Adapt package paths if the changed execution path differs. Invoke `$verify`96before committing. Report the promised execution semantics, race-sensitive97state touched, failure cases tested, performance evidence when relevant, and98any behavior that remains best-effort.