2 pontos por GN⁺ 2026-04-25 | 1 comentários | Compartilhar no WhatsApp
  • Integra em um único arquivo SQLite uma fila durável, streams, pub/sub e agendador, permitindo processar tarefas assíncronas sem brokers separados como Redis ou Celery
  • Faz polling de PRAGMA data_version a cada 1 ms para alcançar tempo de resposta entre processos na faixa de milissegundos de um dígito, sem necessidade de polling no nível da aplicação ou de daemon
  • notify(), stream() e queue() são todos registrados dentro da transação do chamador, sendo confirmados junto com as escritas de negócio ou revertidos juntos, reduzindo o problema de dual-write
  • A fila de tarefas inclui tentativas de repetição, prioridade, execução com atraso, dead-letter, scheduler, named lock e rate limiting, e os streams oferecem entrega at-least-once com armazenamento de offset por consumidor
  • Em ambientes que usam SQLite como armazenamento principal, é possível reunir a aplicação e o processamento assíncrono em um único arquivo de banco de dados, reduzindo a complexidade operacional
  • Fornece três primitivas principais
    • queue(): fila de tarefas at-least-once — tentativas, prioridade, tarefas com atraso, dead-letter, visibility timeout
    • stream(): pub/sub durável — rastreamento de offset por consumidor, replay at-least-once
    • notify(): pub/sub efêmero — fire-and-forget, sem replay de histórico
  • Com o decorador @queue.task() no estilo Huey, transforma funções em tarefas de fila e oferece suporte a trabalhos periódicos com base em crontab() + scheduler com eleição de líder
  • O esquema da fila aplica partial index na tabela _honker_live; o claim é feito com um único UPDATE … RETURNING e o ack com um único DELETE, mantendo desempenho constante independentemente da quantidade de linhas mortas
  • Como extensão carregável do SQLite (libhonker_ext), permite acesso às mesmas tabelas em todos os clientes SQLite 3.9+ — um worker em Python pode fazer claim de tarefas enviadas por outra linguagem
  • Fornece guias de integração com SQLAlchemy, Django, Drizzle, Kysely, sqlx, GORM, ActiveRecord e Ecto, entre outros ORMs importantes
  • Até transações durante SIGKILL permanecem seguras graças ao ACID do SQLite, e quando um worker falha o claim é retomado automaticamente após o vencimento do visibility timeout
  • Oferece bindings para 8 linguagens: Python, Node.js, Rust, Go, Ruby, Bun, Elixir e C++, cada uma publicada separadamente em PyPI, npm, crates.io, Hex e RubyGems
  • Implementado em Rust (honker-core + honker-extension)
  • Licença Apache 2.0

