Add State Saving to PingPong example.

This commit is contained in:
Alejandro Cabeza Romero 2024-12-23 19:04:18 +01:00
parent 1dbf29be79
commit 3645d5d9d6
No known key found for this signature in database
GPG Key ID: DA3D14AE478030FD
8 changed files with 136 additions and 29 deletions

1
examples/ping_pong/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
saved_states/

View File

@ -7,4 +7,9 @@ edition = "2021"
overwatch-rs = { path = "../../overwatch-rs" }
overwatch-derive = { path = "../../overwatch-derive" }
async-trait = "0.1.83"
tokio = { version = "1", features = ["macros"] }
tokio = { version = "1.42.0", features = ["macros"] }
thiserror = "2.0.8"
serde_json = "1.0.134"
serde = { version = "1.0.216", features = ["derive"] }
const_format = "0.2.34"
project-root = "0.2.2"

View File

@ -1,17 +1,29 @@
# PingPong
This example project demonstrates how to set up a basic Overwatch application in Rust.
### Behaviour
This project demonstrates a simple communication pattern between two services.
1. Every second, the `Ping` service sends a message to the `Pong` service.
2. The `Pong` service receives the message and prints it to the console. Afterwards, it sends a message back to the `Ping` service.
2. The `Pong` service receives the message and prints it to the console. Afterwards, it sends a message back to the
`Ping` service.
3. The `Ping` service receives the message and prints it to the console.
- After each received `Pong` message, a counter is incremented and saved into a file from which it can be restored
when the application is restarted.
### Features
- **Services**: Shows how to define and register services within an Overwatch application.
- **Message Passing**: Demonstrates communication between services using the relay.
- **Messages**: Demonstrates communication between services using the relay.
- **Settings**: Shows how to define and access settings within an Overwatch application.
- **States**: Demonstrates how to use the state to store data within an Overwatch application.
- **StateOperators**: Shows how to use the state operator to interact with the state.
- In this example, in combination with **State**, it shows how to save and load data from the state.
### About
This project serves as a barebones template to get started with the Overwatch library.
This project serves as a barebones template to get started with the Overwatch library.
It provides the foundational structure for creating more complex applications.

View File

