Skip to content

Releases: 17tayyy/ardiq

v1.0.0

Choose a tag to compare

@github-actions github-actions released this 13 Aug 01:07

ArdiQ 1.0.0. The API is frozen.

Everything on the road to 1.0 is done, and the five papercuts from the first
real migration off Celery are all closed. From here, no name, argument or return
type in the public surface changes without a 2.0.

Two features close the list.

Unique tasks

Two "rebuild this shop's index" jobs for the same shop do the same work twice.
A task declared unique=True now takes its id from the call itself, so an
identical call that is already waiting or running is not enqueued again. You get
back the Job that is already doing the work:

@app.task(unique=True)
async def rebuild_index(shop_id: int): ...


first = await rebuild_index.enqueue(42)
second = await rebuild_index.enqueue(42)   # nothing new is enqueued
assert second.id == first.id

Identity is name plus arguments, so other shops are unaffected and keyword order
doesn't matter. The window lasts exactly as long as the task does, retries and
crashed-worker reclaim included, because the deduplication key is the task's own
data key, which the worker deletes when the task settles. Nothing new is stored
in Redis, and a worker that dies cannot leave a lock behind it.

The id is derived from the payload, so every process computes the same one:
duplicates collapse inside an enqueue_many batch, and a producer that never
imports the task module can deduplicate with ref("rebuild_index", unique=True).
Per call, .options(unique=True) turns it on and .options(unique=False) off.

Re-enqueuing a unique call after it has finished starts a fresh run and replaces
the previous result under that id.

--workers N

ardiq run app:app --workers 4 starts four worker processes against one queue
and supervises them: one banner, each child logging under its own worker_id,
and SIGINT/SIGTERM reaching all of them. If a worker exits non-zero the
supervisor stops the rest and exits with that code, so a crashed worker fails
the deployment instead of leaving it quietly running short-handed. Under
--burst they all exit when the queue is drained.

The children are separate processes rather than forks, which is what keeps the
3.12 shutdown mitigation working in each of them. --workers 1 stays
in-process, so the default path is unchanged.

Also in this release

  • Ardiq(redis_url="") raises a ValueError naming the parameter instead of
    failing inside the Redis client while the app object is still being built. It
    does not fall back to the default URL: an unset variable that quietly connects
    to localhost is worse than an error.
  • The classifier moves to Development Status :: 5 - Production/Stable.
  • 179 tests, 97% coverage of the Python layer.

Upgrading

Nothing to do. 0.6.0 code runs unchanged, the wire format is untouched, and a
1.0.0 producer works with a 0.6.0 worker and the other way round. The only
behaviour that changed is an empty redis_url, which used to be an unreadable
error a moment later.

Install: pip install ardiq or uv add ardiq · docs

v0.6.0

Choose a tag to compare

@github-actions github-actions released this 11 Aug 02:52

Full Changelog: v0.5.0...v0.6.0

v0.5.0

Choose a tag to compare

@github-actions github-actions released this 06 Aug 23:40

A lighter worker, a lane that can't swallow work, and enqueue calls your type checker can read.

The worker lost 13% of its memory

A worker started with ardiq run was carrying about 5 MB it never used: Typer, to parse three flags, and Rich, to draw a box once at startup. In a library whose whole argument is a small footprint, the welcome banner cost more memory than the Rust core saves.

The banner is now drawn by hand and the CLI runs on argparse, so neither library is imported at all. Measured on the benchmark suite, same machine, same run:

RSS Throughput
0.4.0 38.08 MB 375.3 tasks/s
0.5.0 33.12 MB 374.7 tasks/s

Throughput is unchanged — the difference is run-to-run noise. Nothing about the banner changed on screen except that it now has room to breathe.

The cli extra no longer installs anything. With nothing heavy left to isolate, pip install ardiq gives you the library and the ardiq command, with msgpack as the only runtime dependency. The extra stays declared but empty, so pip install 'ardiq[cli]' in an existing Dockerfile keeps resolving.

Breaking: a priority must name a lane a worker reads

@app.task(priority="urgent") on an app configured with ["low", "default", "high"] used to be accepted. The task went into a stream no consumer reads, and then both things an operator would check lied, in opposite directions at once:

app.queue_size() : 0          <- the queue looks empty
job.status()     : queued     <- the job says it is waiting

Forever, with no TTL to clean it up. It now raises, naming the lanes that exist:

app = Ardiq(priorities=["low", "default", "high"])

@app.task(priority="urgent")
async def charge(...): ...
# ValueError: priority 'urgent' is not one of ['low', 'default', 'high']
#             — no worker reads that lane

The check runs at registration and on every dispatch path — task.enqueue, options(priority=), app.send and ref().enqueue all funnel through one place.

What this breaks: a producer that declares fewer lanes than its worker. A web app on Ardiq(priorities=["default"]) could enqueue to "high" yesterday and cannot today. That is the exact case the check exists for — it was writing to a stream nothing consumed — so declare the full list on both sides.

Enqueue arguments are type-checked

