Skip to content

Worker finish signal via mailbox return

Description

Worker finish signal via mailbox return.

  • Master spawns a worker via io.concurrent, sends 3 Events + a ShutdownCommand sentinel.
  • On the sentinel, the worker sends its own mailbox back to the master's inbox.
  • Master confirms the returned item is an Mbox and the expected instance.
  • Master closes and destroys the worker's mailbox, then awaits the worker's future.

Diagram

 master ──Event×3 + ShutdownCommand──► worker_mbx ──► worker task
                                                          │ process
                                                          │ send worker_mbx ──► master_inbox
                                                          ▼ exit
 master ◄──worker_mbx (as ItemHandle)── master_inbox
 master: close + destroy worker_mbx (tag+pointer verified first)

Source

pub fn worker_finish_signal_via_mailbox_return(allocator: std.mem.Allocator, io: std.Io) !void {
    var master_inbox_slot: Slot = null;
    try mailbox.new(io, allocator, &master_inbox_slot);
    const master_inbox: *Mbox = Mbox.moveFromSlot(&master_inbox_slot).?;
    defer {
        var rem: polynode.ItemList = master_inbox.close();
        releaseInbox(&rem, allocator);
        mailbox.destroy(master_inbox, allocator);
    }

    var worker_mbx_slot: Slot = null;
    try mailbox.new(io, allocator, &worker_mbx_slot);
    const worker_mbx: *Mbox = Mbox.moveFromSlot(&worker_mbx_slot).?;

    try sendJobsAndShutdown(worker_mbx, allocator);

    var worker_ctx: WorkerCtx = undefined;
    var fut = try spawnWorker(master_inbox, worker_mbx, &worker_ctx, allocator, io);

    try receiveAndVerify(master_inbox, worker_mbx, allocator);
    std.log.info("master: received worker_mbx back — worker finished (processed={d})", .{worker_ctx.processed});

    fut.await(io);
}

const WorkerCtx = struct {
    master_inbox: *Mbox,
    worker_mbx: *Mbox,
    alloc: std.mem.Allocator,
    processed: usize = 0,
};

fn cleanupReturnedMailbox(slot: *Slot, alloc: std.mem.Allocator) void {
    const returned: *Mbox = Mbox.mustFromPoly(slot.*.?);
    var rem: polynode.ItemList = returned.close();
    items.freeList(&rem, alloc);
    mailbox.destroy(returned, alloc);
    slot.* = null;
}

/// Release one handle from the master's inbox.
///
/// The inbox carries application items *and* the worker's mailbox, so the
/// release has to ask which one the handle is. `Mbox.fromPoly` is the
/// checking form — it returns null for an application item instead of
/// panicking.
fn releaseHandle(ih: *PolyNode, alloc: std.mem.Allocator) void {
    if (Mbox.fromPoly(ih)) |returned| {
        var left: polynode.ItemList = returned.close();
        items.freeList(&left, alloc);
        mailbox.destroy(returned, alloc);
    } else {
        items.freeItem(ih, alloc);
    }
}

/// Release everything the master's inbox still holds.
fn releaseInbox(rem: *polynode.ItemList, alloc: std.mem.Allocator) void {
    while (rem.popFirst()) |ih| {
        releaseHandle(ih, alloc);
    }
}

fn workerFn(ctx: *WorkerCtx) void {
    while (true) {
        var slot: Slot = null;
        defer items.freeSlot(&slot, ctx.alloc);
        ctx.worker_mbx.receive(&slot, null) catch return;
        const poly: *PolyNode = slot.?;

        if (items.ShutdownCommand.ShutdownCommandPolyHelper.fromPoly(poly) != null) {
            items.freeSlot(&slot, ctx.alloc);
            slot = Mbox.toPoly(ctx.worker_mbx);
            ctx.master_inbox.send(&slot) catch {};
            slot = null;
            return;
        }

        if (items.Event.EventPolyHelper.fromPoly(poly)) |ev| {
            ctx.processed += 1;
            std.log.info("worker processed Event code={d}", .{ev.code});
            items.freeSlot(&slot, ctx.alloc);
        }
    }
}

fn sendJobsAndShutdown(worker_mbx: *Mbox, alloc: std.mem.Allocator) !void {
    var i: usize = 0;
    while (i < 3) : (i += 1) {
        var slot: Slot = null;
        defer items.Event.EventPolyHelper.destroy(alloc, &slot);
        try items.Event.EventPolyHelper.create(alloc, &slot);
        items.Event.EventPolyHelper.mustFromSlot(&slot).code = @as(i32, @intCast(i + 1));
        try worker_mbx.send(&slot);
    }

    var slot: Slot = null;
    defer items.ShutdownCommand.ShutdownCommandPolyHelper.destroy(alloc, &slot);
    try items.ShutdownCommand.ShutdownCommandPolyHelper.create(alloc, &slot);
    try worker_mbx.send(&slot);

    std.log.info("master: sent 3 Events + ShutdownCommand to worker", .{});
}

fn spawnWorker(master_inbox: *Mbox, worker_mbx: *Mbox, ctx: *WorkerCtx, alloc: std.mem.Allocator, io: std.Io) !std.Io.Future(void) {
    ctx.* = .{ .master_inbox = master_inbox, .worker_mbx = worker_mbx, .alloc = alloc };
    return io.concurrent(workerFn, .{ctx});
}

fn receiveAndVerify(master_inbox: *Mbox, worker_mbx: *Mbox, alloc: std.mem.Allocator) !void {
    var slot: Slot = null;
    defer if (slot) |poly| {
        releaseHandle(poly, alloc);
        slot = null;
    };
    try master_inbox.receive(&slot, null);
    try helpers.expect(error.WorkerFinishFailed, Mbox.is_it_you(slot.?.*.tag), "expected an Mbox");
    try helpers.expect(error.WorkerFinishFailed, Mbox.mustFromPoly(slot.?) == worker_mbx, "wrong mailbox returned");
    cleanupReturnedMailbox(&slot, alloc);
}

const items = @import("../items/items.zig");
const helpers = @import("../helpers/helpers.zig");
const matryoshka = @import("matryoshka");
const std = @import("std");
const mailbox = matryoshka.mailbox;
const polynode = matryoshka.polynode;
const PolyNode = polynode.PolyNode;
const Slot = polynode.Slot;
const Mbox = matryoshka.Mbox;