Skip to content

ADR-006: Two-Phase CLI Bootstrap

Fecha: 2026-02-19 Estado: Aceptado Deciders: Architecture Team, Backend Team

Contexto y Problema

El worker CLI (cli/background-worker.php) ejecuta jobs en background sin contexto HTTP. No hay JWT token, no hay request, no hay headers.

Sin embargo, BatchInvoicingOrchestrator tiene una cadena de dependencias que requiere datos del JWT:

BatchInvoicingOrchestrator
  → BatchInvoicingAuditService → necesita Payload (cuit, nro_cliente)
  → ArcaClientFactory → necesita Payload (cuit)

El objeto Payload normalmente se construye desde el JWT token en contexto HTTP. En CLI, este token no existe.

Problema: ¿Cómo proporcionar contexto de ejecución (cuit, nro_cliente, id_usuario, etc.) al worker CLI sin JWT?

Decisión

Two-Phase CLI Bootstrap: El worker ejecuta en dos fases separadas.

Fase 1 — Lectura minima del job (sin DI container)

El worker recibe {job_id} {schema} {db} como argumentos CLI (pasados por JobDispatcher). Luego hace una conexion PDO directa y minima a DB_INI para leer solo el payload del job:

php
// cli/background-worker.php - Fase 1
$jobId  = (int) $argv[1];
$schema = trim($argv[2]);
$db     = trim($argv[3]);

// Conexion minima a DB_INI (sin DI container)
$pdo = new PDO($dsn, $user, $password, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
$stmt = $pdo->prepare('SELECT payload FROM background_jobs WHERE id = :id LIMIT 1');
$stmt->execute(['id' => $jobId]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);

// Extraer contexto del payload
$jobPayload = json_decode($row['payload'], true);
$context = $jobPayload['_context'] ?? [];
// $context = ['cuit' => '...', 'nro_cliente' => '...', 'id_usuario' => 1,
//             'id' => 1, 'sistema' => '...', 'schema_level' => '...', 'prueba' => bool]

// Propagar contexto al bootstrap via variables de entorno
putenv("CLI_JOB_DB={$db}");
putenv("CLI_JOB_SCHEMA={$schema}");
putenv('CLI_ARCA_CUIT='         . ($context['cuit']        ?? '0'));
putenv('CLI_ARCA_NRO_CLIENTE='  . ($context['nro_cliente'] ?? '0'));
putenv('CLI_PAYLOAD_ID_USUARIO='. ($context['id_usuario']  ?? '0'));
putenv('CLI_PAYLOAD_ID='        . ($context['id']          ?? '0'));
putenv('CLI_PAYLOAD_SISTEMA='   . ($context['sistema']     ?? '0'));
putenv('CLI_JOB_PRUEBA='        . (!empty($context['prueba']) ? '1' : '0'));

$pdo = null; // Cerrar conexion de Fase 1

Fase 2 — Bootstrap completo con Payload sintetico

bootstrap-cli.php lee las variables de entorno establecidas en Fase 1 y construye:

  1. ConnectionManager con conexion oficial apuntando a $db y conexion ini a DB_INI
  2. Payload sintetico con los datos del _context
  3. DI container con shared-definitions + overrides CLI
php
// cli/bootstrap-cli.php
$cliJobDb     = getenv('CLI_JOB_DB');
$cliJobSchema = getenv('CLI_JOB_SCHEMA') ?: 'public';
// ... demas env vars ...

$connectionManager = new ConnectionManager();
$connectionManager->setConfig('oficial', ['database' => $cliJobDb, /* ... */]);
$connectionManager->setConfig('ini', ['database' => DB_INI, 'skip_schema_context' => true]);
$connectionManager->setAlias('principal', 'oficial');

$syntheticPayload = new Payload(
    id:          $payloadId,
    db:          $cliJobDb,
    schema:      $cliJobSchema,
    sistema:     $payloadSistema,
    nro_cliente: $arcaNroCliente,
    id_usuario:  $payloadIdUsuario,
    cuit:        $arcaCuit,
    // ... timestamps JWT sinteticos ...
);

// DI container inyecta Payload, ConnectionManager y Logger como overrides CLI
$container = $containerBuilder->build();
return $container;

De vuelta en background-worker.php, Fase 2 configura el schema y ejecuta:

php
// cli/background-worker.php - Fase 2
$container = require __DIR__ . '/bootstrap-cli.php';

$connectionManager = $container->get(ConnectionManager::class);
$connectionManager->setSchemaContext($schema);

$executor = $container->get(JobExecutor::class);
$executor->execute($jobId);

JobController almacena _context en el payload

Cuando JobController despacha un job, copia los datos relevantes del JWT Payload al payload del job bajo la clave _context:

php
// JobController::dispatch()
$jobPayload = $body['payload'] ?? [];

// Guardar contexto JWT para que el worker CLI pueda reconstruir Payload
$jobPayload['_context'] = [
    'cuit'         => $this->payload->cuit,
    'nro_cliente'  => $this->payload->nro_cliente,
    'id_usuario'   => $this->payload->id_usuario,
    'id'           => $this->payload->id,
    'sistema'      => $this->payload->sistema,
    'schema_level' => $schemaLevel,
    'prueba'       => $prueba,
];

$jobId = $this->dispatcher->dispatch($type, $jobPayload, $userId, $schema, $db, $nroSistema, $prueba);

Consecuencias

Positivas

  • ✅ Workers son completamente autosuficientes (no necesitan JWT al iniciar)
  • ✅ El contexto viaja con el job payload (serializado en BD)
  • ✅ No requiere modificar BatchInvoicingOrchestrator ni sus dependencias
  • ✅ Consistente con ADR-004 (Wrapper Pattern): no se modifica código existente
  • ✅ Fase 1 es extremadamente liviana (solo PDO, sin framework)
  • ✅ Cualquier servicio que dependa de Payload funciona transparentemente

Negativas

  • ❌ Datos de contexto duplicados (JWT + payload _context)
  • ❌ Si Payload agrega campos nuevos, hay que actualizar _context
  • ❌ Dos fases de bootstrap aumentan complejidad del worker

Alternativas Rechazadas

Alternativa A: Pasar credenciales como argumentos CLI

bash
php cli/background-worker.php 123 --cuit=20123456789 --nro_cliente=1 --id_usuario=5

Rechazado: Los argumentos CLI son visibles en la lista de procesos (ps aux). Datos como CUIT y credenciales quedarían expuestos. Riesgo de seguridad inaceptable.

Alternativa B: Almacenar JWT token en la BD

sql
INSERT INTO background_jobs (... jwt_token ...) VALUES (... $token ...)

Rechazado: Almacenar JWT completo en BD es un riesgo de seguridad. Si la BD es comprometida, los tokens permiten suplantar usuarios. Además, tokens JWT expiran, y el worker podría ejecutar después de la expiración.

Alternativa C: Bootstrap único con lazy-loading de Payload

Rechazado: Requeriría que todos los servicios soporten Payload nullable o lazy, lo cual implicaría refactorizar BatchInvoicingAuditService, ArcaClientFactory, etc. Viola ADR-004 (no modificar código existente).

Referencias