1 comentários

 
GN⁺ 2026-04-25
Comentários do Hacker News
  • Eu fiz isso. O Honker adiciona NOTIFY/LISTEN entre processos ao SQLite, entregando eventos em estilo push com latência de poucos milissegundos usando apenas o arquivo SQLite existente, sem daemon nem broker
    Como o SQLite não tem servidor como o Postgres, a ideia central foi mover a fonte de polling para um stat(2) leve no arquivo WAL, em vez de consultar periodicamente. Como o SQLite é eficiente mesmo com muitas consultas pequenas (https://www.sqlite.org/np1queryprob.html), não dá para dizer que isso é uma melhoria gigantesca, mas é interessante que seja independente de linguagem, já que basta observar o WAL e chamar funções do SQLite
    Também coloquei em cima disso pub/sub efêmero, fila de trabalho durável com retry e dead-letter, e stream de eventos com offsets por consumidor. Os três são linhas dentro do arquivo .db do app existente, então podem ser commitados atomicamente com as escritas de negócio, e se houver rollback os dois desaparecem juntos
    Originalmente era litenotify/joblite, mas eu tinha comprado honker.dev de brincadeira e, vendo que nomes como Oban, pg-boss, Huey, RabbitMQ, Celery e Sidekiq também são todos meio engraçados, acabei ficando com esse mesmo. Espero que seja útil ou pelo menos engraçado, e o aviso de software alfa continua valendo

    • Isso parece voltado principalmente para linguagens em que só é fácil lidar com concorrência baseada em processos
      Em coisas como Java/Go/Clojure/C#, como o SQLite de qualquer forma tem um único writer, parece mais simples e limpo a aplicação gerenciar esse writer e usar alguma fila concorrente da própria linguagem para saber que escritas aconteceram e acordar só as threads relevantes
      Ainda assim, é divertido ver o WAL sendo usado dessa forma criativa, e em linguagens como Python/JS/TS/Ruby, onde concorrência baseada em processos é comum, isso parece encaixar bem como mecanismo de notificação
    • Descobri com isso que até fazer stat() a cada 1 ms é surpreendentemente barato
      No meu hardware leva menos de 1 μs por chamada, então esse nível de polling nem chega a 0,1% de uso de CPU
    • Posso estar deixando passar algo, mas me parece que PRAGMA data_version seria melhor que stat(2)
      https://sqlite.org/pragma.html#pragma_data_version
      Na API C também existe o mais direto SQLITE_FCNTL_DATA_VERSION
      https://sqlite.org/c3ref/c_fcntl_begin_atomic_write.html#sqlitefcntldataversion
    • Bem legal. Eu também já fiz metade de algo parecido
      Fico curioso se isso também pode ser usado como um stream persistente de mensagens, tipo um Kafka leve. Queria saber se também dá para ter semântica de replay de todas as mensagens passadas + em tempo real de um certo tópico a partir de um timestamp específico
      Dá para imitar isso com polling, como no pub/sub, mas como você disse provavelmente não seria o ideal
    • Talvez ficasse ainda melhor armazenando também o estado do subscriber
      Se você guardar posição de leitura, nome da fila e filtros, em vez de acordar todas as threads de subscription a cada mudança no stat(2) para cada uma fazer seu próprio SELECT com N=1, a thread de polling poderia fazer Events INNER JOIN Subscribers e acordar só os subscribers que realmente combinam
  • Valeu pelo feedback. Abri um PR incorporando as sugestões
    https://github.com/russellromney/honker/pulls/1
    Agora mudou para uma estrutura de polling em 3 camadas: PRAGMA data_version a cada 1 ms, stat a cada 100 ms e tratamento de reconexão em caso de erro

    1. Passei a usar PRAGMA data_version a cada 1 ms para substituir a detecção anterior de mudança de size/mtime baseada em stat. Como é o commit counter do próprio SQLite, ele é monotônico, não sofre com clock skew e lida corretamente com truncamento do WAL e rollback. É uma consulta nonblocking de cerca de 3 µs, e troquei isso não por desempenho, mas por correção. Na verdade é até um pouco mais lento. O risco de truncamento acabou sendo mais real do que eu imaginava
      Nos testes, o SQLITE_FCNTL_DATA_VERSION da API C não funcionou entre conexões. Então, por enquanto, ainda estou pagando o custo de passar pela camada VFS e aceitando explicitamente esse tradeoff
    2. Se a consulta de data_version falhar, assumo casos como erro temporário de disco, hiccup no NFS ou corrupção da conexão, tento reconectar e, por precaução, também acordo os subscribers
    3. A cada 100 ms, uso stat para comparar (dev, ino) com os valores do startup e detectar substituição de arquivo. Isso cobre casos como atomic rename, restore do litestream e remount de volume. O data_version segue o fd aberto, então mesmo se o arquivo mudar ele continua vendo o inode original, e por isso não detecta isso
      Com isso o Honker ficou melhor, e eu também aprendi bastante
  • Fazendo uma propaganda discreta: no futuro PostgreSQL 19, o LISTEN/NOTIFY foi otimizado para escalar muito melhor em signaling seletivo
    É um patch voltado para casos em que muitos backends ficam escutando canais diferentes
    https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=282b1cde9

    • Boa propaganda, e bem pertinente ao tópico
  • Fico pensando se não daria para observar mudanças no WAL com inotify ou algum wrapper multiplataforma, sem polling

    • Isso quebra no multiplataforma. Especialmente no Mac, às vezes engole as notificações silenciosamente, então é difícil confiar
      stat simplesmente funciona em todo lugar
  • O atrativo em relação a IPC separado é que isso fica em commit atômico com os dados de negócio
    Entrega externa de mensagens sempre tem o problema de "a notificação foi, mas a transação sofreu rollback", e isso fica bagunçado rápido
    Uma coisa que eu queria saber é sobre checkpoint do WAL. Quando o SQLite trunca o WAL de volta para 0, não sei se o polling com stat() lida corretamente com isso. Tenho a impressão de que pode haver uma janela em que eventos sejam perdidos

    • Acho que a atomicidade é praticamente tudo aqui
      Já sofri antes com uma combinação Postgres+SQS em que o enqueue era disparado por trigger em outra connection antes de o commit ficar visível. Coloquei lógica de retry, adicionei polling do lado do worker e, no fim, movi o enqueue para dentro da transação; mas aí você basicamente acaba recriando o que o Honker faz, só que com mais moving parts
      Bugs do tipo "a notificação foi, mas a linha ainda não foi commitada" costumam ser silenciosos e dependentes de timing, então são realmente horríveis de rastrear
    • O arquivo WAL continua lá e só é truncado, então isso acaba sendo capturado como update por si só
      Dito isso, ainda não há teste para essa parte, então preciso confirmar melhor. Bom ponto, vou verificar
  • Valeu
    Houve um grande aumento de apps pequenos baseados em SQLite, e a maioria deles precisa de fila e scheduler
    Já rodei algumas coisas por conta própria, mas sempre senti falta da elegância das soluções da família Postgres
    Pretendo testar isso logo

    • A expressão proliferação de pequenas coisas descreve perfeitamente o agrupamento que meus hábitos de side project acabaram criando
      Se topar com algum problema, seria ótimo se abrisse um PR ou issue no repositório
  • Dá vontade de usar kqueue/FSEvents aqui, mas, pelo que eu sabia, no Darwin ele descarta notificações do mesmo processo
    Se publisher e listener estiverem no mesmo processo, às vezes o listener nem acorda, e isso fica bem chato de rastrear. Por mais feio que o polling com stat pareça, no fim parece ser o que realmente funciona em qualquer lugar
    Também fiquei curioso se, quando o arquivo encolhe de novo no checkpoint do WAL, isso gera wakeup, ou se o poller filtra redução de tamanho

    • Este comentário está completamente errado
      Eventos VNODE do kqueue são entregues desde que o processo tenha permissão de acesso ao arquivo; não existe filtro que descarte por ser o mesmo processo
    • Isso realmente precisa ser testado
      Vou verificar e depois volto para contar
  • Muito legal. Fico curioso se, sob carga, o gargalo fica mais no throughput de escrita do SQLite ou na camada de notificação do WAL

    • O gargalo está mais na escrita e no fluxo de claim/ack
      Também varia bastante conforme o journal mode e o synchronous mode
      A notificação, seja no esquema antigo com stat(2) ou no novo baseado em PRAGMA, é muito barata. Em outro comentário também foi dito que stat(2) fica na casa de ~1 µs
  • Projeto muito bom. Eu também estou construindo algo que força o SQLite bem além do uso típico
    É animador ver mais gente explorando até onde o SQLite realmente consegue ir

  • Fico curioso se dá para integrar isso também quando se usa SQLAlchemy
    Do jeito que está agora, parece que ele quer criar a própria connection ao banco