Barriers¶
native.barrier waits for a known set of acknowledgements, then continues once. Use it when you
send work out (typically documents put into a bucket) and the other end answers later, one ack per
item, over whatever channel it likes.
A barrier is a work list: a set of member names, usually bucket keys, and one outcome per member. Nothing waits and nothing polls. Each ack is an ordinary event that reaches a script; the script records it, and the call that resolves the last member sends the continuation.
sequenceDiagram
participant S as sender script
participant B as barrier (native.db)
participant P as partner
participant A as ack script
participant F as continuation
S->>B: create(members = keys)
S->>P: bucket.In.put(key) x N
P-->>A: ack (S3 object, queue message, callback)
A->>B: ack(id, key, outcome)
Note over A,B: the last member resolved
A->>F: queue message { barrier = id }
F->>B: hydrate, read outcomes
Script API¶
-- send
local id = native.barrier.create({
id = batch, -- optional; created again with the same id does nothing
members = names, -- what the acks will name, e.g. "a.xml"
continue_with = "finalize", -- script that runs when every member is resolved
state = { ref = ev.ref }, -- handed to the continuation
deadline_s = 3600, -- optional; only feeds `status().overdue`
})
for _, name in names do native.bucket.In.put(batch .. "/" .. name, docs[name]) end
-- ack: runs for every acknowledgement, whatever channel it came by
local r = native.barrier.ack(batch, name, { ok = true, result = { id = "r-17" } })
-- finalize
function on_queue(ctx, ev)
local b = native.barrier.hydrate(ev) -- nil if not complete, or expired / cleaned up
if not b then return end
for member, outcome in b:results() do
if not outcome.ok then native.log("warn", member .. ": " .. tostring(outcome.error)) end
end
end
| Function | |
|---|---|
create(opts) |
registers the members and returns the id. members is required, at most 10 000, each at most 200 bytes, no duplicates. id is generated when absent. continue_with needs native.queue ("queue": true). state is any JSON, at most 32 KB. Calling it again with the id of a barrier that is already complete does nothing; with the id of one cut short by a crash it finishes the job |
ack(id, member, outcome?) |
records the outcome, retires the member and, if none are left, sends the continuation. Returns { duplicate, complete, dispatched } |
pending(id, limit?) |
the members still waiting, in name order (at most 100 per call) |
status(id) |
{ id, total, ready, complete, dispatched, overdue, due_s } |
hydrate(event) |
in the continuation: the barrier named by event.barrier, or nil |
BarrierResult from hydrate:
id, total, state |
the barrier id, the number of members, the value passed to create |
results() |
iterator of member, outcome over every member, in name order. Pages through the table 100 at a time, so it works for lists far larger than one value |
failures() |
array of { member, error, result } for the members whose outcome is ok = false |
Outcomes¶
An ack carries a small outcome: { ok = true | false, error = string?, result = any? }, at most
8 KB (put anything larger in a bucket and send its key as the result). ok defaults to true.
A failed ack still resolves its member. Otherwise one rejected document would hold the whole
batch until its deadline. The barrier completes and the continuation decides what a failure means
(b:failures()).
The first ack for a member wins. A later one, a duplicate delivery or a partner's retry, is
reported as duplicate = true and changes nothing.
Getting acks in¶
The barrier does not care how an ack arrives. Everything that can reach a script can ack: an S3
notification for an object the partner wrote, a message the partner put on the queue, a
callback. Route the event to one ack script and call native.barrier.ack there.
The ack has to say which barrier and which member. The simplest convention is to put both in the keys you control:
In/<batch>/<doc>.xml what you send; the member is "<doc>.xml"
Acks/<batch>/<doc>.json what the partner writes back
-- router
local function on_route(_ctx, event)
if event.bucket == "Acks" then return "ack" end
return "default"
end
-- script "ack"
function on_queue(ctx, ev)
local batch, doc = string.match(ev.key, "^([^/]+)/(.+)%.json$")
if not batch then return end
local body = native.json.decode(native.bucket.Acks.get(ev.key) or "{}")
native.barrier.ack(batch, doc .. ".xml", { ok = body.status == "ok", error = body.error })
end
Two batches that both contain invoice.xml do not collide, because the barrier id is part of the
address. If the partner can echo back nothing but the document key, store a member -> batch row in
native.db when you send and look it up in the ack script.
Guarantees¶
- No lost ack.
ackis a sequence of idempotent steps (record the outcome if absent, delete the member's pending row, look for any pending row left) and there is no counter. A process that dies half way is repaired by the redelivered ack, which repeats the steps. There is no window in which the barrier can end up one short. - A continuation per completion, at least once. The call that finds nothing pending claims the dispatch with a 60 second lease, sends the message, then marks it sent. Two calls racing for the last member send once. A call that dies after sending but before marking leaves a lease that lapses, and a later ack (a redelivery) sends again, so write the continuation to be safe to repeat.
- A busy claim is an error. While another call holds the lease,
ackraisesbarrier completion is being dispatched by another call; retry. Let it fail: the queue redelivers and the next attempt sees the send finished. - Unknown members are refused. Acking a name that is not on the list raises, so a stray event cannot complete a barrier.
- Tenant-private. The rows live in a partition that
native.dbcannot address, so scripts cannot read or forge them except throughnative.barrier.
Cost and limits¶
Every backend call is one max_data_ops (limits).
| Call | Data ops |
|---|---|
create with N members |
N + 3 |
ack |
5; 8 for the call that sends the continuation; 4 for a duplicate |
results() |
1 per 100 members |
pending, hydrate |
2 |
status |
3 |
create is one invocation, so the work list must fit in the tenant's max_data_ops
(default 1000, ceiling 10 000). Raise the tenant's limit for large batches.
Rows expire after a week like all native.db items. ack stops accepting an hour
before that, with barrier expired, so an expired pending row can never make a barrier look
finished. hydrate returns nil after expiry.
Barrier rows are not deleted when the continuation finishes; they expire. A barrier id can be reused only after that.
Deployment¶
Needs the db section of the global config, and for continue_with
the tenant's "queue": true and a queue.url. No other IAM: the same table actions as
native.db, including Query.
Without a queue, a barrier still works as a plain work list: ack tells you when complete, and
you act on it yourself. Nothing guarantees only one caller sees complete = true then, so claim the
work with native.db.put_if_absent.
Not covered¶
- A member that never acks. Nothing runs.
deadline_smakesstatus(id).overduetrue, but something has to look: schedule a script that readsstatus, or send a delayed queue message fromcreate's caller (delay_sup to 15 minutes, re-sending itself). A timer trigger is on the roadmap. - Growing a barrier. The members are fixed at
create. - Recovering a lost notification. If a partner's ack object exists but its event was lost, nothing acks the member. List the bucket and ack the missing keys by hand.