refactor: convert the metrics worker into a tokio task
Rust CI / cargo fmt (push) Successful in 14s
Rust CI / cargo clippy (push) Successful in 47s
Rust CI / cargo check (x86_64-unknown-linux-gnu) (push) Successful in 44s
Rust CI / cargo test (x86_64-unknown-linux-gnu) (push) Successful in 56s
Rust CI / cargo check (x86_64-unknown-linux-musl) (push) Successful in 50s
Rust CI / cargo doc (push) Successful in 56s
Rust CI / cargo test (x86_64-unknown-linux-musl) (push) Successful in 1m7s
Supply chain / actionlint (push) Successful in 17s
Supply chain / cargo-deny bans licenses sources (push) Successful in 1m32s
Supply chain / cargo-deny advisories (push) Successful in 1m33s
Build binaries / Build (armv7-musleabi) (push) Successful in 1m43s
Build binaries / Build (aarch64-musl) (push) Successful in 1m53s
Build binaries / Build (arm-musleabi) (push) Successful in 1m51s
Build binaries / Build (arm-musleabihf) (push) Successful in 1m47s
Build binaries / Build (armv7-musleabihf) (push) Successful in 1m34s
Build binaries / Build (i686-musl) (push) Successful in 1m35s
Build binaries / Build (mips-musl) (push) Successful in 1m53s
Build binaries / Build (mips64-muslabi64) (push) Successful in 1m53s
Build binaries / Build (mips64el-muslabi64) (push) Successful in 1m54s
Build binaries / Build (mipsel-musl) (push) Successful in 1m51s
Build binaries / Build (x86_64-musl) (push) Successful in 1m37s
Build binaries / Build (riscv64gc-musl) (push) Successful in 1m40s

This commit is contained in:
Nils Andreas Svee committed 2026-10-28 23:27:50 +07:00
1 parent c37c7b1b6c
commit 2ba2f966a6
2 files changed
+47 -35

No files matched your search

