rws_gateway_api/gateway_api/mesh/
config.rs1use std::sync::{Arc, Mutex};
12use std::time::Duration;
13
14use crate::application::Application;
15use crate::request::Request;
16use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
17use crate::server::ConnectionInfo;
18
19use super::MeshSnapshot;
20
21pub const PATH: &str = "/mesh/v1/snapshot";
23pub const VERSION_HEADER: &str = "X-Rws-Mesh-Version";
25pub const DEFAULT_PORT: u16 = 15010;
27pub const LONG_POLL: Duration = Duration::from_secs(30);
29
30#[derive(Clone)]
32pub struct Publisher {
33 current: Arc<Mutex<(u64, Arc<String>)>>,
34 changed: tokio::sync::watch::Sender<u64>,
35}
36
37impl Default for Publisher {
38 fn default() -> Self {
39 Publisher { current: Arc::new(Mutex::new((0, Arc::new(String::new())))), changed: tokio::sync::watch::channel(0).0 }
40 }
41}
42
43impl Publisher {
44 pub fn new() -> Publisher {
45 Publisher::default()
46 }
47
48 pub fn publish(&self, mesh: &MeshSnapshot) {
53 let Ok(body) = serde_json::to_string(mesh) else { return };
54 let version = content_version(&body);
55 let mut current = self.current.lock().unwrap_or_else(|e| e.into_inner());
56 if current.0 == version {
57 return;
58 }
59 *current = (version, Arc::new(body));
60 self.changed.send_replace(version);
61 }
62
63 pub fn current(&self) -> (u64, Arc<String>) {
65 self.current.lock().unwrap_or_else(|e| e.into_inner()).clone()
66 }
67
68 fn response(&self) -> Response {
69 let (version, body) = self.current();
70 if version == 0 {
71 return Response::text(STATUS_CODE_REASON_PHRASE.n503_service_unavailable, "no snapshot yet");
72 }
73 let mut response = Response::text(STATUS_CODE_REASON_PHRASE.n200_ok, &body);
74 response.headers.retain(|h| !h.name.eq_ignore_ascii_case("content-type"));
75 response.headers.push(crate::header::Header::new("Content-Type", "application/json"));
76 response.headers.push(crate::header::Header::new(VERSION_HEADER, version.to_string()));
77 response.headers.push(crate::header::Header::new("Cache-Control", "no-store"));
78 response
79 }
80
81 async fn wait_for_change(&self, seen: u64) {
83 let mut changed = self.changed.subscribe();
84 if *changed.borrow_and_update() != seen {
85 return;
86 }
87 let _ = tokio::time::timeout(LONG_POLL, changed.changed()).await;
88 }
89}
90
91pub fn content_version(body: &str) -> u64 {
94 let digest = crate::core::sha256::sha256(body.as_bytes());
95 u64::from_be_bytes(digest[..8].try_into().unwrap_or_default()).max(1)
96}
97
98pub fn requested_version(uri: &str) -> Option<u64> {
100 let query = uri.split_once('?')?.1;
101 query.split('&').find_map(|pair| pair.strip_prefix("version=")?.parse().ok())
102}
103
104fn is_snapshot_request(request: &Request) -> bool {
105 request.method == "GET" && request.request_uri.split('?').next() == Some(PATH)
106}
107
108impl Application for Publisher {
109 fn execute(&self, request: &Request, _connection: &ConnectionInfo) -> Result<Response, String> {
110 if !is_snapshot_request(request) {
111 return Ok(Response::text(STATUS_CODE_REASON_PHRASE.n404_not_found, "not found"));
112 }
113 Ok(self.response())
114 }
115
116 fn execute_async<'a>(&'a self, request: &'a Request, _connection: &'a ConnectionInfo) -> Option<crate::application::AsyncResponse<'a>> {
117 if !is_snapshot_request(request) {
118 return None;
119 }
120 Some(Box::pin(async move {
121 if let Some(seen) = requested_version(&request.request_uri) {
122 self.wait_for_change(seen).await;
123 }
124 Ok(self.response())
125 }))
126 }
127}
128
129pub async fn serve(publisher: Publisher, port: u16) {
131 let listener = match tokio::net::TcpListener::bind(("0.0.0.0", port)).await {
132 Ok(listener) => listener,
133 Err(e) => {
134 eprintln!("[gateway-api] mesh: can't serve the configuration on port {port}: {e}");
135 return;
136 }
137 };
138 loop {
139 let Ok((stream, peer)) = listener.accept().await else {
140 tokio::time::sleep(Duration::from_millis(50)).await;
141 continue;
142 };
143 tokio::spawn(crate::server::serve_plain_connection(stream, peer, publisher.clone()));
144 }
145}