mumble_server_runtime_gateway/
runtime.rs1use std::collections::HashMap;
13use std::sync::atomic::{AtomicU64, Ordering};
14use std::sync::{Arc, PoisonError, RwLock, RwLockReadGuard, RwLockWriteGuard};
15
16use mumble_server_runtime_shard::{
17 ConnectionId, Handover, Occupant, ReconcileReport, SessionId, Shard, ShardCommand, ShardHandle,
18 ShardId, ShardLogic, SharedIds,
19};
20use tokio::sync::{mpsc, oneshot};
21use tokio::task::{JoinHandle, JoinSet};
22
23use crate::peer::{Peer, Peers, ShardPlane};
24
25const MAILBOX_DEPTH: usize = 256;
27
28#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct ShardStatus {
31 pub shard: ShardId,
32 pub connections: usize,
33 pub worst_lag: u64,
37}
38
39#[derive(Debug)]
41enum RuntimeCommand {
42 Move {
43 connection: ConnectionId,
44 to: ShardId,
45 },
46 Destroy {
47 shard: ShardId,
48 reason: String,
49 },
50}
51
52struct ShardEntry {
54 handle: ShardHandle,
55 plane: ShardPlane,
56 task: JoinHandle<()>,
59}
60
61struct RuntimeInner {
62 ids: SharedIds,
63 peers: Arc<Peers>,
64 shards: RwLock<HashMap<ShardId, ShardEntry>>,
65 next_shard: AtomicU64,
66 next_connection: AtomicU64,
67 commands: mpsc::Sender<RuntimeCommand>,
68}
69
70pub struct Runtime {
75 handle: RuntimeHandle,
76 supervisor: JoinHandle<()>,
77}
78
79impl Runtime {
80 #[must_use]
82 pub fn start() -> Runtime {
83 let (commands, mailbox) = mpsc::channel(MAILBOX_DEPTH);
84 let inner = Arc::new(RuntimeInner {
85 ids: SharedIds::new(),
86 peers: Arc::new(Peers::new()),
87 shards: RwLock::new(HashMap::new()),
88 next_shard: AtomicU64::new(1),
89 next_connection: AtomicU64::new(1),
90 commands,
91 });
92 let supervisor = tokio::spawn(supervise(Arc::clone(&inner), mailbox));
93 Runtime {
94 handle: RuntimeHandle { inner },
95 supervisor,
96 }
97 }
98
99 #[must_use]
100 pub fn handle(&self) -> RuntimeHandle {
101 self.handle.clone()
102 }
103
104 pub fn shutdown(self) {
106 self.supervisor.abort();
107 write(&self.handle.inner.shards).clear();
108 }
109}
110
111#[derive(Clone)]
113pub struct RuntimeHandle {
114 inner: Arc<RuntimeInner>,
115}
116
117impl std::fmt::Debug for RuntimeHandle {
118 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119 formatter
120 .debug_struct("RuntimeHandle")
121 .field("shards", &read(&self.inner.shards).len())
122 .field("connections", &self.inner.peers.len())
123 .finish()
124 }
125}
126
127impl RuntimeHandle {
128 #[must_use]
134 pub fn ids(&self) -> &SharedIds {
135 &self.inner.ids
136 }
137
138 #[must_use]
139 pub fn peers(&self) -> &Arc<Peers> {
140 &self.inner.peers
141 }
142
143 #[must_use]
145 pub fn next_connection(&self) -> ConnectionId {
146 ConnectionId(self.inner.next_connection.fetch_add(1, Ordering::Relaxed))
147 }
148
149 pub fn session_for(
156 &self,
157 connection: ConnectionId,
158 ) -> Result<SessionId, mumble_server_runtime_shard::Exhausted> {
159 self.inner.ids.session(Occupant::Connection(connection))
160 }
161
162 pub fn create_shard<L: ShardLogic>(&self, build: impl FnOnce(ShardHandle) -> L) -> ShardHandle {
168 self.create_shard_with_reports(build, |_report| {})
169 }
170
171 pub fn create_shard_with_reports<L, O>(
177 &self,
178 build: impl FnOnce(ShardHandle) -> L,
179 observer: O,
180 ) -> ShardHandle
181 where
182 L: ShardLogic,
183 O: FnMut(&ReconcileReport) + Send + 'static,
184 {
185 let id = ShardId(self.inner.next_shard.fetch_add(1, Ordering::Relaxed));
186 let (handle, wake, mailbox) = mumble_server_runtime_shard::spawn_parts(id);
187 let logic = build(handle.clone());
188 let mut shard = Shard::with_ids(id, logic, self.inner.ids.clone());
189 shard.route_effects(self.effects());
190 let routing = shard.routing();
191 let task = tokio::spawn(async move {
192 let _shard =
193 mumble_server_runtime_shard::run_with_reports(shard, wake, mailbox, observer).await;
194 });
195
196 write(&self.inner.shards).insert(
197 id,
198 ShardEntry {
199 handle: handle.clone(),
200 plane: ShardPlane { shard: id, routing },
201 task,
202 },
203 );
204 handle
205 }
206
207 pub fn destroy_shard(&self, shard: ShardId, reason: &str) {
215 let refused = self.inner.commands.try_send(RuntimeCommand::Destroy {
216 shard,
217 reason: reason.to_owned(),
218 });
219 if let Err(error) = refused {
220 eprintln!("mumble-server-runtime-gateway: cannot destroy shard {shard:?}: {error}");
221 }
222 }
223
224 fn effects(&self) -> mumble_server_runtime_shard::Effects {
233 let runtime = Arc::downgrade(&self.inner);
234 Arc::new(move |effect| {
235 let Some(inner) = runtime.upgrade() else {
236 return;
237 };
238 match effect {
239 mumble_server_runtime_shard::Effect::Move { connection, to } => {
240 RuntimeHandle { inner }.move_connection(connection, to);
241 }
242 other => {
245 eprintln!("mumble-server-runtime-gateway: no runtime support for {other:?}")
246 }
247 }
248 })
249 }
250
251 pub fn move_connection(&self, connection: ConnectionId, to: ShardId) {
269 let refused = self
270 .inner
271 .commands
272 .try_send(RuntimeCommand::Move { connection, to });
273 if let Err(error) = refused {
274 eprintln!(
275 "mumble-server-runtime-gateway: cannot move {connection:?} to {to:?}: {error}"
276 );
277 }
278 }
279
280 pub fn attach(
290 &self,
291 peer: &Arc<Peer>,
292 shard: ShardId,
293 ) -> Result<oneshot::Receiver<()>, AttachError> {
294 let plane = self.plane(shard).ok_or(AttachError::NoSuchShard(shard))?;
295 let handle = self
296 .shard_handle(shard)
297 .ok_or(AttachError::NoSuchShard(shard))?;
298 peer.move_to(plane);
299
300 let (ready, awaited) = oneshot::channel();
301 handle
302 .send(ShardCommand::Attach {
303 connection: peer.connection(),
304 queue: peer.queue(),
305 cursor: peer.cursor_cell(),
306 held: mumble_server_runtime_shard::ShardView::empty(),
307 ready: Some(ready),
308 })
309 .map_err(|_full| AttachError::Unreachable(shard))?;
310 Ok(awaited)
311 }
312
313 pub fn detach(&self, connection: ConnectionId, shard: ShardId, reason: &str) {
315 let Some(handle) = self.shard_handle(shard) else {
316 return;
317 };
318 if let Err(error) = handle.send(ShardCommand::detach(connection, reason)) {
322 eprintln!(
323 "mumble-server-runtime-gateway: shard {shard:?} did not take a detach: {error}"
324 );
325 }
326 }
327
328 pub fn send(&self, shard: ShardId, command: ShardCommand) -> Result<(), AttachError> {
334 let handle = self
335 .shard_handle(shard)
336 .ok_or(AttachError::NoSuchShard(shard))?;
337 handle
338 .send(command)
339 .map_err(|_full| AttachError::Unreachable(shard))?;
340 Ok(())
341 }
342
343 #[must_use]
345 pub fn status(&self) -> Vec<ShardStatus> {
346 let shards: Vec<ShardId> = read(&self.inner.shards).keys().copied().collect();
347 shards
348 .into_iter()
349 .map(|shard| {
350 let peers = self.inner.peers.on_shard(shard);
351 let cursors: Vec<u64> = peers.iter().map(|peer| peer.cursor()).collect();
352 let worst_lag = match (cursors.iter().min(), cursors.iter().max()) {
353 (Some(behind), Some(ahead)) => ahead.saturating_sub(*behind),
354 _ => 0,
355 };
356 ShardStatus {
357 shard,
358 connections: peers.len(),
359 worst_lag,
360 }
361 })
362 .collect()
363 }
364
365 #[must_use]
366 pub fn shard_handle(&self, shard: ShardId) -> Option<ShardHandle> {
367 read(&self.inner.shards)
368 .get(&shard)
369 .map(|entry| entry.handle.clone())
370 }
371
372 #[must_use]
373 fn plane(&self, shard: ShardId) -> Option<ShardPlane> {
374 read(&self.inner.shards)
375 .get(&shard)
376 .map(|entry| entry.plane.clone())
377 }
378}
379
380#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
382pub enum AttachError {
383 #[error("shard {0:?} does not exist")]
384 NoSuchShard(ShardId),
385 #[error("shard {0:?} is not taking commands")]
386 Unreachable(ShardId),
387}
388
389async fn supervise(inner: Arc<RuntimeInner>, mut mailbox: mpsc::Receiver<RuntimeCommand>) {
391 let mut migrations: JoinSet<()> = JoinSet::new();
395
396 loop {
397 tokio::select! {
398 command = mailbox.recv() => match command {
400 Some(RuntimeCommand::Move { connection, to }) => {
401 let inner = Arc::clone(&inner);
402 migrations.spawn(migrate(inner, connection, to));
403 }
404 Some(RuntimeCommand::Destroy { shard, reason }) => destroy(&inner, shard, &reason),
405 None => break,
407 },
408 Some(_finished) = migrations.join_next(), if !migrations.is_empty() => {}
412 }
413 }
414}
415
416async fn migrate(inner: Arc<RuntimeInner>, connection: ConnectionId, to: ShardId) {
418 let Some(peer) = inner.peers.by_connection(connection) else {
419 return;
421 };
422 let from = peer.shard();
423 if from == to {
424 return;
425 }
426
427 let (source, destination) = {
428 let shards = read(&inner.shards);
429 let source = shards.get(&from).map(|entry| entry.handle.clone());
430 let destination = shards
431 .get(&to)
432 .map(|entry| (entry.handle.clone(), entry.plane.clone()));
433 (source, destination)
434 };
435 let Some((destination, plane)) = destination else {
436 eprintln!(
437 "mumble-server-runtime-gateway: {connection:?} cannot move to {to:?}: no such shard"
438 );
439 return;
440 };
441
442 let held = match source {
446 Some(source) => {
447 let (view, awaited) = oneshot::channel();
448 let sent = source.send(ShardCommand::Detach {
449 connection,
450 reason: format!("moving to {to:?}"),
451 handover: Some(Handover { to, view }),
452 });
453 if sent.is_err() {
454 peer.close();
455 return;
456 }
457 match awaited.await {
458 Ok(held) => held,
459 Err(_dropped) => {
460 peer.close();
461 return;
462 }
463 }
464 }
465 None => {
466 peer.close();
467 return;
468 }
469 };
470
471 peer.move_to(plane);
476
477 let handed = destination.send(ShardCommand::Attach {
478 connection,
479 queue: peer.queue(),
480 cursor: peer.cursor_cell(),
481 held,
482 ready: None,
483 });
484 if handed.is_err() {
485 eprintln!("mumble-server-runtime-gateway: {connection:?} could not be handed to {to:?}");
487 peer.close();
488 }
489}
490
491fn destroy(inner: &Arc<RuntimeInner>, shard: ShardId, reason: &str) {
492 for peer in inner.peers.on_shard(shard) {
495 eprintln!(
496 "mumble-server-runtime-gateway: closing {:?}: its shard was destroyed ({reason})",
497 peer.connection()
498 );
499 peer.close();
500 }
501 write(&inner.shards).remove(&shard);
502}
503
504fn read<T>(lock: &RwLock<T>) -> RwLockReadGuard<'_, T> {
508 lock.read().unwrap_or_else(PoisonError::into_inner)
509}
510
511fn write<T>(lock: &RwLock<T>) -> RwLockWriteGuard<'_, T> {
512 lock.write().unwrap_or_else(PoisonError::into_inner)
513}
514
515impl Drop for ShardEntry {
516 fn drop(&mut self) {
517 self.task.abort();
518 }
519}