Jobs e Execucao Assincrona¶
O MIDDAG separa tres conceitos para trabalho executavel:
- Command -- a unidade de trabalho (o que fazer).
- dispatch_async -- execucao em background via Moodle adhoc task (fire-and-forget).
- Job -- governanca de execucao com retry, correlacao e observabilidade.
Command vs Job¶
| Aspecto | Command | Job |
|---|---|---|
| Papel | Payload da acao (value object) | Registro de governanca (aggregate) |
| Persistencia | Serializado no adhoc task | Tabelas middag_job e middag_job_attempt |
| Identidade | Sem identidade propria | UUID v7 com identidade estavel |
| Retry | Nao governa | Politica de tentativas sob controle do aggregate |
| Correlacao | Nao possui | ID opaco de correlacao |
| Quando usar | Sempre (e a unidade basica) | Quando precisar de observabilidade ou retry |
Nem todo command precisa de um job. A criacao de um job e uma decisao explicita, nao automatica.
Quick example¶
1. Criar o command¶
namespace local_meuplugin\extensions\catalogo\item;
use local_middag\base\command;
final class sync_inventory_command extends command
{
public function __construct(
public readonly int $warehouse_id,
public readonly string $source,
) {}
public function to_payload(): array
{
return [
'warehouse_id' => $this->warehouse_id,
'source' => $this->source,
];
}
public static function from_payload(array $payload): static
{
return new static(
warehouse_id: (int) ($payload['warehouse_id'] ?? 0),
source: (string) ($payload['source'] ?? ''),
);
}
}
2. Criar o handler¶
O handler e resolvido por convencao: {Command}_handler, mesmo namespace, metodo __invoke():
namespace local_meuplugin\extensions\catalogo\item;
use local_middag\base\command_handler;
final class sync_inventory_command_handler extends command_handler
{
public function __construct(
private readonly inventory_service_interface $inventory,
) {}
public function __invoke(sync_inventory_command $command): void
{
$this->inventory->sync($command->warehouse_id, $command->source);
}
}
3. Despachar¶
use local_middag\facade\command_bus;
// Sincrono -- executa agora.
command_bus::handle(new sync_inventory_command(
warehouse_id: 7,
source: 'erp',
));
// Assincrono -- enfileira no cron do Moodle.
command_bus::dispatch_async(new sync_inventory_command(
warehouse_id: 7,
source: 'erp',
));
API reference¶
command_interface¶
Contract base de todo command:
| Metodo | Retorno | Descricao |
|---|---|---|
to_payload() |
array<string, mixed> |
Serializa para primitivos (persistencia) |
from_payload($p) |
static |
Reconstroi o command a partir do payload salvo |
command_bus_interface¶
Bus que resolve e executa commands:
| Metodo | Retorno | Descricao |
|---|---|---|
handle($command) |
void |
Execucao sincrona -- resolve handler e executa imediatamente |
dispatch_async($command, ?$options) |
void |
Serializa o command e enfileira como Moodle adhoc task |
Classes base¶
| Classe | Namespace | Papel |
|---|---|---|
base\command |
local_middag\base |
Classe base para commands de extensions |
base\command_handler |
local_middag\base |
Classe base (opcional) para handlers de extensions |
Dispatch sincrono (handle)¶
Resolve o handler pelo FQCN do command + sufixo _handler, injeta dependencias do container DI, e chama __invoke():
O handler executa no mesmo request. Se falhar, a excecao propaga para o chamador.
Dispatch assincrono (dispatch_async)¶
Serializa o command via to_payload(), cria um Moodle adhoc task, e delega ao cron. O handler sera executado na proxima rodada do cron:
command_bus::dispatch_async(new send_notification_command(
item_id: 42,
recipients: [10, 20, 30],
));
O adhoc task reconstroi o command via from_payload() e executa o handler normalmente. Sem retry, sem correlacao, sem registro persistido -- e fire-and-forget.
Quando usar job¶
Se voce precisa de retry, correlacao ou observabilidade, use o job_service em vez de dispatch_async direto:
use local_middag\facade\job_service;
$job = job_service::dispatch(new sync_inventory_command(7, 'erp'), [
'subjectid' => 7,
'subjecttype' => 'warehouse',
'extension' => 'catalogo',
]);
Ciclo de vida do job¶
| Estado | Descricao |
|---|---|
pending |
Job criado, aguardando execucao |
running |
Handler em execucao (attempt ativo) |
succeeded |
Ultima tentativa concluida com sucesso |
failed |
Ultima tentativa falhou; pode haver retry agendado |
cancelled |
Cancelado explicitamente antes de completar |
Cada tentativa e registrada como job_attempt imutavel -- historico completo e preservado.
Correlacao¶
O job usa um identificador opaco de correlacao para vincular a execucao a operacao de origem, sem acoplamento direto ao aggregate:
$job = job_service::dispatch($command, [
'correlation_id' => 'import-batch-2026-04',
'subjectid' => $company_id,
'subjecttype' => 'company',
'extension' => 'bigquery',
]);
Isso permite consultar: "quais jobs foram disparados para a empresa X no lote de abril?"
Tabela de decisao¶
| Cenario | Mecanismo |
|---|---|
| Acao imediata, resultado necessario agora | command_bus::handle() |
| Background simples, sem necessidade de rastreio | command_bus::dispatch_async() |
| Precisa de retry em caso de falha | job_service::dispatch() |
| Precisa correlacionar multiplas execucoes | job_service::dispatch() |
| Admin precisa ver status na UI | job_service::dispatch() |
| Envio de email ou notificacao | command_bus::dispatch_async() |
| Sincronizacao com API externa (pode falhar) | job_service::dispatch() |
Patterns¶
Fire-and-forget¶
Para trabalho que nao precisa de rastreio (notificacoes, invalidacao de cache):
Simples, leve, sem overhead de governanca.
Idempotencia no handler¶
Handlers devem suportar re-execucao segura. Se o job faz retry apos falha de rede, a segunda execucao nao deve duplicar o efeito:
public function __invoke(sync_inventory_command $command): void
{
// Verifica se ja foi sincronizado antes de agir.
if ($this->inventory->is_synced($command->warehouse_id)) {
return;
}
$this->inventory->sync($command->warehouse_id, $command->source);
}
Payload simples¶
O payload do command deve conter apenas dados primitivos e IDs. Nao coloque objetos complexos, conexoes ou recursos:
// CORRETO: primitivos e IDs.
public function __construct(
public readonly int $item_id,
public readonly int $user_id,
public readonly string $action,
) {}
// ERRADO: objeto complexo no payload.
public function __construct(
public readonly \stdClass $item, // Nao serializa de forma segura.
public readonly \moodle_database $db, // Recurso, nao serializavel.
) {}
Separacao de responsabilidade¶
O command carrega dados. O handler executa logica. Nunca coloque logica de negocio no command:
// CORRETO: command e puro DTO.
final class send_webhook_command extends command
{
public function __construct(
public readonly string $url,
public readonly array $data,
) {}
}
// ERRADO: logica no command.
final class send_webhook_command extends command
{
public function execute(): void // Nao faca isso.
{
file_get_contents($this->url);
}
}
Anti-patterns¶
Dispatch sincrono pesado¶
// ERRADO: operacao demorada bloqueando o request.
command_bus::handle(new import_10k_records_command($file_id));
Operacoes pesadas devem ser assincronas. Use dispatch_async() ou job_service::dispatch().
Handler sem tratamento de erro¶
// ERRADO: excecao nao tratada, sem possibilidade de retry.
public function __invoke(sync_command $command): void
{
$this->http->post($url, $data); // Se falhar, perde-se.
}
Para integracao com servicos externos, use job com retry. Se usar dispatch_async direto, trate excecoes no handler.
Command com estado mutavel¶
// ERRADO: propriedades mutaveis.
final class bad_command extends command
{
public int $attempt_count = 0; // Estado mutavel no command.
public function increment(): void
{
$this->attempt_count++;
}
}
Commands sao value objects imutaveis. Estado de execucao pertence ao job, nao ao command.
Criar job para tudo¶
Nem toda operacao assincrona precisa de job. Jobs adicionam overhead de persistencia e governanca. Para trabalho simples sem necessidade de retry ou rastreio, dispatch_async e suficiente.
Ciclo de vida completo¶
sequenceDiagram
participant S as Service/Controller
participant B as command_bus
participant J as job_service
participant Q as adhoc_task (cron)
participant H as Handler
Note over S,H: Dispatch sincrono
S ->> B: handle(command)
B ->> H: __invoke(command)
Note over S,H: Dispatch async (fire-and-forget)
S ->> B: dispatch_async(command)
B ->> Q: Serializa e enfileira
Q ->> B: Reconstroi e handle()
B ->> H: __invoke(command)
Note over S,H: Dispatch com job (governanca)
S ->> J: dispatch(command, options)
J ->> J: Cria job (pending)
J ->> Q: Enfileira adhoc task
Q ->> J: Marca running, executa
J ->> H: __invoke(command)
H -->> J: Sucesso ou falha
J ->> J: Registra attempt, atualiza estado
Referencias¶
- Comandos -- guia detalhado de commands sync/async
- Modelo de comando assincrono (ADR-705)
- Job como aggregate (ADR-504)
- Core Capabilities (ADR-607)