From Scheduling to Orchestration: The Future of Obelaw Runner Laravel
From Scheduling to Orchestration: The Future of Obelaw Runner Laravel
As Laravel apps grow, cron-like scheduling and isolated jobs stop being enough. You need predictable sequencing, resilient retries, concurrency safety, safe testability, and observable lifecycle hooks. Building on the existing Runner, RunnerPool, and RunnerService foundations, the upcoming obelaw/runner-laravel release brings enterprise-grade orchestration primitives designed for production workloads.
Below we walk through problems teams face, and the new solutions that feel like a natural evolution of the package’s current APIs and style.
Problem: Tasks that depend on other tasks
Large flows (imports → transform → publish) must run in order. Running them independently leads to partial updates and manual glue code.
Solution: explicit runner chains (dependencies). The API follows existing service patterns (e.g., RunnerService) so it integrates cleanly with runner files and Artisan commands.
Example — declare and dispatch a chain:
use Obelaw\Runner\Services\RunnerService;
$service = new RunnerService(RunnerPool::getPaths());
$service->chain([
'import:products', // runs first
'process-pricing', // runs only if previous succeeded
'publish-updates',
])->run();
Under the hood each chained step respects the runner shouldRun() checks and type (once/always), and the chain aborts on failure so downstream steps are not executed accidentally.
Problem: Transient failures cause noisy retries
Retries that run too aggressively cause cascading failures and hidden backpressure.
Solution: built-in retry strategies with exponential backoff. The configuration mirrors the package’s simple, explicit style (config + optional per-runner override).
Example (config):
'retry' => [
'attempts' => 5,
'strategy' => 'exponential', // linear, fixed also supported
'initial_delay_seconds' => 1,
'max_delay_seconds' => 60,
],
Per-runner override:
public function handle(): void
{
$this->withRetries(5, fn($attempt) => 2 ** ($attempt - 1));
// runner logic
}
Retries are coordinated with backoff to reduce thundering and give dependent services time to recover.
Problem: Duplicate execution in multi-server setups
When many workers or servers can start the same runner, duplicate processing and race conditions happen.
Solution: atomic locks using Laravel’s cache locks (Cache::lock) before starting sensitive work. This is consistent with the package’s existing intent to prevent duplicate runs (see RunnerModel / execution tracking).
Example pattern inside a runner:
use Illuminate\Support\Facades\Cache;
$lock = Cache::lock("runner:{$this->getRunnerName()}", 120);
if (! $lock->get()) {
return; // another process holds the lock
}
try {
$this->handleProtected();
} finally {
$lock->release();
}
The runner scaffolding and RunnerService can optionally handle lock acquisition automatically for configured runner classes.
Problem: Hard to validate orchestration without touching prod data
Testing complex flows in staging/CI is risky if runners mutate data or call external APIs.
Solution: a --dry-run mode at the CLI level that simulates execution paths without performing side effects. This aligns with the existing runner:run command and its filtering flags.
Usage:
php artisan runner:run import:products --dry-run
When enabled, the runtime substitutes side-effecting operations with logged simulations and returns the same summary structure the normal run returns. This makes CI and staging verification straightforward.
Problem: Limited observability of runner lifecycle
Teams need hooks to push metrics, alerts, or audit logs from runner executions.
Solution: lifecycle events — RunnerStarted, RunnerFinished, RunnerFailed — that follow Laravel’s event/listener conventions and integrate with EventServiceProvider.
Example dispatching and listener registration:
// dispatch events at key lifecycle moments
RunnerStarted::dispatch($runner);
RunnerFinished::dispatch($runner, $result);
RunnerFailed::dispatch($runner, $exception);
// register listener in EventServiceProvider
protected $listen = [
\Obelaw\Runner\Events\RunnerFailed::class => [
\App\Listeners\NotifyOnRunnerFailure::class,
],
];
Events make it trivial to push metrics to Prometheus, track errors in Sentry, or send on-call notifications on failures.
How this fits the existing design
- The package already centers on simple runner files (anonymous classes extending
Runner),RunnerPoolpath discovery, and aRunnerService/runner:runcommand — features above extend those primitives while preserving the same developer ergonomics. - New orchestration APIs are intentionally explicit and opt-in (e.g., chain composition,
withRetries,--dry-run, and optional auto-locking) so existing runners keep working unchanged.
Call to Action
If you maintain complex background workflows in Laravel, try the new orchestration patterns locally with the --dry-run flag and share feedback.
- Star the repository on GitHub and open issues for features you want prioritized.
- Contribute a PR if you have integrations or metrics ideas (Sentry, Prometheus, Slack hooks).
We built these features to help teams move from ad-hoc scheduling to resilient orchestration — let us know how you use them.
Want a short changelog or a Twitter thread summarizing this post? I can draft either on request.