Programar scripts que realmente se ejecutan: programación de tareas bien hecha

¿Te fue útil?

Escribiste el script. Funciona en tu portátil. Lo programas. Todos se relajan. Luego el informe del lunes está en blanco, la copia de seguridad nunca se hizo y la “limpieza diaria” silenciosamente borró el directorio equivocado. Los trabajos programados no fallan de forma ruidosa por defecto; fallan educadamente, a las 03:07, y luego vuelven a la cama.

Si quieres trabajo programado en el que puedas confiar, trata la “planificación de tareas” como ingeniería de producción: contratos, registros, bloqueo, tiempo, entorno, permisos y observabilidad. Esta es la versión adulta de “simplemente añade cron”.

Qué significa realmente “programación fiable”

Programar no es “ejecuta este comando a las 02:00.” Programar es un contrato: ejecuta aproximadamente con esta cadencia, bajo una identidad conocida, con un entorno conocido, produciendo artefactos conocidos, y emitiendo suficiente evidencia para que puedas probar lo que pasó. O lo que no pasó.

Las cuatro promesas que debe cumplir tu trabajo programado

  • Se ejecuta: El planificador lo desencadena, incluso después de un reinicio, incluso tras cambios de hora, incluso cuando la máquina está ocupada.
  • Se ejecuta una vez: Sin solapamientos, sin duplicados, sin “dos medianoches” porque el horario de verano se puso picante.
  • Se ejecuta de la misma manera: Mismo PATH, mismo directorio de trabajo, misma configuración, mismos permisos, misma localización, misma umask.
  • Deja evidencia: Registros, códigos de salida, marcas temporales, métricas y alertas cuando falta.

Todo lo demás es secundario. Sí, incluso “se ejecuta rápido.” Rápido está bien. Correcto paga el alquiler.

Una opinión que te ahorrará dolor: trata los scripts programados como servicios. No como “una línea bash.” Los servicios tienen archivos unitarios, límites de recursos, registros, reintentos y responsables. El trabajo puede ser un script, pero la operación debería sentirse como un servicio.

Idea parafraseada (si has hecho ops, habrás oído el sentimiento): Los fallos ocurren; la fiabilidad viene de diseñar sistemas que los detectan y recuperan. — idea atribuida a la escuela SRE popularizada por Google.

Breve historia y datos (porque el contexto evita malas decisiones)

  1. cron es lo bastante antiguo como para tener opiniones. Se remonta a los primeros UNIX (finales de los años 70). Por eso es simple, estable, y también por eso asume un mundo sin proliferación de contenedores.
  2. Vixie Cron se convirtió en la “historia cron por defecto” en muchas distribuciones Linux durante años, modelando comportamientos como el manejo del entorno y la expectativa de correo con la salida.
  3. Windows Task Scheduler existe desde los días de Windows 9x/NT y evolucionó hasta ser un motor bastante capaz con desencadenantes, condiciones y “ejecutar aunque el usuario no haya iniciado sesión”. Ya no es solo una GUI.
  4. DST crea horas “faltantes” y “duplicadas”. En muchas zonas horarias, las 02:30 pueden no existir un día al año y repetirse otro día. No es un problema teórico; es generador de incidentes.
  5. systemd timers existen en parte porque cron no puede expresar bien las restricciones modernas (dependencias, sandboxing, registro por unidad y “ejecutar trabajos perdidos tras el reinicio”).
  6. anacron se inventó para portátiles y otras máquinas que no siempre están encendidas en el momento programado, porque cron solo dispara cuando la máquina está en marcha.
  7. la “manada atronadora” es un artefacto de programación: flotas configuradas para “ejecutar a medianoche” pueden atacar a los servicios compartidos. La aleatorización/jitter es una característica de fiabilidad.
  8. enviar la salida por correo solía ser normal. Muchas configuraciones de cron confiaban en el correo local. Los sistemas modernos a menudo no tienen un MTA instalado, por lo que la salida desaparece en el vacío.

Elige el planificador como eliges un sistema de ficheros

Linux/Unix: cron vs systemd timers vs “otra cosa”

Usa cron cuando: necesitas compatibilidad universal, desencadenantes periódicos muy simples y puedes proporcionar tú mismo las funciones de producción que faltan (bloqueo, registro, control del entorno, monitorización).

Usa systemd timers cuando: estás en una distro basada en systemd y quieres primitivas de fiabilidad integradas: logs en journald, dependencias, control de recursos, sandboxing y Persistent=true para recuperar ejecuciones tras el tiempo de inactividad.

Usa Kubernetes CronJobs cuando: el trabajo pertenece al clúster y necesita aislamiento por contenedor, peticiones/límites de recursos y políticas de reintento nativas del clúster. Pero recuerda: solo moviste los modos de fallo, no los eliminaste.

Usa un motor de flujo de trabajo cuando: necesitas DAGs, reintentos por paso, backfills y trazabilidad de auditoría. Si tu “script” se ha vuelto un pequeño negocio, deja de fingir que es un hobby.

