2 puntos por GN⁺ 2026-04-25 | 1 comentarios | Compartir por WhatsApp
  • Integra en un solo archivo de SQLite una cola duradera, streams, pub/sub y scheduler, lo que permite procesar tareas asíncronas sin un broker separado como Redis o Celery
  • Hace polling de PRAGMA data_version cada 1 ms para lograr tiempos de respuesta de un solo dígito en milisegundos entre procesos, sin necesidad de polling a nivel de aplicación ni de un daemon
  • notify(), stream() y queue() se registran dentro de la transacción del llamador, por lo que se confirman junto con las escrituras del negocio o se revierten juntas, reduciendo el problema de dual-write
  • La cola de tareas incluye reintentos, prioridad, ejecución diferida, dead-letter, scheduler, named lock y rate limiting, y los streams soportan entrega at-least-once guardando offsets por consumidor
  • En entornos que usan SQLite como almacenamiento principal, permite operar la aplicación y el procesamiento asíncrono en un solo archivo de base de datos, reduciendo la complejidad operativa
  • Ofrece tres primitivas clave
    • queue(): cola de tareas at-least-once — reintentos, prioridad, tareas diferidas, dead-letter, visibility timeout
    • stream(): pub/sub duradero — seguimiento de offsets por consumidor, replay at-least-once
    • notify(): pub/sub efímero — fire-and-forget, sin replay de historial
  • Convierte funciones en trabajos de cola con el decorador estilo Huey @queue.task(), y soporta tareas periódicas basadas en crontab() + scheduler con elección de líder
  • El esquema de la cola aplica partial index a la tabla _honker_live; el claim se resuelve con un solo UPDATE … RETURNING y el ack con un solo DELETE, manteniendo un rendimiento constante sin importar la cantidad de filas muertas
  • Como extensión cargable de SQLite (libhonker_ext), permite que todos los clientes SQLite 3.9+ accedan a las mismas tablas — un worker en Python puede reclamar tareas enviadas desde otros lenguajes
  • Proporciona guías de integración con ORMs principales como SQLAlchemy, Django, Drizzle, Kysely, sqlx, GORM, ActiveRecord y Ecto
  • Incluso las transacciones interrumpidas por SIGKILL se mantienen seguras gracias al ACID de SQLite, y cuando un worker falla, las tareas se reclaman automáticamente otra vez tras vencer el visibility timeout
  • Ofrece bindings para 8 lenguajes: Python, Node.js, Rust, Go, Ruby, Bun, Elixir y C++, cada uno publicado de forma independiente en PyPI, npm, crates.io, Hex y RubyGems
  • Implementado en Rust (honker-core + honker-extension)
  • Licencia Apache 2.0

