2 points par GN⁺ 6 일 전 | 1 commentaires | Partager sur WhatsApp
  • Intègre une file durable, des streams, du pub/sub et un scheduler dans un seul fichier SQLite, permettant de traiter des tâches asynchrones sans broker séparé comme Redis ou Celery
  • Atteint une réactivité inter-processus de l’ordre de quelques millisecondes en pollant PRAGMA data_version toutes les 1 ms, sans polling au niveau applicatif ni démon
  • notify(), stream() et queue() sont tous écrits dans la transaction de l’appelant, puis soit commités avec les écritures métier soit rollbackés ensemble, ce qui réduit les problèmes de dual-write
  • La file de tâches inclut retries, priorité, exécution différée, dead-letter, scheduler, named lock et rate limiting, tandis que les streams prennent en charge une livraison at-least-once en stockant les offsets par consommateur
  • Dans les environnements qui utilisent SQLite comme stockage principal, il devient possible d’exploiter l’application et le traitement asynchrone avec un seul fichier de base de données, ce qui réduit la complexité opérationnelle
  • Fournit trois primitives clés
    • queue() : file de tâches at-least-once — retries, priorité, tâches différées, dead-letter, visibility timeout
    • stream() : pub/sub durable — suivi des offsets par consommateur, replay at-least-once
    • notify() : pub/sub éphémère — fire-and-forget, sans replay d’historique
  • Un décorateur @queue.task() de style Huey permet de transformer une fonction en tâche de file, avec prise en charge des tâches périodiques basées sur crontab() et d’un scheduler avec élection de leader
  • Le schéma de file applique un partial index à la table _honker_live ; le claim se fait avec un seul UPDATE … RETURNING et l’ack avec un seul DELETE, garantissant des performances stables quel que soit le nombre de lignes mortes
  • En tant qu’extension SQLite chargeable (libhonker_ext), il permet à tous les clients SQLite 3.9+ d’accéder aux mêmes tables — un worker Python peut claim des tâches poussées depuis d’autres langages
  • Fournit des guides d’intégration avec les principaux ORM comme SQLAlchemy, Django, Drizzle, Kysely, sqlx, GORM, ActiveRecord et Ecto
  • Même une transaction interrompue par un SIGKILL reste sûre grâce aux garanties ACID de SQLite, et en cas de crash d’un worker, le reclaming se fait automatiquement après expiration du visibility timeout
  • Fournit des bindings pour 8 langages : Python, Node.js, Rust, Go, Ruby, Bun, Elixir et C++, chacun publié séparément sur PyPI, npm, crates.io, Hex et RubyGems
  • Implémenté en Rust (honker-core + honker-extension)
  • Licence Apache 2.0