Windows: Task Scheduler está bien, si lo tratas como producción

Windows Task Scheduler puede ser muy fiable, pero absolutamente te dejará dispararte en el pie con credenciales, directorios “start in” y suposiciones sobre la sesión del usuario. Si programas PowerShell, hazlo en serio: rutas explícitas, decisiones explícitas sobre la política de ejecución, registra todo y no confíes en unidades de red mapeadas.

El planificador no es tu capa de fiabilidad

Incluso el mejor planificador no puede arreglar:

  • un script que no es idempotente,
  • un trabajo que puede solaparse,
  • un trabajo que depende de “el PATH que haya hoy”,
  • un trabajo que “tiene éxito” mientras genera silenciosamente basura.

Elige un planificador para los desencadenantes y la orquestación. Construye la fiabilidad dentro del trabajo.

Broma 1/2: Un trabajo cron sin registros es como un submarino sin sonar: está callado hasta que chocas con algo caro.

Patrones de diseño para scripts que sobreviven en producción

1) Haz el entorno aburrido a propósito

Cron se ejecuta con un entorno mínimo. Las unidades systemd pueden ejecutarse con un entorno diferente al de tu shell interactivo. Las tareas de Windows se ejecutan con un perfil distinto al de tu sesión RDP. La solución es la misma en todas partes: declara lo que necesitas.

  • Usa rutas absolutas para ejecutables y archivos.
  • Establece PATH explícitamente (y de forma mínima).
  • Establece la localización (LC_ALL=C) si dependes de parsear la salida.
  • Establece umask explícitamente si creas archivos que otros deben leer.
  • Establece un directorio de trabajo explícito (o no dependas de uno).

2) Trata el tiempo como hostil

Zonas horarias, horario de verano, segundos intercalares, desviación del reloj y pasos de NTP: el tiempo te traicionará. Si tu trabajo depende de la fecha, decide si “fecha” significa UTC o hora local y sé explícito.

  • Prefiere UTC para marcas temporales y nombres de particiones.
  • Si un proceso de negocio necesita hora local (“enviar a las 8am local”), usa la hora local pero protege los días de DST.
  • Registra tanto la “hora programada” como la “hora de inicio real” en los registros.

3) Usa bloqueo para prevenir solapamientos

Los solapamientos son la falla silenciosa clásica: todo va bien hasta que una ejecución dura más de lo habitual, empieza la siguiente y ahora tienes dos procesos compitiendo por los mismos ficheros, filas de base de datos o ancho de banda de almacenamiento.

En Linux, usa flock. En Windows, usa un archivo de bloqueo con una apertura exclusiva, o usa primitivas del SO (mutex) si estás en un lenguaje “de verdad”. No hagas bloqueo casero con “comprueba y luego crea”; eso es cosplay de condición de carrera.

4) Haz los scripts idempotentes, o al menos repetibles con seguridad

Los planificadores reintentan. Los humanos vuelven a ejecutar. Las máquinas se reinician a mitad de trabajo. Si volver a ejecutar causa corrupción, no tienes automatización: tienes una máquina tragaperras.

  • Escribe salidas a una ruta temporal y luego renómbralas atómicamente.
  • Usa “archivos marcador” con precaución; incluye versión y marca temporal.
  • Al interactuar con bases de datos, usa transacciones y claves únicas para deduplicación.
  • Si borras datos, ponlos en cuarentena antes de la eliminación definitiva.

5) Haz que el “éxito” sea medible

El código de salida 0 es necesario pero insuficiente. Un trabajo que genera un archivo de copia de seguridad vacío puede seguir saliendo 0. Define qué significa el éxito:

  • La salida esperada existe y no está vacía.
  • El checksum coincide (cuando aplique).
  • El recuento de filas está dentro de los límites esperados.
  • La copia de seguridad es restaurable (restauraciones de prueba periódicas).

6) Registra como si fueras el responsable de guardias (porque lo eres)

Cada trabajo programado debe registrar: hora de inicio, hora de fin, tiempo de ejecución, parámetros clave y un estado final claro. Si toca almacenamiento, registra bytes leídos/escritos y cualquier error de la capa de almacenamiento.

Envía los registros a un lugar donde puedas buscarlos. Si confías en ficheros locales, haz rotación. Si confías en journald, asegúrate de que la retención sea sensata.

7) Establece tiempos de espera y límites de recursos

Un trabajo colgado es peor que uno fallido porque bloquea la siguiente ejecución y deja la máquina sin recursos. Usa timeouts para llamadas de red y límites de tiempo total de ejecución.

systemd es excelente aquí: TimeoutStartSec=, límites de CPU y memoria, programación IO y sandboxing. Cron también puede hacerlo, pero tendrás que escribir más código wrapper.

8) Añade jitter para evitar estampidas en la flota

Si mil servidores ejecutan “limpieza diaria” a medianoche, enhorabuena: inventaste un ataque distribuido contra tu propio almacenamiento.

