Skip to content

fix(core): bound the Laravel scheduler preset's stop grace - #1265

Merged
Hydralerne merged 1 commit into
oblien:mainfrom
sudanese:fix/GH-935-laravel-scheduler-grace
Oct 15, 2026
Merged

Hydralerne merged 1 commit into
oblien:mainfrom
sudanese:fix/GH-935-laravel-scheduler-grace

Conversation

@sudanese

@sudanese sudanese commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The Laravel scheduler preset now stops with SIGTERM and a 10s grace period instead of 90s. The queue and horizon presets keep 90s. The copied roles example in the openship.json reference gets the same scheduler settings.

Motivation

The inspected Laravel 12.x versions lack coordinated schedule:work shutdown. With PHP running as container PID 1 and no termination handler, SIGTERM does not stop scheduling or drain children; work can continue until Docker force-kills the container.

In a local test on Laravel 12.70.0:

  • with stopGracePeriod: "90s", SIGKILL arrived 90.02 s after SIGTERM
  • a lifecycle restart took 93.5 s
  • the "stopped" scheduler kept firing ticks during its grace period

Laravel 13.18.0, with pcntl available, handles INT/TERM/QUIT by stopping new schedule:run launches and waiting for existing runs. This does not guarantee completion of detached background tasks.

The 10s grace deliberately favors shorter shutdown/handover latency over task completion. A foreground scheduled command with more than 10 seconds remaining can be killed on either version, leaving partial effects. Neither withoutOverlapping() nor runInBackground() makes that bound safe. Applications requiring completion must use a verified draining version and size grace for the entire remaining run, with headroom.

This changes registered preset metadata. Automatic preset application is not wired into deployment detection in this revision; deployed roles currently come from explicit configuration. Existing containers retain their saved timeout, and explicit role arrays override presets. Singleton replacement already stops the old scheduler before starting the new one: the benefit is shorter handover delay, not reduced old/new scheduler overlap.

Related issue

Refs #935.

Changes

packages/core

  • src/stacks.ts: the Laravel scheduler preset sets stopSignal: "SIGTERM" and stopGracePeriod: "10s". The command and singleton: true are unchanged. The queue and horizon presets keep "90s". The preset comment now says:
    • the workers' 90 s exceeds the default 60 s queue and Horizon supervisor timeouts
    • the scheduler's 10 s favors short shutdown and handover latency over task completion
    • the inspected 12.x versions lack coordinated schedule:work shutdown
    • Laravel 13.18.0 with signal handling available stops new runs and waits for existing ones, but detached background tasks may not complete
    • a foreground scheduled command with more than 10 seconds left can be killed on either version, so applications that need completion should use a verified draining version and size the grace for the whole remaining run
  • src/stack-roles.test.ts: the test "every preset sets a 90s stop grace period…" becomes "preserves worker and scheduler shutdown settings through the openship.json parser". It expects ["90s", "90s", "10s"] before and after the parser round-trip. The round-trip now includes stopSignal, and the scheduler's stopSignal must be "SIGTERM" both times.

apps/web

  • content/docs/reference/openship-json.mdx ("Warm a worker or scheduler"): the example's scheduler gets "stopSignal": "SIGTERM" and "stopGracePeriod": "10s". The queue keeps "90s". A new sentence follows the example: "These are example shutdown budgets. Longer scheduled runs may need a larger grace period on a Laravel version verified to drain."

Verification

bun run --cwd packages/core test   # 98 files, 1733 passed
bunx vitest run src/stack-roles.test.ts   # (in packages/core) 32 passed
bun run --cwd packages/core lint   # tsc --noEmit, clean

Before the preset change, the updated test fails with expected [ '90s', '90s', '90s' ] to deeply equal [ '90s', '90s', '10s' ].

Mutation checks. Each mutant fails the updated test:

  • Revert the scheduler to "90s" without stopSignal.
  • Drop only stopSignal: expected undefined to be 'SIGTERM'.
  • Set only the grace back to "90s".

The edited docs example is valid JSON. Passed through parseOpenshipConfigJson, it returns no errors and no warnings, with the scheduler at SIGTERM and 10s.

bun run --cwd apps/web lint could not check the page in this checkout. It stops at Cannot find module '@/.source/server', a module that the docs build generates. I did not run next build.

Prettier comparison: stacks.ts, stack-roles.test.ts and openship-json.mdx were not Prettier-clean before this change. I did not reformat them. Prettier reports the same number of differences before and after this change. In stacks.ts and stack-roles.test.ts, none is on a changed line. In openship-json.mdx, Prettier already wanted to reflow the example's one-line JSON entries before this change, and that difference remains.

Checklist

  • One change per PR — one bug, or one agreed feature, with nothing unrelated bundled in
  • The diff is scoped — no reformatting or lint fixes on lines I wasn't otherwise changing
  • A test fails without this change and passes with it
  • Verification covers packages/core tests and typecheck, and a parser check of the docs example. The web typecheck limit and the pre-existing formatting exception are documented above.
  • I understand every line of this diff and can explain it in review

The Laravel scheduler preset used the same 90s stop grace as the
workers. The inspected Laravel 12.x versions lack coordinated
`schedule:work` shutdown. With PHP as container PID 1 and no
termination handler, SIGTERM does not stop scheduling, and work can
continue until Docker force-kills the container. A 90s grace made a
scheduler handover wait that long.

The scheduler preset now sets stopSignal SIGTERM and a 10s grace. The
grace favors shorter shutdown and handover latency over task
completion. A foreground scheduled command with more than 10 seconds
left can be killed on either version. Laravel 13.18.0 with signal
handling available stops new runs on INT, TERM or QUIT and waits for
existing ones, but detached background tasks may not complete.
Applications that need completion should use a verified draining
version and size the grace for the whole remaining run.

This changes registered preset metadata. Deployed roles currently come
from explicit configuration, so the openship.json reference example
gets the same signal and grace, with a note on sizing.

The queue and horizon presets keep 90s, which exceeds the default 60s
queue and Horizon supervisor timeouts. The parser round-trip test now
covers the scheduler's signal and grace.

Refs oblien#935
@Hydralerne
Hydralerne merged commit d51557a into oblien:main Oct 15, 2026
17 checks passed
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.

2 participants