Queued Actions
An action that takes time can run in queued jobs. The request reads the rows, puts their keys into one job per chunk, and answers at once. The worker runs handle() as the user who sent the request.
Queuing an action
Implement ShouldQueue:
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Collection;
use Lomkit\Rest\Actions\Action;
use Lomkit\Rest\Actions\ActionFields;
class ExportOrders extends Action implements ShouldQueue
{
public function handle(ActionFields $fields, Collection $models): void
{
// Runs in the worker, as the user who sent the request.
}
}
- The request pushes one
Lomkit\Rest\Actions\QueuedActionjob per chunk of$chunkCountrows, and answers{"data": {"impacted": 250, "result": null}}.impactedcounts the rows put into the jobs. - One run creates at most
rest.limits.sizes.jobsjobs,100by default and at least1. When the rows need more jobs, each job takes the number of rows divided by that limit, rounded up, instead of$chunkCount: 250 rows with a$chunkCountof2give 84 jobs of 3 rows, not 125 jobs. Raiserest.limits.sizes.jobswhen each job must keep$chunkCountrows, see Limits. respond()runs in the request, once the jobs are pushed.- In tests, assert
Queue::assertPushed(QueuedAction::class). The job shows the action class as its name.
Choosing the queue
Put Laravel's queue attributes on the action, or route the action class with Queue::route():
use Illuminate\Queue\Attributes\Queue;
use Illuminate\Queue\Attributes\Tries;
#[Queue('exports')]
#[Tries(3)]
class ExportOrders extends Action implements ShouldQueue
{
// ...
}
Queue::route(ExportOrders::class, 'exports', 'redis');
#[Connection], #[Queue], #[Delay], #[Tries], #[Timeout], #[Backoff], #[MaxExceptions] and #[FailOnTimeout] apply to the jobs of the action. An attribute wins over a route. An action has no $queue or $connection property and no dispatch() method.
release(), attempts(), fail(), delete() and batch() work in handle(). An action that implements Illuminate\Contracts\Queue\ShouldBeEncrypted gets its job payload encrypted, input values included.
What the worker runs
The job holds the resource class, the action class and key, the input values as the client sent them, the keys of the rows and a reference to the user. The worker builds the request context again:
$this->request()->user()returns the user who sent the request, inhandle(), inperimeterQuery()and in the policies that the package checks. The worker logs nobody in:auth()->user()and theGatefacade see no user there.- The worker finds the action by its key among the
actions()of the resource, so the options set when declaring it are kept, and casts the input values withfields()again. - The worker loads the rows by key through
perimeterQuery()and the global scopes of the model. A row deleted or moved out of the perimeter before the job runs is skipped, and a job whose rows are all gone does not callhandle(). - The worker loads the columns of the rows, without the
includesandaggregatesof the search. Load the relations thathandle()needs there:$models->load('author'). - The rows were checked in the request with
authorizeEach(). The worker does not check them again: call$this->resource->authorizedTo('update', $model)inhandle()to check a row as it is when the job runs. - Each chunk runs in a transaction around
handle(), unless the action usesActionTransaction::None. WithActionTransaction::Row, each row of the job runs in its own transaction aroundhandleRow().
A job fails with Lomkit\Rest\Exceptions\QueuedActionException when the user provider no longer finds the user, or when the resource no longer declares the action, for example after a deploy:
The resource [App\Rest\Resources\PostResource] no longer declares the action [App\Rest\Actions\PublishPosts] under the key [publish-posts].
QueueTenancyBootstrapper of stancl/tenancy 3 does from the job payload. The worker reads the user and the rows on the database connections that are current when the job runs.Do not put a resource, an action or a request context in a job of your own: they cannot be serialized. Pass class names and keys. ActionFields can be serialized: it keeps the cast values and finds the user again when it is unserialized.
Batched actions
Implement Lomkit\Rest\Contracts\BatchableAction next to ShouldQueue to run the jobs in a Laravel batch. withBatch() registers the callbacks of the batch:
use Illuminate\Bus\Batch;
use Illuminate\Bus\PendingBatch;
use Illuminate\Contracts\Queue\ShouldQueue;
use Lomkit\Rest\Actions\Action;
use Lomkit\Rest\Actions\ActionFields;
use Lomkit\Rest\Contracts\BatchableAction;
use Throwable;
class ExportOrders extends Action implements ShouldQueue, BatchableAction
{
public function withBatch(ActionFields $fields, PendingBatch $batch): void
{
$batch->then(function (Batch $batch) {
// Every job ran.
})->catch(function (Batch $batch, Throwable $e) {
// A job failed.
});
}
}
The response gives the id of the batch, between impacted and result:
{"data": {"impacted": 250, "batch": "9f0c4d2e-5a1b-4c3d-8e7f-0a1b2c3d4e5f", "result": null}}
- Read the batch with
Bus::findBatch($id). The batch keeps the resource, the action and the user who started it in its options. - The jobs skip their rows once the batch is cancelled:
$this->batch()?->cancel()inhandle(), orBus::findBatch($id)?->cancel(), stops the jobs that did not run yet. - A batched action sends its batch to the connection and queue of its jobs.
- Exports writes the rows of a search to a CSV file with a batched action.
Following a batch
withBatchProgress() adds a progress route to the resource:
Rest::resource('invoices', InvoiceController::class)->withBatchProgress();
GET /api/invoices/actions/export-orders/batches/9f0c4d2e-5a1b-4c3d-8e7f-0a1b2c3d4e5f
{"data": {"id": "9f0c4d2e-5a1b-4c3d-8e7f-0a1b2c3d4e5f", "total_jobs": 3, "pending_jobs": 1, "failed_jobs": 0, "progress": 67, "finished": false, "cancelled": false}}
- The route is named
{name}.batch. It answers the user who started the batch, through the same guard. - Another user, a guest, another action and an unknown batch get a
404, and so does every batch started by a guest. - The route checks the abilities that the resource's
abilities()returns forEndpoint::Batch, none by default. progressgoes from0to100.