@ -5,10 +5,14 @@ use overwatch_rs::services::handle::ServiceHandle;
// Internal
use crate::service_ping::PingService;
use crate::service_pong::PongService;
use crate::settings::PingSettings;
mod messages;
mod operators;
mod service_ping;
mod service_pong;
mod settings;
mod states;
#[derive(Services)]
struct PingPong {
@ -16,8 +20,19 @@ struct PingPong {
pong: ServiceHandle<PongService>,
}
const PING_STATE_SAVE_PATH: &str = const_format::formatcp!(
"{}/saved_states/ping_state.json",
env!("CARGO_MANIFEST_DIR")
);
fn main() {
let ping_pong_settings = PingPongServiceSettings { ping: (), pong: () };
let ping_settings = PingSettings {
state_save_path: String::from(PING_STATE_SAVE_PATH),
};
let ping_pong_settings = PingPongServiceSettings {
ping: ping_settings,
pong: (),
};
let ping_pong =
OverwatchRunner::<PingPong>::run(ping_pong_settings, None).expect("OverwatchRunner failed");
ping_pong.wait_finished();

View File

@ -0,0 +1,27 @@
// STD
use std::fmt::Debug;
// Crates
use overwatch_rs::services::state::{ServiceState, StateOperator};
// Internal
use crate::states::PingState;
#[derive(Debug, Clone)]
pub struct StateSaveOperator {
save_path: String,
}
#[async_trait::async_trait]
impl StateOperator for StateSaveOperator {
type StateInput = PingState;
fn from_settings(settings: <Self::StateInput as ServiceState>::Settings) -> Self {
Self {
save_path: settings.state_save_path,
}
}
async fn run(&mut self, state: Self::StateInput) {
let json_state = serde_json::to_string(&state).expect("Failed to serialize state");
std::fs::write(&self.save_path, json_state).unwrap();
}
}

View File

@ -1,23 +1,26 @@
// Crates
use overwatch_rs::services::handle::ServiceStateHandle;
use overwatch_rs::services::state::{NoOperator, NoState};
use overwatch_rs::services::{ServiceCore, ServiceData, ServiceId};
use overwatch_rs::DynError;
use std::time::Duration;
use tokio::time::sleep;
// Internal
use crate::messages::{PingMessage, PongMessage};
use crate::operators::StateSaveOperator;
use crate::service_pong::PongService;
use crate::settings::PingSettings;
use crate::states::PingState;
pub struct PingService {
service_state_handle: ServiceStateHandle<Self>,
initial_state: <Self as ServiceData>::State,
}
impl ServiceData for PingService {
const SERVICE_ID: ServiceId = "ping";
type Settings = ();
type State = NoState<Self::Settings>;
type StateOperator = NoOperator<Self::State>;
type Settings = PingSettings;
type State = PingState;
type StateOperator = StateSaveOperator;
type Message = PingMessage;
}
@ -25,16 +28,18 @@ impl ServiceData for PingService {
impl ServiceCore for PingService {
fn init(
service_state_handle: ServiceStateHandle<Self>,
_initial_state: Self::State,
initial_state: Self::State,
) -> Result<Self, DynError> {
Ok(Self {
service_state_handle,
initial_state,
})
}
async fn run(self) -> Result<(), DynError> {
let Self {
service_state_handle,
initial_state,
} = self;
let mut inbound_relay = service_state_handle.inbound_relay;
@ -44,28 +49,31 @@ impl ServiceCore for PingService {
.connect()
.await?;
let mut pong_count = 0;
let Self::State { mut pong_count } = initial_state;
loop {
tokio::select! {
_ = sleep(Duration::from_secs(1)) => {
println!("Sending Ping");
pong_outbound_relay.send(PongMessage::Ping).await.unwrap();
}
Some(message) = inbound_relay.recv() => {
match message {
PingMessage::Pong => {
println!("Received Pong");
pong_count += 1;
}
}
}
true = async {
pong_count >= 5
} => {
println!("Received {} Pongs. Exiting...", pong_count);
break;
}
_ = sleep(Duration::from_secs(1)) => {
println!("Sending Ping");
pong_outbound_relay.send(PongMessage::Ping).await.unwrap();
}
Some(message) = inbound_relay.recv() => {
match message {
PingMessage::Pong => {
pong_count += 1;
service_state_handle.state_updater.update(
Self::State { pong_count }
);
println!("Received Pong. Total: {}", pong_count);
}
}
}
true = async {
pong_count >= 30
} => {
println!("Received {} Pongs. Exiting...", pong_count);
break;
}
}
}

View File

@ -0,0 +1,4 @@
#[derive(Debug, Clone)]
pub struct PingSettings {
pub(crate) state_save_path: String,
}

View File

@ -0,0 +1,35 @@
// STD
use std::io;
// Crates
use overwatch_rs::services::state::ServiceState;
use serde::{Deserialize, Serialize};
// Internal
use crate::settings::PingSettings;
#[derive(thiserror::Error, Debug)]
pub enum PingStateError {}
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub struct PingState {
pub pong_count: u32,
}
impl PingState {
fn load_saved_state(save_path: &str) -> io::Result<Self> {
let json_state = std::fs::read(save_path)?;
let state = serde_json::from_slice(json_state.as_slice())
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
Ok(state)
}
}
impl ServiceState for PingState {
type Settings = PingSettings;
type Error = PingStateError;
fn from_settings(settings: &Self::Settings) -> Result<Self, Self::Error> {
let state = Self::load_saved_state(settings.state_save_path.as_str())
.unwrap_or_else(|_error| Self::default());
Ok(state)
}
}