thespian/test/remote_endpoint_test.zig
CJ van den Berg 077d47f8b2
feat: clean up local_actors/outbound tables when senders exit
The endpoint now links to each local actor when it is first assigned an
outbound wire ID, trapping the exit to remove stale entries from the
outbound and local_actors tables.

To preserve spawn_link semantics, the endpoint stores the spawner's
instance_id() at init time. Trapped exits from the spawner are always
re-propagated regardless of whether the spawner also appears as an outbound
sender.
2026-03-12 22:21:27 +01:00

101 lines
3.1 KiB
Zig

const std = @import("std");
const thespian = @import("thespian");
const cbor = @import("cbor");
const framing = @import("framing");
const protocol = @import("protocol");
const endpoint = @import("endpoint");
const build_options = @import("build_options");
const Allocator = std.mem.Allocator;
const result = thespian.result;
const unexpected = thespian.unexpected;
const pid_ref = thespian.pid_ref;
const Receiver = thespian.Receiver;
const message = thespian.message;
const extract = thespian.extract;
/// Top-level test actor. Registers as "test_receiver", spawns the endpoint,
/// sends ["hello"] to the child's echo actor, and verifies the reply.
const TestActor = struct {
allocator: Allocator,
ep: thespian.pid,
receiver: Receiver(*@This()),
const Args = struct { allocator: Allocator };
fn start(args: Args) result {
return init(args) catch |e| return thespian.exit_error(e, @errorReturnTrace());
}
fn init(args: Args) !void {
// Register self so the endpoint can deliver the echo reply here.
thespian.env.get().proc_set("test_receiver", thespian.self_pid().ref());
const argv = try args.allocator.dupe(u8, message.fmt(.{build_options.remote_child_endpoint_path}).buf);
const ep = try thespian.spawn_link(
args.allocator,
endpoint.Args{
.allocator = args.allocator,
.argv = argv,
.spawner = thespian.self_pid().ref(),
},
endpoint.start,
"endpoint",
);
// Send ["hello"] to the child's "echo" actor through the endpoint.
try ep.send(.{ "send", @as(u64, 1), "echo", .{"hello"} });
const self = try args.allocator.create(@This());
self.* = .{
.allocator = args.allocator,
.ep = ep,
.receiver = .init(receive_fn, deinit, self),
};
errdefer self.deinit();
thespian.receive(&self.receiver);
}
fn deinit(self: *@This()) void {
self.ep.deinit();
self.allocator.destroy(self);
}
fn receive_fn(self: *@This(), from: pid_ref, m: message) result {
return self.receive(from, m) catch |e| return thespian.exit_error(e, @errorReturnTrace());
}
fn receive(_: *@This(), _: pid_ref, m: message) !void {
// The endpoint delivers the echo reply as the raw payload: ["hello"]
if (try m.match(.{"hello"})) {
return thespian.exit("success");
} else {
return unexpected(m);
}
}
};
test "remote: endpoint delivers message cross-process and receives reply" {
const allocator = std.testing.allocator;
var ctx = try thespian.context.init(allocator);
defer ctx.deinit();
var success = false;
var exit_handler = thespian.make_exit_handler(&success, struct {
fn handle(ok: *bool, status: []const u8) void {
ok.* = std.mem.eql(u8, status, "success");
}
}.handle);
_ = try ctx.spawn_link(
TestActor.Args{ .allocator = allocator },
TestActor.start,
"test_actor",
&exit_handler,
null,
);
ctx.run();
if (!success) return error.TestFailed;
}