Añade un retraso aleatorio (acotado) o programa en una ventana. El jitter es fiabilidad barata.

9) Monitoriza la ausencia, no solo la presencia

Alertar por “trabajo falló” está bien. Alertar por “el trabajo no se ejecutó” es mejor. Los horarios perdidos ocurren: temporizadores deshabilitados, crontabs rotos, VMs en pausa, credenciales caducadas.

Empuja una métrica heartbeat o escribe un archivo de marca temporal que compruebe la monitorización. El silencio no es éxito.

Tareas prácticas (comandos, salidas, decisiones)

Estas son comprobaciones operativas reales. Ejecútalas cuando estés construyendo una programación, y ejecútalas de nuevo cuando falle a las 02:13. Cada ítem incluye: comando, qué significa la salida y qué decisión tomar a continuación.

Task 1: Confirmar que el demonio cron está realmente en ejecución

cr0x@server:~$ systemctl status cron
● cron.service - Regular background program processing daemon
     Loaded: loaded (/usr/lib/systemd/system/cron.service; enabled; preset: enabled)
     Active: active (running) since Tue 2026-02-03 00:12:10 UTC; 2 days ago
       Docs: man:cron(8)
   Main PID: 812 (cron)
      Tasks: 1 (limit: 18945)
     Memory: 1.4M
        CPU: 2.912s

Significado: Si no está active (running), tu trabajo nunca tuvo una oportunidad.

Decisión: Si está inactivo/fallado, arregla el servicio primero (habilitar/iniciar). No toques aún el script.

Task 2: Verificar que la entrada del crontab está instalada para el usuario correcto

cr0x@server:~$ crontab -l
MAILTO=""
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
15 2 * * * /opt/jobs/nightly-report.sh

Significado: Puedes ver el horario, el shell, PATH y si la salida se enviará por correo.

Decisión: Si este es el usuario equivocado (común), instálalo con la cuenta correcta o usa cron del sistema con usuario explícito.

Task 3: Revisar los logs de cron para el desencadenamiento de tu trabajo

cr0x@server:~$ grep -E "CRON|nightly-report" /var/log/syslog | tail -n 5
Feb  5 02:15:01 server CRON[24719]: (cr0x) CMD (/opt/jobs/nightly-report.sh)
Feb  5 02:15:01 server CRON[24718]: (CRON) info (No MTA installed, discarding output)

Significado: Cron sí disparó. Además, la salida se está descartando porque no hay sistema de correo y no redirigiste la salida.

Decisión: Arregla el registro inmediatamente: redirige stdout/stderr a un fichero o a syslog/journald.

Task 4: Probar que el script se ejecuta de forma no interactiva con el entorno del planificador

cr0x@server:~$ env -i HOME=/home/cr0x USER=cr0x SHELL=/bin/bash PATH=/usr/bin:/bin /bin/bash -lc '/opt/jobs/nightly-report.sh'
/opt/jobs/nightly-report.sh: line 12: psql: command not found

Significado: Tu PATH interactivo tenía psql; el PATH del trabajo no lo tiene.

Decisión: Usa la ruta absoluta a psql o establece PATH en el script/unit/crontab. No “sources .bashrc” como parche rápido.

Task 5: Añadir bloqueo para prevenir solapamientos (y probarlo)

cr0x@server:~$ flock -n /run/lock/nightly-report.lock /opt/jobs/nightly-report.sh; echo "exit=$?"
exit=0
cr0x@server:~$ flock -n /run/lock/nightly-report.lock /opt/jobs/nightly-report.sh; echo "exit=$?"
exit=1

Significado: La primera ejecución adquirió el bloqueo. La segunda falló inmediatamente (exit 1) porque el bloqueo está retenido.

Decisión: Decide la política: omitir si está bloqueado (a menudo correcto) o esperar con un timeout (a veces correcto). Documenta la decisión.

Task 6: Confirmar que los códigos de salida se propagan y son visibles

cr0x@server:~$ /opt/jobs/nightly-report.sh; echo "job_exit=$?"
job failed: could not connect to database
job_exit=2

Significado: El script devuelve un código distinto de cero e imprime un error claro.

Decisión: Si los códigos de salida son siempre 0, arregla el script. Los planificadores solo pueden reaccionar a lo que les dices.

Task 7: Construir un timer de systemd que recupere ejecuciones tras tiempo de inactividad

cr0x@server:~$ systemctl cat nightly-report.timer
# /etc/systemd/system/nightly-report.timer
[Unit]
Description=Run nightly report

[Timer]
OnCalendar=*-*-* 02:15:00
Persistent=true
RandomizedDelaySec=10m

[Install]
WantedBy=timers.target

Significado: Persistent=true ejecuta el trabajo tras el arranque si se perdió. RandomizedDelaySec añade jitter.

Decisión: Si “debe ejecutarse diariamente pase lo que pase”, usa un timer con persistencia o un motor de flujo de trabajo, no cron simple.

