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:clearantes 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 jobsdelayed; 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_after150 — 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:
La salida muestra qué regla de la cadena falló. Cuesta ~1,5 créditos de ScrapeOps.php artisan competitor:test-extraction --competitor=NIS --url='https://…/producto' --attempts - 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_ldsi el sitio lo publica: es estable y no necesitarender_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 dejson_ldy 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 CATEGORIArequiere 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_afterviejo en colas). En su peor versión:php artisan testconbootstrap/cache/config.phppresente ignoró elphpunit.xmly ejecutómigrate:freshcontra la base de desarrollo real, vaciándola. - Diagnóstico:
ls bootstrap/cache/config.php— si existe, la config está cacheada y los.env/phpunit.xmlno se leen. - Resolución:
php artisan config:cleary reiniciar Horizon. Regla operativa vigente: no usarphp artisan config:cacheen el servidor de desarrollo y no correrphp artisan testen ese host (hay un guard entests/CreatesApplication.phpque aborta si la conexión no es una base de test, pero la regla sigue vigente).