MoonMQ
Documentation menu

Client API

src/client is the reference Lua client. Runnable examples: src/examples/tcp_client.lua, src/examples/consumer_group.lua.

local Client = require("src.client")
local c = assert(Client.new{ host = "127.0.0.1", port = 9092,
                             username = "admin", password = "admin",
                             idempotent = true })
 
local ack   = assert(c:produce("orders", "key-1", "payload"))  -- {partition, offset, seq}
local acks, berr = c:produce_batch("orders", {               -- one frame, one fsync/partition
    { key = "k1", value = "v1" },
    { key = "k2", value = "v2" },
})                                                             -- berr set: only #acks landed
local res   = assert(c:join_group("billing", { "orders" }))    -- {member_id, assignment}
local ends  = assert(c:list_offsets("orders"))                 -- per partition {earliest, latest, …}

produce takes optional headers as a 4th argument, and produce_batch records take a headers field. Fetched records carry headers. See headers.md.

For a replicated cluster pass hosts = { … } instead of host/port; the client follows leader= hints across the list. For TLS and SCRAM options see security.md.

Offsets and lag

list_offsets reports where each partition starts and ends, which is what makes lag (latest - committed) computable and what lets a consumer seek to either end. Under read_committed measure against lso instead — that is the offset the broker will actually deliver up to.

Seeking by time

To seek by wall-clock rather than by offset, offsets_for_times resolves the earliest offset per partition whose record timestamp is at or after a given millisecond timestamp — Kafka's offsetForTimes. It reads the sparse .timeindex every partition already writes, so it is a short binary search plus a bounded scan rather than a walk of the log:

local at = assert(c:offsets_for_times("orders", 1700000000000))
-- { { partition = 1, offset = 8134, found = true }, ... }

found = false means nothing at or after that time exists yet; offset is then the partition's latest, i.e. where the first such record will land, so "seek to T and follow" works on a partition that has not reached T yet.

Admin

Topics can be described, reconfigured and deleted at runtime, and consumer groups inspected:

assert(c:alter_topic_config("orders", { retention = 86400 }))  -- seconds
assert(c:create_partitions("orders", 8))                       -- grow to 8 in total
local d = assert(c:describe_group("billing"))
-- d.members[i].assignment, d.offsets[i] = { topic, partition, offset }

Pairing describe_group's committed offsets with list_offsets is what makes lag a broker-side fact rather than something a sidecar has to reconstruct. A group holding committed offsets but no live members — an abandoned or between-deploys consumer — shows up in list_groups with state empty, which is usually the thing you went looking for.

create_partitions takes the new total, not the number to add, and only grows. Existing records stay where they are, and consumer groups rebalance right away so the new partitions get a reader. A key's partition is hash(key) % count, so after growing, new records for a key may land on a different partition than its older ones. Ordering per key holds only from that point on, as in Kafka.

Reference

Every method returns nil, err on failure. produce_batch is the one exception: on a partial failure it returns the acks that did land and the error, so check the second value rather than wrapping it in assert.

Client.new options

Option Default Meaning
host, port 127.0.0.1, 9092 Broker to connect to
hosts — List of "host:port"; replaces host/port and follows leader= hints across it
username, password — Credentials; omit for an open broker
mechanism plain plain, scram-sha-256 or external (certificate login) — see security.md
tls — true or a table (cafile, certfile, keyfile, server_name, insecure)
channel_binding on with TLS false disables it; "tls-server-end-point" pins the older binding type
timeout 30 Socket timeout in seconds
compression none none, gzip or snappy for produced records
isolation read_uncommitted read_committed hides aborted and open transactions — see transactions.md
idempotent false Ask for a producer id so retries are deduplicated
producer_name — Durable producer identity; required for transactions
client_name, client_version — Reported to the broker with IDENTIFY_CLIENT
reactor — Run on a MoonMQ reactor instead of blocking sockets

Methods

Method Returns
produce(topic, key, value[, headers]) {partition, offset, seq}
produce_batch(topic, records) list of acks, plus an error on partial failure
produce_at_seq(topic, key, value, seq[, headers]) ack; idempotent clients only, for retrying a known sequence
fetch(topic[, group[, max_records[, batched]]]) list of records (max_records defaults to 100)
subscribe(topic[, group]) · next_record([timeout]) true · the next pushed record, or nil, "timeout"
commit(topic, partition, offset) true
nack(topic, partition, offset[, reason]) NACK result — see dlq.md
create_topic(name, partitions) · delete_topic(name) true
list_topics() · describe_topic(name) topic list · topic description
alter_topic_config(name, config) · create_partitions(name, total) true
list_offsets(topic) · offsets_for_times(topic, ms) per-partition offsets
join_group(group, topics[, member_id]) {member_id, assignment}; remembered for the calls below
group_heartbeat([group, member_id]) · leave_group([group, member_id]) true
list_groups() · describe_group(group) · delete_group(group) group list · description · true
begin_transaction() · commit_transaction() · abort_transaction() true; needs producer_name
send_offsets_to_transaction(group, {{topic, partition, offset}, …}) true
close() —

Client.decode_dlq_value(value) unpacks the envelope of a dead-lettered record.

Edit docs/client.md on GitHub