Task 8: Inspeccionar la unidad de servicio para límites de recursos y registro

cr0x@server:~$ systemctl cat nightly-report.service
# /etc/systemd/system/nightly-report.service
[Unit]
Description=Nightly report job
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
User=cr0x
Group=cr0x
WorkingDirectory=/opt/jobs
Environment=PATH=/usr/local/bin:/usr/bin:/bin
ExecStart=/usr/bin/flock -n /run/lock/nightly-report.lock /opt/jobs/nightly-report.sh
TimeoutStartSec=30m
Nice=10
IOSchedulingClass=best-effort
IOSchedulingPriority=7
StandardOutput=journal
StandardError=journal

Significado: Esto es lo que parece la “programación en producción”: dependencias, bloqueo, timeout y logs en journald.

Decisión: Si tu trabajo compite con cargas interactivas o servicios con uso intensivo de almacenamiento, establece prioridades IO/CPU aquí en lugar de esperar.

Task 9: Comprobar cuándo se ejecutó el timer por última vez y si está atrasado

cr0x@server:~$ systemctl list-timers --all | grep nightly-report
nightly-report.timer  loaded active waiting  Thu 2026-02-05 02:23:11 UTC  3h ago  Thu 2026-02-06 02:15:00 UTC  20h left nightly-report.service

Significado: Obtienes tiempos de última/siguiente ejecución. Si falta waiting o la última ejecución es antigua, algo anda mal.

Decisión: Si está atrasado, comprueba si el timer está habilitado, si el reloj está correcto y si la unidad está fallando o atascada.

Task 10: Leer los registros solo de este trabajo

cr0x@server:~$ journalctl -u nightly-report.service -n 20 --no-pager
Feb 05 02:15:02 server nightly-report.sh[25101]: start ts=2026-02-05T02:15:02Z
Feb 05 02:15:02 server nightly-report.sh[25101]: connecting db=reporting
Feb 05 02:19:41 server nightly-report.sh[25101]: wrote /var/reports/nightly-2026-02-05.csv bytes=1842201
Feb 05 02:19:41 server nightly-report.sh[25101]: done status=ok runtime_s=279

Significado: Puedes demostrar que se ejecutó, cuánto tardó y qué produjo.

Decisión: Si los registros no muestran un inicio/fin claros, mejora el registro del script antes de mejorar otra cosa.

Task 11: Detectar solapamientos o procesos desbocados

cr0x@server:~$ pgrep -af nightly-report.sh
25101 /bin/bash /opt/jobs/nightly-report.sh

Significado: Ves si se está ejecutando ahora y con qué PID/comando.

Decisión: Si existen múltiples instancias, añade bloqueo y considera un límite de tiempo/timeout.

Task 12: Comprobar el poseedor del bloqueo si estás atascado “bloqueado para siempre”

cr0x@server:~$ ls -l /run/lock/nightly-report.lock
-rw-r--r-- 1 cr0x cr0x 0 Feb  5 02:15 /run/lock/nightly-report.lock
cr0x@server:~$ lsof /run/lock/nightly-report.lock
COMMAND   PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
flock   25100 cr0x    3w   REG  253,0        0  987 /run/lock/nightly-report.lock

Significado: El archivo puede existir sin estar bloqueado; lsof muestra si algún proceso lo mantiene.

Decisión: Si un proceso muerto no mantiene el bloqueo, tu enfoque de bloqueo está mal (no uses “archivo de bloqueo existe”). Usa flock correctamente.

Task 13: Validar la sincronización del tiempo (porque las programaciones asumen que el tiempo es real)

cr0x@server:~$ timedatectl
               Local time: Thu 2026-02-05 05:24:12 UTC
           Universal time: Thu 2026-02-05 05:24:12 UTC
                 RTC time: Thu 2026-02-05 05:24:11
                Time zone: Etc/UTC (UTC, +0000)
System clock synchronized: yes
              NTP service: active
          RTC in local TZ: no

Significado: Si el reloj no está sincronizado, “2:15” se convierte en una sugerencia.

Decisión: Arregla la sincronización del tiempo antes de perseguir bugs fantasma del planificador.

Task 14: Comprobar espacio en disco y presión de inodos (el almacenamiento mata trabajos silenciosamente)

cr0x@server:~$ df -h /var /tmp
Filesystem      Size  Used Avail Use% Mounted on
/dev/sda2       100G   94G  1.8G  99% /
tmpfs            16G  128M   16G   1% /tmp
cr0x@server:~$ df -i /var
Filesystem       Inodes  IUsed  IFree IUse% Mounted on
/dev/sda2       6553600 6501000  52600   99% /

Significado: Puedes tener bytes libres pero sin inodos, o al revés. Cualquiera puede romper tareas de “escribir salida”.

Decisión: Si el uso es >95%, deja de fingir que es un problema de planificación. Libera espacio, rota logs, arregla archivos temporales desbocados.

