Saltar a contenido

Sistema de Gestión de Sincronizaciones Fallidas

Nota de vigencia (2026-08-11): este análisis es anterior a los fixes de junio 2026 (lookup scopeado de product_item_view y política de reintentos — ver incidente 2026-06). Partes pueden estar desactualizadas; vigencia a confirmar por el equipo.

Problema

El sistema acumula sincronizaciones fallidas (~4,600 actualmente) que no se procesan de forma automatizada. Los reintentos manuales pueden inundar las colas y no distinguen entre errores recuperables y no recuperables.

Análisis de Errores Actuales

Distribución por Canal

Canal Fallidos % del Total
Tienda Naranja 2,952 63%
Contimarket 1,156 25%
WooCommerce 307 7%
Medusa 244 5%

Clasificación de Errores

Retryable (~50% - 2,310 jobs)

Errores temporales que se pueden resolver con reintentos:

  • SIN IMAGEN (1,168): Producto sin imagen, reintentar cuando tenga
  • DESINCRONIZADO/409 (613): Producto existe en destino, reintentar sincroniza
  • Connection refused/timeout (~350): Errores de red temporales
  • Too many attempts (147): Jobs que fallaron por reintentos agotados
  • Stock reservado (34): Condición temporal

No Retryable (~35% - 1,612 jobs)

Errores que requieren intervención manual o corrección de datos:

  • SIN CATEGORIA (1,540): Requiere mapeo de categoría en el sistema
  • No autorizado (60): Problema de permisos en plataforma destino
  • Nombre duplicado (10): Conflicto de nombre en TiendaNaranja
  • SKU duplicado (2): Conflicto de SKU en WooCommerce
  • Client error 4xx (7): Errores de validación de datos

Requiere Investigación (~15% - 737 jobs)

Errores que necesitan análisis caso por caso:

  • Producto no encontrado (271): ¿Eliminado en destino?
  • ID no válido (291): ¿Producto eliminado en WooCommerce?
  • FAILED genérico (142): Sin mensaje descriptivo

Solución Propuesta

Componentes

app/
├── Enums/
│   └── SyncErrorCategory.php          # Categorías de error
├── Services/
│   └── Sync/
│       └── FailedSyncClassifier.php   # Clasificador de errores
├── Console/Commands/
│   └── ProcessFailedSyncsCommand.php  # Comando principal
└── Models/
    └── ProductSyncLog.php             # (modificar) agregar scopes

1. Enum de Categorías

enum SyncErrorCategory: string
{
    case MISSING_IMAGE = 'missing_image';
    case MISSING_CATEGORY = 'missing_category';
    case NETWORK_ERROR = 'network_error';
    case DESYNC = 'desync';
    case NOT_FOUND = 'not_found';
    case PERMISSION = 'permission';
    case DUPLICATE = 'duplicate';
    case RATE_LIMIT = 'rate_limit';
    case UNKNOWN = 'unknown';

    public function isRetryable(): bool
    {
        return match($this) {
            self::MISSING_IMAGE,
            self::NETWORK_ERROR,
            self::DESYNC,
            self::RATE_LIMIT => true,
            default => false,
        };
    }
}

2. Clasificador de Errores

Servicio que analiza response_message y response_code para determinar la categoría:

class FailedSyncClassifier
{
    public function classify(ProductSyncLog $log): SyncErrorCategory
    {
        $message = $log->response_message ?? '';
        $code = $log->response_code;

        // Patrones de clasificación por mensaje
        return match(true) {
            str_contains($message, 'SIN IMAGEN') => SyncErrorCategory::MISSING_IMAGE,
            str_contains($message, 'SIN CATEGORIA') => SyncErrorCategory::MISSING_CATEGORY,
            str_contains($message, 'DESINCRONIZADO') => SyncErrorCategory::DESYNC,
            str_contains($message, 'Connection refused') => SyncErrorCategory::NETWORK_ERROR,
            // ... más patrones
            default => SyncErrorCategory::UNKNOWN,
        };
    }
}

3. Comando de Procesamiento

php artisan sync:process-failed [opciones]

