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, SessionId, Shard, ShardCommand, ShardHandle, ShardId,
18 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 let id = ShardId(self.inner.next_shard.fetch_add(1, Ordering::Relaxed));
169 let (handle, wake, mailbox) = mumble_server_runtime_shard::spawn_parts(id);
170 let logic = build(handle.clone());
171 let mut shard = Shard::with_ids(id, logic, self.inner.ids.clone());
172 shard.route_effects(self.effects());
173 let routing = shard.routing();
174 let task = tokio::spawn(async move {
175 let _shard = mumble_server_runtime_shard::run(shard, wake, mailbox).await;
176 });
177
178 write(&self.inner.shards).insert(
179 id,
180 ShardEntry {
181 handle: handle.clone(),
182 plane: ShardPlane { shard: id, routing },
183 task,
184 },
185 );
186 handle
187 }
188
189 pub fn destroy_shard(&self, shard: ShardId, reason: &str) {
197 let refused = self.inner.commands.try_send(RuntimeCommand::Destroy {
198 shard,
199 reason: reason.to_owned(),
200 });
201 if let Err(error) = refused {
202 eprintln!("mumble-server-runtime-gateway: cannot destroy shard {shard:?}: {error}");
203 }
204 }
205
206 fn effects(&self) -> mumble_server_runtime_shard::Effects {
215 let runtime = Arc::downgrade(&self.inner);
216 Arc::new(move |effect| {
217 let Some(inner) = runtime.upgrade() else {
218 return;
219 };
220 match effect {
221 mumble_server_runtime_shard::Effect::Move { connection, to } => {
222 RuntimeHandle { inner }.move_connection(connection, to);
223 }
224 other => {
227 eprintln!("mumble-server-runtime-gateway: no runtime support for {other:?}")
228 }
229 }
230 })
231 }
232
233 pub fn move_connection(&self, connection: ConnectionId, to: ShardId) {
251 let refused = self
252 .inner
253 .commands
254 .try_send(RuntimeCommand::Move { connection, to });
255 if let Err(error) = refused {
256 eprintln!(
257 "mumble-server-runtime-gateway: cannot move {connection:?} to {to:?}: {error}"
258 );
259 }
260 }
261
262 pub fn attach(
272 &self,
273 peer: &Arc<Peer>,
274 shard: ShardId,
275 ) -> Result<oneshot::Receiver<()>, AttachError> {
276 let plane = self.plane(shard).ok_or(AttachError::NoSuchShard(shard))?;
277 let handle = self
278 .shard_handle(shard)
279 .ok_or(AttachError::NoSuchShard(shard))?;
280 peer.move_to(plane);
281
282 let (ready, awaited) = oneshot::channel();
283 handle
284 .send(ShardCommand::Attach {
285 connection: peer.connection(),
286 queue: peer.queue(),
287 cursor: peer.cursor_cell(),
288 held: mumble_server_runtime_shard::ShardView::empty(),
289 ready: Some(ready),
290 })
291 .map_err(|_full| AttachError::Unreachable(shard))?;
292 Ok(awaited)
293 }
294
295 pub fn detach(&self, connection: ConnectionId, shard: ShardId, reason: &str) {
297 let Some(handle) = self.shard_handle(shard) else {
298 return;
299 };
300 if let Err(error) = handle.send(ShardCommand::detach(connection, reason)) {
304 eprintln!(
305 "mumble-server-runtime-gateway: shard {shard:?} did not take a detach: {error}"
306 );
307 }
308 }
309
310 pub fn send(&self, shard: ShardId, command: ShardCommand) -> Result<(), AttachError> {
316 let handle = self
317 .shard_handle(shard)
318 .ok_or(AttachError::NoSuchShard(shard))?;
319 handle
320 .send(command)
321 .map_err(|_full| AttachError::Unreachable(shard))?;
322 Ok(())
323 }
324
325 #[must_use]
327 pub fn status(&self) -> Vec<ShardStatus> {
328 let shards: Vec<ShardId> = read(&self.inner.shards).keys().copied().collect();
329 shards
330 .into_iter()
331 .map(|shard| {
332 let peers = self.inner.peers.on_shard(shard);
333 let cursors: Vec<u64> = peers.iter().map(|peer| peer.cursor()).collect();
334 let worst_lag = match (cursors.iter().min(), cursors.iter().max()) {
335 (Some(behind), Some(ahead)) => ahead.saturating_sub(*behind),
336 _ => 0,
337 };
338 ShardStatus {
339 shard,
340 connections: peers.len(),
341 worst_lag,
342 }
343 })
344 .collect()
345 }
346
347 #[must_use]
348 pub fn shard_handle(&self, shard: ShardId) -> Option<ShardHandle> {
349 read(&self.inner.shards)
350 .get(&shard)
351 .map(|entry| entry.handle.clone())
352 }
353
354 #[must_use]
355 fn plane(&self, shard: ShardId) -> Option<ShardPlane> {
356 read(&self.inner.shards)
357 .get(&shard)
358 .map(|entry| entry.plane.clone())
359 }
360}
361
362#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
364pub enum AttachError {
365 #[error("shard {0:?} does not exist")]
366 NoSuchShard(ShardId),
367 #[error("shard {0:?} is not taking commands")]
368 Unreachable(ShardId),
369}
370
371async fn supervise(inner: Arc<RuntimeInner>, mut mailbox: mpsc::Receiver<RuntimeCommand>) {
373 let mut migrations: JoinSet<()> = JoinSet::new();
377
378 loop {
379 tokio::select! {
380 command = mailbox.recv() => match command {
382 Some(RuntimeCommand::Move { connection, to }) => {
383 let inner = Arc::clone(&inner);
384 migrations.spawn(migrate(inner, connection, to));
385 }
386 Some(RuntimeCommand::Destroy { shard, reason }) => destroy(&inner, shard, &reason),
387 None => break,
389 },
390 Some(_finished) = migrations.join_next(), if !migrations.is_empty() => {}
394 }
395 }
396}
397
398async fn migrate(inner: Arc<RuntimeInner>, connection: ConnectionId, to: ShardId) {
400 let Some(peer) = inner.peers.by_connection(connection) else {
401 return;
403 };
404 let from = peer.shard();
405 if from == to {
406 return;
407 }
408
409 let (source, destination) = {
410 let shards = read(&inner.shards);
411 let source = shards.get(&from).map(|entry| entry.handle.clone());
412 let destination = shards
413 .get(&to)
414 .map(|entry| (entry.handle.clone(), entry.plane.clone()));
415 (source, destination)
416 };
417 let Some((destination, plane)) = destination else {
418 eprintln!(
419 "mumble-server-runtime-gateway: {connection:?} cannot move to {to:?}: no such shard"
420 );
421 return;
422 };
423
424 let held = match source {
428 Some(source) => {
429 let (view, awaited) = oneshot::channel();
430 let sent = source.send(ShardCommand::Detach {
431 connection,
432 reason: format!("moving to {to:?}"),
433 handover: Some(Handover { to, view }),
434 });
435 if sent.is_err() {
436 peer.close();
437 return;
438 }
439 match awaited.await {
440 Ok(held) => held,
441 Err(_dropped) => {
442 peer.close();
443 return;
444 }
445 }
446 }
447 None => {
448 peer.close();
449 return;
450 }
451 };
452
453 peer.move_to(plane);
458
459 let handed = destination.send(ShardCommand::Attach {
460 connection,
461 queue: peer.queue(),
462 cursor: peer.cursor_cell(),
463 held,
464 ready: None,
465 });
466 if handed.is_err() {
467 eprintln!("mumble-server-runtime-gateway: {connection:?} could not be handed to {to:?}");
469 peer.close();
470 }
471}
472
473fn destroy(inner: &Arc<RuntimeInner>, shard: ShardId, reason: &str) {
474 for peer in inner.peers.on_shard(shard) {
477 eprintln!(
478 "mumble-server-runtime-gateway: closing {:?}: its shard was destroyed ({reason})",
479 peer.connection()
480 );
481 peer.close();
482 }
483 write(&inner.shards).remove(&shard);
484}
485
486fn read<T>(lock: &RwLock<T>) -> RwLockReadGuard<'_, T> {
490 lock.read().unwrap_or_else(PoisonError::into_inner)
491}
492
493fn write<T>(lock: &RwLock<T>) -> RwLockWriteGuard<'_, T> {
494 lock.write().unwrap_or_else(PoisonError::into_inner)
495}
496
497impl Drop for ShardEntry {
498 fn drop(&mut self) {
499 self.task.abort();
500 }
501}