Skip to content

Make the orchestrator a real worker so it stops exiting on boot - #235

Merged
ralyodio merged 1 commit into
masterfrom
fix/orchestrator-worker
Aug 11, 2026
Merged

Make the orchestrator a real worker so it stops exiting on boot#235
ralyodio merged 1 commit into
masterfrom
fix/orchestrator-worker

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

The meshhook Railway service reports EXITED on every deploy. The build is fine — the process genuinely ends.

Why

startOrchestrator() registered two EventEmitter listeners and returned. bus.on(...) does not hold the Node event loop open, so the process printed its banner and exited immediately.

The same EventEmitter was also why work went nowhere. The web app enqueues a run when a webhook arrives; the orchestrator listened on its own in-process bus in a different container and never heard it. #234 fixed the producer to write to the durable queue but left the consumer on the bus, so runs were still silently dropped.

What changed

Both queues are now database-backed:

queue purpose
workflow_jobs a run needs its next step decided
workflow_steps a node needs executing

The orchestrator polls both with the existing Worker. That keeps the process alive, lets several instances share the work, and means queued work survives a restart.

Steps get a 120s visibility timeout rather than the default 30s — a step makes an outbound HTTP call with its own retries, and a shorter lease could lapse while the work is legitimately still in flight, causing a duplicate execution.

Bugs found while rewiring

  • A terminated run restarted forever. A terminate node reports next: null, which replay could not distinguish from "not started", so it looped back to the first node. Replay now tracks termination separately.
  • Completing a run was not idempotent — a redelivered job appended a second run_completed event and rewrote finished_at.
  • An unknown node type silently succeeded, letting a run complete as though an unexecutable node had worked. It now throws, so the job retries or dead-letters.
  • Missing config crash-looped. The process now exits non-zero with a clear message when TURSO_DATABASE_URL is unset or still points at Postgres.
  • SIGTERM dropped in-flight leases. Redeploys now drain before exiting instead of stranding jobs until their visibility timeout expires.

Verification

End to end against a local libSQL file:

seeded run 08cba119-…
ALIVE: orchestrator still running after 8s
events: step_started, step_succeeded, step_started, step_succeeded, run_completed
queued: (empty)
exited cleanly on SIGTERM

380 tests passing (122 vitest including 12 new orchestrator tests, 258 node:test). Web app still builds.

Still needed to actually run in production

The service has no application environment variables set — no TURSO_DATABASE_URL, TURSO_AUTH_TOKEN, or SECRETS_ENCRYPTION_KEY. With this change it will exit(1) with a clear message instead of crash-looping, but it still needs a database before it can do any work.

🤖 Generated with Claude Code

The Railway container reported EXITED on every deploy. The build was fine;
the process genuinely ended. `startOrchestrator()` registered two
EventEmitter listeners and returned, and `bus.on(...)` does not hold the
event loop open — so it printed its banner and exited.

The same EventEmitter was also why work went nowhere. The web app enqueues a
run when a webhook arrives; the orchestrator listened on its own in-process
bus in a different container and never heard it. The previous commit fixed
the producer to write to the durable queue but left the consumer on the bus,
so runs were still dropped.

Both queues are now database-backed:

  workflow_jobs   a run needs its next step decided
  workflow_steps  a node needs executing

The orchestrator polls both with the existing Worker, which keeps the process
alive, lets several instances share the work, and survives a restart. Steps
get a 120s visibility timeout because they make outbound HTTP calls with
their own retries, and a 30s lease could lapse mid-flight and cause a
duplicate execution.

Also fixed while rewiring:
- A terminate node reported next: null, which replay could not distinguish
  from "not started" — the run restarted from its first node forever. Replay
  now tracks termination separately.
- Completing a run is idempotent; a redelivered job appended a second
  run_completed event and rewrote finished_at.
- An unknown node type threw instead of silently succeeding, which had been
  letting a run complete as though an unexecutable node had worked.
- The process exits non-zero with a clear message when TURSO_DATABASE_URL is
  missing or still points at Postgres, rather than crash-looping on the first
  query.
- SIGTERM/SIGINT drain in-flight jobs before exiting, so a redeploy does not
  strand leases until they time out.

Verified end to end against a local libSQL file: the process stays up, drains
a queued run through step_started/step_succeeded to run_completed, leaves the
queue empty, and exits cleanly on SIGTERM.

Tests: 380 passing (122 vitest incl. 12 new orchestrator tests, 258 node:test).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

17 finding(s)

HIGH/CRITICAL: 6 | MEDIUM: 10 | LOW: 1

Severity Rule Location
HIGH secret-database-url apps/web/.env.example:9
HIGH secret-database-url docs/Environment-Setup.md:46
HIGH secret-database-url docs/Environment-Setup.md:132
HIGH secret-slack-webhook docs/WEBHOOK_CONFIGURATION.md:145
HIGH secret-database-url RAILWAY_DEPLOYMENT.md:58
HIGH secret-generic-credential src/nodes/README.md:271
MEDIUM sql-template-interpolation apps/web/src/routes/api/secrets/[id]/+server.js:80
MEDIUM sql-template-interpolation apps/web/src/routes/api/workflows/[id]/+server.js:141
MEDIUM sql-template-interpolation docs/Turso-Migration.md:70
MEDIUM manifest-install-lifecycle-script package.json:9
MEDIUM js-shell-exec-interpolation scripts/generate-issue-prds.mjs:71
MEDIUM js-shell-exec-interpolation scripts/generate-issue-prds.mjs:586
MEDIUM js-shell-exec-interpolation scripts/generate-issue-prds.mjs:651
MEDIUM js-shell-exec-interpolation scripts/generate-issue-prds.mjs:685
MEDIUM js-shell-exec-interpolation scripts/gh-project-status.js:71
MEDIUM js-shell-exec-interpolation scripts/github-issues-sync.mjs:33
LOW secret-generic-credential src/nodes/webhook.test.js:287

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit 2bc9a1f into master Aug 11, 2026
4 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.

1 participant