# IO Cron

## Cron-runner for the IO framework, wrapping the Symfony Scheduler package

## Usage

- This module adds a Daedalus command `daedalus cron:run` to execute the cron jobs. This can be run manually but ideally
  a `systemd` service or similar should be created to manage this. An example `systemd` service is included in the root
  of the module.

## Registering tasks

Cron jobs should be registered using the IO HOOK system, hooking onto the
`\apexl\Io\modules\cron\cronModule::HOOK__REGISTER_CRON_TASK`.

This hook will be passed a `ScheduleCollection`. Add Schedules to it via `addAction()` and return the collection.

Actions MUST implement the `apexl\Io\modules\cron\Interface\CronAction` interface, which requires a single `run()`
method. The action is passed to `addAction()` as a fully-constructed instance, so it can carry DI-injected services as
constructor dependencies — `addAction()` handles the Symfony Scheduler serialisation issue internally by wrapping the
action in a `StaticMessageProvider`.

## Statefulness

Schedules can be made 'stateful'. This means that if the `cron:run` service is not running at the time they are
scheduled. They will be run immediately, the next time the Cron process is started.

The `->stateful()` method requires a cache implementing `Symfony\Contracts\Cache\CacheInterface`. This module includes
an implementation of this that uses IO's System variables to store data: `apexl\Io\modules\cron\Service\SchedulerCache`.

This can be used in combination with `->processOnlyLastMissedRun(true)`, which ensures that only one missed instance of
each schedule is run, if many have been missed.

`addAction()` returns the built `Schedule`, so statefulness can be chained on directly:

```php
$schedules->addAction($trigger, $action)
    ->stateful($this->schedulerCache)
    ->processOnlyLastMissedRun(true);
```

## Continuous tasks

For tasks that should run repeatedly with no fixed schedule — i.e. the next iteration starts as soon as the previous
finishes — use the bundled `ContinuousTrigger`. It returns `$lastRun + 1s` from `getNextRunDate()`, so the synchronous
in-process Scheduler dispatches the next iteration immediately after each handler returns, with a 1s floor to avoid
pegging a CPU on no-op/fast-failing runs.

## Example

```php
final readonly class MyCronAction implements CronAction
{
    public function __construct(
        private SomeService $service,  // DI-injected, not serialisable — that's fine
    ) {}

    public function run(): void
    {
        $this->service->doSomething();
    }
}


final readonly class RegisterMyCronJobHook implements AbstractHook
{
    public function __construct(
        private MyCronAction $action,
        private SchedulerCache $schedulerCache,
    ) {}

    public function hooks(): array
    {
        return [cronModule::HOOK__REGISTER_CRON_TASK];
    }

    public function __invoke(ScheduleCollection $schedules): ScheduleCollection
    {
        $schedules->addAction(CronExpressionTrigger::fromSpec('@monthly'), $this->action)
            ->stateful($this->schedulerCache)
            ->processOnlyLastMissedRun(true);

        return $schedules;
    }
}
```

For continuous tasks:

```php
$schedules->addAction(new ContinuousTrigger(), $this->action);
```