Function tasks
Function tasks add named handler dispatch and stored results on top of Honker queues. The convenience API is not identical in every binding:
| Binding | Function-task API |
|---|---|
| Python | Decorators, in-process workers, worker CLI, and periodic-task decorator |
| Java | Explicit TaskRegistry, TaskHandle, runTasks, and result handles |
| Kotlin | Kotlin registry helpers over the Java task runtime |
| Node, Rust, Go, Ruby, Bun, Elixir, .NET, C++ | Queue and result primitives; no function-task registry/dispatcher |
All task-capable bindings store the same queue envelope:
{"__honker_task__":{"task":"send-email","args":["alice@example.com","Hi"],"kwargs":{}}}That makes the storage format portable. Run each queued task with a worker whose registry contains the named handler. Cross-language handler dispatch is not yet covered by Honker’s compatibility tests, so keep producers and task workers on the same task API unless you test that combination yourself.
Define, enqueue, and run
Section titled “Define, enqueue, and run”import timeimport honker
db = honker.open("app.db")q = db.queue("default")
@q.task(name="send-email", retries=3, timeout_s=30)def send_email(to: str, subject: str) -> dict: return {"sent_at": time.time()}
result = send_email("alice@example.com", "Hi")# Run `python -m honker worker myapp.tasks:db --queue=default`# in another process before waiting for the result.value = result.get(timeout=10) # Or: await result.aget(timeout=10)Calling send_email(...) enqueues the call and returns a TaskResult; it does not run the function in the caller.
import dev.honker.*;import java.time.Duration;
Database db = Honker.open("app.db");TaskRegistry registry = new TaskRegistry(db);TaskHandle sendEmail = registry.register( "send-email", "default", call -> "{\"sent\":true}");
TaskResult result = sendEmail.enqueue( "[\"alice@example.com\",\"Hi\"]", "{}");
try (TaskWorkerHandle worker = db.runTasks(registry)) { String value = result.waitFor( WaitOptions.timeout(Duration.ofSeconds(10)) );}Java deliberately uses explicit registration. The core does not scan annotations.
import dev.honker.kotlin.*import java.time.Durationimport kotlinx.coroutines.runBlocking
runBlocking { honker("app.db").use { db -> val registry = db.taskRegistry() val sendEmail = registry.registerJson( "send-email", "default" ) { call -> """{"sent":true}""" }
val result = sendEmail.enqueueJson( """["alice@example.com","Hi"]""" )
db.runTasks(registry).use { val value = result.await(Duration.ofSeconds(10)) } }}Kotlin adds builder, coroutine, and JSON helpers while using the Java registry and worker runtime.
Python worker options
Section titled “Python worker options”Python’s decorator registry is process-global. Import the module containing the decorated functions before starting a worker.
CLI (Python only)
Section titled “CLI (Python only)”python -m honker worker myapp.tasks:db --queue=default --concurrency=4The positional argument is an import path followed by :variable_name. Useful flags are --queue NAME (repeatable), --concurrency N, and --list.
In-process (Python only)
Section titled “In-process (Python only)”import asyncio
# Import the modules containing @q.task functions first.asyncio.run(db.run_workers(concurrency=4))db.run_workers runs until its stop_event is set or the task is cancelled. Java and Kotlin instead keep the TaskWorkerHandle returned by db.runTasks(...) open for the worker lifetime.
Names and safe renames
Section titled “Names and safe renames”Python defaults to f"{fn.__module__}.{fn.__qualname__}"; Java and Kotlin require an explicit name. A queued job continues to reference the old name after a code rename, so prefer a stable explicit name from day one:
@q.task(name="send-email")def send_email(...): ...Otherwise, keep a handler registered under the old name until old jobs have drained.
Defaults
Section titled “Defaults”Python @task and JVM registered tasks default to stored results with a one-hour TTL, three attempts, and a 60-second retry delay. Override them with Python decorator arguments or JVM TaskOptions. Python additionally supports timeout_s; JVM task handlers do not have an equivalent task-level timeout option. Python @periodic_task is the exception: result storage defaults off.
Arguments and return values must be JSON-compatible. Raw payloads on a task-worker queue and unknown task names go to the dead-letter table. Malformed JSON/envelopes are not a portable task-layer contract; validate custom producers before relying on their failure behavior.
Periodic tasks (Python only)
Section titled “Periodic tasks (Python only)”from honker import crontab, every_s
@q.periodic_task(crontab("0 3 * * *"), name="nightly-backup")def nightly_backup(): ...
@q.periodic_task(every_s(1), name="heartbeat")def heartbeat(): ...@periodic_task registers both the function handler and a scheduler entry with an empty argument list. Java and Kotlin can combine their explicit task registry with the scheduler primitives, but do not provide periodic-task sugar.
What’s stored where
Section titled “What’s stored where”_honker_live— pending and in-flight task envelopes._honker_dead— exhausted retries, raw task-queue payloads, unknown tasks, and explicit failures._honker_results— return values keyed by job id._honker_scheduler_tasks— scheduler registrations, including Python periodic tasks.
All tables share the same .db file as your business data.