1 commentaires

 
GN⁺ 6 일 전
Avis sur Hacker News
  • C’est moi qui ai construit ça. Honker ajoute un NOTIFY/LISTEN inter-processus à SQLite, ce qui permet une diffusion d’événements de type push avec une latence à un chiffre en ms, sans daemon ni broker, uniquement avec le fichier SQLite existant
    Comme SQLite n’a pas de serveur comme Postgres, l’idée clé a été de déplacer la source de polling vers un simple stat(2) léger sur le fichier WAL au lieu d’interroger périodiquement la base. SQLite reste efficace même avec beaucoup de petites requêtes (https://www.sqlite.org/np1queryprob.html), donc ce n’est peut-être pas une révolution énorme, mais le fait qu’il suffise de surveiller le WAL et d’appeler des fonctions SQLite est intéressant, car cela le rend indépendant du langage
    J’y ai aussi ajouté un pub/sub éphémère, une durable work queue avec retries et dead-letter, ainsi qu’un event stream avec offsets par consommateur. Dans les trois cas, tout est stocké sous forme de lignes dans le fichier .db de l’application existante, donc on peut committer atomiquement avec les écritures métier, et si on rollback, les deux disparaissent ensemble
    À l’origine, ça s’appelait litenotify/joblite, puis j’ai réalisé que j’avais acheté honker.dev pour plaisanter, et comme Oban, pg-boss, Huey, RabbitMQ, Celery et Sidekiq ont tous des noms un peu ridicules, j’ai gardé celui-là. J’espère que ce sera utile, ou au moins drôle, et l’avertissement « logiciel alpha » reste pleinement valable

    • Ça semble surtout destiné aux langages où seule la concurrence à base de processus est simple à gérer
      Dans des écosystèmes comme Java/Go/Clojure/C#, comme SQLite reste de toute façon single writer, il paraît plus simple et plus propre que l’application gère ce writer et utilise une file concurrente au niveau du langage pour savoir quelles écritures ont eu lieu et ne réveiller que les threads concernés
      Cela dit, c’est amusant de voir le WAL utilisé de manière aussi créative, et dans des langages comme Python/JS/TS/Ruby où la concurrence par processus est courante, ça semble plutôt bien coller comme mécanisme de notification
    • Je viens d’apprendre qu’un stat() toutes les 1 ms coûte étonnamment peu
      Sur mon matériel, l’appel prend moins de 1μs, donc avec ce niveau de polling, l’utilisation CPU reste sous les 0,1 %
    • Il se peut que je rate quelque chose, mais je me demande si PRAGMA data_version ne serait pas mieux que stat(2)
      https://sqlite.org/pragma.html#pragma_data_version
      Et côté API C, il y a aussi le plus direct SQLITE_FCNTL_DATA_VERSION
      https://sqlite.org/c3ref/c_fcntl_begin_atomic_write.html#sqlitefcntldataversion
    • C’est assez cool. J’en ai moi-même à moitié construit un similaire
      Je me demande si on pourrait aussi l’utiliser comme Kafka léger, c’est-à-dire comme flux de messages persistant. Je me demande si on pourrait avoir une sémantique du type rejouer tous les messages passés + temps réel à partir d’un certain timestamp pour un topic donné
      On pourrait probablement l’imiter par polling comme pour du pub/sub, mais comme tu l’as dit, ce ne serait sans doute pas optimal
    • Ce serait peut-être encore mieux si l’état des abonnés était stocké aussi
      Si on gardait la position de lecture, le nom de la queue, les filtres, etc., alors au lieu de réveiller tous les threads d’abonnement à chaque changement détecté par stat(2) pour qu’ils fassent chacun un SELECT avec N=1, le thread de polling pourrait faire un Events INNER JOIN Subscribers et ne réveiller que les abonnés réellement concernés
  • Merci pour les retours. J’ai ouvert une PR qui intègre les suggestions
    https://github.com/russellromney/honker/pulls/1
    C’est maintenant une structure de polling à 3 niveaux : PRAGMA data_version toutes les 1 ms, stat toutes les 100 ms, et reconnexion en cas d’erreur

    1. J’utilise désormais PRAGMA data_version toutes les 1 ms à la place de la détection précédente des changements de taille/mtime basée sur stat. Comme c’est le commit counter interne de SQLite, il est monotone, ne dépend pas d’un éventuel clock skew, et gère correctement les WAL truncations comme les rollbacks. C’est une requête non bloquante d’environ 3µs, et je l’ai adoptée pour la justesse, pas pour les performances. En fait, c’est même légèrement plus lent. Le risque lié à la troncature s’est révélé plus réaliste que je ne le pensais
      Après test, SQLITE_FCNTL_DATA_VERSION de l’API C ne fonctionnait pas entre connexions. Pour l’instant, j’assume donc toujours le coût du passage par la couche VFS, et j’accepte explicitement ce compromis
    2. Si la requête data_version échoue, je tente une reconnexion en supposant un problème temporaire disque, un hiccup NFS ou une corruption de connexion, et par précaution je réveille aussi les abonnés
    3. Toutes les 100 ms, je compare avec stat le (dev, ino) courant à la valeur observée au démarrage afin de détecter un remplacement de fichier. Ça couvre des cas comme un atomic rename, une restauration litestream ou un remount de volume. data_version, lui, suit le fd déjà ouvert, donc si le fichier change, il continue à regarder l’inode d’origine et ne peut pas le détecter
      Au final, Honker s’est amélioré et moi aussi j’ai beaucoup appris
  • Petite autopromo discrète : dans PostgreSQL 19, LISTEN/NOTIFY a été optimisé pour bien mieux passer à l’échelle dans les scénarios de selective signaling
    Le patch vise les cas où de nombreux backends écoutent des channels différents
    https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=282b1cde9

    • Bonne autopromo, et très pertinente par rapport au sujet
  • Je me demande s’il ne serait pas possible de surveiller les changements du WAL avec inotify ou un wrapper multiplateforme, au lieu de faire du polling

    • Le côté multiplateforme casse. En particulier sur Mac, il arrive que les événements soient silencieusement avalés, donc c’est difficile à considérer comme fiable
      stat fonctionne simplement partout
  • Ce qui le rend plus attrayant qu’un IPC séparé, c’est le commit atomique avec les données métier
    Avec une messagerie externe, on se retrouve toujours avec le problème du type « la notification est partie mais la transaction a rollback », et ça devient vite sale
    Je me demande quand même pour le WAL checkpoint : quand SQLite retronque le WAL à 0, je ne sais pas si un polling par stat() le gère correctement. J’ai l’impression qu’il pourrait y avoir une fenêtre où des événements se perdent

    • À mon avis, l’atomicité fait pratiquement tout
      J’ai déjà galéré avec une combinaison Postgres+SQS où un trigger envoyait l’enqueue avant que le commit soit visible sur une autre connexion. On a ajouté de la logique de retry, puis du polling côté worker, puis on a fini par remettre l’enqueue dans la transaction ; mais à ce stade, on était juste en train de reconstruire ce que fait Honker avec plus de pièces mobiles
      Les bugs du type « la notification est partie mais la ligne n’est pas encore commitée » sont généralement silencieux et dépendent du timing, donc ils sont vraiment pénibles à traquer
    • Le fichier WAL reste présent et n’est que tronqué, donc en soi ça devrait bien être vu comme une mise à jour
      Cela dit, je n’ai pas encore de tests sur ce point, donc il faut que je vérifie davantage. C’est une bonne remarque, je vais m’en occuper
  • Merci
    Il y a de plus en plus de petites applis basées sur SQLite, et la plupart ont besoin d’une queue et d’un scheduler
    J’en ai déjà bricolé quelques-uns moi-même, mais l’élégance des solutions de la famille Postgres m’a toujours manqué
    Celui-ci, je vais clairement l’essayer très vite

    • L’expression petite prolifération décrit parfaitement l’amas d’outils que mes habitudes de side projects ont fini par produire
      Si tu rencontres un problème, n’hésite pas à laisser une PR ou une issue sur le repo
  • J’avoue que ça donne envie d’utiliser kqueue/FSEvents ici, mais il me semble que Darwin perd les notifications venant du même processus
    Si le publisher et le listener sont dans le même processus, il arrive que le listener ne soit pas réveillé du tout, ce qui est assez pénible à déboguer. Le polling par stat, même s’il est moche, a au final l’air d’être ce qui fonctionne réellement partout
    Je me demande aussi si, lors d’un WAL checkpoint, quand le fichier rétrécit à nouveau, cela déclenche bien un wakeup, ou si le poller filtre les diminutions de taille

    • Ce commentaire est complètement faux
      Les événements VNODE de kqueue sont délivrés dès lors que le processus a le droit d’accéder au fichier, et il n’existe pas de filtre qui les supprime parce que cela vient du même processus
    • Ça mérite d’être testé en pratique
      Je vais vérifier et je reviendrai avec le résultat
  • Très sympa. Je me demande si, sous charge, le goulot d’étranglement est surtout le débit d’écriture SQLite, ou bien la couche de notification WAL

    • Le goulot est du côté des écritures et du flux claim/ack
      Ça varie aussi beaucoup selon le journal mode et le synchronous mode
      La notification, que ce soit via l’ancienne approche stat(2) ou la nouvelle basée sur PRAGMA, coûte très peu. Comme dit dans un autre commentaire, stat(2) tourne autour de 1µs
  • Beau projet. Moi aussi je construis quelque chose qui pousse SQLite bien au-delà de ses usages habituels
    C’est encourageant de voir davantage de gens explorer jusqu’où SQLite peut réellement aller

  • Je me demande s’il est possible de l’intégrer même quand on utilise SQLAlchemy
    Tel que c’est présenté pour l’instant, on dirait qu’il veut créer lui-même sa propre connexion DB