1 comentarios

 
GN⁺ 2026-04-25
Opiniones de Hacker News
  • Yo hice esto. Honker agrega NOTIFY/LISTEN entre procesos a SQLite, para ofrecer entrega de eventos estilo push con latencia de un solo dígito en ms usando solo el archivo SQLite existente, sin daemons ni brokers
    Como SQLite no tiene un servidor como Postgres, la clave fue mover la fuente de polling desde consultas periódicas a un stat(2) ligero sobre el archivo WAL. SQLite sigue siendo eficiente incluso con muchas consultas pequeñas (https://www.sqlite.org/np1queryprob.html), así que no diría que sea una mejora enorme, pero me parece interesante que solo con observar el WAL y llamar funciones de SQLite ya sea independiente del lenguaje
    Encima de eso también monté pub/sub efímero, una cola de trabajo durable con reintentos y dead-letter, y un stream de eventos con offsets por consumidor. Los tres son filas dentro del archivo .db de una app existente, así que se pueden confirmar atómicamente junto con las escrituras del negocio, y si hay rollback ambos desaparecen juntos
    Originalmente era litenotify/joblite, pero había comprado honker.dev medio en broma y, viendo que nombres como Oban, pg-boss, Huey, RabbitMQ, Celery y Sidekiq también son medio ridículos, me quedé con ese. Ojalá sea útil o al menos dé risa, y la advertencia de que es software alfa sigue totalmente vigente

    • Parece estar pensado sobre todo para lenguajes donde es más fácil manejar solo concurrencia basada en procesos
      En cosas como Java/Go/Clojure/C#, como SQLite de todos modos es single writer, se ve más simple y limpio que la aplicación administre ese writer y, con una cola concurrente del lenguaje, sepa qué escrituras ocurrieron y despierte solo a los hilos relacionados
      Aun así, es divertido ver el WAL usado de una forma tan creativa, y en lenguajes como Python/JS/TS/Ruby, donde la concurrencia basada en procesos es común, parece encajar bastante bien como mecanismo de notify
    • Esta vez aprendí que hacer stat() cada 1ms es sorprendentemente muy barato
      En mi hardware tarda menos de 1μs por llamada, así que con ese nivel de polling el uso de CPU ni llega a 0.1%
    • Capaz se me escapa algo, pero me parece que PRAGMA data_version sería mejor que stat(2)
      https://sqlite.org/pragma.html#pragma_data_version
      Y si estás en la API de C, también está el más directo SQLITE_FCNTL_DATA_VERSION
      https://sqlite.org/c3ref/c_fcntl_begin_atomic_write.html#sqlitefcntldataversion
    • Está bastante bueno. Yo también hice algo parecido a medias
      Me pregunto si esto también serviría como stream de mensajes persistente, tipo Kafka liviano. También me da curiosidad si se podrían tener semánticas de replay completo de mensajes pasados + en tiempo real desde cierto timestamp para un topic específico
      Supongo que se podría imitar con polling como en pub/sub, pero como dijiste, probablemente no sea lo óptimo
    • Podría quedar aún mejor si también se guarda el estado del subscriber
      Si guardas cosas como posición de lectura, nombre de la cola y filtros, en vez de despertar todos los hilos de subscription cada vez que cambia stat(2) para que cada uno haga su SELECT con N=1, el hilo de polling podría hacer Events INNER JOIN Subscribers y despertar solo a los subscribers que realmente hagan match
  • Gracias por el feedback. Ya subí un PR incorporando las sugerencias
    https://github.com/russellromney/honker/pulls/1
    Ahora cambió a una estructura de polling de 3 capas: PRAGMA data_version cada 1ms, stat cada 100ms y reconexión en caso de error

    1. Reemplacé la detección previa con stat de cambios en size/mtime usando PRAGMA data_version cada 1ms. Como es el commit counter del propio SQLite, es monotónico, no le afecta el clock skew y maneja bien truncación del WAL y rollback. Es una query nonblocking de unos 3µs, y el cambio fue por correctitud, no por rendimiento. De hecho es apenas más lenta. El riesgo de truncación resultó ser más realista de lo que parecía
      Probando vi que SQLITE_FCNTL_DATA_VERSION de la API de C no funcionaba entre conexiones. Así que por ahora sigo pagando el costo de pasar por la capa VFS y aceptando ese tradeoff explícitamente
    2. Si falla la query de data_version, intento reconectar asumiendo casos como errores temporales de disco, un hiccup de NFS o corrupción de la conexión, y por prevención también despierto a los subscribers
    3. Cada 100ms uso stat para comparar (dev, ino) con los valores del arranque y detectar reemplazo de archivo. Eso cubre casos como atomic rename, restore con litestream o remount del volumen; data_version sigue el fd abierto, así que si cambia el archivo seguiría viendo el inode original y no detectaría eso
      Gracias a esto Honker mejoró y yo también aprendí bastante
  • Un poco de autopromoción: en el próximo PostgreSQL 19, LISTEN/NOTIFY fue optimizado para escalar mucho mejor en signaling selectivo
    Es un parche pensado para casos con muchos backends escuchando canales distintos
    https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=282b1cde9

    • Buena promoción, y además muy bien alineada con el tema
  • Me pregunto si no se podría observar cambios del WAL con inotify o algún wrapper multiplataforma en vez de hacer polling

    • Se rompe lo multiplataforma. Sobre todo en Mac, donde a veces se lo traga silenciosamente, así que cuesta confiar en eso
      stat simplemente funciona en todos lados
  • Lo atractivo frente a IPC separado es que queda confirmado atómicamente con los datos del negocio
    La mensajería externa siempre tiene el problema de “la notificación salió pero la transacción hizo rollback”, y eso se ensucia rápido
    Algo que sí me pregunto es el checkpoint del WAL. No sé si el polling con stat() maneja bien cuando SQLite vuelve a truncar el WAL a 0. Siento que podría haber una ventana en la que se pierdan eventos

    • La atomicidad es prácticamente todo
      Antes sufrí con una combinación de Postgres+SQS porque un trigger enviaba el enqueue antes de que el commit fuera visible en otra conexión. Le agregamos lógica de retry, polling del lado del worker y al final terminamos metiendo el enqueue dentro de la transacción; y llegado a ese punto básicamente era rehacer lo que hace Honker, pero con más moving parts
      Los bugs tipo “la notificación salió pero la fila todavía no está confirmada” suelen ser silenciosos y dependientes del timing, así que son realmente horribles de rastrear
    • El archivo WAL sigue ahí y solo se trunca, así que en sí eso sí aparece como un update
      Igual todavía no tengo tests para esa parte, así que necesito verificarlo mejor. Buen punto, lo voy a revisar
  • Gracias
    Han aumentado mucho las apps pequeñas basadas en SQLite, y casi todas necesitan cola y scheduler
    Yo mismo probé algunas cosas, pero siempre extrañé la elegancia de las soluciones del ecosistema Postgres
    A esto sí le voy a dar una prueba de inmediato

    • La expresión proliferación pequeña describe perfecto el grupo de side projects que formaron mis hábitos
      Si te topas con algún problema, estaría buenísimo que dejaras un PR o un issue en el repo
  • Aquí dan ganas de usar kqueue/FSEvents, pero tenía entendido que Darwin descarta notificaciones del mismo proceso
    Si publisher y listener están en el mismo proceso, a veces el listener ni siquiera se despierta, y rastrear eso se vuelve bastante sucio. El polling con stat se ve feo, pero al final parece ser lo que realmente funciona en todas partes
    También me pregunto si cuando el archivo vuelve a achicarse en un checkpoint del WAL eso dispara un wakeup, o si el poller filtra las disminuciones de tamaño

    • Este comentario está completamente equivocado
      Los eventos VNODE de kqueue se entregan siempre que el proceso tenga permisos de acceso al archivo, y no hay ningún filtro que los descarte por ser del mismo proceso
    • Esto sí necesita una prueba real
      Lo voy a revisar y luego comento
  • Está muy bueno. Me da curiosidad si, bajo carga, el cuello de botella principal es el write throughput de SQLite o la capa de notificaciones del WAL

    • El cuello de botella está del lado de las escrituras y del flujo de claim/ack
      También cambia bastante según el journal mode y el synchronous mode
      La notificación, ya sea con el viejo enfoque de stat(2) o con el nuevo basado en PRAGMA, es muy barata. En otro comentario ya se mencionó que stat(2) anda alrededor de 1µs
  • Muy buen proyecto. Yo también estoy construyendo algo que empuja SQLite mucho más allá de su uso típico
    Da ánimo ver a más gente explorando hasta dónde puede llegar SQLite realmente

  • Me pregunto si también se puede integrar en casos donde se usa SQLAlchemy
    Por cómo se ve ahora, parece que intenta crear por su cuenta la conexión a la base de datos