Saltar a contenido

Estándares de Implementación de Jobs de Sincronización

Este documento define los criterios obligatorios para implementar jobs de sincronización en el sistema integrador.


Checklist Obligatorio

Todo job de sincronización DEBE cumplir con:

# Requisito Descripción
1 $tries Definir número máximo de reintentos (recomendado: 3)
2 $backoff Definir delay entre reintentos en segundos (recomendado: 15-60)
3 TracksProductSyncFailures Usar el trait para registro automático de fallos
4 try/catch con throw Capturar excepciones, loggear, y RE-LANZAR
5 withoutRelations() Evitar serializar relaciones que pueden ser eliminadas
6 Logging estructurado Usar canales específicos con contexto completo

Plantilla de Job de Sincronización

<?php

namespace App\Jobs;

use App\Constants\LogChannels;
use App\Constants\SyncChannels;
use App\Jobs\Concerns\TracksProductSyncFailures;
use App\Models\ProductItem;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;

class SyncToExampleJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
    use TracksProductSyncFailures;

    // ============================================================
    // CONFIGURACIÓN DE REINTENTOS (OBLIGATORIO)
    // ============================================================

    /**
     * Número máximo de intentos antes de marcar como fallido.
     */
    public int $tries = 3;

    /**
     * Segundos de espera entre reintentos.
     * Puede ser array para backoff exponencial: [10, 30, 60]
     */
    public int $backoff = 30;

    // ============================================================
    // PROPIEDADES (evitar serializar relaciones)
    // ============================================================

    protected ProductItem $productItem;
    protected int $productItemId; // ID primitivo como backup

    // ============================================================
    // CONSTRUCTOR
    // ============================================================

    public function __construct(ProductItem $productItem)
    {
        // Guardar modelo SIN relaciones para evitar ModelNotFoundException
        $this->productItem = $productItem->withoutRelations();

        // Guardar ID primitivo como backup
        $this->productItemId = $productItem->id;
    }

    // ============================================================
    // HANDLE (OBLIGATORIO: try/catch con throw)
    // ============================================================

    public function handle(): void
    {
        // Refrescar datos al inicio del handle
        $this->productItem->refresh();

        Log::channel(LogChannels::SYNC_STRATEGY)->info(
            "[EXAMPLE] Iniciando sync para SKU {$this->productItem->sku}",
            $this->logContext()
        );

        try {
            // === LÓGICA DE SINCRONIZACIÓN ===
            $this->performSync();

            Log::channel(LogChannels::SYNC_STRATEGY)->info(
                "[EXAMPLE] Sync completado para SKU {$this->productItem->sku}",
                $this->logContext()
            );

        } catch (\Exception $e) {
            Log::channel(LogChannels::SYNC_STRATEGY)->error(
                "[EXAMPLE] Error en sync: {$e->getMessage()}",
                $this->logContext() + ['exception' => $e->getMessage()]
            );

            // CRÍTICO: Siempre relanzar para que el job falle correctamente
            // y el trait TracksProductSyncFailures registre el fallo
            throw $e;
        }
    }

    // ============================================================
    // MÉTODOS REQUERIDOS POR TracksProductSyncFailures
    // ============================================================

    protected function getProductId(): ?int
    {
        return $this->productItemId;
    }

    protected function getSyncChannel(): string
    {
        return SyncChannels::EXAMPLE; // Usar constante apropiada
    }

    // ============================================================
    // HELPERS PRIVADOS
    // ============================================================

    private function performSync(): void
    {
        // Implementar lógica específica de sincronización
    }

    private function logContext(): array
    {
        return [
            'product_item_id' => $this->productItemId,
            'sku' => $this->productItem->sku ?? 'N/A',
            'channel' => $this->getSyncChannel(),
        ];
    }
}

Errores Comunes a Evitar

❌ NO: Tragar excepciones

// MAL - El job se marca como exitoso aunque falló
public function handle(): void
{
    try {
        $this->doSync();
    } catch (\Exception $e) {
        Log::error($e->getMessage());
        // Falta: throw $e;
    }
}

❌ NO: Serializar relaciones

// MAL - Si la relación se elimina, el job falla al deserializar
public function __construct(SupplierProduct $product)
{
    $this->product = $product; // Puede tener relaciones cargadas
}

❌ NO: Olvidar $tries

// MAL - Default es 1, falla sin reintentos
class SyncJob implements ShouldQueue
{
    // Falta: public int $tries = 3;
}

❌ NO: Ignorar el método failed()

// MAL - Sin TracksProductSyncFailures, los fallos no se registran en ProductSyncLog
class SyncJob implements ShouldQueue
{
    // Falta: use TracksProductSyncFailures;
}

Manejo de Errores HTTP Específicos

Para jobs que llaman APIs externas, manejar códigos HTTP específicamente:

public function handle(): void
{
    try {
        $this->callExternalApi();
    } catch (ClientException $e) {
        $code = $e->getResponse()?->getStatusCode();

        // Errores recuperables: re-encolar con delay
        if ($code === 429 || ($code >= 500 && $code < 600)) {
            Log::warning("[SYNC] HTTP {$code}, re-encolando en 60s");
            $this->release(60);
            return;
        }

        // Errores no recuperables: fallar definitivamente
        throw $e;
    }
}

Constantes de Canal

Usar siempre las constantes definidas en App\Constants\SyncChannels:

namespace App\Constants;

class SyncChannels
{
    public const WOO = 'woocommerce';
    public const TN = 'Tienda Naranja';
    public const CONTI = 'contimarket';
    public const ALGOLIA = 'algolia reindex';
    public const MEDUSA = 'medusa';
}

Jobs Opcionales: ShouldBeUnique

Para jobs que no deben duplicarse en la cola:

use Illuminate\Contracts\Queue\ShouldBeUnique;

class EvaluateProductJob implements ShouldQueue, ShouldBeUnique
{
    public int $uniqueFor = 120; // Lock por 2 minutos

    public function uniqueId(): string
    {
        return 'evaluate-' . $this->productItemId;
    }
}

Verificación de Cumplimiento

Antes de hacer merge de un nuevo job o modificación, verificar:

  • [ ] ¿Tiene $tries definido?
  • [ ] ¿Tiene $backoff definido?
  • [ ] ¿Usa TracksProductSyncFailures trait?
  • [ ] ¿El catch block hace throw $e?
  • [ ] ¿Usa withoutRelations() en constructor?
  • [ ] ¿Los logs tienen contexto estructurado?
  • [ ] ¿Maneja errores HTTP recuperables (429, 5xx)?

Referencias