Task queues and schedules for MoonBit: workers, retries, periodic beats, and the chain, group and chord canvas. Brokers and result stores are separate modules.
#task.def(name="orders.place", queue="orders", retries="1")
pub fn place(order : Order, ctx : @moontask.Context) -> String raise {
...
}let tasks = register_tasks(app)
let receipt = tasks.place.send({ id: "a1", cups: 2 }).get(timeout=5000L)rule(name: "tasks", command: "moonx moonbitstack/moontask/cmd/tasks -C \"$mod_dir\" \"$input\" -o \"$output\"")dev_build(rule: "tasks", input: "jobs.mbt", output: "jobs.g.mbt")
formatter(ignore: [ "jobs.g.mbt" ])async fn first_run() -> Unit {
let now = fn() { @env.now().reinterpret_as_int64() }
let broker = @moontask.MemoryBroker::new(clock=now)
let app = @moontask.App::new(broker, backend=broker, counter=broker, clock=now)
let greet = app.task("greet", fn(name : String, ctx : @moontask.Context) {
"hello \{name}, attempt \{ctx.attempt}"
})
let worker = app.worker(concurrency=4)
@async.with_task_group(async fn(group) {
group.spawn_bg(async fn() { worker.run() }, no_wait=true)
let handle = greet.send("ada")
println(handle.get(timeout=5000L))
worker.stop()
})
}| Package | What it is |
|---|---|
| moontask/spec | The contracts — Broker, Backend, Counter, Inspector — and the message, outcome and state they carry |
| moontask | App, Task, Handle, Worker, Beat, the canvas, the executor a graph runs on, and an in-memory broker that is also a store and a counter |
| moontask/schedule | A job on a rhythm inside one process, with no broker behind it |
| moontask/flow | Workflows: steps that are tasks, ordered by a state graph, each run stamped with the instant it belongs to |
| moontask/wait | The one place a wait is written, so the WebAssembly targets are handled once |
| Module | What it is |
|---|---|
| moontask-redis | A broker and result store on Redis, with per-consumer leases and recovery of a worker that stopped — or on Celery's own key layout, so one queue can be shared with a Celery worker |
| moontask-sql | A result store and a workflow run log on SQL, over any moondb async driver, so the same code runs on SQLite, MySQL and PostgreSQL |
| moontask-admin | A management interface as an ASGI application: a page, reports, sending, revoking, and a live stream of counts |
| Field | Meaning | Default |
|---|---|---|
| queue | Where the message goes | "default" |
| retries | How many times a failed run is tried again | 0 |
| backoff | The wait between attempts | exponential, 1s base, 10min cap, jittered |
| timeout | How long one run may take | no limit |
| acks_late | Confirm after the run instead of on receipt | false |
| ignore_result | Keep no result | false |
| rate | Runs per window, per worker | no limit |
async fn canvas() -> Unit {
let now = fn() { 1_700_000_000_000L }
let broker = @moontask.MemoryBroker::new(clock=now)
let app = @moontask.App::new(broker, backend=broker, counter=broker, clock=now)
let step = app.task("step", fn(value : Int32, _) { value * 10 })
let total = app.task("total", fn(values : Array[Int32], _) {
let mut sum = 0
for value in values {
sum = sum + value
}
sum
})
let chained = @moontask.chain([step.sig(1), step.sig(2), step.sig(3)]).send(app)
let grouped = @moontask.group([step.sig(4), step.sig(5)]).send(app)
let joined = @moontask.chord(
[step.sig(1), step.sig(2), step.sig(3)],
total.sig([]),
).send(app)
let mapped = step.map([6, 7]).send(app)
let starred = total.starmap([[1, 2], [3, 4]]).send(app)
let chunked = step.chunks([[1], [2], [3]], size=2).send(app)
println(
"\{chained.ids().length()} \{grouped.ids().length()} \{joined.ids().length()}",
)
println(
"\{mapped.ids().length()} \{starred.ids().length()} \{chunked.ids().length()}",
)
}| Call | What it publishes | What the handle answers |
|---|---|---|
| task.map(items) | one message per item, the item being that run's whole argument | the results in the order given |
| task.starmap(tuples) | one message per tuple, the tuple being that run's argument list | the results in the order given |
| task.chunks(tuples, size=2) | one message per slice, each running its tuples in order in the worker that took it | one list per slice, the last slice holding what is left |
fn rhythm() -> Unit {
let now = fn() { 1_700_000_000_000L }
let broker = @moontask.MemoryBroker::new(clock=now)
let app = @moontask.App::new(broker, backend=broker, counter=broker, clock=now)
let tick = app.task("tick", fn(_ : Unit, _) { () })
let nightly = @cron.parse("0 3 * * *") catch { _ => abort("valid expression") }
let beat = app.beat()
beat.add("tick", tick, (), @moontask.Schedule::Every(interval=1000L))
beat.add("nightly", tick, (), @moontask.Schedule::Cron(expr=nightly))
println(beat.pending().length())
}async fn workflow() -> Unit {
let now = fn() { 1_700_000_000_000L }
let broker = @moontask.MemoryBroker::new(clock=now)
let app = @moontask.App::new(broker, backend=broker, counter=broker, clock=now)
let extract = app.task("extract", fn(_ : Unit, _) -> Json { { "rows": 3 } })
let load = app.task("load", fn(args : Json, _) -> Json {
{ "written": 3, "from": args }
})
let flow = @flow.Flow::new(
"etl",
schedule=@moontask.Schedule::Every(interval=3_600_000L),
)
flow.step("extract", extract)
flow.step("load", load, deps=["extract"])
let worker = app.worker()
@async.with_task_group(async fn(group) {
group.spawn_bg(async fn() { worker.run() }, no_wait=true)
let run = flow.run(app, 1_700_000_000_000L)
println(run.to_string())
println(run.output_of("load").unwrap().stringify())
worker.stop()
})
}import {
"moonbitstack/moontask",
"moonbitstack/moontask/flow",
"moonbitstack/moontask-sql/sql",
"moonbitstack/moonpostgres",
}
async fn sql_backend {
let conn = @moonpostgres.PgConn::connect("127.0.0.1", 5432, "postgres", "postgres", "test")
let store = @sql.Store::new(conn, clock=fn() { @env.now().reinterpret_as_int64() })
store.ensure()
let app = @moontask.App::new(broker, backend=store, clock=fn() { @env.now().reinterpret_as_int64() })
let flow = @flow.Flow::new("etl")
flow.record(store)
}let db = @sqlite.SqliteDriver::open("queue.db")
let store = @sql.Store::inline(db, clock=fn() { @env.now().reinterpret_as_int64() })
store.ensure()let conn = @socket.Connection::connect(@moonredis.ClientOptions::at("127.0.0.1", 6379))
let broker = @redis.Broker::new(conn, clock=now, consumer="worker-7")
let app = @moontask.App::new(broker, backend=broker.store(), counter=broker.store(), clock=now)let broker = @redis.Broker::celery(conn, clock=now, consumer="worker-7")| the queue | the list named after it, split across Celery's priority steps |
| the payload | Celery's envelope: task and id in headers, arguments in the body |
| what a consumer holds | Celery's unacked hash and its index, so either side can hand back a claim the other left |
| a chain | each message carries the rest of its chain; a link that is not immutable is called with the previous link's result prepended |
| a chord | the parts' results land in Celery's join keys as they settle, so the last part — on either side — closes it |
| an eta | the message sits on the queue and the consumer holds it, which is what Celery does |
pub(open) trait Broker {
async fn publish(Self, queue : String, message : @spec.Message) -> Unit raise @spec.Refused
async fn consume(Self, queues : ArrayView[String], prefetch~ : Int32) -> @spec.Delivery raise @spec.Refused
async fn ack(Self, delivery : @spec.Delivery) -> Unit raise @spec.Refused
async fn reject(Self, delivery : @spec.Delivery, requeue~ : Bool) -> Unit raise @spec.Refused
}moon run src/examples/01-queue
moon run src/examples/02-canvas
moon run src/examples/03-retry
moon run src/examples/04-flow
moon run src/examples/05-floor
moon run src/examples/06-annotated
moon run src/examples/07-fanouttype Limiterimpl Backend for MemoryBrokerimpl Broker for MemoryBrokerasync fn consume(self : MemoryBroker, queues : ArrayView[String], prefetch~ : Int) -> Delivery raise Refusedimpl Counter for MemoryBrokerasync fn settle(self : MemoryBroker, chord : Chord, id : String, outcome : Outcome) -> ChordState raise Refusedimpl Inspector for MemoryBrokerasync fn MemoryBroker::consume(self : MemoryBroker, queues : ArrayView[String], prefetch~ : Int) -> Delivery raise Refusedasync fn MemoryBroker::peek(self : MemoryBroker, queue : String, limit~ : Int) -> Array[Message] raise Refusedasync fn MemoryBroker::publish(self : MemoryBroker, queue : String, message : Message) -> Unit raise Refusedasync fn MemoryBroker::reject(self : MemoryBroker, delivery : Delivery, requeue~ : Bool) -> Unit raise Refusedasync fn MemoryBroker::settle(self : MemoryBroker, chord : Chord, id : String, outcome : Outcome) -> ChordState raise Refusedasync fn MemoryBroker::store(self : MemoryBroker, id : String, outcome : Outcome) -> Unit raise Refusedtype Registeredlet chunk_name : StringInstall
Download zipTask queues and schedules for MoonBit: workers, retries, periodic beats, and the chain, group and chord canvas. Brokers and result stores are separate modules.