Volver al inicio

Async-background: tareas en segundo plano en Ruby sin Redis ni Postgres

Async-background — gema para Ruby que proporciona tareas en segundo plano sin dependencias externas. Resuelve problemas de gestión de recursos y confiabilidad. Adecuada para aplicaciones en Async y Falcon.

¿Cómo ejecutar tareas en segundo plano en Ruby sin Redis? La solución async-background
Advertisement 728x90

Async-background: Un sistema ligero de trabajos en segundo plano para Ruby sin dependencias externas

Los desarrolladores de aplicaciones Ruby a menudo necesitan ejecutar trabajos en segundo plano, pero las soluciones listas como Sidekiq requieren infraestructura adicional. La nueva gema async-background ofrece una alternativa que se ejecuta en Async sin Redis ni Postgres, con control granular de recursos.

Por qué las soluciones existentes se quedaban cortas

Sidekiq se usa típicamente para trabajos en segundo plano en Ruby, pero requiere Redis. Para proyectos pequeños con solo unos pocos trabajos, eso es excesivo: un contenedor extra en docker-compose, otro punto de fallo y costos de infraestructura. Además, Sidekiq no permite una distribución flexible de carga entre workers —un informe nocturno pesado puede ralentizar el procesamiento de correos de usuarios.

Otras opciones también tienen sus inconvenientes:

Google AdInline article slot
  • whenever — genera un crontab del sistema, sin cola ni integración con la app.
  • rufus-scheduler — se ejecuta en hilos separados, causando cambios de contexto y problemas de duplicación en apps Async con múltiples procesos.
  • good_job y solid_queue — atados a ActiveRecord y Postgres. Esto crea riesgos: los trabajos en segundo plano compiten con la carga transaccional por recursos de la base de datos, lo que potencialmente ralentiza la app principal.

Por lo tanto, había necesidad de una solución que combine programaciones cron, intervalos y una cola dinámica sin dependencias externas, más control sobre la utilización de recursos.

Principios de diseño clave

El autor de la gema delineó seis principios fundamentales:

  • Bucle de eventos único. El planificador se ejecuta en el mismo bucle reactivo que la app, sin hilos separados.
  • Cero infraestructura. La cola se almacena en disco (SQLite en modo WAL o un archivo en tmp/), sin Redis ni Postgres.
  • Dependencias opcionales. Funciones extra (como métricas) se añaden vía gemas opcionales.
  • Seguridad multi-proceso sin coordinador. Asegura que un trabajo se ejecute solo en un proceso al usar Falcon (forks).
  • Persistencia como seguro, velocidad como mecanismo separado. La fiabilidad proviene del almacenamiento SQLite, la velocidad de los wakeups vía socket UNIX.
  • Utilización gestionada de workers. Los desarrolladores pueden asignar trabajos a workers específicos vía variables de entorno y config YAML.

Estos principios permitieron construir una gema que se integra perfectamente en apps Async existentes sin infraestructura extra.

Google AdInline article slot

Características arquitectónicas

MinHeap en lugar de polling de ticks

En lugar de un bucle con sleep 1 (despertando cada segundo para comprobar todos los trabajos), se usa un min-heap binario (MinHeap). Esto permite:

  • Obtener el siguiente trabajo en tiempo O(1) con peek.
  • Actualizar el heap tras ejecutar un trabajo en tiempo O(log n) con replace_top.
  • Dormir con precisión hasta el siguiente evento, ahorrando recursos de CPU.

La implementación tiene solo 74 líneas y no tiene dependencias externas.

Dos escalas temporales

Para trabajos basados en intervalos (p. ej., every: 60), se usan relojes monotónicos (CLOCK_MONOTONIC) para evitar problemas con la sincronización de hora (NTP). Para trabajos cron (p. ej., cron: "0 3 *"), se usa el tiempo de reloj de pared (Time.now) ya que el horario está ligado al tiempo del calendario.

Google AdInline article slot

En código, se ve así:

next_run_at = if task_config[:interval]
  now + jitter + task_config[:interval]
else
  now_wall = Time.now
  wall_wait = task_config[:cron].next_time(now_wall).to_f - now_wall.to_f
  now + jitter + [wall_wait, MIN_SLEEP_TIME].max
end

Sharding vía CRC32

Al usar Falcon (que hace forks), los trabajos deben ejecutarse solo en un proceso. El sharding determinista lo logra: se calcula Zlib.crc32(name) % total_workers + 1 para el nombre del trabajo. Si coincide con el índice del worker actual, se carga el trabajo.

assigned = config['worker']&.to_i || ((Zlib.crc32(name) % total_workers) + 1)
next unless assigned == worker_index

Esto elimina la necesidad de bloqueos distribuidos.

Skip-on-Overlap

Si un trabajo dura más que su intervalo (p. ej., 90 segundos para un intervalo de 60 segundos), la nueva instancia se omite. En su lugar, se registra el trabajo actual, se omite y se reprograme para el siguiente slot:

if entry.running
  logger.warn('Async::Background') { "#{entry.name}: skipped, previous run still active" }
  metrics.job_skipped(entry)
  entry.reschedule(monotonic_now)
  heap.replace_top(entry)
  next
end

Cola: SQLite + UNIX Socket

La cola dinámica usa dos mecanismos:

  • SQLite — fuente de verdad. Los trabajos se persisten en disco para fiabilidad.
  • UNIX socket — para notificaciones instantáneas. Tras añadir un trabajo a la BD, se envía una señal vía el socket para que el worker lo procese inmediatamente.

El polling cada 5 segundos actúa como respaldo en caso de pérdida de señal.

Recuperación de fallos

Si un worker falla a mitad de un trabajo, el estado queda en "running". Al reiniciar, Queue::Store#recover encuentra esos registros y los devuelve a la cola.

Puntos clave

  • Sin dependencias de servicios externos. Async-background depende solo del sistema de archivos, simplificando el despliegue.
  • Control de recursos. Aislar workers para diferentes tipos de trabajos (cron, cola) previene contención de recursos.
  • Fiabilidad. Los trabajos no se pierden gracias a la persistencia SQLite y mecanismos de recuperación.
  • Flexibilidad. La config YAML y variables de entorno facilitan ajustar la distribución de trabajos.
  • Rendimiento. Min-heap y sockets UNIX ofrecen baja latencia y uso eficiente de CPU.

— Editorial Team

Advertisement 728x90

Leer después