Task 15: Detectar cuellos de botella en IO que alargan tiempos y causan solapamientos

cr0x@server:~$ iostat -xz 1 3
Linux 6.5.0 (server) 	02/05/2026 	_x86_64_	(8 CPU)

avg-cpu:  %user   %nice %system %iowait  %steal   %idle
           8.12    0.00    3.44   31.55    0.00   56.89

Device            r/s     rkB/s   rrqm/s  %rrqm r_await rareq-sz     w/s     wkB/s   wrqm/s  %wrqm w_await wareq-sz  aqu-sz  %util
sda              3.10     90.2     0.00   0.0   12.33    29.1     40.0   10240.0    12.0   23.1  210.45   256.0     8.41  98.7

Significado: Alto %iowait y %util cercano al 100% indica saturación del disco. Un w_await alto sugiere que las escrituras están en cola.

Decisión: Si tu trabajo se volvió “lento”, comprueba IO. Luego considera programarlo fuera de las horas pico, limitarlo o mover escrituras pesadas a un almacenamiento mejor.

Task 16: Confirmar que los permisos coinciden con la identidad del planificador

cr0x@server:~$ sudo -u cr0x test -w /var/reports && echo writable || echo not_writable
not_writable

Significado: El usuario que ejecuta el trabajo no puede escribir en la ruta objetivo.

Decisión: Arregla la propiedad/ACL del directorio o ejecuta el trabajo con la cuenta de servicio correcta. No “simplemente ejecutes como root” a menos que disfrutes los postmortems.

Task 17: Validar dependencias DNS/red para trabajos que llaman servicios

cr0x@server:~$ getent hosts db.internal
10.40.12.8   db.internal
cr0x@server:~$ nc -vz db.internal 5432
Connection to db.internal (10.40.12.8) 5432 port [tcp/postgresql] succeeded!

Significado: La resolución de nombre funciona; el puerto es accesible.

Decisión: Si falla DNS o el puerto está bloqueado, arregla la política/red antes de reescribir el script.

Task 18: Comprobación estilo Windows en Linux: encontrar scripts con finales de línea CRLF

cr0x@server:~$ file -b /opt/jobs/nightly-report.sh
Bourne-Again shell script, ASCII text executable, with CRLF line terminators

Significado: CRLF puede romper intérpretes de forma sutil, especialmente con shebangs.

Decisión: Convierte a LF (dos2unix), haz commit correctamente y deja de copiar scripts por correo.

Tres micro-historias corporativas (las que aprendes una vez)

Micro-historia 1: El incidente causado por una suposición equivocada (las zonas horarias no son un detalle)

Una empresa mediana ejecutaba un trabajo nocturno de “congelado de datos” a las 00:05 hora local. El script marcaba filas con freeze_date=$(date +%F), luego los sistemas aguas abajo usaban esa fecha como clave de partición. Durante años funcionó, porque “hora local” y “día hábil” parecían lo mismo.

Luego se expandieron a una segunda región. Alguien hizo lo sensato para la infraestructura y puso los servidores en UTC. El planificador seguía ejecutando a “00:05” porque eso decía el crontab. Pero ahora “00:05” era UTC, no local. El congelado se ejecutó horas antes de lo previsto para la región original y horas después para la nueva región.

El daño real no fue la hora de ejecución: fue la marca de fecha. Algunas filas que debían etiquetarse para el siguiente día hábil recibieron la fecha del día anterior. Las particiones tenían “datos faltantes”, los paneles mostraron una caída repentina y el equipo financiero entró en un pequeño pánico cortés.

La primera solución propuesta fue clásica: “Vuelve a poner la zona horaria”. Habría ayudado a una región y perjudicado a la otra. La segunda solución fue mejor: definir un límite de día hábil en el código (TZ explícito para el cálculo de la fecha) y registrar marcas temporales en UTC. El trabajo ahora calculaba la clave de partición usando una zona horaria de negocio configurada y registraba tanto la clave como la marca UTC.

La lección duradera fue simple y molesta: no puedes externalizar la semántica a date. Si la salida de un trabajo depende de “qué día es”, debes definir qué reloj quieres usar.

Micro-historia 2: La optimización que salió mal (la compresión no es gratis)

Otra organización tenía un trabajo de backup que volcaba una base de datos y la comprimía. Los costes de almacenamiento subían, así que alguien “optimizó” subiendo la compresión al máximo y aumentando el paralelismo. Las copias se hicieron más pequeñas. Todos felicitaron la gráfica.

Dos semanas después, las 02:00 se convirtieron en la nueva hora pico. El trabajo de backup saturó CPU y IO de disco, y lo hizo con perfecta consistencia. Otros trabajos programados—rotación de logs, ETL e incluso escaneos de seguridad—empezaron a solaparse y a agotar los tiempos. El planificador no fallaba; ejecutaba fielmente un plan que ya no encajaba en la realidad.

