Honker - extensión que implementa NOTIFY/LISTEN de Postgres en SQLite
(github.com/russellromney)- 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_versioncada 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()yqueue()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 encrontab()+ 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 soloUPDATE … RETURNINGy el ack con un soloDELETE, 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
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 lenguajeEncima 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
.dbde una app existente, así que se pueden confirmar atómicamente junto con las escrituras del negocio, y si hay rollback ambos desaparecen juntosOriginalmente era litenotify/joblite, pero había comprado
honker.devmedio 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 vigenteEn 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
stat()cada 1ms es sorprendentemente muy baratoEn 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%
PRAGMA data_versionsería mejor questat(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_VERSIONhttps://sqlite.org/c3ref/c_fcntl_begin_atomic_write.html#sqlitefcntldataversion
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
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 hacerEvents INNER JOIN Subscribersy despertar solo a los subscribers que realmente hagan matchGracias 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_versioncada 1ms,statcada 100ms y reconexión en caso de errorstatde cambios en size/mtime usandoPRAGMA data_versioncada 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íaProbando vi que
SQLITE_FCNTL_DATA_VERSIONde 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ícitamentedata_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 subscribersstatpara 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_versionsigue el fd abierto, así que si cambia el archivo seguiría viendo el inode original y no detectaría esoGracias 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
Me pregunto si no se podría observar cambios del WAL con inotify o algún wrapper multiplataforma en vez de hacer polling
statsimplemente funciona en todos ladosLo 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 eventosAntes 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
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
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
statse ve feo, pero al final parece ser lo que realmente funciona en todas partesTambié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
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
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
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 enPRAGMA, es muy barata. En otro comentario ya se mencionó questat(2)anda alrededor de 1µsMuy 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