fix(app): scheduled processes added via project update now register with the scheduler and respect run_on_start#493
Closed
makingaipractical wants to merge 2 commits into
Conversation
… on remove
UpdateProject's add/update/delete paths forgot scheduled processes exist.
Three independent oversights surface together when a user runs
`process-compose project update -f <yaml>` against a config that adds,
modifies, or removes a scheduled-cron / scheduled-interval entry:
(a) addProcessAndRun unconditionally called runProcess for new entries,
so scheduled processes fired IMMEDIATELY on add even when
`run_on_start: false`. The Run() startup loop has the correct guard
(skip scheduled processes — they're owned by the scheduler); this
patch matches that behavior in addProcessAndRun.
(b) processScheduler.AddProcess was only ever called inside Run()'s
startup loop. So a scheduled process added at runtime via
UpdateProject (or UpdateProcess's remove-then-re-add path) was
never registered with gocron — its `next_run_time` stayed nil and
its cron expression was silently dropped forever.
(c) ProcessConfig.Compare's reflect.DeepEqual block omitted the
Schedule field. So if a user changed only the cron expression on an
existing scheduled process, UpdateProject treated it as up-to-date
and silently ignored the change. Compare predates scheduled
processes (v1.87.0) and was never updated.
The fix also introduces Scheduler.RemoveProcess for the symmetric cleanup
path — distinct from PauseProcess, which preserves the entry so
ResumeProcess can re-arm it. RemoveProcess tolerates
gocron.ErrJobNotFound (e.g. when the underlying job was already wiped by
a previous Stop or lost to a race with NewJob's eventually-consistent
commit), and always cleans up the scheduler's tracking map.
Tested live against a 16-process supervisor: a newly-added
scheduled-cron entry with `run_on_start: false` now has the expected
`next_run_time` and does not fire on `project update`. Previously the
same operation would fire the new process immediately AND leave its cron
schedule orphaned.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three new test surfaces lock in the fix so it can't quietly regress
under future simplifications:
- src/scheduler/scheduler_test.go: 4 cases for the new RemoveProcess
(found, unknown, already-paused, gocron-shutdown-tolerance)
- src/types/process_test.go: 4 cases extending the table-driven
TestCompareProcessConfigs — different cron, different run_on_start,
nil-vs-set Schedule, identical Schedule equal
- src/app/system_test.go: TestUpdateProject_ScheduledProcess —
end-to-end add → schedule-change → remove of a scheduled process
through UpdateProject, asserting scheduler registration and proper
cleanup. Separate from the existing TestUpdateProject so the
pre-fix behavior assertion in that test stays untouched.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Author
|
Closing — withdrawing this PR. Apologies for the noise. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Summary
Three closely related bugs in
UpdateProject(CLI:process-compose project update, REST:POST /project) cause scheduled processes to misbehave when added, modified, or compared as part of a running-project update:project updatefires IMMEDIATELY, ignoringrun_on_start: false.next_run_timestays nil and the cron is silently dropped forever.cron/interval/run_on_start/timezoneis detected as no-change byProcessConfig.Compare, so the new schedule is silently ignored.Live evidence from my setup (Mac, 16 daemons supervised, v1.110.0): I appended one scheduled-cron entry to the project YAML and ran
process-compose project update -f <yaml>. The new entry fired immediately and got "orphaned" —next_run_time: null— so its0 8 * * *cron would never have fired again until a full supervisor restart.The bug
(1) Immediate fire on add —
src/app/project_runner.goUpdateProjectroutes new entries toaddProcessAndRun:IsDeferred()returns true only forIsForeground || Disabled— it does not considerSchedule. The startup loop has the correct guard (lines 186-189) —addProcessAndRunis just missing the same one.(2) Schedule orphaned —
src/scheduler/scheduler.go+src/app/project_runner.goprocessScheduler.AddProcessis called in exactly one place — the startupRun()loop at line 169. NeitherUpdateProject,UpdateProcess,addProcessAndRun,StartProcess, norRestartProcessregister new entries with the scheduler. A scheduled process added at runtime is permanently orphaned from gocron.Symmetrically,
removeProcessnever deregisters from the scheduler — there's noScheduler.RemoveProcessmethod at all (onlyPauseProcess, which keeps the entry ins.schedulessoResumeProcesscan re-arm it).(3) Compare omits Schedule —
src/types/process.goProcessConfig.Compare(lines 107-149) walks ~25 fields by direct compare orreflect.DeepEqual.Scheduleis literally absent from the field list. The function predates scheduled processes (introduced in v1.87.0); the maintenance was never done.If only
Schedule.Cronchanges between the running config and the new YAML, Compare returnstrue(equal) → UpdateProject classifies the process as up-to-date → the cron change is silently ignored.The fix
Three minimal source changes + one new scheduler method:
src/scheduler/scheduler.go— addRemoveProcess(full removal, distinct fromPauseProcess's preserve-for-resume semantics). Toleratesgocron.ErrJobNotFound(the underlying gocron job table is eventually-consistent againstNewJoband may have already wiped the job on shutdown) and always cleans up the local tracking map:src/app/project_runner.go— patchaddProcessAndRunto route scheduled processes toScheduler.AddProcessand skiprunProcess. Mirrors the startup-loop semantics. PatchremoveProcessto callScheduler.RemoveProcessfor full deregistration.src/types/process.go— add!reflect.DeepEqual(p.Schedule, another.Schedule)to Compare's DeepEqual chain.Why patching
addProcessAndRun(rather than branching in each call site): the helper is called from bothUpdateProject(new procs) andUpdateProcess(the remove-then-re-add path). Both call sites should treat scheduled processes the same way. One change in the helper covers both, and means future call sites get the right behavior for free.Minimal reproduction
Before / After (real environment)
I run a 16-process supervisor on macOS. I added one scheduled-cron entry to the YAML and ran
project update. Capturedcurl localhost:8080/processes:Before fix (v1.110.0):
→ Fired immediately at the update moment, orphaned from scheduler.
After fix (this PR):
→ Quiet, scheduler-registered, will fire at the configured time.
Tests
Three test surfaces lock in the fix:
src/scheduler/scheduler_test.go— 4 cases for the newRemoveProcess(found, unknown, already-paused, gocron-shutdown-tolerance).src/types/process_test.go— 4 cases extending the table-drivenTestCompareProcessConfigs: different cron, differentrun_on_start, nil-vs-set Schedule, identical schedules equal.src/app/system_test.go—TestUpdateProject_ScheduledProcessend-to-end throughUpdateProject: add → schedule-change → remove. Separate from the existingTestUpdateProjectso the pre-fix behavior assertion in that test stays untouched.make testracepasses locally.make lintclean.make ci'sbuild-nixstep skipped (non-Nix host).There's one pre-existing failure in
TestBuildProcessEnvironment/PC_PROC_NAME+PC_REPLICA_NUMonmain(unrelated to this change — environment-variable test, no scheduler involvement). Confirmed by runninggo test ./src/app/ -run TestBuildProcessEnvironmentagainst unmodifiedmain.Why this fix vs. alternatives
UpdateProject's new-procs loop andUpdateProcess's add-after-remove path. The helper is the canonical "add a process to the runtime" entry point — putting the schedule guard there means the next caller (e.g. a future scaling path) gets it for free.UpdateProcess's second compare catch it.UpdateProcessdoes callAssignProcessExecutableAndArgsbefore its own Compare, butUpdateProjectclassifies entries via Compare before dispatching toUpdateProcess— and Schedule-only changes never make it past the first classification. Compare needs to know about Schedule directly.PauseProcessinstead of addingRemoveProcess. Pause keeps the entry ins.schedulessoResumeProcesscan re-arm it. For a process being deleted from the project entirely, that's a leak — and a name collision risk if a laterAddProcessreuses the same name. The two operations have distinct contracts.Related
fix(loader): persist user-supplied FileNames, not the extends-mutated slice). Same family of bug: state leak across reload. Cross-referencing as precedent — not a dependency.project updatein not in HTTP api #381 (closed) — the PR that addedproject updateto the HTTP API. The current PR exposes that endpoint's scheduled-process path was never wired up correctly.I have one more open question — a separate bouncing issue I observed on the live supervisor where two pre-existing scheduled processes were classified as "Updated" on a
project updateagainst a structurally-identical YAML, causing a full pulsar-postgres restart. The source-level explanation (UpdateProjectcalling Compare beforeAssignProcessExecutableAndArgswhile the in-memory copy is already assigned) doesn't quite match the code inloader.Loadwhich already runsassignExecutableAndArgsbefore returning. I'm filing that separately as an issue with debug-log evidence; happy to follow up with another PR if you confirm the diagnosis.Thanks for
process-compose— it's been a quiet workhorse for a 16-daemon Mac supervisor setup of mine for the past few weeks.