El primer síntoma no fue “backup falló”. El primer síntoma fue “servicios aleatorios lentos por la mañana”. El responsable de guardia persiguió latencia de aplicaciones, luego bloqueos en la base de datos, luego la red. Solo después de que alguien medía la espera de IO del host apareció el patrón: el backup “optimizado” estaba convirtiendo el host en un triste ladrillo vibrante.

La solución no fue deshacer la compresión por completo. Fue acotar el radio de impacto: bajar el nivel de compresión, limitar CPU, darle prioridad IO y moverlo a una ventana de tiempo distinta con jitter en la flota. También introdujeron un tiempo máximo de ejecución; si el trabajo lo excedía, fallaba ruidosamente y hacía paging, en lugar de robarse la noche en silencio.

La optimización que ignora la contención no es optimización. Es trasladar el coste de “espacio en disco” al “sueño de todos”.

Micro-historia 3: La práctica aburrida pero correcta que salvó el día (idempotencia + escrituras atómicas)

Un equipo ejecutaba un trabajo programado que generaba un CSV usado para la facturación de clientes. La versión anterior escribía directamente en /srv/billing/current.csv. Una noche la máquina se reinició a mitad de escritura tras una actualización del kernel. El archivo existía, el trabajo “se ejecutó” y los sistemas aguas abajo consumieron felizmente un CSV truncado. Facturas erróneas siguieron. No fue catastrófico, pero sí caro en tiempo humano.

El equipo cambió una cosa: el trabajo ahora escribía en un archivo temporal con nombre único, validaba el recuento de filas y un checksum, y luego lo renombraba atómicamente en su lugar. También conservaban el archivo conocido-bueno anterior durante unos días. Era aburrido. Era correcto.

Meses después, un fallo de almacenamiento causó un breve error de IO a mitad de ejecución. El trabajo falló antes del rename, dejando intacto el viejo current.csv. Los sistemas aguas abajo siguieron usando la última salida buena mientras el trabajo alertaba al responsable de guardia de que no pudo producir el nuevo artefacto.

Sin carreras. Sin “¿qué cambió?”. Sin recuperación forense de datos parciales. Solo una falla limpia y un contrato de salida estable. La fiabilidad a menudo parece negarse a ser inteligente.

Guion rápido de diagnóstico

Cuando un trabajo programado no se ejecuta (o se ejecuta mal), necesitas una secuencia que encuentre el cuello de botella rápido. No una sesión de depuración basada en sensaciones.

Primero: ¿El planificador desencadenó algo?

  • cron: comprueba las entradas del syslog para la línea CMD; confirma que la entrada de crontab existe y que el demonio está en ejecución.
  • systemd: comprueba list-timers, el estado de la unidad y la última ejecución; confirma que el timer está habilitado.
  • Windows: comprueba Last Run Time / Last Run Result y la pestaña History (si está habilitada).

Si no hay registro de desencadenamiento, para. Arregla la programación y la habilitación. No toques el script.

Segundo: ¿Arrancó pero murió inmediatamente?

  • Lee los registros: journald o tu fichero de registro redirigido.
  • Busca “command not found”, permiso denegado, configuración faltante, directorio de trabajo equivocado.
  • Vuelve a ejecutar bajo un entorno mínimo para reproducir.

Si muere inmediatamente, casi siempre es entorno, identidad o permisos.

Tercero: ¿Se ejecutó pero tardó demasiado?

  • Comprueba solapamientos: múltiples PIDs, contención de bloqueo, mensajes de “ya se está ejecutando”.
  • Comprueba cuellos de botella de recursos: disco lleno, agotamiento de inodos, espera de IO, robo de CPU, timeouts de red.
  • Comprueba dependencias aguas abajo: bloqueos en la base de datos, limitación de API, DNS.

Si el tiempo de ejecución se alargó, arregla la contención y añade timeouts/límites antes de añadir reintentos.

Cuarto: ¿“Tuvo éxito” pero produjo basura?

  • Valida artefactos: tamaño, checksum, recuento de filas, versiones de esquema.
  • Busca escrituras parciales: marcas temporales de archivos, uso de rename atómico.
  • Revisa los criterios de éxito en el código: ¿estás comprobando algo realmente?

La salida mala silenciosa es peor que el fallo. Haz imposible publicar salida mala.

Broma 2/2: Lo único más fiable que un trabajo cron es un trabajo cron que falla en el momento en que dejas de vigilarlo.

Errores comunes: síntoma → causa raíz → solución

1) Síntoma: “Funciona manualmente pero no según lo programado”

Causa raíz: diferencias de PATH/entorno; directorio de trabajo faltante; el script depende de archivos de inicialización del shell interactivo.

Solución: Usa rutas absolutas, establece PATH explícitamente, define WorkingDirectory en systemd, evita hacer source de .bashrc. Reproduce con env -i.

2) Síntoma: Sin logs, sin salida, sin errores

Causa raíz: salida descartada (sin MTA para cron), o logs escritos en un lugar no accesible para la identidad del planificador.

