blob: daa65c8dc15646c48b175f15a541f66442ea094b [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
use crate::deps::SimClock;
use crate::executor::{PendingSpawns, TimerHandle};
use clock::Clock;
use iggy_binary_protocol::GenericHeader;
use message_bus::client_listener::RequestHandler;
use message_bus::fd_transfer::DupedFd;
use message_bus::installer::conn_info::ClientConnMeta;
use message_bus::replica::listener::MessageHandler;
use message_bus::{
BusMessage, ClientConnectionLostFn, ConnectionInstaller, MessageBus, ReplicaHandshakeDoneFn,
SendError,
};
use server_common::{
MESSAGE_ALIGN, Message,
iobuf::{Frozen, Owned},
};
use std::cell::RefCell;
use std::collections::{HashMap, HashSet, VecDeque};
use std::future::Future;
use std::ops::Deref;
use std::rc::Rc;
use std::time::Duration;
/// Convert an inbound Frozen (bus trait payload) into a `Message` for the
/// simulator's packet layer. The sim keeps `Message<GenericHeader>` in its
/// envelope because the network and tick loop reason about typed headers,
/// not raw byte buffers. One memcpy per send is acceptable under sim.
fn frozen_to_message(frozen: &Frozen<MESSAGE_ALIGN>) -> Message<GenericHeader> {
Message::try_from(Owned::<MESSAGE_ALIGN>::copy_from_slice(frozen.as_slice()))
.expect("simulator bus must receive a valid generic message")
}
/// Message envelope for tracking sender/recipient
#[derive(Debug)]
pub enum EnvelopePayload {
Replica(Message<GenericHeader>),
Client(Message<GenericHeader>),
}
#[derive(Debug)]
pub struct Envelope {
pub from_replica: Option<u8>,
pub to_replica: Option<u8>,
pub to_client: Option<u128>,
pub payload: EnvelopePayload,
}
/// Per-replica outbox for staging outbound messages.
///
/// Consensus code calls `send_to_replica()` / `send_to_client()` which stage
/// messages here. The simulator's tick loop drains each replica's outbox and
/// feeds the messages into the [`Network`](crate::network::Network) for simulated delivery.
pub struct SimOutbox {
/// Replica id that owns this outbox. Populated as `from_replica` on every envelope.
self_id: u8,
clients: RefCell<HashSet<u128>>,
replicas: RefCell<HashSet<u8>>,
pending_messages: RefCell<VecDeque<Envelope>>,
/// Virtual clock backing [`MessageBus::sleep`], so the shard pump's
/// consensus tick advances with the simulation, not the wall clock.
timer: TimerHandle,
/// Pending-spawn queue shared with the executor, backing
/// [`MessageBus::spawn`]: tasks spawned off the pump (dispatch's poll
/// IO, client-request drains, metadata submits) stage here, and the
/// executor drains them into deterministic tasks tagged to this replica.
spawns: Rc<PendingSpawns>,
/// Per-connection metadata registered when a `SimClient` connects, so the
/// dispatch layer's `client_meta` lookups resolve (production reads the same
/// from `IggyMessageBus`). Empty until the shell wires clients.
client_metas: RefCell<HashMap<u128, Rc<ClientConnMeta>>>,
/// Client-disconnect callback the dispatch handler registers; the harness
/// fires it via `notify_client_connection_lost` to drive the real
/// session-removal + logout path when it models a client disconnect.
client_lost_fn: RefCell<Option<ClientConnectionLostFn>>,
}
impl std::fmt::Debug for SimOutbox {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// `client_lost_fn` (an `Rc<dyn Fn>`) is not `Debug`, so this is hand
// written and omits it.
f.debug_struct("SimOutbox")
.field("self_id", &self.self_id)
.field("clients", &self.clients)
.field("replicas", &self.replicas)
.field("pending_messages", &self.pending_messages)
.field("timer", &self.timer)
.field("spawns", &self.spawns)
.field("client_metas", &self.client_metas)
.finish_non_exhaustive()
}
}
impl SimOutbox {
#[must_use]
pub fn new(self_id: u8, timer: TimerHandle, spawns: Rc<PendingSpawns>) -> Self {
Self {
self_id,
clients: RefCell::new(HashSet::new()),
replicas: RefCell::new(HashSet::new()),
pending_messages: RefCell::new(VecDeque::new()),
timer,
spawns,
client_metas: RefCell::new(HashMap::new()),
client_lost_fn: RefCell::new(None),
}
}
/// Drain all staged messages from this outbox.
pub fn drain(&self) -> Vec<Envelope> {
self.pending_messages.borrow_mut().drain(..).collect()
}
pub fn add_client(&mut self, client: u128) -> bool {
self.clients.borrow_mut().insert(client)
}
pub fn add_replica(&mut self, replica: u8) -> bool {
self.replicas.borrow_mut().insert(replica)
}
pub fn remove_client(&mut self, client: u128) -> bool {
self.clients.borrow_mut().remove(&client)
}
pub fn remove_replica(&mut self, replica: u8) -> bool {
self.replicas.borrow_mut().remove(&replica)
}
/// Register a connecting client's metadata so dispatch's `client_meta`
/// resolves it (mirrors `IggyMessageBus` at connection install).
pub fn insert_client_meta(&self, meta: ClientConnMeta) {
self.client_metas
.borrow_mut()
.insert(meta.client_id, Rc::new(meta));
}
/// Drop a disconnected client's metadata.
pub fn remove_client_meta(&self, client_id: u128) {
self.client_metas.borrow_mut().remove(&client_id);
}
/// Per-connection metadata for `client_id`, or `None` if not connected.
#[must_use]
pub fn client_meta(&self, client_id: u128) -> Option<Rc<ClientConnMeta>> {
self.client_metas.borrow().get(&client_id).cloned()
}
/// Register the client-connection-lost callback (the dispatch handler
/// installs it during handler construction).
pub fn set_client_connection_lost_fn(&self, f: ClientConnectionLostFn) {
*self.client_lost_fn.borrow_mut() = Some(f);
}
/// Fire the registered connection-lost callback for `client_id`, a no-op if
/// none is installed. The harness calls this to model a client disconnect,
/// driving the real session-removal + logout dispatch path. The lock is
/// released before the call so the callback may re-enter the bus.
pub fn notify_client_connection_lost(&self, client_id: u128) {
let handler = self.client_lost_fn.borrow().clone();
if let Some(handler) = handler {
handler(client_id);
}
}
}
// The bus is single-threaded and `!Send` (it holds the `Rc` spawn queue),
// so its async methods return `!Send` futures. That is correct for the sim
// and matches the `SimJournal` I/O impls in `deps.rs`.
#[allow(clippy::future_not_send)]
impl MessageBus for SimOutbox {
fn track_background(&self, _handle: message_bus::JoinHandle<()>) {}
async fn send_to_client(
&self,
client_id: u128,
data: impl Into<BusMessage>,
) -> Result<(), SendError> {
if !self.clients.borrow().contains(&client_id) {
return Err(SendError::ClientNotFound(client_id));
}
self.pending_messages.borrow_mut().push_back(Envelope {
from_replica: Some(self.self_id),
to_replica: None,
to_client: Some(client_id),
payload: EnvelopePayload::Client(frozen_to_message(&data.into().into_contiguous())),
});
Ok(())
}
async fn send_to_replica(
&self,
replica: u8,
data: Frozen<MESSAGE_ALIGN>,
) -> Result<(), SendError> {
if !self.replicas.borrow().contains(&replica) {
return Err(SendError::ReplicaNotConnected(replica));
}
self.pending_messages.borrow_mut().push_back(Envelope {
from_replica: Some(self.self_id),
to_replica: Some(replica),
to_client: None,
payload: EnvelopePayload::Replica(frozen_to_message(&data)),
});
Ok(())
}
fn set_connection_lost_fn(&self, _f: message_bus::ConnectionLostFn) {}
fn set_replica_forward_fn(&self, _f: message_bus::ReplicaForwardFn) {}
fn set_client_forward_fn(&self, _f: message_bus::ClientForwardFn) {}
fn sleep(&self, duration: Duration) -> impl Future<Output = ()> {
self.timer.sleep(duration)
}
fn spawn(&self, future: impl Future<Output = ()> + 'static) {
self.spawns.push(self.self_id, Box::pin(future));
}
fn realtime_micros(&self) -> u64 {
// Same virtual timeline `SimClock` injects into every consensus
// group, so the bus clock a shard-side handler reads and the
// prepare-timestamp clock agree to the microsecond.
SimClock::new(self.timer.clone()).realtime().as_micros()
}
}
/// Newtype wrapper for shared [`SimOutbox`] that implements [`MessageBus`]
#[derive(Debug, Clone)]
pub struct SharedSimOutbox(pub Rc<SimOutbox>);
impl Deref for SharedSimOutbox {
type Target = SimOutbox;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[allow(clippy::future_not_send)]
impl MessageBus for SharedSimOutbox {
fn track_background(&self, handle: message_bus::JoinHandle<()>) {
self.0.track_background(handle);
}
async fn send_to_client(
&self,
client_id: u128,
data: impl Into<BusMessage>,
) -> Result<(), SendError> {
self.0.send_to_client(client_id, data).await
}
async fn send_to_replica(
&self,
replica: u8,
data: Frozen<MESSAGE_ALIGN>,
) -> Result<(), SendError> {
self.0.send_to_replica(replica, data).await
}
fn set_connection_lost_fn(&self, f: message_bus::ConnectionLostFn) {
self.0.set_connection_lost_fn(f);
}
fn set_replica_forward_fn(&self, f: message_bus::ReplicaForwardFn) {
self.0.set_replica_forward_fn(f);
}
fn set_client_forward_fn(&self, f: message_bus::ClientForwardFn) {
self.0.set_client_forward_fn(f);
}
fn sleep(&self, duration: Duration) -> impl Future<Output = ()> {
self.0.sleep(duration)
}
fn spawn(&self, future: impl Future<Output = ()> + 'static) {
self.0.spawn(future);
}
fn realtime_micros(&self) -> u64 {
self.0.realtime_micros()
}
}
/// The router's dispatch/pump impl block requires `ConnectionInstaller`
/// (`router.rs`, `B: MessageBus + ConnectionInstaller + Clone`). The
/// simulator delivers messages as in-memory frames, never as delegated
/// fds, so the install surface is unreachable: fd installs panic loudly
/// if a future change ever routes a connection-setup frame into the sim,
/// while the shard-0 bookkeeping acks are harmless no-ops.
impl ConnectionInstaller for SharedSimOutbox {
fn install_replica_inbound_fd(
&self,
_fd: DupedFd,
_on_message: MessageHandler,
_on_done: ReplicaHandshakeDoneFn,
) {
panic!("simulator has no fd transfer: replica inbound install is unreachable");
}
fn install_replica_outbound_fd(
&self,
_fd: DupedFd,
_replica_id: u8,
_on_message: MessageHandler,
_on_done: ReplicaHandshakeDoneFn,
) {
panic!("simulator has no fd transfer: replica outbound install is unreachable");
}
fn release_replica_handshake_slot(&self, _slot: u64) {}
fn clear_replica_dial_pending(&self, _replica_id: u8) {}
fn install_client_fd(&self, _fd: DupedFd, _meta: ClientConnMeta, _on_request: RequestHandler) {
panic!("simulator has no fd transfer: client install is unreachable");
}
fn install_client_ws_fd(
&self,
_fd: DupedFd,
_meta: ClientConnMeta,
_on_request: RequestHandler,
) {
panic!("simulator has no fd transfer: ws client install is unreachable");
}
// Real, unlike the fd installs above: dispatch reads client_meta to seed
// the session manager, and registers the disconnect callback the harness
// fires. Both delegate to the in-memory registry on the inner outbox.
fn client_meta(&self, client_id: u128) -> Option<Rc<ClientConnMeta>> {
self.0.client_meta(client_id)
}
fn set_client_connection_lost_fn(&self, f: ClientConnectionLostFn) {
self.0.set_client_connection_lost_fn(f);
}
}
#[cfg(test)]
mod tests {
use super::*;
use message_bus::installer::conn_info::ClientTransportKind;
use std::cell::Cell;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
fn test_outbox() -> SimOutbox {
SimOutbox::new(0, TimerHandle::new(), Rc::new(PendingSpawns::default()))
}
#[test]
fn client_meta_round_trips_and_lost_fn_fires() {
let bus = test_outbox();
let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 1234);
bus.insert_client_meta(ClientConnMeta::new(42, addr, ClientTransportKind::Tcp));
let meta = bus.client_meta(42).expect("registered client meta");
assert_eq!(meta.client_id, 42);
assert_eq!(meta.peer_addr, addr);
assert!(bus.client_meta(99).is_none(), "unknown client has no meta");
let lost = Rc::new(Cell::new(0u128));
let lost_clone = Rc::clone(&lost);
bus.set_client_connection_lost_fn(Rc::new(move |id| lost_clone.set(id)));
bus.notify_client_connection_lost(42);
assert_eq!(lost.get(), 42, "registered lost-fn must fire with the id");
bus.remove_client_meta(42);
assert!(bus.client_meta(42).is_none(), "removed client meta is gone");
}
}