MMOframework
Why AI Playground Libraries Documentation Search Agency
FR EN
Index

JobStore

MMO\core\jobs · 1 symbol · generated 27 Oct 2026

class

JobStore

JobStore.php:32

Where long jobs (aJob) live: one folder per job, that the job's own process writes and any number of readers (a page, an SSE stream, the command line) read -- the job never talks to a browser.

class MMO\core\jobs\JobStore extends MMO\core\classes\aModule
Namespace
MMO\core\jobs
Extends
aModule
A job's folder (`{folder}/{id}/`) holds: - `job.json`: what to run and where it stands: `{id, class, title, params, info, state, pid, created, started, ended, heartbeat, progress, result, error}` ([`get`](https://mmoframework.fr/en/documentation/mmo.core/php/jobs/JobStore.md#jobstore-get)). - `events.ndjson`: what it said, one JSON event per line, append only ([`emit`](https://mmoframework.fr/en/documentation/mmo.core/php/jobs/JobStore.md#jobstore-emit)/[`events`](https://mmoframework.fr/en/documentation/mmo.core/php/jobs/JobStore.md#jobstore-events)). An event's position is the byte offset of its end, so a reader resumes exactly where it stopped (an SSE `Last-Event-ID`). - `stop`: present once a stop was asked ([`stop`](https://mmoframework.fr/en/documentation/mmo.core/php/jobs/JobStore.md#jobstore-stop)); the job checks it ([`aJob::shouldStop`](https://mmoframework.fr/en/documentation/mmo.core/php/jobs/aJob.md#ajob-shouldstop)). - `run.php` and `output.log`: the launcher [`spawn`](https://mmoframework.fr/en/documentation/mmo.core/php/jobs/JobStore.md#jobstore-spawn) writes and the process's own stdout/stderr. States: `queued` (created, not started yet), `running`, then one of `done`, `failed`, `stopped`. A `queued`/`running` job whose process is gone is reported `failed` ([`get`](https://mmoframework.fr/en/documentation/mmo.core/php/jobs/JobStore.md#jobstore-get)).
$store = JobStore::Shared(); $id = $store->start(DemoJob::class, ['steps' => 10]); // returns at once, the job runs detached $store->get($id)['state']; // 'running' ['events' => $events, 'offset' => $offset] = $store->events($id); // what it said so far $store->stop($id); // asks it to stop at its next check

Index

DONE
FAILED
ID_PATTERNan id: its creation date, then random hex (sorting ids sorts jobs by date)
QUEUED
RUNNING
STOPPED
bootstrap
folder
keepDays
php
startTimeout
create($class, $params, $info)Record a new job, without starting it (start does both).
delete($id)Delete a job's folder (not while it runs).
emit($id, $event)Append an event to a job's events (events.ndjson); t (microtime) is added when missing.
events($id, $offset, $max)A job's events after a position: only whole lines, so a line still being written is read next time.
exists($id)
get($id)A job, as recorded (JobStore), with its state checked against its process: a queued or running job whose process is gone (killed, crashed before its shutdown handler) is recorded failed.
getBootstrap()
getFolder()
getPhp()
isLost($job)
isStopRequested($id)
kill($id)Kill a job's process right away (SIGKILL), when asking it to stop (stop) is not enough: it gets no chance to clean up, and ends stopped.
list($class, $limit)Every job, newest first (get).
path($id, $file)
prune($days)Delete the finished jobs older than $keepDays.
read($id)
running($class)
spawn($id)Run a recorded job in its own process, detached from this one (setsid, so it outlives a web request and its php-fpm worker), with no time limit.
start($class, $params, $info)Record a job and run it in its own detached process (create, spawn).
stop($id)Ask a job to stop: it does at its next aJob::shouldStop check, and ends stopped.
update($id, $changes)Change a job's record, under a lock (the job's process and its readers both write it).
write($id, $job)Write a job's record whole, atomically (a reader never sees half of it).

Constants6

DONE

const DONE = 'done'

FAILED

const FAILED = 'failed'

ID_PATTERN

an id: its creation date, then random hex (sorting ids sorts jobs by date)

const ID_PATTERN = '/^\\d{8}-\\d{6}-[0-9a-f]{8}$/'

QUEUED

const QUEUED = 'queued'

RUNNING

const RUNNING = 'running'

STOPPED

const STOPPED = 'stopped'

Properties5

bootstrap

?string $bootstrap = null
Type
null | string

keepDays

int $keepDays = 30
Type
int

startTimeout

int $startTimeout = 60
Type
int

Methods22

create($class, $params, $info)

JobStore.php:110

Record a new job, without starting it (start does both).

function create(string $class, array $params = [], array $info = []): string
$classstring
$params = []arraywhat aJob::run receives
$info = []arrayanything the caller wants kept with it (who started it, from where)
returnsstringthe job's id
throwsif $class is not an aJob.
throwsif $class is exclusive (aJob::Exclusive) and one of its jobs is already running.
$id = $store->create(DemoJob::class, ['steps' => 3], ['user' => 'god']);

emit($id, $event)

JobStore.php:311

Append an event to a job's events (events.ndjson); t (microtime) is added when missing.

function emit(string $id, array $event): int
$idstring
$eventarray
returnsintthe event's position (the byte offset of its end: what a reader passes back to events)
$store->emit($id, ['type' => 'log', 'level' => 'info', 'message' => 'hello']);

events($id, $offset, $max)

JobStore.php:337

A job's events after a position: only whole lines, so a line still being written is read next time.

function events(string $id, int $offset = 0, int $max = 1048576): array
$idstring
$offset = 0intwhere the last read stopped (0: from the start)
$max = 1048576intthe most bytes to read at once
returnsarrayarray<array>, offset: int} each event with its id (its position), and where to read next
['events' => $events, 'offset' => $offset] = $store->events($id); //later, only what came since ['events' => $more, 'offset' => $offset] = $store->events($id, $offset);

