Skip to content

Commit e026cbc

Browse files
committed
feat: add monitor for debugging
1 parent ecab9ea commit e026cbc

File tree

8 files changed

+338
-33
lines changed

8 files changed

+338
-33
lines changed

modules/clock/src/clock.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//! Generates regular clock.tick events
33
44
use anyhow::Result;
5-
use caryatid_sdk::{module, Context, MessageBounds, Module};
5+
use caryatid_sdk::{module, Context, MessageBounds};
66
use chrono::{DateTime, Utc};
77
use config::Config;
88
use std::sync::Arc;
@@ -70,6 +70,7 @@ impl<M: From<ClockTickMessage> + MessageBounds> Clock<M> {
7070
mod tests {
7171
use super::*;
7272
use caryatid_sdk::mock_bus::MockBus;
73+
use caryatid_sdk::Module;
7374
use config::{Config, FileFormat};
7475
use tokio::sync::{mpsc, watch::Sender, Notify};
7576
use tokio::time::{timeout, Duration};
@@ -99,6 +100,7 @@ mod tests {
99100
struct TestSetup {
100101
module: Arc<dyn Module<Message>>,
101102
context: Arc<Context<Message>>,
103+
startup_watch: Sender<bool>,
102104
}
103105

104106
impl TestSetup {
@@ -120,12 +122,9 @@ mod tests {
120122
// Create mock bus
121123
let bus = Arc::new(MockBus::<Message>::new(&config));
122124

125+
let startup_watch = Sender::new(false);
123126
// Create a context
124-
let context = Arc::new(Context::new(
125-
config.clone(),
126-
bus.clone(),
127-
Sender::<bool>::new(false),
128-
));
127+
let context = Arc::new(Context::new(config.clone(), bus, startup_watch.subscribe()));
129128

130129
// Create the clock
131130
let clock = Clock::<Message> {
@@ -136,11 +135,12 @@ mod tests {
136135
Self {
137136
module: Arc::new(clock),
138137
context,
138+
startup_watch,
139139
}
140140
}
141141

142142
fn start(&self) {
143-
let _ = self.context.startup_watch.send(true);
143+
let _ = self.startup_watch.send(true);
144144
}
145145
}
146146

modules/rest_server/src/rest_server.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//! Provides a REST endpoint which integrates with the message bus
33
44
use anyhow::Result;
5-
use caryatid_sdk::{module, Context, MessageBounds, Module};
5+
use caryatid_sdk::{module, Context, MessageBounds};
66
use config::Config;
77
use std::{collections::HashMap, sync::Arc};
88
use tracing::{error, info};
@@ -176,6 +176,7 @@ impl<M: From<RESTRequest> + GetRESTResponse + MessageBounds> RESTServer<M> {
176176
mod tests {
177177
use super::*;
178178
use caryatid_sdk::mock_bus::MockBus;
179+
use caryatid_sdk::Module;
179180
use config::{Config, FileFormat};
180181
use futures::future;
181182
use hyper::Client;
@@ -227,6 +228,7 @@ mod tests {
227228
struct TestSetup {
228229
module: Arc<dyn Module<Message>>,
229230
context: Arc<Context<Message>>,
231+
startup_watch: Sender<bool>,
230232
}
231233

232234
impl TestSetup {
@@ -249,10 +251,11 @@ mod tests {
249251
let mock_bus = Arc::new(MockBus::<Message>::new(&config));
250252

251253
// Create a context
254+
let startup_watch = Sender::new(false);
252255
let context = Arc::new(Context::new(
253256
config.clone(),
254-
mock_bus.clone(),
255-
Sender::<bool>::new(false),
257+
mock_bus,
258+
startup_watch.subscribe(),
256259
));
257260

258261
// Create the server
@@ -264,11 +267,12 @@ mod tests {
264267
Self {
265268
module: Arc::new(rest_server),
266269
context,
270+
startup_watch,
267271
}
268272
}
269273

270274
fn start(&self) {
271-
let _ = self.context.startup_watch.send(true);
275+
let _ = self.startup_watch.send(true);
272276
}
273277
}
274278

process/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ anyhow = "1.0"
1414
tokio = { version = "1", features = ["full"] }
1515
config = "0.15.11"
1616
minicbor-serde = { version = "0.6", features = ["alloc"] }
17+
dashmap = "6"
1718
tracing = "0.1.40"
1819
serde = "1.0.210"
1920
serde_json = "1.0"

process/src/monitor.rs

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
use std::{
2+
collections::BTreeMap,
3+
future::Future,
4+
path::PathBuf,
5+
sync::Arc,
6+
task::Poll,
7+
time::{Duration, Instant},
8+
};
9+
10+
use anyhow::Result;
11+
use async_trait::async_trait;
12+
use caryatid_sdk::{MessageBounds, MessageBus, Subscription, SubscriptionBounds};
13+
use dashmap::DashMap;
14+
use futures::{future::BoxFuture, FutureExt};
15+
use serde::{Deserialize, Serialize};
16+
use tokio::{fs, time};
17+
18+
#[derive(Default, Clone)]
19+
struct ReadStreamState {
20+
read: u64,
21+
pending_since: Option<Instant>,
22+
}
23+
24+
#[derive(Default, Clone)]
25+
struct WriteStreamState {
26+
written: u64,
27+
pending_since: Option<Instant>,
28+
}
29+
30+
#[derive(Default, Clone)]
31+
struct ModuleState {
32+
reads: DashMap<String, ReadStreamState>,
33+
writes: DashMap<String, WriteStreamState>,
34+
}
35+
36+
#[derive(Serialize)]
37+
struct SerializedReadStreamState {
38+
read: u64,
39+
#[serde(skip_serializing_if = "Option::is_none")]
40+
unread: Option<u64>,
41+
#[serde(skip_serializing_if = "Option::is_none")]
42+
pending_for: Option<String>,
43+
}
44+
45+
#[derive(Serialize)]
46+
struct SerializedWriteStreamState {
47+
written: u64,
48+
#[serde(skip_serializing_if = "Option::is_none")]
49+
pending_for: Option<String>,
50+
}
51+
52+
#[derive(Serialize)]
53+
struct SerializedModuleState {
54+
reads: BTreeMap<String, SerializedReadStreamState>,
55+
writes: BTreeMap<String, SerializedWriteStreamState>,
56+
}
57+
58+
const fn default_frequency() -> Duration {
59+
Duration::from_secs(5)
60+
}
61+
62+
#[derive(Deserialize)]
63+
pub struct MonitorConfig {
64+
output: PathBuf,
65+
#[serde(default = "default_frequency")]
66+
frequency: Duration,
67+
}
68+
69+
pub struct Monitor {
70+
modules: BTreeMap<String, Arc<ModuleState>>,
71+
stream_writes: Arc<DashMap<String, u64>>,
72+
output_path: PathBuf,
73+
write_frequency: Duration,
74+
}
75+
impl Monitor {
76+
pub fn new(config: MonitorConfig) -> Self {
77+
Self {
78+
modules: BTreeMap::new(),
79+
stream_writes: Arc::new(DashMap::new()),
80+
output_path: config.output,
81+
write_frequency: config.frequency,
82+
}
83+
}
84+
85+
pub fn spy_on_bus<M: MessageBounds>(
86+
&mut self,
87+
module_name: &str,
88+
message_bus: Arc<dyn MessageBus<M>>,
89+
) -> Arc<dyn MessageBus<M>> {
90+
let state = Arc::new(ModuleState::default());
91+
self.modules.insert(module_name.to_string(), state.clone());
92+
93+
Arc::new(MonitorBus {
94+
inner: message_bus,
95+
stream_writes: self.stream_writes.clone(),
96+
state,
97+
})
98+
}
99+
100+
pub async fn monitor(self) {
101+
loop {
102+
time::sleep(self.write_frequency).await;
103+
let now = Instant::now();
104+
let state = self
105+
.modules
106+
.iter()
107+
.map(|(name, state)| {
108+
let reads = state
109+
.reads
110+
.iter()
111+
.map(|kvp| {
112+
let (topic, data) = kvp.pair();
113+
let read = data.read;
114+
let unread = self
115+
.stream_writes
116+
.get(topic)
117+
.and_then(|w| w.checked_sub(read))
118+
.filter(|u| *u > 0);
119+
let pending_for = data
120+
.pending_since
121+
.map(|d| format!("{:?}", now.duration_since(d)));
122+
let state = SerializedReadStreamState {
123+
read,
124+
unread,
125+
pending_for,
126+
};
127+
(topic.clone(), state)
128+
})
129+
.collect();
130+
131+
let writes = state
132+
.writes
133+
.iter()
134+
.map(|kvp| {
135+
let (topic, data) = kvp.pair();
136+
let written = data.written;
137+
let pending_for = data
138+
.pending_since
139+
.map(|d| format!("{:?}", now.duration_since(d)));
140+
let state = SerializedWriteStreamState {
141+
written,
142+
pending_for,
143+
};
144+
(topic.clone(), state)
145+
})
146+
.collect();
147+
148+
(name.clone(), SerializedModuleState { reads, writes })
149+
})
150+
.collect::<BTreeMap<_, _>>();
151+
let serialized = serde_json::to_vec_pretty(&state).expect("could not serialize state");
152+
fs::write(&self.output_path, serialized)
153+
.await
154+
.expect("could not write file");
155+
}
156+
}
157+
}
158+
159+
pub struct MonitorBus<M: MessageBounds> {
160+
inner: Arc<dyn MessageBus<M>>,
161+
stream_writes: Arc<DashMap<String, u64>>,
162+
state: Arc<ModuleState>,
163+
}
164+
165+
#[async_trait]
166+
impl<M: MessageBounds> MessageBus<M> for MonitorBus<M> {
167+
async fn publish(&self, topic: &str, message: Arc<M>) -> Result<()> {
168+
self.state
169+
.writes
170+
.entry(topic.to_string())
171+
.or_default()
172+
.pending_since = Some(Instant::now());
173+
let res = self.inner.publish(topic, message).await;
174+
let mut writes = self.state.writes.entry(topic.to_string()).or_default();
175+
writes.written += 1;
176+
writes.pending_since = None;
177+
*self.stream_writes.entry(topic.to_string()).or_default() += 1;
178+
res
179+
}
180+
181+
fn request_timeout(&self) -> std::time::Duration {
182+
self.inner.request_timeout()
183+
}
184+
185+
async fn subscribe(&self, topic: &str) -> Result<Box<dyn Subscription<M>>> {
186+
self.state.reads.entry(topic.to_string()).or_default();
187+
Ok(Box::new(MonitorSubscription {
188+
inner: self.inner.subscribe(topic).await?,
189+
state: self.state.clone(),
190+
topic: topic.to_string(),
191+
}))
192+
}
193+
194+
async fn shutdown(&self) -> Result<()> {
195+
self.inner.shutdown().await
196+
}
197+
}
198+
199+
struct MonitorSubscription<M: MessageBounds> {
200+
inner: Box<dyn Subscription<M>>,
201+
state: Arc<ModuleState>,
202+
topic: String,
203+
}
204+
impl<M: MessageBounds> SubscriptionBounds for MonitorSubscription<M> {}
205+
206+
impl<M: MessageBounds> Subscription<M> for MonitorSubscription<M> {
207+
fn read(&mut self) -> BoxFuture<'_, Result<(String, Arc<M>)>> {
208+
Box::pin(
209+
MonitorReadFuture {
210+
inner: self.inner.read(),
211+
state: &self.state,
212+
topic: &self.topic,
213+
}
214+
.fuse(),
215+
)
216+
}
217+
}
218+
219+
struct MonitorReadFuture<'a, M: MessageBounds> {
220+
inner: BoxFuture<'a, Result<(String, Arc<M>)>>,
221+
state: &'a ModuleState,
222+
topic: &'a str,
223+
}
224+
impl<'a, M: MessageBounds> Future for MonitorReadFuture<'a, M> {
225+
type Output = Result<(String, Arc<M>)>;
226+
227+
fn poll(
228+
mut self: std::pin::Pin<&mut Self>,
229+
cx: &mut std::task::Context<'_>,
230+
) -> std::task::Poll<Self::Output> {
231+
let res = self.inner.poll_unpin(cx);
232+
let mut entry = self.state.reads.entry(self.topic.to_string()).or_default();
233+
match &res {
234+
Poll::Pending => {
235+
if entry.pending_since.is_none() {
236+
entry.pending_since = Some(Instant::now());
237+
}
238+
}
239+
Poll::Ready(r) => {
240+
entry.pending_since = None;
241+
if r.is_ok() {
242+
entry.read += 1;
243+
}
244+
}
245+
}
246+
res
247+
}
248+
}
249+
impl<'a, M: MessageBounds> Drop for MonitorReadFuture<'a, M> {
250+
fn drop(&mut self) {
251+
let mut entry = self.state.reads.entry(self.topic.to_string()).or_default();
252+
entry.pending_since = None;
253+
}
254+
}

0 commit comments

Comments
 (0)