Solución: Redirige stdout/stderr a un fichero o a journald. Asegura permisos del directorio de logs y rotación.

3) Síntoma: El trabajo se ejecuta dos veces (o se solapa) y corrompe datos

Causa raíz: falta de bloqueo; tiempo de ejecución largo; reintentos sin idempotencia; hora duplicada por DST.

Solución: Añade bloqueo con flock. Añade límites de tiempo. Haz salidas atómicas e idempotentes. Considera programar en UTC o manejar TZ explícitamente.

4) Síntoma: “Falla aleatoriamente” con errores de red

Causa raíz: flaps de DNS, fallos transitorios de dependencias, sin reintentos/backoff, red no lista al inicio.

Solución: Añade comprobaciones de dependencias, reintentos acotados con backoff, y en systemd usa After=network-online.target más Wants=network-online.target.

5) Síntoma: Se ejecuta, pero aguas abajo dicen que la salida está vacía/ parcial

Causa raíz: escrituras directas al nombre final; el consumidor lee mientras el productor escribe; reinicio a mitad de escritura.

Solución: Escribe en temp + fsync si hace falta + rename atómico. Conserva la última salida conocida-buena.

6) Síntoma: Funciona durante semanas y luego deja de hacerlo para siempre

Causa raíz: caducidad de credenciales (tokens API, Kerberos, rotación de contraseñas), bloqueo eterno por implementación incorrecta de lock, disco llenándose gradualmente.

Solución: Alerta por “trabajo no se ejecutó” y por “salida faltante”. Usa locks del SO (flock). Monitoriza uso de disco y rota logs.

7) Síntoma: “Está programado, pero nunca se recupera tras tiempo de inactividad”

Causa raíz: cron no rellena ejecuciones perdidas; timers no persistentes; portátil/VM estuvo apagado.

Solución: Usa systemd timers con Persistent=true, o diseña el trabajo para ejecutarse según un checkpoint de “última ejecución exitosa” en lugar de solo el reloj.

8) Síntoma: Picos de CPU al tiempo programado en toda la flota

Causa raíz: horarios idénticos causan manada atronadora; sin jitter.

Solución: Añade retraso aleatorio en los timers de systemd o un sleep aleatorio acotado en el wrapper; distribuye los horarios.

Listas de comprobación / plan paso a paso

Checklist A: Antes de programar cualquier script

  1. Define el éxito: ¿qué artefacto/efecto lateral demuestra que funcionó?
  2. Define la cadencia: “diario” no es preciso. ¿Requiere catch-up?
  3. Define la identidad: ¿qué usuario/cuenta de servicio lo ejecuta? ¿Qué permisos?
  4. Define dependencias: red, base de datos, montajes de almacenamiento, secretos.
  5. Hazlo repetible: idempotente o seguro de volver a ejecutar.
  6. Haz que no se solape: bloqueo más tiempo máximo de ejecución.
  7. Haz los logs inevitables: stdout/stderr capturados; estado de inicio/fin.
  8. Haz el tiempo explícito: UTC vs local; comportamiento en DST documentado.
  9. Planifica la monitorización: alerta por fallo y por ejecución faltante.

Checklist B: Un patrón wrapper “suficientemente bueno” (Linux)

Incluso si mantienes cron, envuelve el script. Tu wrapper es donde vive la disciplina de producción: entorno, bloqueo, registro, timeouts.

cr0x@server:~$ cat /opt/jobs/wrappers/nightly-report-wrapper.sh
#!/bin/bash
set -euo pipefail

export PATH="/usr/local/bin:/usr/bin:/bin"
export LC_ALL="C"
umask 027

log_dir="/var/log/jobs"
mkdir -p "$log_dir"
log_file="$log_dir/nightly-report.log"

exec >> "$log_file" 2>&1

echo "start ts=$(date -u +%FT%TZ) host=$(hostname -f)"

timeout 30m flock -n /run/lock/nightly-report.lock /opt/jobs/nightly-report.sh

echo "done ts=$(date -u +%FT%TZ) status=ok"

Por qué funciona: falla rápido (set -euo pipefail), captura logs, aplica un timeout y previene solapamientos. También hace el entorno predecible.

Checklist C: Plan de despliegue de timers systemd (host único a flota)

  1. Escribe la unidad .service con User/Group explícitos, PATH, WorkingDirectory, timeout y registro.
  2. Escribe el .timer con OnCalendar, Persistent=true y jitter.
  3. Prueba: systemctl start job.service manualmente; verifica logs y código de salida.
  4. Habilita el timer: systemctl enable --now job.timer.
  5. Observa: list-timers y journald durante al menos un ciclo completo.
  6. Añade monitorización: alerta si la última ejecución exitosa > ventana esperada; alerta por salida no cero.
  7. Sólo entonces: despliega a más hosts. No hagas cambios masivos a medianoche. No eres un ingeniero del caos; intentas dormir.

