Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions crates/busbar-contract/include/busbar_plugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -1149,9 +1149,9 @@ struct bb_mech_Blob {
uint32_t flags;
};

/* The host's context for one plugin instance: opaque to the plugin, handed back on `wake`. */
/* The host's context for one plugin instance: an opaque handle the host verifies on every call. */
struct bb_mech_HostCtx {
void *ptr;
uint64_t handle;
};

/* A ticket: `(slot, generation)`. UNIQUE PER INSTANCE across all workers, so a plugin may key its */
Expand Down Expand Up @@ -4189,7 +4189,7 @@ BB_ASSERT(offsetof(bb_mech_CompletionHandle, seq) == 8, "bb_mech_CompletionHandl
BB_ASSERT(offsetof(bb_mech_CompletionHandle, _reserved) == 12, "bb_mech_CompletionHandle._reserved: offset");
BB_ASSERT(sizeof(bb_mech_HostCtx) == 8, "bb_mech_HostCtx: size");
BB_ASSERT(BB_ALIGNOF(bb_mech_HostCtx) == 8, "bb_mech_HostCtx: alignment");
BB_ASSERT(offsetof(bb_mech_HostCtx, ptr) == 0, "bb_mech_HostCtx.ptr: offset");
BB_ASSERT(offsetof(bb_mech_HostCtx, handle) == 0, "bb_mech_HostCtx.handle: offset");
BB_ASSERT(sizeof(bb_mech_HostTables) == 48, "bb_mech_HostTables: size");
BB_ASSERT(BB_ALIGNOF(bb_mech_HostTables) == 8, "bb_mech_HostTables: alignment");
BB_ASSERT(offsetof(bb_mech_HostTables, size) == 0, "bb_mech_HostTables.size: offset");
Expand Down
26 changes: 20 additions & 6 deletions crates/busbar-contract/src/abi/mechanism/ticket.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@
//! * A host service that can pend answers with a [`CompletionHandle`] `(ticket, seq)`. On resume the
//! plugin re-issues the same handle and receives the stored result: the host never runs it twice.

use std::os::raw::c_void;

use crate::abi::host::conn::connector::ConnectorSlots;
use crate::abi::host::io::IoSlots;
use crate::abi::host::service::HostSlots;
Expand Down Expand Up @@ -58,12 +56,28 @@ pub struct CompletionHandle {
pub _reserved: u32,
}

/// The host's context for one plugin instance: opaque to the plugin, handed back on `wake`.
/// The host's context for one plugin instance: an opaque handle the host verifies on every call.
///
/// BUSBAR-1.6.0.md Part 2 #74: no host address crosses the ABI. The handle is the host's own
/// index for the instance beside a keyed tag over it; the host recomputes the tag on every `wake`,
/// service, connector and I/O call and refuses a handle it did not mint before anything is read
/// through it. A plugin copies it and hands it back, and never interprets it. `0` is no context.
#[repr(C)]
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HostCtx {
/// The host's per-instance state; never dereferenced by a plugin.
pub ptr: *mut c_void,
/// The host's handle for the instance; never interpreted by a plugin.
pub handle: u64,
}

impl HostCtx {
/// No context: what a host hands before it binds one, and what a test double hands.
pub const NONE: HostCtx = HostCtx { handle: 0 };

/// Whether this is [`HostCtx::NONE`].
#[must_use]
pub const fn is_none(self) -> bool {
self.handle == 0
}
}

/// `host.wake(ctx, ticket)`: callable from any thread, never blocks, never fails. A wake for a
Expand Down
8 changes: 4 additions & 4 deletions crates/busbar-contract/src/abi/sdk/services.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,8 +223,8 @@ pub struct Wake {
wake: WakeFn,
}

// SAFETY: the context is the host's per-instance state and the wake a plain code address; the
// mechanism states the wake callable from any thread for the instance's life.
// SAFETY: the context is an opaque `u64` handle (#74: no host address crosses) and the wake a plain
// code address; the mechanism states the wake callable from any thread for the instance's life.
unsafe impl Send for Wake {}
// SAFETY: as above.
unsafe impl Sync for Wake {}
Expand Down Expand Up @@ -252,8 +252,8 @@ pub struct Services {
table: *const HostSlots,
}

// SAFETY: the table is the host's static and the context the host's leaked per-instance state; the
// host states both are callable from any thread for the instance's life.
// SAFETY: the table is the host's static and the context an opaque `u64` handle (#74); the host
// states both are usable from any thread for the instance's life.
unsafe impl Send for Services {}
// SAFETY: as above.
unsafe impl Sync for Services {}
Expand Down
4 changes: 1 addition & 3 deletions crates/busbar-contract/src/abi/sdk/tests/auth_door_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,7 @@ fn head<T>(op: u32) -> InHead {
flags: 0,
deadline_class: 0,
_reserved: [0; 3],
host: HostCtx {
ptr: ptr::null_mut(),
},
host: HostCtx::NONE,
ticket: Ticket::NONE,
deadline_ns: 0,
trace_id: [0; 16],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,9 +168,7 @@ fn head<T>(op: u32) -> InHead {
flags: 0,
deadline_class: 0,
_reserved: [0; 3],
host: HostCtx {
ptr: ptr::null_mut(),
},
host: HostCtx::NONE,
ticket: Ticket::NONE,
deadline_ns: 0,
trace_id: [0; 16],
Expand Down
8 changes: 2 additions & 6 deletions crates/busbar-contract/src/abi/sdk/tests/conn_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,7 @@ fn host(conns: *const ConnectorSlots) -> Host {
Host::of(&HostTables {
size: std::mem::size_of::<HostTables>() as u32,
_reserved: 0,
ctx: HostCtx {
ptr: std::ptr::null_mut(),
},
ctx: HostCtx::NONE,
wake: None,
conns,
services: services(),
Expand Down Expand Up @@ -242,9 +240,7 @@ fn open_learns_the_hosts_verdict_on_a_need() {
let bare = Host::of(&HostTables {
size: std::mem::size_of::<HostTables>() as u32,
_reserved: 0,
ctx: HostCtx {
ptr: std::ptr::null_mut(),
},
ctx: HostCtx::NONE,
wake: None,
conns: &SLOTS,
services: std::ptr::null(),
Expand Down
4 changes: 1 addition & 3 deletions crates/busbar-contract/src/abi/sdk/tests/door_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,9 +178,7 @@ fn in_head<T>(op: u32, ticket: Ticket) -> InHead {
flags: 0,
deadline_class: 0,
_reserved: [0; 3],
host: HostCtx {
ptr: ptr::null_mut(),
},
host: HostCtx::NONE,
ticket,
deadline_ns: 0,
trace_id: [0; 16],
Expand Down
4 changes: 1 addition & 3 deletions crates/busbar-contract/src/abi/sdk/tests/exchange_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,9 +265,7 @@ fn host() -> Host {
Host::of(&HostTables {
size: std::mem::size_of::<HostTables>() as u32,
_reserved: 0,
ctx: HostCtx {
ptr: std::ptr::null_mut(),
},
ctx: HostCtx::NONE,
wake: None,
conns: &SLOTS,
services: std::ptr::null(),
Expand Down
4 changes: 1 addition & 3 deletions crates/busbar-contract/src/abi/sdk/tests/hook_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -729,9 +729,7 @@ fn red_a_hook_waiting_on_its_exchange_pends_and_resumes_on_the_wake() {
let tables = HostTables {
size: std::mem::size_of::<HostTables>() as u32,
_reserved: 0,
ctx: HostCtx {
ptr: std::ptr::null_mut(),
},
ctx: HostCtx::NONE,
wake: None,
conns: &PENDING_SLOTS,
services: std::ptr::null(),
Expand Down
16 changes: 2 additions & 14 deletions crates/busbar-contract/src/abi/sdk/tests/io_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,7 @@ static TABLE: IoSlots = IoSlots {
};

fn io(ticket: Ticket) -> Io<'static> {
Io::new(
HostCtx {
ptr: std::ptr::null_mut(),
},
&TABLE,
ticket,
)
Io::new(HostCtx::NONE, &TABLE, ticket)
}

#[test]
Expand Down Expand Up @@ -136,13 +130,7 @@ fn a_slot_that_may_pend_is_not_made_on_no_ticket_and_an_absent_slot_is_unarmed()
io(TICKET).write(2, b"x"),
Poll::Ready(Err(IoFailure::Unarmed))
);
let bare = Io::new(
HostCtx {
ptr: std::ptr::null_mut(),
},
std::ptr::null(),
TICKET,
);
let bare = Io::new(HostCtx::NONE, std::ptr::null(), TICKET);
assert!(!bare.armed());
}

Expand Down
4 changes: 1 addition & 3 deletions crates/busbar-contract/src/abi/sdk/tests/life_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,7 @@ fn in_head<T>(op: u32) -> InHead {
flags: 0,
deadline_class: 0,
_reserved: [0; 3],
host: HostCtx {
ptr: ptr::null_mut(),
},
host: HostCtx::NONE,
ticket: Ticket::NONE,
deadline_ns: 0,
trace_id: [0; 16],
Expand Down
4 changes: 1 addition & 3 deletions crates/busbar-contract/src/abi/sdk/tests/safe_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,9 +201,7 @@ fn in_head<T>(op: u32) -> InHead {
flags: 0,
deadline_class: 0,
_reserved: [0; 3],
host: HostCtx {
ptr: ptr::null_mut(),
},
host: HostCtx::NONE,
ticket: Ticket::NONE,
deadline_ns: 0,
trace_id: [0; 16],
Expand Down
8 changes: 2 additions & 6 deletions crates/busbar-contract/src/abi/sdk/tests/services_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,7 @@ fn services(t: &HostSlots) -> Services {
let tables = HostTables {
size: size_of::<HostTables>() as u32,
_reserved: 0,
ctx: HostCtx {
ptr: std::ptr::null_mut(),
},
ctx: HostCtx::NONE,
wake: None,
conns: std::ptr::null(),
services: t,
Expand Down Expand Up @@ -171,9 +169,7 @@ fn no_table_is_no_services() {
let tables = HostTables {
size: size_of::<HostTables>() as u32,
_reserved: 0,
ctx: HostCtx {
ptr: std::ptr::null_mut(),
},
ctx: HostCtx::NONE,
wake: None,
conns: std::ptr::null(),
services: std::ptr::null(),
Expand Down
7 changes: 1 addition & 6 deletions crates/busbar-contract/tests/abi_versions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,5 @@ fn every_slot_type_is_extern_c() {
Outcome::Ready
);
assert!(door().is_null());
wake(
HostCtx {
ptr: std::ptr::null_mut(),
},
Ticket::NONE,
);
wake(HostCtx::NONE, Ticket::NONE);
}
2 changes: 1 addition & 1 deletion crates/busbar-contract/tests/golden/abi-layout.golden
Original file line number Diff line number Diff line change
Expand Up @@ -863,7 +863,7 @@ MechCompletionHandle.seq=8
MechCompletionHandle._reserved=12
MechCompletionHandle.__size=16
MechCompletionHandle.__align=4
MechHostCtx.ptr=0
MechHostCtx.handle=0
MechHostCtx.__size=8
MechHostCtx.__align=8
MechHostTables.size=0
Expand Down
2 changes: 1 addition & 1 deletion crates/busbar-contract/tests/layout_golden.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1010,7 +1010,7 @@ fn compute_layout() -> String {
record!(s, MechKindTailHead, [size, _reserved]);
record!(s, MechTicket, [slot, generation]);
record!(s, MechCompletionHandle, [ticket, seq, _reserved]);
record!(s, MechHostCtx, [ptr]);
record!(s, MechHostCtx, [handle]);
record!(
s,
MechHostTables,
Expand Down
4 changes: 1 addition & 3 deletions crates/busbar-contract/tests/sdk_door_cdylib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,7 @@ fn in_head(size: usize, op: u32) -> InHead {
flags: 0,
deadline_class: 0,
_reserved: [0; 3],
host: HostCtx {
ptr: ptr::null_mut(),
},
host: HostCtx::NONE,
ticket: Ticket::NONE,
deadline_ns: 0,
trace_id: [0; 16],
Expand Down
22 changes: 22 additions & 0 deletions crates/busbar-kernel/src/endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,10 @@ fn list_models_dialect(
}

pub async fn healthz(crate::state::CurrentApp(app): crate::state::CurrentApp) -> Response {
if let Some(halted) = plugin_dispatch_halted(busbar_plugin_loader::dispatch::handle::latched())
{
return halted;
}
let t = now();
// Side-effect-FREE readiness check: `/healthz` is unauthenticated and high-frequency (k8s
// liveness, load balancers), so it must NOT transition expired-Open lanes to HalfOpen or steal
Expand All @@ -306,6 +310,20 @@ pub async fn healthz(crate::state::CurrentApp(app): crate::state::CurrentApp) ->
}
}

/// THE PLUGIN HANDLE LATCH (#74): once plugins handed the host enough contexts it did not mint from
/// outside any crossing, every host call made outside a crossing is refused for the rest of the
/// process (a plugin's background wakes included), so the node is not ready. The reason line is
/// neutral (this route is unauthenticated); the detail is the ERROR line the latch logged.
fn plugin_dispatch_halted(latched: bool) -> Option<Response> {
latched.then(|| {
(
StatusCode::SERVICE_UNAVAILABLE,
"plugin dispatch halted; restart required",
)
.into_response()
})
}

/// Whether any configured non-model plane's generation opened: its runtime slot was built for this
/// generation and, for a door plane, its open answered ([`crate::plane::door::opened`]).
fn a_plane_opened(app: &crate::state::App) -> bool {
Expand All @@ -326,3 +344,7 @@ fn a_plane_opened(app: &crate::state::App) -> bool {
#[cfg(test)]
#[path = "tests/endpoints_doc_tests.rs"]
mod endpoints_doc_tests;

#[cfg(test)]
#[path = "tests/plugin_dispatch_halted_tests.rs"]
mod plugin_dispatch_halted_tests;
15 changes: 15 additions & 0 deletions crates/busbar-kernel/src/tests/plugin_dispatch_halted_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2026 Busbar Inc and contributors

//! THE PLUGIN HANDLE LATCH ON `/healthz` (#74).

use super::*;

/// RED: while the handle latch is tripped `/healthz` answers 503 with the neutral reason, and with
/// it untripped the readiness check goes on to the lanes.
#[test]
fn healthz_answers_503_while_the_handle_latch_is_tripped() {
let halted = plugin_dispatch_halted(true).expect("a tripped latch halts readiness");
assert_eq!(halted.status(), StatusCode::SERVICE_UNAVAILABLE);
assert!(plugin_dispatch_halted(false).is_none());
}
4 changes: 1 addition & 3 deletions crates/busbar-plane-decisions/tests/door_harness/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,7 @@ unsafe impl Sync for Tables {}
static TABLES: Tables = Tables(HostTables {
size: std::mem::size_of::<HostTables>() as u32,
_reserved: 0,
ctx: HostCtx {
ptr: std::ptr::null_mut(),
},
ctx: HostCtx::NONE,
wake: None,
conns: std::ptr::null(),
io: std::ptr::null(),
Expand Down
4 changes: 1 addition & 3 deletions crates/busbar-plane-mcp/tests/door_clock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,7 @@ fn with_seal<R>(
let tables = HostTables {
size: std::mem::size_of::<HostTables>() as u32,
_reserved: 0,
ctx: HostCtx {
ptr: std::ptr::null_mut(),
},
ctx: HostCtx::NONE,
wake: None,
conns: std::ptr::null(),
io: std::ptr::null(),
Expand Down
4 changes: 1 addition & 3 deletions crates/busbar-plane-mcp/tests/program.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,9 +192,7 @@ fn host() -> Host {
Host::of(&HostTables {
size: std::mem::size_of::<HostTables>() as u32,
_reserved: 0,
ctx: HostCtx {
ptr: std::ptr::null_mut(),
},
ctx: HostCtx::NONE,
wake: None,
conns: &SLOTS,
services: std::ptr::null(),
Expand Down
4 changes: 1 addition & 3 deletions crates/busbar-plane-mcp/tests/sessions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -415,9 +415,7 @@ unsafe impl Sync for Tables {}
static TABLES: Tables = Tables(HostTables {
size: std::mem::size_of::<HostTables>() as u32,
_reserved: 0,
ctx: HostCtx {
ptr: std::ptr::null_mut(),
},
ctx: HostCtx::NONE,
wake: None,
conns: &CONN_SLOTS,
io: std::ptr::null(),
Expand Down
4 changes: 1 addition & 3 deletions crates/busbar-transport-stdio/tests/door.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,9 +177,7 @@ impl Host {
let tables = Box::new(HostTables {
size: size_of::<HostTables>() as u32,
_reserved: 0,
ctx: HostCtx {
ptr: std::ptr::null_mut(),
},
ctx: HostCtx::NONE,
wake: None,
conns: std::ptr::null(),
services: std::ptr::null(),
Expand Down
Loading