Saltar a contenido

Runbook — Compulandia Integrador

Deploy

Dónde corre:

Tras actualizar el código en el servidor, existe reload.sh en la raíz del checkout (script local, no versionado en git):

./reload.sh
# equivale a:
php artisan config:clear
php artisan optimize
supervisorctl restart horizon

Importante: siempre config:clear antes de reiniciar Horizon. Con config cacheada quedan valores viejos (ej. retry_after) y ya causó incidentes (ver falla 3 y incidente 2026-07).

Cómo verificar que el deploy salió bien:

php artisan horizon:status        # "Horizon is running"
php artisan horizon:supervisors   # supervisores con workers activos
# Panel web: /horizon — las colas deben drenar y las duraciones ser de segundos

Logs

Todos los logs de sync usan el formato [{CANAL}:{product_id}:{sku}] mensaje (grep-eable por producto). Canales definidos en app/Constants/LogChannels.php y rutas en config/logging.php.

Qué Dónde Cómo verlos
Aplicación general storage/logs/laravel.log (rotación diaria) tail -f storage/logs/laravel.log
Syncs de entrada storage/logs/CL/, FX/, NGO/, XZ/, PCM/, cpi/ tail -f storage/logs/CL/CL.log
Jobs de salida storage/logs/jobs/woocommerce/, jobs/tienda_naranja/, jobs/contimarket/, jobs/medusa/, jobs/umarket/ grep "\[WC:14275" storage/logs/jobs/woocommerce/WooCommerce.log
Precios storage/logs/jobs/PRICING/, storage/logs/PRICING/ tail -f storage/logs/jobs/PRICING/pricing.log
Algolia / IA / Kafka SAP storage/logs/jobs/algolia/, jobs/CONTENT_GENERATION/, jobs/sap_kafka/ tail -f ...
Precios de competencia storage/logs/jobs/competitor_prices/ tail -f storage/logs/jobs/competitor_prices/competitor_prices-$(date +%F).log
Colas y jobs fallidos Panel /horizon + tabla failed_jobs php artisan tinker --execute="echo DB::table('failed_jobs')->where('failed_at','>',now()->subHour())->count();"
Errores agregados Sentry

Rollback

Condición previa: si el deploy incluyó migraciones, evaluar si son reversibles antes de volver atrás (las migraciones viven en subdirectorios numerados, ver ADR-0001).

git checkout <commit-anterior>
./reload.sh    # config:clear + optimize + restart de Horizon

Verificación: igual que en deploy (horizon:status, panel /horizon).

Fallas frecuentes

(Fuentes: post-mortems en incidentes/ y análisis en failed-sync-manager — fallas reales, no hipotéticas.)

1. Colas de salida atascadas: miles de jobs sin drenar, CPU ociosa

Ocurrió en junio 2026 (post-mortem completo: incidente 2026-06).

  • Síntoma: colas woocommerce_products, tn_products, contimarket, medusa_products, etc. con cientos/miles de jobs delayed; productos que no se reflejan en los canales; CPU y RAM del servidor ociosas.
  • Diagnóstico: los workers están bloqueados en I/O, no faltos de capacidad. Mirar duraciones antes que stack traces:
    php artisan horizon:supervisors
    php artisan tinker --execute="echo DB::table('failed_jobs')->latest('failed_at')->value('exception');"
    # MaxAttemptsExceededException = job que excede el timeout del worker
    php audit/audit_scoped.php 30   # cronometra el lookup de product_item_view
    
  • Resolución: confirmar que sigue desplegado el lookup scopeado (ProductItemView::forProduct(), ADR-0002) y la política de reintentos (timeout 120s, backoff exponencial, retry_after 150 — ADR-0003). Si Medusa responde lento, es el gatillo típico de la cascada: revisar su carga y los timeouts HTTP (MEDUSA_HTTP_TIMEOUT). Tras cualquier cambio de config: php artisan config:clear && php artisan horizon:terminate.

2. Precios de competencia congelados: el sitio cambió de estructura

Diseño completo: RFC 008 · ADR-0005. Consumo de los datos: integraciones/precios-competencia.

  • Síntoma: los precios de un competidor dejan de actualizarse. Como un fallo nunca pisa el último valor válido, los datos siguen ahí y se ven normales — lo único que delata el problema es que la fecha no avanza. En la pestaña «Competencia» del producto aparecen como último intento falló o rota.

  • La alerta ya existe y está en el log. La corrida diaria publica en su resumen la proporción de fallos de extracción por competidor. Una línea como «NIS: 30 relevadas, 5 exitosas, 25 fallos de extracción (83%)» es la señal: no se cayeron 25 páginas, el sitio cambió el HTML.