get($id)

JobStore.php:197

A job, as recorded (JobStore), with its state checked against its process: a queued or running job whose process is gone (killed, crashed before its shutdown handler) is recorded failed.

function get(string $id): array
$idstring
returnsarray
throws
$store->get($id); // ['id' => ..., 'state' => 'running', 'progress' => ['done' => 3, 'total' => 10, ...], ...]

getFolder()

JobStore.php:60
function getFolder(): string
returnsstringthe folder holding every job (created if needed).

getPhp()

JobStore.php:87
function getPhp(): string
returnsstringthe php command line binary: $php, else this one when running on the command line; under a web server PHP_BINARY is php-fpm or apache's, so the cli one next to it.

isLost($job)

JobStore.php:413
protected function isLost(array $job): bool
$jobarray
returnsboolwhether a queued or running job's process is gone (or never came).

kill($id)

JobStore.php:269

Kill a job's process right away (SIGKILL), when asking it to stop (stop) is not enough: it gets no chance to clean up, and ends stopped.

function kill(string $id): array
$idstring
returnsarraythe job

list($class, $limit)

JobStore.php:218

Every job, newest first (get).

function list(?string $class = null, ?int $limit = null): array
$class = nullnull | stringonly this class's jobs
$limit = nullnull | int
returnsarray
$store->list(DemoJob::class, 10);

path($id, $file)

JobStore.php:73
function path(string $id, string $file = ''): string
$idstring
$file = ''stringa file of the job's folder, '' for the folder itself
returnsstring
throwsif $id is not a job id (it is used in a path).
$store->path($id, 'events.ndjson'); // '/tmp/mmo.jobs/20261019-142501-a1b2c3d4/events.ndjson'

running($class)

JobStore.php:231
function running(string $class): ?array
$classstring
returnsnull | arraythe queued or running job of this class, if there is one.

spawn($id)

JobStore.php:161

Run a recorded job in its own process, detached from this one (setsid, so it outlives a web request and its php-fpm worker), with no time limit. It writes a launcher (run.php) that sets the application up (getBootstrap) and hands over to JobRunner::Main.

function spawn(string $id): int
$idstring
returnsintthe process's pid
throwsif the process could not be started.

start($class, $params, $info)

JobStore.php:147

Record a job and run it in its own detached process (create, spawn).

function start(string $class, array $params = [], array $info = []): string
$classstring
$params = []array
$info = []array
returnsstringthe job's id
$id = JobStore::Shared()->start(DemoJob::class, ['steps' => 10]);

stop($id)

JobStore.php:244

Ask a job to stop: it does at its next aJob::shouldStop check, and ends stopped. A queued job that has not started yet is stopped at once.

function stop(string $id): array
$idstring
returnsarraythe job

update($id, $changes)

JobStore.php:388

Change a job's record, under a lock (the job's process and its readers both write it).

function update(string $id, callable|array $changes): array
$idstring
$changescallable | array
returnsarraythe record after the changes
$store->update($id, ['state' => JobStore::RUNNING, 'started' => time()]);

