Skip to main content

steel_core/server/
world_tick_workers.rs

1use 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            // Holding primary level data delays its world-local time phase, while
172            // the shared counter remains readable without this lock.
173            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}