Opciones: - --channel= : Filtrar por canal (tn, contimarket, woocommerce, medusa) - --limit=20 : Jobs a procesar por ejecución - --category= : Filtrar por categoría de error - --retry-only : Solo procesar retryables - --archive-non-retryable : Marcar no-retryables como archivados - --dry-run : Mostrar qué haría sin ejecutar - --report : Generar reporte sin procesar

Comportamiento: 1. Consulta jobs fallidos (más antiguos primero) 2. Clasifica cada error 3. Filtra según opciones 4. Despacha jobs retryables con delay entre cada uno 5. Marca no-retryables como archivados (opcional) 6. Genera reporte de ejecución

4. Migración (Opcional)

Schema::table('product_sync_logs', function (Blueprint $table) {
    $table->string('error_category', 30)->nullable()->after('response_message');
    $table->unsignedTinyInteger('retry_count')->default(0)->after('error_category');
    $table->timestamp('archived_at')->nullable()->after('retry_count');

    $table->index(['status', 'error_category']);
    $table->index(['status', 'archived_at']);
});

Configuración de Ejecución

Schedule (routes/console.php)

// Procesar fallidos retryables cada 10 minutos
Schedule::command('sync:process-failed --retry-only --limit=15')
    ->everyTenMinutes()
    ->withoutOverlapping()
    ->runInBackground();

// Reporte diario de estado
Schedule::command('sync:process-failed --report')
    ->dailyAt('08:00')
    ->emailOutputTo('admin@compulandia.com.py');

Parámetros Recomendados

Parámetro Valor Justificación
Batch size 15-20 Evita inundar colas
Intervalo 10 min Procesamiento gradual
Max reintentos 3 Evita loops infinitos
Delay entre jobs 2 seg Respeta rate limits

Reportes

Reporte de Ejecución

=== Sync Failed Jobs Report ===
Fecha: 2026-01-05 10:30:00

Procesados: 20
  - Reintentados: 12
  - Archivados: 5
  - Omitidos: 3

Por Categoría:
  - missing_image: 8 (reintentados)
  - network_error: 4 (reintentados)
  - missing_category: 5 (archivados)
  - unknown: 3 (omitidos)

Pendientes:
  - Tienda Naranja: 2,940
  - Contimarket: 1,144
  - WooCommerce: 307
  - Medusa: 244

Reporte de Estado Global

=== Failed Syncs Status ===

Total Fallidos: 4,635

Por Categoría:
  - Retryable: 2,310 (50%)
  - No Retryable: 1,612 (35%)
  - Investigar: 713 (15%)

Tendencia (últimos 7 días):
  - Nuevos fallidos: +245
  - Resueltos: -180
  - Neto: +65

Flujo de Procesamiento

┌─────────────────┐
│  Cron (10 min)  │
└────────┬────────┘
         │
         ▼
┌─────────────────────┐
│ ProcessFailedSyncs  │
│   --retry-only      │
│   --limit=15        │
└────────┬────────────┘
         │
         ▼
┌─────────────────────┐
│  Query failed logs  │
│  (oldest first)     │
└────────┬────────────┘
         │
         ▼
┌─────────────────────┐
│  Classify each      │◄─── FailedSyncClassifier
│  error              │
└────────┬────────────┘
         │
         ▼
    ┌────┴────┐
    │ Retryable? │
    └────┬────┘
    yes  │  no
    ▼    │    ▼
┌───────┐│┌──────────┐
│Dispatch││ Archive/ │
│ Job   │││ Skip     │
└───┬───┘│└────┬─────┘
    │    │     │
    ▼    ▼     ▼
┌─────────────────────┐
│  Update log status  │
│  + retry_count      │
└────────┬────────────┘
         │
         ▼
┌─────────────────────┐
│  Generate report    │
└─────────────────────┘

Consideraciones

Rate Limiting

  • Delay de 2 segundos entre dispatches
  • Máximo 15-20 jobs por ejecución
  • withoutOverlapping() previene ejecuciones paralelas

Priorización

  • Jobs más antiguos primero (FIFO)
  • Opción de priorizar por canal si es necesario

Monitoreo

  • Logs en canal dedicado SYNC_RETRY_MANAGER
  • Métricas: procesados, reintentados, archivados
  • Alertas si pendientes superan umbral

Limpieza

  • Archivados se pueden purgar después de 30 días
  • Comando separado sync:cleanup-archived --older-than=30