Skip to content

Commit 5cd5224

Browse files
authored
[timeseries] add metrics for ingest backpressure diagnosis (opendata-oss#436)
1 parent 0961492 commit 5cd5224

5 files changed

Lines changed: 437 additions & 87 deletions

File tree

common/src/coordinator/handle.rs

Lines changed: 123 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use super::metrics;
12
use super::{BroadcastedView, WriteCommand};
23
use super::{Delta, Durability, WriteError, WriteResult};
34
use crate::StorageRead;
@@ -6,7 +7,7 @@ use crate::storage::StorageSnapshot;
67
use futures::FutureExt;
78
use futures::future::Shared;
89
use std::sync::Arc;
9-
use std::time::Duration;
10+
use std::time::{Duration, Instant};
1011
use tokio::sync::{broadcast, mpsc, oneshot, watch};
1112

1213
/// A point-in-time view of all applied writes, broadcast by the coordinator
@@ -142,18 +143,21 @@ impl<M: Clone + Send + 'static> WriteHandle<M> {
142143
/// This is the main interface for interacting with the write coordinator.
143144
/// It can be cloned and shared across tasks.
144145
pub struct WriteCoordinatorHandle<D: Delta> {
146+
name: Arc<str>,
145147
write_tx: mpsc::Sender<WriteCommand<D>>,
146148
watchers: EpochWatcher,
147149
view: Arc<BroadcastedView<D>>,
148150
}
149151

150152
impl<D: Delta> WriteCoordinatorHandle<D> {
151153
pub(crate) fn new(
154+
name: String,
152155
write_tx: mpsc::Sender<WriteCommand<D>>,
153156
watchers: EpochWatcher,
154157
view: Arc<BroadcastedView<D>>,
155158
) -> Self {
156159
Self {
160+
name: Arc::from(name),
157161
write_tx,
158162
watchers,
159163
view,
@@ -167,6 +171,35 @@ impl<D: Delta> WriteCoordinatorHandle<D> {
167171
pub fn flushed_epoch(&self) -> u64 {
168172
*self.watchers.written_rx.borrow()
169173
}
174+
175+
/// Sample queue depth on each send. Cheap atomic reads on `mpsc::Sender`.
176+
fn record_queue_depth(&self) {
177+
let max = self.write_tx.max_capacity();
178+
let free = self.write_tx.capacity();
179+
::metrics::gauge!(
180+
metrics::COORDINATOR_QUEUE_DEPTH,
181+
"channel" => self.name.to_string(),
182+
)
183+
.set(max.saturating_sub(free) as f64);
184+
}
185+
186+
fn record_send(&self, command: &'static str, status: &'static str, started: Instant) {
187+
::metrics::histogram!(
188+
metrics::COORDINATOR_SEND_DURATION_SECONDS,
189+
"command" => command,
190+
"status" => status,
191+
)
192+
.record(started.elapsed().as_secs_f64());
193+
}
194+
195+
fn record_backpressure(&self, command: &'static str, reason: &'static str) {
196+
::metrics::counter!(
197+
metrics::COORDINATOR_QUEUE_BACKPRESSURE_TOTAL,
198+
"command" => command,
199+
"reason" => reason,
200+
)
201+
.increment(1);
202+
}
170203
}
171204

172205
impl<D: Delta> WriteCoordinatorHandle<D> {
@@ -186,27 +219,37 @@ impl<D: Delta> WriteCoordinatorHandle<D> {
186219
write: D::Write,
187220
timeout: Duration,
188221
) -> Result<WriteHandle<D::ApplyResult>, WriteError<D::Write>> {
222+
const COMMAND: &str = "write_timeout";
223+
self.record_queue_depth();
224+
let started = Instant::now();
189225
let (tx, rx) = oneshot::channel();
190-
self.write_tx
226+
let send_result = self
227+
.write_tx
191228
.send_timeout(
192229
WriteCommand::Write {
193230
write,
194231
result_tx: tx,
195232
},
196233
timeout,
197234
)
198-
.await
199-
.map_err(|e| match e {
200-
mpsc::error::SendTimeoutError::Timeout(WriteCommand::Write { write, .. }) => {
201-
WriteError::TimeoutError(write)
202-
}
203-
mpsc::error::SendTimeoutError::Closed(WriteCommand::Write { write, .. }) => {
204-
WriteError::Shutdown
205-
}
206-
_ => unreachable!("sent a Write command"),
207-
})?;
208-
209-
Ok(WriteHandle::new(rx, self.watchers.clone()))
235+
.await;
236+
match send_result {
237+
Ok(()) => {
238+
self.record_send(COMMAND, "ok", started);
239+
Ok(WriteHandle::new(rx, self.watchers.clone()))
240+
}
241+
Err(mpsc::error::SendTimeoutError::Timeout(WriteCommand::Write { write, .. })) => {
242+
self.record_send(COMMAND, "timeout", started);
243+
self.record_backpressure(COMMAND, "timeout");
244+
Err(WriteError::TimeoutError(write))
245+
}
246+
Err(mpsc::error::SendTimeoutError::Closed(WriteCommand::Write { .. })) => {
247+
self.record_send(COMMAND, "shutdown", started);
248+
self.record_backpressure(COMMAND, "closed");
249+
Err(WriteError::Shutdown)
250+
}
251+
Err(_) => unreachable!("sent a Write command"),
252+
}
210253
}
211254

212255
/// Submit a write to the coordinator, blocking indefinitely until there is
@@ -222,19 +265,29 @@ impl<D: Delta> WriteCoordinatorHandle<D> {
222265
&self,
223266
write: D::Write,
224267
) -> Result<WriteHandle<D::ApplyResult>, WriteError<D::Write>> {
268+
const COMMAND: &str = "write";
269+
self.record_queue_depth();
270+
let started = Instant::now();
225271
let (tx, rx) = oneshot::channel();
226-
self.write_tx
272+
let send_result = self
273+
.write_tx
227274
.send(WriteCommand::Write {
228275
write,
229276
result_tx: tx,
230277
})
231-
.await
232-
.map_err(|e| match e {
233-
mpsc::error::SendError(WriteCommand::Write { write, .. }) => WriteError::Shutdown,
234-
_ => unreachable!("sent a Write command"),
235-
})?;
236-
237-
Ok(WriteHandle::new(rx, self.watchers.clone()))
278+
.await;
279+
match send_result {
280+
Ok(()) => {
281+
self.record_send(COMMAND, "ok", started);
282+
Ok(WriteHandle::new(rx, self.watchers.clone()))
283+
}
284+
Err(mpsc::error::SendError(WriteCommand::Write { .. })) => {
285+
self.record_send(COMMAND, "shutdown", started);
286+
self.record_backpressure(COMMAND, "closed");
287+
Err(WriteError::Shutdown)
288+
}
289+
Err(_) => unreachable!("sent a Write command"),
290+
}
238291
}
239292

240293
/// Submit a write to the coordinator.
@@ -247,23 +300,31 @@ impl<D: Delta> WriteCoordinatorHandle<D> {
247300
&self,
248301
write: D::Write,
249302
) -> Result<WriteHandle<D::ApplyResult>, WriteError<D::Write>> {
303+
const COMMAND: &str = "try_write";
304+
self.record_queue_depth();
305+
let started = Instant::now();
250306
let (tx, rx) = oneshot::channel();
251-
self.write_tx
252-
.try_send(WriteCommand::Write {
253-
write,
254-
result_tx: tx,
255-
})
256-
.map_err(|e| match e {
257-
mpsc::error::TrySendError::Full(WriteCommand::Write { write, .. }) => {
258-
WriteError::Backpressure(write)
259-
}
260-
mpsc::error::TrySendError::Closed(WriteCommand::Write { write, .. }) => {
261-
WriteError::Shutdown
262-
}
263-
_ => unreachable!("sent a Write command"),
264-
})?;
265-
266-
Ok(WriteHandle::new(rx, self.watchers.clone()))
307+
let send_result = self.write_tx.try_send(WriteCommand::Write {
308+
write,
309+
result_tx: tx,
310+
});
311+
match send_result {
312+
Ok(()) => {
313+
self.record_send(COMMAND, "ok", started);
314+
Ok(WriteHandle::new(rx, self.watchers.clone()))
315+
}
316+
Err(mpsc::error::TrySendError::Full(WriteCommand::Write { write, .. })) => {
317+
self.record_send(COMMAND, "backpressure", started);
318+
self.record_backpressure(COMMAND, "full");
319+
Err(WriteError::Backpressure(write))
320+
}
321+
Err(mpsc::error::TrySendError::Closed(WriteCommand::Write { .. })) => {
322+
self.record_send(COMMAND, "shutdown", started);
323+
self.record_backpressure(COMMAND, "closed");
324+
Err(WriteError::Shutdown)
325+
}
326+
Err(_) => unreachable!("sent a Write command"),
327+
}
267328
}
268329

269330
/// Request a flush of the current delta.
@@ -273,18 +334,30 @@ impl<D: Delta> WriteCoordinatorHandle<D> {
273334
/// to guarantee durability, and the durable watermark will be advanced.
274335
/// Returns a handle that can be used to wait for the flush to complete.
275336
pub async fn flush(&self, flush_storage: bool) -> WriteResult<WriteHandle> {
337+
const COMMAND: &str = "flush";
338+
self.record_queue_depth();
339+
let started = Instant::now();
276340
let (tx, rx) = oneshot::channel();
277-
self.write_tx
278-
.try_send(WriteCommand::Flush {
279-
epoch_tx: tx,
280-
flush_storage,
281-
})
282-
.map_err(|e| match e {
283-
mpsc::error::TrySendError::Full(_) => WriteError::Backpressure(()),
284-
mpsc::error::TrySendError::Closed(_) => WriteError::Shutdown,
285-
})?;
286-
287-
Ok(WriteHandle::new(rx, self.watchers.clone()))
341+
let send_result = self.write_tx.try_send(WriteCommand::Flush {
342+
epoch_tx: tx,
343+
flush_storage,
344+
});
345+
match send_result {
346+
Ok(()) => {
347+
self.record_send(COMMAND, "ok", started);
348+
Ok(WriteHandle::new(rx, self.watchers.clone()))
349+
}
350+
Err(mpsc::error::TrySendError::Full(_)) => {
351+
self.record_send(COMMAND, "backpressure", started);
352+
self.record_backpressure(COMMAND, "full");
353+
Err(WriteError::Backpressure(()))
354+
}
355+
Err(mpsc::error::TrySendError::Closed(_)) => {
356+
self.record_send(COMMAND, "shutdown", started);
357+
self.record_backpressure(COMMAND, "closed");
358+
Err(WriteError::Shutdown)
359+
}
360+
}
288361
}
289362

290363
pub fn view(&self) -> Arc<View<D>> {
@@ -299,6 +372,7 @@ impl<D: Delta> WriteCoordinatorHandle<D> {
299372
impl<D: Delta> Clone for WriteCoordinatorHandle<D> {
300373
fn clone(&self) -> Self {
301374
Self {
375+
name: self.name.clone(),
302376
write_tx: self.write_tx.clone(),
303377
watchers: self.watchers.clone(),
304378
view: self.view.clone(),

common/src/coordinator/metrics.rs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
//! Metrics for the write coordinator.
2+
//!
3+
//! These metric names are stable strings shared between the coordinator and
4+
//! the flush task. Calls are no-ops when no global recorder is installed.
5+
6+
// ── Write-queue ingress (the channel that backs WriteCoordinatorHandle) ──
7+
8+
/// Histogram of how long it takes to enqueue a coordinator command.
9+
/// Labels: `command` ∈ {`write`, `write_timeout`, `try_write`, `flush`},
10+
/// `status` ∈ {`ok`, `backpressure`, `timeout`, `shutdown`}.
11+
pub(crate) const COORDINATOR_SEND_DURATION_SECONDS: &str =
12+
"opendata_write_coordinator_send_duration_seconds";
13+
14+
/// Counter incremented when the queue rejects a send because it is full.
15+
/// Labels: `command`, `reason` ∈ {`full`, `timeout`, `closed`}.
16+
pub(crate) const COORDINATOR_QUEUE_BACKPRESSURE_TOTAL: &str =
17+
"opendata_write_coordinator_queue_backpressure_total";
18+
19+
/// Approximate depth of the write queue, sampled on each send.
20+
/// Labels: `channel`.
21+
pub(crate) const COORDINATOR_QUEUE_DEPTH: &str = "opendata_write_coordinator_queue_depth";
22+
23+
// ── Delta lifecycle (apply / freeze / flush dispatch) ──
24+
25+
/// Histogram of `Delta::apply` durations on the coordinator hot path.
26+
pub(crate) const COORDINATOR_DELTA_APPLY_DURATION_SECONDS: &str =
27+
"opendata_write_coordinator_delta_apply_duration_seconds";
28+
29+
/// Estimated current delta size in bytes (gauge), sampled after each apply.
30+
pub(crate) const COORDINATOR_DELTA_ESTIMATED_BYTES: &str =
31+
"opendata_write_coordinator_delta_estimated_bytes";
32+
33+
/// Histogram of `Delta::freeze` durations.
34+
pub(crate) const COORDINATOR_DELTA_FREEZE_DURATION_SECONDS: &str =
35+
"opendata_write_coordinator_delta_freeze_duration_seconds";
36+
37+
/// Histogram of how long the coordinator waits when sending a flush event to
38+
/// the flush task. A high value means the flush task is the bottleneck and
39+
/// new writes will not be accepted until the event is dispatched.
40+
pub(crate) const COORDINATOR_FLUSH_EVENT_SEND_DURATION_SECONDS: &str =
41+
"opendata_write_coordinator_flush_event_send_duration_seconds";
42+
43+
/// Approximate depth of the flush event channel, sampled on each send.
44+
pub(crate) const COORDINATOR_FLUSH_EVENT_QUEUE_DEPTH: &str =
45+
"opendata_write_coordinator_flush_event_queue_depth";
46+
47+
/// Counter of flush triggers, labeled by reason.
48+
/// Labels: `reason` ∈ {`size_threshold`, `interval`, `explicit`, `shutdown`}.
49+
pub(crate) const COORDINATOR_FLUSH_TOTAL: &str = "opendata_write_coordinator_flush_total";
50+
51+
// ── Flush task (background) ──
52+
53+
/// Histogram of `Flusher::flush_delta` durations on the background flush task.
54+
pub(crate) const COORDINATOR_FLUSH_DELTA_DURATION_SECONDS: &str =
55+
"opendata_write_coordinator_flush_delta_duration_seconds";
56+
57+
/// Histogram of `Flusher::flush_storage` durations on the background flush task.
58+
pub(crate) const COORDINATOR_FLUSH_STORAGE_DURATION_SECONDS: &str =
59+
"opendata_write_coordinator_flush_storage_duration_seconds";
60+
61+
/// Describe all coordinator metrics on the global recorder. Safe to call more
62+
/// than once; the `metrics` crate dedupes descriptions.
63+
pub fn describe_coordinator_metrics() {
64+
metrics::describe_histogram!(
65+
COORDINATOR_SEND_DURATION_SECONDS,
66+
"Latency of submitting a command to the write coordinator queue (seconds)"
67+
);
68+
metrics::describe_counter!(
69+
COORDINATOR_QUEUE_BACKPRESSURE_TOTAL,
70+
"Coordinator queue rejections due to a full or closed queue"
71+
);
72+
metrics::describe_gauge!(
73+
COORDINATOR_QUEUE_DEPTH,
74+
"Approximate write coordinator queue depth, sampled on each send"
75+
);
76+
metrics::describe_histogram!(
77+
COORDINATOR_DELTA_APPLY_DURATION_SECONDS,
78+
"Time spent in Delta::apply on the coordinator hot path (seconds)"
79+
);
80+
metrics::describe_gauge!(
81+
COORDINATOR_DELTA_ESTIMATED_BYTES,
82+
"Estimated bytes in the current (mutable) delta"
83+
);
84+
metrics::describe_histogram!(
85+
COORDINATOR_DELTA_FREEZE_DURATION_SECONDS,
86+
"Time spent freezing the current delta on the coordinator hot path (seconds)"
87+
);
88+
metrics::describe_histogram!(
89+
COORDINATOR_FLUSH_EVENT_SEND_DURATION_SECONDS,
90+
"Time spent dispatching a flush event from the coordinator to the flush task (seconds)"
91+
);
92+
metrics::describe_gauge!(
93+
COORDINATOR_FLUSH_EVENT_QUEUE_DEPTH,
94+
"Approximate depth of the coordinator's flush-event channel"
95+
);
96+
metrics::describe_counter!(
97+
COORDINATOR_FLUSH_TOTAL,
98+
"Coordinator flush triggers labeled by reason"
99+
);
100+
metrics::describe_histogram!(
101+
COORDINATOR_FLUSH_DELTA_DURATION_SECONDS,
102+
"Background Flusher::flush_delta duration (seconds)"
103+
);
104+
metrics::describe_histogram!(
105+
COORDINATOR_FLUSH_STORAGE_DURATION_SECONDS,
106+
"Background Flusher::flush_storage duration (seconds)"
107+
);
108+
}

0 commit comments

Comments
 (0)