write($id, $job)

JobStore.php:405

Write a job's record whole, atomically (a reader never sees half of it).

protected function write(string $id, array $job): void
$idstring
$jobarray
returnsvoid
Inherited members (48)
ConstantsDeclared byDescription
ALERTiLogLevelAlert: Action must be taken immediately.
ALLiLogLevelMask for all
ALL_PLUSiLogLeveldebug everything
BACKTRACEiLogLevel
CRITICALiLogLevelCritical: Critical conditions.
DEBUGiLogLevelDebug: Detailed debug information.
DEEPiLogLeveldebug everything with arguments
EMERGENCYiLogLevelEmergency: System is unusable.
ERRORiLogLevelError: Runtime errors that do not require immediate action but should typically be logged and monitored.
ERROR_PLUSiLogLevelMask for all error
FATALiLogLevelMask for all fatal error
INFOiLogLevelInfo: Interesting events.
INFO_PLUSiLogLevelMask for all errors/warning/notices/info
MEMORYiLogLevelinclude backtrace object
NOTICEiLogLevelNotice: Normal but significant events.
NOTICE_PLUSiLogLevelMask for all errors/warning/notices/info
TRACEiLogLevelinclude backtrace as origins
WARNINGiLogLevelWarning: Exceptional occurrences that are not errors.
WARNING_PLUSiLogLevelMask for all errors and warnings
PropertiesDeclared byDescription
verbosityaModule
MethodsDeclared byDescription
alert($message, $context)aModuleAction must be taken immediately.
backtraces($message, $context)aModuleForce-log a message at INFO level together with a full stack trace/memory snapshot (iLogLevel::BACKTRACE), bypassing the verbosity mask entirely (any non-zero verbosity is enough to trigger it).
Configure($config)aModuleMerge $config into this module's configuration in the shared Configuration.
critical($message, $context)aModuleCritical conditions. No-op if CRITICAL is currently filtered out by the verbosity mask.
debug($message, $context)aModuleDetailed debug information.
debugName()aModuleHuman-readable identifier used as the default {origin} in this instance's log messages.
DefaultConfig()aModuleThe config a module starts from before anything loaded via Configuration is merged in.
emergency($message, $context)aModuleSystem is unusable. No-op if EMERGENCY is currently filtered out by the verbosity mask.
error($message, $context)aModuleRuntime errors that do not require immediate action but should typically be logged and monitored.
getVerbosity()aModule
info($message, $context)aModuleInteresting events. No-op if INFO is currently filtered out by the verbosity mask.
Instantiate($config)aModule(Re)create the shared instance immediately, configured with $config merged into the class's existing config.
IsShared()aModule
log($level, $message, $context)aModuleLogs with an arbitrary level.
notice($message, $context)aModuleNormal but significant events.
offsetExists($key)aModule\ArrayAccess::offsetExists -- tells whether $key is a declared property at all, regardless of whether it is currently readable/writable through array access.
offsetGet($key)aModule\ArrayAccess::offsetGet -- reads a property directly, unless it has a #[Serialize(serialize: 'methodName')] attribute, in which case that zero-argument method is called instead (e.g.
offsetSet($key, $value)aModule\ArrayAccess::offsetSet -- sets a property directly, unless it has a #[Serialize(unserialize: 'methodName')] attribute, in which case that method is called with $value instead (e.g.
offsetUnset($key)aModule\ArrayAccess::offsetUnset -- equivalent to $module[$key] = null.
serialize($path, $serializer)aModule\MMO\core\interfaces\iSerializable::serialize -- exports every property (via
setVerbosity($verb)aModuleSet the current verbosity mask, replacing whatever it was before.
Shared()aModuleGet the shared instance, creating (and configuring) it on first call.
testVerbosity($level)aModuleTest whether a given level (or set of levels) is currently enabled by this instance's verbosity mask.
traitUnshare($serialize)aModuleRemove the shared instance, so the next Shared() call creates a fresh one.
unserialize($data, $path, $instances)aModule\MMO\core\interfaces\iSerializable::unserialize -- merges $data onto DefaultConfig, runs it through the (possibly overridden) constructor, then applies each resulting value to its property (via
Unshare($serialize)aModuleLike tSingleton::Unshare, but also disconnects this module's instance from every event it was listening to (via Event::DisconnectListeners) before dropping it.
warning($message, $context)aModuleExceptional occurrences that are not errors.
__construct($config)aModuleprotected constructor, so it is not possible to do new Module(...) called by the serialization process just before being injected with data.