grep "CAMBIO DE ESTRUCTURA" storage/logs/jobs/competitor_prices/*.log
grep "Corrida finalizada" storage/logs/jobs/competitor_prices/*.log | tail -3
  • Diagnóstico: distinguir red de extracción. Es la distinción que hace útil al módulo.
Desenlace en el log Qué pasó Qué hacer
fallo_red_transitorio timeout, 429, 5xx Nada: se reintenta solo en la próxima corrida
fallo_red_permanente 404, 410 La página no existe más. Actualizar o eliminar la URL desde la pestaña del producto
fallo_extraccion La página llegó bien, ninguna regla matcheó Cambió el sitio. Ir al paso siguiente
valor_implausible El número extraído se aparta más del 50% del último precio conocido Dos causas posibles, y se distinguen mirando la página. Ver abajo
  • valor_implausible: leer el motivo antes de tocar nada. La línea del log dice los dos precios y el porcentaje:
Sin actualizar {"item":16554,"sku":"CPI-1460130","desenlace":"valor_implausible",
 "motivo":"Variación de 117.7% respecto del último valor (Gs. 248.000 → Gs. 540.000)
 supera el máximo de 50%.","url":"https://…"}
Si al abrir la URL ves… Qué pasó Qué hacer
Un número que no es el precio del producto (el «12» de «12 cuotas», un precio de financiación, otro producto) Un selector quedó apuntando a otra cosa Corregir la cadena del competidor, como en el caso de cambio de estructura
El precio nuevo es correcto: el sitio realmente lo cambió El guardarraíl bloqueó un cambio legítimo Ir a la pestaña «Competencia» del producto y Verificar → Confirmar. La verificación manual NO aplica el guardarraíl: lo aplica sólo el relevamiento desatendido

Esto no se resuelve solo, y hay que resolverlo. El precio de comparación es el último guardado, y como el relevamiento nunca lo escribe, la misma URL va a fallar todos los días indefinidamente, gastando créditos en cada corrida y quedando etiquetada como rota a los 21 días. Un valor_implausible que se repite dos días seguidos ya es una tarea pendiente para una persona, no un aviso.

Caso real (2026-08-28): el auricular JBL Sense Lite de Tienda Móvil pasó de Gs. 248.000 a Gs. 540.000 —un 117,7%— porque el sitio subió el precio. El guardarraíl hizo bien en no pisar el dato viejo sin que nadie mirara; la resolución fue humana.

  • Resolución: se corrige editando configuración, NO desplegando código. Es el motivo de que la extracción sea dinámica (RNF-4).

  • Reproducir el fallo sin abrir el navegador:

    php artisan competitor:test-extraction --competitor=NIS --url='https://…/producto' --attempts
    
    La salida muestra qué regla de la cadena falló. Cuesta ~1,5 créditos de ScrapeOps.

  • Abrir el HTML del sitio y encontrar dónde quedó el precio. Guardarlo como fixture para no volver a gastar créditos en cada prueba:
    php artisan competitor:test-extraction --competitor=NIS --url='…' --save-fixture=nis-nuevo
    php artisan competitor:test-extraction --competitor=NIS --fixture=nis-nuevo --attempts
    
  • En Definiciones → Competidores → Editar, agregar la regla nueva a la cadena del campo que falló. Preferir siempre json_ld si el sitio lo publica: es estable y no necesita render_js.
  • Verificar con el botón «Probar» antes de guardar. La columna regla ganadora dice cuál de la cadena resolvió.
  • Correr una vuelta acotada:

    php artisan competitor:survey --competitor=NIS --limit=5
    

  • Qué NO hacer: no borrar las vinculaciones ni el competidor. Las URLs están curadas a mano y son trabajo humano acumulado; el competidor no se borra, se desactiva. Los precios viejos no molestan: su fecha dice que son viejos.

  • Costo. Si la corrección exige activar render_js, el pedido pasa de 1,5 a 15 créditos — diez veces más. Es una decisión, no un default: conviene agotar primero las alternativas de json_ld y selectores.

3. Sincronizaciones fallidas acumuladas por miles

Análisis real con ~4.600 fallidos: failed-sync-manager.

  • Síntoma: la tabla failed_jobs / los registros de sync fallido crecen; la mayor parte concentrada en TiendaNaranja y Contimarket.
  • Diagnóstico: clasificar por mensaje de error. Los patrones reales más frecuentes: SIN IMAGEN (retryable cuando el producto tenga imagen), SIN CATEGORIA (no retryable: falta mapeo de categoría en el sistema), 409/desincronizado (retryable), timeouts de red (retryable), No autorizado (permisos en la plataforma destino).
  • Resolución: los retryables se reintentan (sin inundar las colas: en lotes acotados); SIN CATEGORIA requiere mapear la categoría en el panel antes de reintentar; duplicados de SKU/nombre requieren corrección manual en el canal.

4. Config cacheada produce comportamiento viejo (y rompió los tests)

Ocurrió en julio 2026 (post-mortem: incidente 2026-07).

  • Síntoma: cambios de configuración que "no se aplican" (ej. retry_after viejo en colas). En su peor versión: php artisan test con bootstrap/cache/config.php presente ignoró el phpunit.xml y ejecutó migrate:fresh contra la base de desarrollo real, vaciándola.
  • Diagnóstico: ls bootstrap/cache/config.php — si existe, la config está cacheada y los .env/phpunit.xml no se leen.
  • Resolución: php artisan config:clear y reiniciar Horizon. Regla operativa vigente: no usar php artisan config:cache en el servidor de desarrollo y no correr php artisan test en ese host (hay un guard en tests/CreatesApplication.php que aborta si la conexión no es una base de test, pero la regla sigue vigente).