Checklist D: Reglas de programación conscientes del almacenamiento

  1. No programes trabajos intensivos en escritura al mismo tiempo que backups, compactaciones o replicación de snapshots.
  2. Vigila espacio libre y uso de inodos en directorios de salida y temporales.
  3. Usa escrituras atómicas para artefactos compartidos.
  4. Limita compresión y concurrencia; mide la espera de IO.
  5. Planifica el crecimiento de logs; rotealos o envía los logs.

Preguntas frecuentes

1) Cron o systemd timers: ¿cuál debería usar en Linux?

Si tienes systemd, prefiere timers para todo lo que te importe: registro integrado, control de dependencias, Persistent=true y límites de recursos. Cron está bien para tareas periódicas simples y no críticas, o cuando la portabilidad importa.

2) ¿Cómo evito que los trabajos se solapen?

Usa locks del SO. En Linux, envuelve el comando con flock. Además establece un tiempo máximo de ejecución (timeout) para que “atascado” no se convierta en “bloqueado para siempre”.

3) ¿Por qué mi trabajo funciona por SSH pero falla en cron?

Tu shell interactivo establece PATH, locales y a veces credenciales. Cron no lo hace. Reproduce con env -i, luego haz que el script declare lo que necesita (rutas absolutas, rutas de configuración explícitas, localización explícita).

4) ¿Debo redirigir la salida a un fichero o a journald?

Si ya usas systemd, journald suele ser lo más limpio: buscable, etiquetado por unidad y manejable centralmente. Si usas cron, un fichero está bien—solo rota y asegúrate de que los permisos permitan escribir.

5) ¿Cuál es la forma correcta de manejar secretos para trabajos programados?

No los hardcodees en scripts o crontabs. Usa un gestor de secretos si tienes uno, o al menos ficheros de configuración legibles solo por root con permisos estrictos. Para systemd, considera archivos de entorno con acceso controlado. Rota secretos y alerta ante fallos de autenticación.

6) ¿Cómo hago que un trabajo “recupere” ejecuciones tras tiempo de inactividad?

cron no rellena ejecuciones. Usa systemd timers con Persistent=true, o diseña el trabajo para procesar desde un checkpoint almacenado (“última marca temporal exitosa”) en lugar de depender solo del reloj.

7) ¿Cómo manejo DST para un trabajo que debe ejecutarse a una hora local de negocio?

Decide el comportamiento para la hora “faltante” y la hora “duplicada” de DST. Opciones comunes: ejecutar a una hora segura (por ejemplo, 03:15), o ejecutar en UTC y traducir salidas. Si la hora local es necesaria, registra la zona horaria, conserva marcas UTC y protege contra ejecuciones duplicadas con bloqueo e idempotencia.

8) Mi trabajo es a veces lento. ¿Debería añadir reintentos?

No como primera medida. La lentitud suele ser contención (saturación de IO, bloqueos de BD) o un problema de dependencia. Mide dónde se consume el tiempo, limita la duración y evita solapamientos. Reintentos sin controles pueden multiplicar la carga y empeorar el incidente.

9) ¿Cuál es la monitorización mínima que debo añadir?

Dos señales: (1) marca temporal de la última ejecución exitosa, (2) último estado de salida. Alerta si el trabajo está atrasado/faltante o sale con código no cero. También vigila la frescura del artefacto si produce un fichero o informe.

10) ¿Cuándo debo dejar de usar “scripts programados” y pasar a una herramienta de workflow?

Cuando tienes dependencias entre pasos, necesitas backfills, requieres trazabilidad detallada o lógica de reintento compleja. Si estás construyendo un DAG con bash y correo, ya te respondiste la pregunta.

Conclusión: pasos prácticos siguientes

Si quieres scripts programados que realmente se ejecuten, no empieces discutiendo cron versus timers. Empieza por hacer tu trabajo tolerable: entorno explícito, bloqueo, idempotencia, registro y monitorización de ausencias.

  1. Elige la identidad de ejecución y asegura sus permisos (mínimos privilegios, permisos previsibles).
  2. Añade bloqueo (flock) y un tiempo máximo de ejecución (timeouts).
  3. Haz las salidas atómicas (archivo temporal + rename) y define comprobaciones de éxito.
  4. Captura logs en un lugar buscable y rota/reténlos intencionalmente.
  5. Alerta por “fallado” y por “no se ejecutó”, no solo por “imprimió un error”.
  6. Si estás en Linux con systemd: migra los trabajos críticos a timers con Persistent=true y jitter.
  7. Realiza un ejercicio controlado de fallo: rompe DNS o llena un directorio temporal en un entorno de test y confirma que tu trabajo falla ruidosamente y de forma segura.

Programar es fácil. Programar de forma fiable es una disciplina. Haz las partes aburridas ahora para que tu yo futuro pueda dormir a las 02:15.

← Anterior
Reenvío de puertos en WSL2: Haz que tus servicios sean accesibles desde la LAN
Siguiente →
Recuperar archivos eliminados en NTFS sin software fraudulento

Deja un comentario