+3 -3
View File
@@ -398,13 +398,13 @@ fn run(settings: &Settings) -> anyhow::Result<()> {
let dropped = Arc::new(AtomicU32::new(0)); let dropped = Arc::new(AtomicU32::new(0));
let (metrics_tx, metrics_thread_handle) = if settings.observability.enabled { let (metrics_tx, metrics_thread_handle) = if settings.observability.enabled {
let (tx, rx) = std::sync::mpsc::sync_channel(1000); let (tx, rx) = tokio::sync::mpsc::channel(1000);
let metrics = Metrics { let metrics = Metrics {
settings: settings.clone(), settings: settings.clone(),
metrics_rx: rx, metrics_rx: rx,
metrics_dropped: Arc::clone(&dropped), metrics_dropped: Arc::clone(&dropped),
}; };
let handle = spawn_worker("metrics", &error_tx, move || metrics.run())?; let handle = spawn_task(&rt, &error_tx, metrics.run());
(Some(tx), Some(handle)) (Some(tx), Some(handle))
} else { } else {
(None, None) (None, None)
@@ -506,7 +506,7 @@ fn run(settings: &Settings) -> anyhow::Result<()> {
drop(metrics_tx); drop(metrics_tx);
if let Some(handle) = metrics_thread_handle { if let Some(handle) = metrics_thread_handle {
let _ = handle.join(); let _ = rt.block_on(handle);
} }
restore_shaper( restore_shaper(
+44 -32
View File
@@ -8,13 +8,13 @@ use crate::settings::Settings;
use crate::time::{ClockSample, Realtime}; use crate::time::{ClockSample, Realtime};
use log::{error, info, warn}; use log::{error, info, warn};
use std::fmt::Write; use std::fmt::Write;
use std::net::{IpAddr, TcpStream, UdpSocket}; use std::net::IpAddr;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::mpsc;
use std::sync::mpsc::RecvTimeoutError;
use std::sync::mpsc::SyncSender;
use std::time::Duration; use std::time::Duration;
use tokio::io::AsyncWriteExt;
use tokio::net::{TcpStream, UdpSocket};
use tokio::sync::mpsc;
const MAX_RECONNECT_BACKOFF: u64 = 60; const MAX_RECONNECT_BACKOFF: u64 = 60;
@@ -29,10 +29,9 @@ enum Transport {
} }
impl Transport { impl Transport {
fn new_udp(host: &str, port: u16) -> anyhow::Result<Self> { async fn new_udp(host: &str, port: u16) -> anyhow::Result<Self> {
let socket = UdpSocket::bind("0.0.0.0:0")?; let socket = UdpSocket::bind("0.0.0.0:0").await?;
socket.set_write_timeout(Some(Duration::from_millis(100)))?; socket.connect((host, port)).await?;
socket.connect((host, port))?;
Ok(Transport::Udp(socket)) Ok(Transport::Udp(socket))
} }
@@ -45,11 +44,15 @@ impl Transport {
} }
} }
fn send(&mut self, data: &str) { async fn send(&mut self, data: &str) {
match self { match self {
Transport::Udp(socket) => { Transport::Udp(socket) => {
if let Err(e) = socket.send(data.as_bytes()) { match tokio::time::timeout(Duration::from_millis(100), socket.send(data.as_bytes()))
warn!("UDP send failed: {}", e); .await
{
Ok(Ok(_)) => {}
Ok(Err(e)) => warn!("UDP send failed: {e}"),
Err(_) => warn!("UDP send timed out"),
} }
} }
Transport::Tcp { Transport::Tcp {
@@ -59,9 +62,8 @@ impl Transport {
reconnect_backoff, reconnect_backoff,
} => { } => {
if stream.is_none() { if stream.is_none() {
match TcpStream::connect((host.as_str(), *port)) { match TcpStream::connect((host.as_str(), *port)).await {
Ok(s) => { Ok(s) => {
s.set_write_timeout(Some(Duration::from_millis(500))).ok();
info!("Connected to metrics collector at {}:{}", host, port); info!("Connected to metrics collector at {}:{}", host, port);
*stream = Some(s); *stream = Some(s);
*reconnect_backoff = 1; *reconnect_backoff = 1;
@@ -79,12 +81,16 @@ impl Transport {
} }
let full_data = format!("{}\n", data); let full_data = format!("{}\n", data);
// take the stream out, write, put it back or set None on failure
let write_error = stream if let Some(s) = stream.as_mut() {
.as_mut() let result = tokio::time::timeout(
.and_then(|s| std::io::Write::write_all(s, full_data.as_bytes()).err()); Duration::from_millis(500),
if let Some(e) = write_error { s.write_all(full_data.as_bytes()),
warn!("TCP send failed: {}", e); )
.await;
if !matches!(result, Ok(Ok(()))) {
warn!("TCP send failed: {result:?}");
*stream = None; *stream = None;
*reconnect_backoff = (*reconnect_backoff * 2).min(MAX_RECONNECT_BACKOFF); *reconnect_backoff = (*reconnect_backoff * 2).min(MAX_RECONNECT_BACKOFF);
} }
@@ -92,6 +98,7 @@ impl Transport {
} }
} }
} }
}
pub enum Metric { pub enum Metric {
Ping { Ping {
@@ -129,12 +136,12 @@ pub enum Metric {
#[derive(Clone)] #[derive(Clone)]
pub struct MetricsSender { pub struct MetricsSender {
tx: Option<SyncSender<(Metric, u64)>>, tx: Option<mpsc::Sender<(Metric, u64)>>,
dropped: Arc<AtomicU32>, dropped: Arc<AtomicU32>,
} }
impl MetricsSender { impl MetricsSender {
pub fn new(tx: SyncSender<(Metric, u64)>, dropped: Arc<AtomicU32>) -> Self { pub fn new(tx: mpsc::Sender<(Metric, u64)>, dropped: Arc<AtomicU32>) -> Self {
Self { Self {
tx: Some(tx), tx: Some(tx),
dropped, dropped,
@@ -165,7 +172,7 @@ pub struct Metrics {
} }
impl Metrics { impl Metrics {
pub fn run(self) -> anyhow::Result<()> { pub async fn run(mut self) -> anyhow::Result<()> {
let host = match &self.settings.observability.host { let host = match &self.settings.observability.host {
Some(h) => h.clone(), Some(h) => h.clone(),
None => { None => {
@@ -182,7 +189,7 @@ impl Metrics {
let mut transport = match self.settings.observability.protocol { let mut transport = match self.settings.observability.protocol {
ObservabilityProtocol::Udp => { ObservabilityProtocol::Udp => {
info!("Metrics exporter configured for UDP to {}:{}", host, port); info!("Metrics exporter configured for UDP to {}:{}", host, port);
Transport::new_udp(&host, port)? Transport::new_udp(&host, port).await?
} }
ObservabilityProtocol::Tcp => { ObservabilityProtocol::Tcp => {
info!("Metrics exporter configured for TCP to {}:{}", host, port); info!("Metrics exporter configured for TCP to {}:{}", host, port);
@@ -194,21 +201,26 @@ impl Metrics {
loop { loop {
// block until first metric or timeout // block until first metric or timeout
match self.metrics_rx.recv_timeout(timeout) { tokio::select! {
Ok(metric) => batch.push(metric), metric = self.metrics_rx.recv() => {
Err(RecvTimeoutError::Timeout) => { match metric {
Some(metric) => batch.push(metric),
None => break,
}
}
_ = tokio::time::sleep(timeout) => {
let dropped = self.metrics_dropped.swap(0, Ordering::Relaxed); let dropped = self.metrics_dropped.swap(0, Ordering::Relaxed);
if dropped > 0 { if dropped > 0 {
let ts = ClockSample::<Realtime>::now().as_nanos(); let ts = ClockSample::<Realtime>::now().as_nanos();
batch.push((Metric::Dropped { count: dropped }, ts)); batch.push((Metric::Dropped { count: dropped }, ts));
} }
if !batch.is_empty() { if !batch.is_empty() {
self.flush(&mut transport, &batch, host_tag); self.flush(&mut transport, &batch, host_tag).await;
batch.clear(); batch.clear();
} }
continue; continue;
} }
Err(RecvTimeoutError::Disconnected) => break,
} }
// greedily drain without blocking // greedily drain without blocking
@@ -220,7 +232,7 @@ impl Metrics {
} }
if batch.len() >= batch_size { if batch.len() >= batch_size {
self.flush(&mut transport, &batch, host_tag); self.flush(&mut transport, &batch, host_tag).await;
batch.clear(); batch.clear();
} }
} }
@@ -228,7 +240,7 @@ impl Metrics {
Ok(()) Ok(())
} }
fn flush(&self, transport: &mut Transport, batch: &[(Metric, u64)], host_tag: &str) { async fn flush(&self, transport: &mut Transport, batch: &[(Metric, u64)], host_tag: &str) {
match transport { match transport {
/* /*
* the UDP receive buffer might be too small on the receiver to accept batched data, * the UDP receive buffer might be too small on the receiver to accept batched data,
@@ -239,7 +251,7 @@ impl Metrics {
for (metric, timestamp_ns) in batch { for (metric, timestamp_ns) in batch {
data.clear(); data.clear();
self.write_lines(metric, *timestamp_ns, host_tag, &mut data); self.write_lines(metric, *timestamp_ns, host_tag, &mut data);
transport.send(&data); transport.send(&data).await;
} }
} }
Transport::Tcp { .. } => { Transport::Tcp { .. } => {
@@ -247,7 +259,7 @@ impl Metrics {
for (metric, timestamp_ns) in batch { for (metric, timestamp_ns) in batch {
self.write_lines(metric, *timestamp_ns, host_tag, &mut data); self.write_lines(metric, *timestamp_ns, host_tag, &mut data);
} }
transport.send(&data); transport.send(&data).await;
} }
} }
} }