steel_core/server/
world_tick_workers.rs1use std::{io, sync::Arc, thread};
2
3use crossbeam::channel::{self, Sender};
4use thiserror::Error;
5use tokio::sync::oneshot;
6
7use crate::world::{World, WorldGameTickTimings};
8
9struct WorldTickRequest {
10 tick_count: u64,
11 runs_normally: bool,
12 response: oneshot::Sender<WorldGameTickTimings>,
13}
14
15struct WorldTickWorker {
16 world_key: Arc<str>,
17 requests: Option<Sender<WorldTickRequest>>,
18 thread: Option<thread::JoinHandle<()>>,
19}
20
21impl WorldTickWorker {
22 fn spawn(index: usize, world: Arc<World>) -> io::Result<Self> {
23 let world_key = Arc::<str>::from(world.key.to_string());
24 let (request_sender, request_receiver) = channel::bounded::<WorldTickRequest>(1);
25 let thread = thread::Builder::new()
26 .name(format!("world-tick-{index}"))
27 .spawn(move || {
28 while let Ok(request) = request_receiver.recv() {
29 let timings = world.tick_game(request.tick_count, request.runs_normally);
30 let _ = request.response.send(timings);
31 }
32 })?;
33
34 Ok(Self {
35 world_key,
36 requests: Some(request_sender),
37 thread: Some(thread),
38 })
39 }
40
41 fn start_tick(
42 &self,
43 tick_count: u64,
44 runs_normally: bool,
45 ) -> Result<oneshot::Receiver<WorldGameTickTimings>, WorldTickWorkerError> {
46 let (response, receiver) = oneshot::channel();
47 let Some(requests) = &self.requests else {
48 return Err(WorldTickWorkerError::Unavailable {
49 world: Arc::clone(&self.world_key),
50 });
51 };
52 requests
53 .send(WorldTickRequest {
54 tick_count,
55 runs_normally,
56 response,
57 })
58 .map_err(|_| WorldTickWorkerError::Unavailable {
59 world: Arc::clone(&self.world_key),
60 })?;
61 Ok(receiver)
62 }
63}
64
65impl Drop for WorldTickWorker {
66 fn drop(&mut self) {
67 drop(self.requests.take());
68 let Some(thread) = self.thread.take() else {
69 return;
70 };
71 if thread.join().is_err() {
72 log::error!(
73 "World tick worker for {} panicked during execution",
74 self.world_key
75 );
76 }
77 }
78}
79
80#[derive(Debug, Error)]
81pub(super) enum WorldTickWorkerError {
82 #[error("world tick worker for {world} is unavailable")]
83 Unavailable { world: Arc<str> },
84 #[error("world tick worker for {world} stopped without returning timings")]
85 MissingResponse { world: Arc<str> },
86}
87
88pub(super) struct WorldTickWorkers {
89 workers: Vec<WorldTickWorker>,
90}
91
92impl WorldTickWorkers {
93 pub(super) fn spawn<'a>(worlds: impl IntoIterator<Item = &'a Arc<World>>) -> io::Result<Self> {
94 let mut workers = Vec::new();
95 for (index, world) in worlds.into_iter().enumerate() {
96 workers.push(WorldTickWorker::spawn(index, Arc::clone(world))?);
97 }
98 Ok(Self { workers })
99 }
100
101 pub(super) async fn tick_all(
102 &self,
103 tick_count: u64,
104 runs_normally: bool,
105 ) -> Result<Vec<WorldGameTickTimings>, WorldTickWorkerError> {
106 let mut responses = Vec::with_capacity(self.workers.len());
107 for worker in &self.workers {
108 responses.push(worker.start_tick(tick_count, runs_normally)?);
109 }
110
111 let mut timings = Vec::with_capacity(responses.len());
112 for (worker, response) in self.workers.iter().zip(responses) {
113 timings.push(
114 response
115 .await
116 .map_err(|_| WorldTickWorkerError::MissingResponse {
117 world: Arc::clone(&worker.world_key),
118 })?,
119 );
120 }
121 Ok(timings)
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use futures::executor::block_on;
128 use tokio::sync::oneshot::error::TryRecvError;
129
130 use super::WorldTickWorkers;
131 use crate::test_support::test_domain;
132
133 #[test]
134 fn persistent_workers_tick_every_world_across_boundaries() {
135 let worlds = test_domain("workers", &["primary", "derived"]);
136 let first = worlds.default_world("workers").expect("primary");
137 let second = worlds
138 .get(&steel_utils::Identifier::new_static("workers", "derived"))
139 .expect("derived");
140 let Ok(workers) = WorldTickWorkers::spawn([first, second]) else {
141 panic!("world tick workers should start");
142 };
143
144 worlds.advance_domain_game_times();
145 let Ok(first_tick) = block_on(workers.tick_all(1, true)) else {
146 panic!("world tick workers should finish the first tick");
147 };
148 assert_eq!(first_tick.len(), 2);
149 assert_eq!(first.game_time(), 1);
150 assert_eq!(second.game_time(), 1);
151
152 worlds.advance_domain_game_times();
153 let Ok(second_tick) = block_on(workers.tick_all(2, true)) else {
154 panic!("world tick workers should finish the second tick");
155 };
156 assert_eq!(second_tick.len(), 2);
157 assert_eq!(first.game_time(), 2);
158 assert_eq!(second.game_time(), 2);
159 }
160
161 #[test]
162 fn shared_time_is_published_while_primary_worker_is_delayed() {
163 let worlds = test_domain("delayed", &["primary", "derived"]);
164 let primary = worlds.default_world("delayed").expect("primary");
165 let derived = worlds
166 .get(&steel_utils::Identifier::new_static("delayed", "derived"))
167 .expect("derived");
168 let workers = WorldTickWorkers::spawn([primary, derived]).expect("workers");
169 for tick in 1..=2 {
170 worlds.advance_domain_game_times();
171 let guard = primary.level_data.write();
174 let mut primary_response = workers.workers[0]
175 .start_tick(tick, true)
176 .expect("dispatch primary");
177 let derived_response = workers.workers[1]
178 .start_tick(tick, true)
179 .expect("dispatch derived");
180 block_on(derived_response).expect("derived completes while primary is delayed");
181 assert!(matches!(
182 primary_response.try_recv(),
183 Err(TryRecvError::Empty)
184 ));
185 for world in worlds.values() {
186 assert_eq!(world.game_time(), tick as i64);
187 }
188 drop(guard);
189 block_on(primary_response).expect("primary completes after release");
190 for world in worlds.values() {
191 assert_eq!(world.game_time(), tick as i64);
192 }
193 }
194 }
195}