Task is now generic over the function you decorated, so .enqueue(...) takes the arguments the task declares:

@app.task()
async def charge(user_id: int, amount: float) -> str: ...

await charge.enqueue(1, 9.99)       # ok
await charge.enqueue("1", 9.99)     # error: str is not int
await charge.enqueue(1)             # error: missing 'amount'

.options(...) carries the signature through, so charge.options(delay_ms=5000).enqueue(...) is checked too. Tasks reached by name are the exception: app.send and app.ref have no local function to read a signature from, so they stay Task[..., Any] and are unchecked — pretending otherwise would be worse than being honest about it.

Upgrading

$ pip install --upgrade ardiq

If you pass priority= anywhere, check that every value appears in the priorities list of each process that enqueues. Everything else is backwards compatible.

v0.4.0

Choose a tag to compare

@github-actions github-actions released this 06 Aug 01:23

Everything in this release comes out of migrating a production Celery app to ArdiQ. Four things got in the way; two of them were bad enough to justify breaking the API.

Breaking changes

Two tasks can no longer share a name. The registry was a plain dict keyed on fn.__name__, so a second registration silently won and the first task stopped existing — no error, no log. You found out from the work that never happened. In the migration this hit forward_message_to_backend, which existed in two channel modules; one of them would have gone dead in production. Registering a name that is already taken now raises and names the module that owns it. @task and @cron share the guard, so a cron can't shadow a task either.

A task with no priority now lands in the middle lane, not the lowest. The lanes are stored reversed internally so the drain loop reads highest-first, and the default fell out of that as the bottom lane — meaning with the documented ["low", "default", "high"], the lane named default received nothing. Forgetting the argument quietly demoted your work, and demoted work still completes, so nothing ever told you. Set Ardiq(default_priority=...) to make the choice explicit.

New

Typed errors. Every failure from the Rust core used to surface as a bare RuntimeError, so "Redis is down" was indistinguishable from a bug in your callback. Redis I/O failures now raise BrokerError, and everything else raises ArdiqError — including a malformed redis_url, which is a config mistake rather than an outage. Both subclass RuntimeError, so existing handlers keep working.

from ardiq import BrokerError

try:
    await send_email.enqueue(to="a@b.com")
except BrokerError:
    ...  # the broker, not your code

current_task() returns the running task's id, name and attempt from inside the body, the way Celery's self.request.id did. It returns None outside a worker, like asyncio.current_task(), so a shared logging helper can call it anywhere. It's a ContextVar, so it reaches sync tasks in their thread and anything they call without threading an argument through.

from ardiq import current_task

@app.task()
async def send_email(to: str) -> None:
    log.info("sending", extra={"task_id": current_task().id})

Also

  • send() and ref() drop a task's declared priority — priority selects the stream, so the producer settles it, and the producer has no registry. Now documented rather than silently surprising.
  • PyPI and the README now link to the documentation at ardiq.bytay.dev.
  • CONTRIBUTING.md and issue forms, for anyone who wants to help.

Upgrading

$ pip install --upgrade ardiq

Start your worker once after upgrading. If two tasks share a name, it will now tell you instead of quietly dropping one — that error is the point of the release. If you relied on unprioritised tasks landing in the lowest lane, pass default_priority explicitly.

v0.3.0

Choose a tag to compare

@github-actions github-actions released this 05 Aug 18:40

Full Changelog: v0.2.2...v0.3.0

v0.2.2

Choose a tag to compare

@github-actions github-actions released this 16 Jul 00:33

Full Changelog: v0.2.1...v0.2.2

v0.2.1

Choose a tag to compare

@github-actions github-actions released this 28 Jun 18:07

Full Changelog: v0.2.0...v0.2.1

v0.2.0

Choose a tag to compare

@github-actions github-actions released this 13 Jun 14:28

ArdiQ 0.2.0 — recurring tasks, a lighter install, and full typing.

Upgrade note

pip install ardiq no longer includes the ardiq command. The CLI now lives in an optional extra:

pip install 'ardiq[cli]'

The base package is the library — a single runtime dependency (msgpack) — enough to define tasks, enqueue them, and run a worker from your own code (await app.run()). Only the ardiq run worker command needs the extra.

Highlights

  • Recurring tasks@app.cron("*/5 * * * *") (5-field cron, UTC) or @app.cron(every=30) (interval). Each occurrence is an ordinary task with its own result, status, retries and timeout — built on the existing delayed queue, with no new dependencies.
  • Lighter installtyper (and its rich tail) moved to the [cli] extra; the library now has a single runtime dependency.
  • Fully typed — ships the py.typed marker (PEP 561), so your type-checker now sees ArdiQ's annotations; the public API is fully documented.
  • Modular package layout — internal refactor into focused modules (no API changes).

Docs

ardiq.bytay.dev — see the new Recurring tasks guide.

Full Changelog: v0.1.1...v0.2.0

v0.1.1

Choose a tag to compare

@17tayyy 17tayyy released this 04 Jun 23:15

Full Changelog: v0.1.0...v0.1.1