AdaptiveInstance trait
The contract a primitive satisfies to be registered with the sidecar.
pub trait AdaptiveInstance: Send + Sync + 'static {
fn header(&self) -> &HandshakeHeader;
fn ring(&self) -> &ObservationRing;
fn make_policy(&self) -> Box<dyn Policy>;
/// Called by the sidecar when the policy returns a new strategy
/// tag. Default implementation: just set the tag on the header.
fn apply_migration(&self, new_tag: u32) {
self.header().set_tag(new_tag);
}
}Required methods
header() -> &HandshakeHeader
Returns a reference to the instance’s HandshakeHeader. The sidecar
keeps a NonNull<HandshakeHeader> (captured at registration time)
that aliases this address; the SidecarBox machinery guarantees
the header stays alive (and at a stable address) until the handle
is dropped.
ring() -> &ObservationRing
Returns a reference to the instance’s ObservationRing. Same
aliasing contract as header(). The sidecar’s scan thread calls
ring().pop() repeatedly during each scan cycle to drain
observations.
make_policy() -> Box<dyn Policy>
Factory for the policy that governs this instance. Called once at
registration time. Returning Box<NoMigrationPolicy> opts the
primitive out of any migration decisions (the sidecar still drains
the observation ring; it just never asks the policy for a tag).
Optional method
apply_migration(&self, new_tag: u32)
Default implementation: self.header().set_tag(new_tag). PIC-only
update; no data-layout migration.
Primitives that need a heavy migration (data-layout swap)
override this to perform the swap, then update the tag. The
substrate ships MigrationGuard in subetha_core::migration
to bracket the swap so readers entering the old layout drain
before the old layout is freed. Example shape:
fn apply_migration(&self, new_tag: u32) {
let strategy = MyStrategy::from_u32(new_tag);
let new_payload = self.build_payload_for(strategy);
// begin() bumps the generation and installs new_tag atomically
// (both representations are now live).
let guard = MigrationGuard::begin(self.header(), new_tag);
self.install_new_payload(new_payload);
guard.wait_quiescent(); // drains the old generation's readers
// after wait_quiescent returns, the old payload is safe to free.
}MigrationGuard::begin does the bump-generation + tag-swap in one
step (so the new PIC branch target is already published when it
returns); wait_quiescent blocks until in-flight readers on the old
generation have drained, after which the coordinator owns the old
payload exclusively and can free it. The guard does not auto-drain on
Drop - the explicit wait_quiescent call is required - and no
separate set_tag call is needed. How the primitive installs the new
payload is up to it: a Vec<u8> swap, a Box<dyn Strategy> swap, or
anything else the reader side can observe by reading the generation.
Bounds
Send + Sync: the sidecar’s scan thread (different from whichever thread owns the primitive) callsapply_migrationon the instance via the raw*const dyn AdaptiveInstanceit captured at registration. Both bounds are required for that call to be sound.'static: the sidecar’s registry holds the raw pointer untilunregister. The lifetime cannot be reflected in the registry’s storage type, so the trait requires'static.SidecarBoxboxes the value to give it a heap address that is stable for the box’s lifetime; theDropordering (handle drops first, blocking on scan, then inner drops) prevents use-after-free.
Registering an instance
Three ways:
// 1. The high-level wrapper. Recommended.
// For MMF primitives (`subetha-cxc`) whose constructors
// return Result<Self>, call SidecarBox::new explicitly:
let m = SidecarBox::new(
SharedHashMap::<u32, u64>::create("/tmp/m.bin", 1024).unwrap()
);
// 2. The raw registration. Useful when SidecarBox doesn't fit the
// ownership story (e.g., a custom AdaptiveInstance impl that
// lives behind an Arc<MyPrim> from construction).
let arc: Arc<MyCustomPrim> = MyCustomPrim::new_arc();
let header = NonNull::from(arc.header());
let ring = NonNull::from(arc.ring());
let policy = arc.make_policy();
let instance_ptr: *const dyn AdaptiveInstance = &*arc;
let id = unsafe {
global().register_raw(
header, ring,
Some(NonNull::new_unchecked(instance_ptr as *mut _)),
policy,
)
};
// ... use arc ...
global().unregister(id); // BEFORE the last Arc drops!
// 3. Stats-only (no migration). Pass None for the instance pointer.
// apply_migration is never called for this instance.
let id = unsafe {
global().register_raw(header, ring, None, Box::new(NoMigrationPolicy))
};Warning
Form 2 puts the lifetime invariant on you. unregister must
happen before the underlying memory is freed. SidecarBox exists
precisely so the common case does not require manual ordering.
See also
SidecarBox<T>- the RAII wrapper.Policy- whatmake_policy()returns.HandshakeHeader- whatheader()returns.ObservationRing- whatring()returns.