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