[fix](trx-server): prevent audio thread crash on ALSA EPIPE

CPAL error callbacks can fire millions of times per second on ALSA
EPIPE (errno -32). Previously each invocation did a string allocation
and mutex lock, saturating CPU and eventually crashing the server.

- Use atomic swap in both input and output error callbacks so only the
  first error fires the expensive log+notify path; all subsequent hits
  cost a single atomic op and return immediately.
- Replace stream.play()? with explicit error handling in run_playback
  so a play failure triggers stream recreation instead of permanently
  terminating the playback thread.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <stanislawgrams@gmail.com>
This commit is contained in:
2026-02-23 13:24:55 +01:00
parent bbe37c4fd2
commit 1a282f5ec6
+24 -11
View File
@@ -317,9 +317,13 @@ fn run_capture(
let stream_failed = stream_failed.clone(); let stream_failed = stream_failed.clone();
let stream_err_tx = stream_err_tx.clone(); let stream_err_tx = stream_err_tx.clone();
move |err| { move |err| {
input_err_logger.log(&err.to_string()); // swap ensures only the first error does expensive work;
stream_failed.store(true, Ordering::SeqCst); // subsequent callbacks (can fire millions/s on ALSA EPIPE)
let _ = stream_err_tx.try_send(()); // return immediately after a single atomic op.
if !stream_failed.swap(true, Ordering::SeqCst) {
input_err_logger.log(&err.to_string());
let _ = stream_err_tx.try_send(());
}
} }
}, },
None, None,
@@ -503,9 +507,13 @@ fn run_playback(
let stream_failed = stream_failed.clone(); let stream_failed = stream_failed.clone();
let stream_err_tx = stream_err_tx.clone(); let stream_err_tx = stream_err_tx.clone();
move |err| { move |err| {
output_err_logger.log(&err.to_string()); // swap ensures only the first error does expensive work;
stream_failed.store(true, Ordering::SeqCst); // subsequent callbacks (can fire millions/s on ALSA EPIPE)
let _ = stream_err_tx.try_send(()); // return immediately after a single atomic op.
if !stream_failed.swap(true, Ordering::SeqCst) {
output_err_logger.log(&err.to_string());
let _ = stream_err_tx.try_send(());
}
} }
}, },
None, None,
@@ -521,10 +529,12 @@ fn run_playback(
} }
}; };
if !playing { if playing {
// stay paused until packets arrive if let Err(e) = stream.play() {
} else { warn!("Audio playback: stream.play failed, recreating: {}", e);
stream.play()?; std::thread::sleep(AUDIO_STREAM_RECOVERY_DELAY);
continue;
}
} }
loop { loop {
@@ -544,7 +554,10 @@ fn run_playback(
match rx.try_recv() { match rx.try_recv() {
Ok(packet) => { Ok(packet) => {
if !playing { if !playing {
stream.play()?; if let Err(e) = stream.play() {
warn!("Audio playback: stream.play failed, recreating: {}", e);
break;
}
playing = true; playing = true;
info!("Audio playback: started"); info!("Audio playback: started");
} }