Skip to content

Runbook

Todos los comandos se ejecutan desde migrations/scripts/ con el venv activado:

bash
cd migrations/scripts
source .venv/bin/activate

Modos del servicio

text
--mode full          Pasada completa (sin filtro de fecha). Para carga inicial.
--mode incremental   Una sola pasada que solo trae cambios desde last_sync_at.
--mode loop          Pasada incremental cada --interval segundos, en bucle.

Default: incremental.

Carga inicial

bash
# Fase 0 — Bootstrap del tenant (companies + users)
python3 sync_service.py --resource bootstrap

# Carga inicial completa (todo full)
python3 sync_service.py --resource all --mode full

# Alternativa: por fases (mismo efecto, pero permite verificar entre pasos)
python3 sync_service.py --resource custom_values --mode full
python3 sync_service.py --resource tags --mode full
python3 sync_service.py --resource pipelines --mode full
python3 sync_service.py --resource forms --mode full
python3 sync_service.py --resource businesses --mode full   # ← B2B → contacts corporate
python3 sync_service.py --resource contacts --mode full
python3 sync_service.py --resource opportunities --mode full
python3 sync_service.py --resource conversations --mode full
python3 sync_service.py --resource messages --mode full
python3 sync_service.py --resource notes --mode full
python3 sync_service.py --resource tasks --mode full

Pendiente de aprobación (ver TODO): calendars / appointments / persistencia del catálogo custom_field_definitions.

Sync incremental

Una pasada (cron-style)

bash
# Pasada incremental de todo
python3 sync_service.py --resource all --mode incremental

# Solo un recurso
python3 sync_service.py --resource contacts --mode incremental

Loop continuo (servicio)

bash
# Cada 10 minutos (default)
python3 sync_service.py --resource all --mode loop

# Cada 5 minutos
python3 sync_service.py --resource all --mode loop --interval 300

# En background con logs a archivo
nohup python3 sync_service.py --resource all --mode loop \
  > ../logs/sync-loop.log 2>&1 &

Como cron job

cron
# /etc/crontab — cada 10 min
*/10 * * * * marcosismaelrodriguez949 cd /home/.../migrations/scripts && \
  ./.venv/bin/python3 sync_service.py --resource all --mode incremental \
  >> ../logs/sync-cron.log 2>&1

Como systemd service (loop)

/etc/systemd/system/crm-sync.service:

ini
[Unit]
Description=CRM GHL→imcrmdev sync service
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
WorkingDirectory=/home/marcosismaelrodriguez949/crm/migrations/scripts
ExecStart=/home/marcosismaelrodriguez949/crm/migrations/scripts/.venv/bin/python3 \
  sync_service.py --resource all --mode loop --interval 600
Restart=on-failure
RestartSec=30
StandardOutput=append:/home/marcosismaelrodriguez949/crm/migrations/logs/sync.log
StandardError=append:/home/marcosismaelrodriguez949/crm/migrations/logs/sync.log

[Install]
WantedBy=multi-user.target
bash
sudo systemctl daemon-reload
sudo systemctl enable --now crm-sync.service
sudo journalctl -u crm-sync.service -f

Resumir de un crash en full

Dentro de una pasada full, el script persiste el cursor de paginación en migrations/state/<resource>.json. Si muere por cualquier razón:

bash
# Solo vuelve a correr el mismo comando
python3 sync_service.py --resource contacts --mode full

Lee el state, salta lo ya procesado, y continúa. No se duplican documentos porque el upsert es por (company_id, external_id).

Para forzar un re-inicio limpio (peligroso, borra el state):

bash
python3 sync_service.py --resource contacts --mode full --reset-state

Modo dry-run

bash
python3 sync_service.py --resource contacts --mode full --dry-run --limit 5

Lee 5 contactos de GHL, muestra el doc transformado pero no escribe nada.

Logging

Los logs se escriben a migrations/logs/:

  • migrate-YYYY-MM-DD.log — log general (INFO+) del proceso
  • dlq-<resource>.jsonl — Dead Letter Queue: docs que fallaron validación o E11000
  • sync-loop.log / sync-cron.log — outputs de los modos en background

Monitoreo en vivo

bash
# Conteo en tiempo real durante la corrida
watch -n 5 'mongosh "$TARGET_MONGO_URI" --quiet --eval "
  print(\"contacts_new:    \" + db.contacts_new.countDocuments({}));
  print(\"opportunities:   \" + db.opportunities.countDocuments({}));
  print(\"conversations:   \" + db.conversations.countDocuments({}));
  print(\"messages:        \" + db.messages.countDocuments({}));
  print(\"tasks:           \" + db.tasks.countDocuments({}));
"'

Verificar last_sync_at por recurso

bash
for f in ../state/*.json; do
  printf "%-20s " "$(basename $f .json):"
  python3 -c "import json; d=json.load(open('$f')); print(d.get('last_sync_at', '(nunca)'))"
done

Casos comunes

"Token does not have access to this location"

El PIT que estás usando no es el de Trebol o le falta scope. Revisar scripts/.env.

Errores 429 frecuentes

Bajar RATE_LIMIT_RPS en scripts/.env a 5 o 4 y reiniciar el servicio. El cursor está preservado.

E11000 duplicate key

Visible en contacts_new.email. El script ya maneja esto: el primer contacto gana, los demás van al DLQ con email omitido. Revisar migrations/logs/dlq-contacts-duplicate-email.jsonl.

Una pasada incremental no trae nada y debería

Posibles causas:

  • last_sync_at está más adelantado que el cambio real (puede pasar si el reloj difiere).
  • El campo dateUpdated en GHL no se actualizó (raro pero pasa con cambios de child entities como notes).
  • Solución de fuerza bruta: forzar una pasada full del recurso afectado.

Generar reporte de conteos

bash
python3 sync_service.py --resource report

Cutover a producción

Una vez validada la sincronización en el Mongo de pruebas (puerto 27019):

bash
# Estos comandos van desde la raíz de migrations/

# 1. Dump del Mongo migrado
docker exec crm-migration-mongo-1 mongodump \
  --uri="mongodb://admin:migration_pwd_change_me@localhost:27017/imcrmdev?authSource=admin" \
  --out=/tmp/dump

# 2. Copiar dump al host
docker cp crm-migration-mongo-1:/tmp/dump ./dump-trebol

# 3. Restore en Mongo de producción de Trebol
mongorestore --uri="mongodb://admin:PROD_PWD@trebol-mongo:27017/imcrmdev?authSource=admin" \
  --drop ./dump-trebol/imcrmdev

# 4. Apuntar el sync service al Mongo de producción
#    (cambiar TARGET_MONGO_URI en .env)
#    y arrancar el servicio en modo loop allá

El --drop borra cada colección antes de restaurar. Usar solo en una BD que aún no esté en uso por usuarios reales.