Skip to content

Process Groups

Join processes into named groups and broadcast to every member across the cluster. Modeled on Erlang/OTP pg: groups are dynamic, a process can belong to many groups, and membership is tracked cluster-wide and is eventually consistent.

For the scope entry kind and its configuration, see Process Groups. For the broader clustering model, see the Cluster Guide.

local pg = require("pg")

A process group lives inside a scope — a pg.scope registry entry. Open it to get an instance you operate on:

local group, err = pg.open("app:pg")
if err then
return nil, err
end
ParameterTypeDescription
idstringScope entry ID (format: "namespace:name")

Returns: pg.Instance, error

Permission: pg.open on the scope id

The instance is released automatically when the process exits; call release() to free it earlier. All other operations are methods on the instance, called with :.

local ok, err = group:join("workers") -- single group
local ok, err = group:join({"workers", "all"}) -- batch
local ok, err = group:leave("workers")
ParameterTypeDescription
groupstring | string[]Group name, or a list of names for a batch operation

Returns: boolean, error

A process may join the same group more than once; it must leave the same number of times to fully depart (multi-join semantics). leave is best-effort across a batch and returns an error only when the process was a member of none of the named groups.

Permissions: pg.join / pg.leave on each group name

local members, err = group:get_members("workers") -- all nodes
local local_members, err = group:get_local_members("workers") -- this node only
ParameterTypeDescription
groupstringGroup name

Returns: string[], error — an array of PID strings (empty for an unknown group)

Permissions: pg.get_members / pg.get_local_members on the group name

local groups, err = group:which_groups() -- all groups in the cluster
local local_groups, err = group:which_local_groups() -- groups with a local member

Returns: string[], error — group names that currently have at least one member

Permissions: pg.which_groups / pg.which_local_groups

Send a message to every member of a group. Each member receives it under topic from the calling process — handle it with process.listen(topic).

local ok, err = group:broadcast("workers", "task", {id = 42}) -- all nodes
local ok, err = group:broadcast_local("workers", "task", {id = 42}) -- this node only
ParameterTypeDescription
groupstringTarget group
topicstringMessage topic
...anyZero or more payload values

Returns: boolean, error

Permissions: pg.broadcast / pg.broadcast_local on the group name

monitor subscribes to join/leave events for one group and returns the current members atomically — no membership change can slip between the snapshot and the subscription.

local sub, members, err = group:monitor("workers")
if err then
return nil, err
end
for _, pid in ipairs(members) do
-- current members at subscription time
end
local ch = sub:channel()
local event = ch:receive() -- {kind = "member.joined" | "member.left", path = "workers", data = {...}}
sub:close() -- unsubscribe; sub:close({flush = true}) drains queued events first
ParameterTypeDescription
groupstringGroup to watch

Returns: pg.Subscription, string[], error — the subscription and a snapshot of current members

Permission: pg.monitor on the group name

events subscribes to membership changes across every group in the scope and returns a snapshot of all groups to their members.

local sub, snapshot, err = group:events()
-- snapshot: { ["workers"] = {pid, ...}, ["all"] = {pid, ...} }
local event = sub:channel():receive()
sub:close()

Returns: pg.Subscription, table, error

Permission: pg.events

Events delivered on a subscription channel carry:

FieldTypeDescription
systemstringAlways "pg"
kindstring"member.joined" or "member.left"
pathstringThe group name
datatable{Group = string, PIDs = string[]} — the affected members

Subscription channels are buffered (capacity 64). If a slow consumer fills the buffer, further events are retained in the process mailbox in order and delivered once the consumer drains the channel (the subscription stalls rather than dropping events).

group:release()

Frees the instance immediately. Idempotent; after release, every method returns an error. Cleanup also runs automatically when the process exits.

Returns: boolean

PermissionMethodResource
pg.openpg.open()scope id
pg.joinjoin()group name
pg.leaveleave()group name
pg.get_membersget_members()group name
pg.get_local_membersget_local_members()group name
pg.which_groupswhich_groups()(scope)
pg.which_local_groupswhich_local_groups()(scope)
pg.broadcastbroadcast()group name
pg.broadcast_localbroadcast_local()group name
pg.monitormonitor()group name
pg.eventsevents()(scope)
ConditionKind
Permission deniederrors.PERMISSION_DENIED
Missing or empty argumenterrors.INVALID
Scope not founderrors.INTERNAL
Leave a group with no membershiperrors.INVALID
Instance releasederrors.INVALID

See Error Handling for working with errors.