saluki_components/sources/dogstatsd/replay/
capture.rs1use std::{
2 path::{Path, PathBuf},
3 sync::{Arc, Mutex},
4 time::Duration,
5};
6
7use saluki_env::WorkloadProvider;
8use saluki_error::{generic_error, GenericError};
9
10use super::writer::{CaptureRecord, CaptureTargetDir, TrafficCaptureWriter};
11
12const UNAVAILABLE_CAPTURE_CONTROL_ERROR: &str =
13 "DogStatsD capture control is unavailable because the source is not running.";
14
15#[derive(Clone)]
17pub(crate) struct TrafficCapture {
18 inner: Arc<TrafficCaptureInner>,
19}
20
21struct TrafficCaptureInner {
22 writer: TrafficCaptureWriter,
23 default_capture_dir: PathBuf,
24}
25
26impl TrafficCapture {
27 #[cfg(test)]
29 pub(crate) fn new(default_capture_dir: PathBuf, queue_depth: usize) -> Self {
30 Self::with_workload_provider(default_capture_dir, queue_depth, None)
31 }
32
33 pub(crate) fn with_workload_provider(
35 default_capture_dir: PathBuf, queue_depth: usize,
36 workload_provider: Option<Arc<dyn WorkloadProvider + Send + Sync>>,
37 ) -> Self {
38 Self {
39 inner: Arc::new(TrafficCaptureInner {
40 writer: TrafficCaptureWriter::with_workload_provider(queue_depth, workload_provider),
41 default_capture_dir,
42 }),
43 }
44 }
45
46 pub(crate) fn is_ongoing(&self) -> bool {
48 self.inner.writer.is_ongoing()
49 }
50
51 pub(crate) fn start_capture(
53 &self, requested_dir: Option<&Path>, duration: Duration, compressed: bool,
54 ) -> Result<PathBuf, GenericError> {
55 let target_dir = match requested_dir {
56 Some(path) => CaptureTargetDir::Explicit(path.to_path_buf()),
57 None => CaptureTargetDir::Implicit(self.inner.default_capture_dir.clone()),
58 };
59
60 self.inner.writer.start_capture(target_dir, duration, compressed)
61 }
62
63 pub(crate) fn stop_capture(&self) {
65 self.inner.writer.stop_capture();
66 }
67
68 pub(crate) fn enqueue(&self, record: CaptureRecord) -> bool {
70 self.inner.writer.enqueue(record)
71 }
72}
73
74#[derive(Clone, Default)]
79pub struct DogStatsDCaptureControl {
80 inner: Arc<Mutex<Option<TrafficCapture>>>,
81}
82
83impl DogStatsDCaptureControl {
84 pub fn new() -> Self {
86 Self::default()
87 }
88
89 pub(crate) fn bind(&self, capture: TrafficCapture) {
91 let mut state = self.inner.lock().expect("capture control mutex poisoned");
92 *state = Some(capture);
93 }
94
95 pub fn is_ongoing(&self) -> Result<bool, GenericError> {
101 Ok(self.bound_capture()?.is_ongoing())
102 }
103
104 pub fn start_capture(
111 &self, requested_dir: Option<&Path>, duration: Duration, compressed: bool,
112 ) -> Result<PathBuf, GenericError> {
113 self.bound_capture()?.start_capture(requested_dir, duration, compressed)
114 }
115
116 pub fn stop_capture(&self) -> Result<(), GenericError> {
122 self.bound_capture()?.stop_capture();
123 Ok(())
124 }
125
126 fn bound_capture(&self) -> Result<TrafficCapture, GenericError> {
127 let state = self.inner.lock().expect("capture control mutex poisoned");
128 state
129 .clone()
130 .ok_or_else(|| generic_error!("{}", UNAVAILABLE_CAPTURE_CONTROL_ERROR))
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use std::{fs, time::Duration};
137
138 use super::super::test_support::{unique_dir, wait_until_inactive};
139 use super::{DogStatsDCaptureControl, TrafficCapture, UNAVAILABLE_CAPTURE_CONTROL_ERROR};
140
141 #[test]
142 fn control_requires_bound_runtime() {
143 let control = DogStatsDCaptureControl::new();
144
145 let error = control
146 .start_capture(None, Duration::from_millis(25), false)
147 .expect_err("unbound control should fail");
148
149 assert_eq!(error.to_string(), UNAVAILABLE_CAPTURE_CONTROL_ERROR);
150 }
151
152 #[test]
153 fn control_drives_bound_capture_runtime() {
154 let control = DogStatsDCaptureControl::new();
155 let target_dir = unique_dir("capture-control");
156 let capture = TrafficCapture::new(target_dir.clone(), 1);
157 control.bind(capture);
158
159 let capture_path = control
160 .start_capture(None, Duration::from_millis(250), false)
161 .expect("capture should start");
162
163 let error = control
164 .start_capture(None, Duration::from_millis(250), false)
165 .expect_err("second capture should fail");
166 assert!(error.to_string().contains("capture already in progress"));
167
168 control.stop_capture().expect("stop should succeed");
169 control.stop_capture().expect("second stop should be safe");
170 wait_until_inactive(|| control.is_ongoing().expect("control should be bound"));
171
172 assert!(capture_path.exists());
173
174 let _ = fs::remove_dir_all(target_dir);
175 }
176}