gisila_queue 0.3.0
gisila_queue: ^0.3.0 copied to clipboard
A Celery-like distributed task queue for Dart: register named tasks, enqueue them from anywhere with .delay()/.applyAsync(), and process them with worker pools backed by a pluggable broker (Redis or i [...]
Changelog #
0.3.0 — 2026-10-28 (BREAKING) #
- Apps sharing a Redis or RabbitMQ no longer interfere by accident
(Q-12). Every key, queue and channel was already under a
prefix, but each component took its own and all defaulted togq, so two apps on one Redis database took each other's tasks and only one of their beats ran.GisilaQueue.redis(namespace: 'myapp', ...)builds the broker, result backend, control channel, event sink (events: true), beat lock and beat state store with one namespace;Beat(app)uses that lock and state store when given none.- Breaking:
GisilaQueue(...)andBeat(...)throwArgumentErrorwhen their components use different prefixes (they used to split the app silently). NewNamespacedinterface, implemented by every Redis and RabbitMQ component;app.namespaceandbeat.namespacereport the shared prefix. - A worker or beat on the default
gqnamespace logs a warning. - Behaviour change: a worker that receives a task name it doesn't know
puts the message back with a delay (
unknownTaskBackoff, 1 s doubling up to a minute) and dead-letters it only aftermaxRequeues(10), so the worker that has the task can still run it. It used to fail and dead-letter it at once, also writing a failure into the shared result backend. - The worker, beat, inspect and Rose CLIs take
--namespace(--prefixstill works).
0.2.0 — 2026-10-28 (BREAKING) #
Schedule.cron('*/15 9-17 * * 1-5'): 5-field cron schedules for Beat.send(..., dedupeKey:, dedupeFor:): a repeat send with the same key within the window returns the first task's id instead of enqueuing again (needs a result backend; atomic on Redis).Worker.stop(timeout:); the worker CLI takes--drain-timeout(30 s).RedisBrokerpromotes delayed messages and reaps stale ones at most once permaintenanceIntervalper process (1 s, or half the visibility timeout), instead of on every slot's every poll.- RabbitMQ delays no longer block each other. Delayed messages wait in
<prefix>.delay.<ms>buckets (100 ms × 2ⁿ, each with one queue-level TTL) instead of one queue with per-message TTLs, where RabbitMQ only expired the head: a 1-hour job used to hold every shorter one behind it. The consumer moves a message through further buckets until it is due and holds the last < 100 ms itself, so nothing is handed out early. The old<prefix>.scheduledqueue is still declared so its messages drain. - Beat failover. A standby that becomes leader reloads every entry's
persisted last-run time first, instead of using what it read while it
stood by (it refired whatever the old leader had just run). Ticks no
longer overlap.
RedisBeatLockrenews and releases with Lua compare-and-expire / compare-and-delete, so it never extends or deletes a lock another instance took over. - Dead-letter queue. Messages that fail for good (after their last
retry, unknown task names, more than
maxRequeuesRejectTask(requeue: true)s, or more thanmaxRedeliveriesunacked redeliveries) are put ondead_letterwithTaskMessage.deadLetter={reason, queue, at}.app.replayDeadLetters()sends them back.Worker/app.workertakedeadLetterQueue(null turns it off),maxRequeues(10) andmaxRedeliveries(5). Messages carry arequeuescounter. - Hard time limits hold the slot. A handler past
timeLimitis still recorded as timed out immediately, but its concurrency slot stays busy until the handler returns (with a warning everytimeLimit), so abandoned handlers no longer pile up beyondconcurrency.ctx.isAbandonedtells a handler it was abandoned.stop()stops waiting for them. - Redis integration tests need
GISILA_TEST_REDIS_PORT; they no longer default to a local Redis on 6379. - Redis password, ACL user, TLS and database. Every Redis component
(
RedisBroker,RedisResultBackend,RedisEventSink/Source,RedisControlChannel,RedisBeatLock,RedisBeatStateStore) takesoptions: RedisOptions(...), sent asAUTH/SELECTon each connection (TLS with an optionalSecurityContext).RedisOptions.parseUrlreadsredis:///rediss://URLs; the worker, beat, inspect and Rose CLIs take--redis-urlor theREDIS_URLenvironment variable. - Rose dashboard is no longer open by default (breaking). It binds
127.0.0.1unless told otherwise (it was0.0.0.0), andstart()throws when it would serve fire/retry/revoke/pause on a non-loopback address withoutbasicAuth(allowUnauthenticated: true/--allow-unauthenticatedto override). It sends no CORS headers (it sentAccess-Control-Allow-Origin: *) and refuses POSTs that aren'tapplication/jsonwith415, so other sites can't drive it through a signed-in browser. Basic Auth is compared in constant time. - Long tasks no longer run twice. Workers now
toucheach running message at a third of the broker'svisibilityTimeout, so the Redis reaper only redelivers messages whose worker has actually gone away. NewBroker.visibilityTimeout/Broker.touch(no-ops by default). - Per-delivery acks.
reservetags each message with a freshTaskMessage.deliveryTag;ackon Redis and RabbitMQ acts on that delivery only, so two copies of one task id (a redelivery, or a retry re-enqueued before the original was acked) can't clear each other. - No lost Redis messages. Reserve, ETA promotion and redelivery are each
a single Lua script, closing the crash windows between
BRPOPand the ledger write and betweenZREMandLPUSH. Idle workers nowBLPOPper-queue notify lists (<prefix>:notify:<queue>) instead of the queues themselves; messages pushed by older producers are still picked up, after the reserve timeout. Undecodable payloads go to<prefix>:dead(RedisBroker.deadLetterLimit,deadLetterCount()) instead of being dropped. The broker targets a single Redis, not Redis Cluster. - Worker slots survive errors. A failure outside the task handler (result backend, event sink, broker ack) is logged and the slot keeps consuming; the message stays unacked for redelivery.
0.1.1 #
RabbitBroker— RabbitMQ (AMQP) broker viadart_amqp. Durable queues, manual ack for at-least-once delivery, and ETA/countdown via per-message TTL- dead-letter exchange (stock RabbitMQ, no delayed-message plugin). Tagged
integration tests (
rabbitmq) and--broker=rabbitmqon the throughput benchmark.
- dead-letter exchange (stock RabbitMQ, no delayed-message plugin). Tagged
integration tests (
- GisilaRose dashboard polish: light-theme UI with timestamps, task detail
drawer, per-worker drill-down, Flower-style fire/retry/revoke and queue
pause/resume, optional HTTP Basic Auth,
GET /api/workers/<id>,worker=task filter, andactive_task_idson workers. - Fix worker
timeLimitabandon path: avoidFuture.catchError((_) => null), which typed handlers (e.g.Future<bool>) reject as an invalid null error replacement and crash the worker.
0.1.0 #
Initial release.
GisilaQueueapp with a named-task registry and typedTaskRefproducers (.delay()/.applyAsync()).- Pluggable
Broker:InMemoryBroker(single-isolate / tests) andRedisBroker(distributed, list-based queues + sorted-set ETA scheduling). - Pluggable
ResultBackend:InMemoryResultBackendandRedisResultBackend(TTL'd result keys); backend is optional for fire-and-forget use. Workerwith cooperative async concurrency, named-queue priority routing, retries with exponential backoff + jitter,ctx.retry()/RejectTask, and success/failure/retry observer hooks.AsyncResultfor polling task state and awaiting return values.Beatscheduler for periodic tasks (Schedule.every,Schedule.dailyAt).