refactor: further error callback improvements#1199
Conversation
This prevents error callbacks from being called before the stream handle is returned to the user.
d51aae2 to
14fe3a4
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated 7 comments.
Comments suppressed due to low confidence (5)
src/host/wasapi/device.rs:166
- This still unblocks the worker before
build_output_stream_rawreturns, so the worker can win the scheduler race and invoke data/error callbacks before the caller receives theStreamhandle. To satisfy the readiness guarantee, the unblock needs to happen from an action the caller can take after receiving the handle (for example the firstplay/command), not immediately beforeOk(stream).
stream.signal_ready();
src/host/alsa/mod.rs:263
- This still releases the ALSA output worker before
build_output_stream_rawreturns, so the worker can report the realtime-priority error or enter the audio loop before the caller receives theStreamhandle. The ready signal needs to be tied to a post-return action (such asplay) rather than being sent immediately beforeOk(stream).
stream.signal_ready();
src/host/pulseaudio/mod.rs:439
- This still unblocks the PulseAudio playback workers before
build_output_stream_rawreturns, so the driver/latency threads can invoke callbacks before the caller receives theStreamhandle. The readiness signal should be triggered by a post-return action rather than immediately beforeOk(stream).
stream.signal_ready();
src/host/pipewire/device.rs:548
- Failing to get the PipeWire registry now aborts stream creation. This registry is only needed for default-device change monitoring; the previous behavior treated it as best-effort and still created the stream without change notifications, so users can now lose otherwise usable output streams when monitoring setup fails.
let _ = init_tx.send(Err(Error::with_message(
ErrorKind::BackendError,
format!("PipeWire: could not acquire registry: {e}"),
)));
return;
src/host/jack/stream.rs:100
- This only gates the JACK process callback. The notification handler is still activated below and its
xrun/shutdownpaths call the user error callback without checkingplaying, so JACK errors can still be delivered in the window betweenactivate_asyncand returning theStreamhandle.
let playing = Arc::new(AtomicBool::new(false));
d01734c to
63bf488
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated 9 comments.
Comments suppressed due to low confidence (8)
src/host/pipewire/device.rs:548
- This makes failure to acquire the registry fatal for default-device streams, whereas the previous code continued without device-change notifications. That turns an optional monitor failure into stream creation failure, which can prevent otherwise usable streams from being built.
Err(e) => {
let _ = init_tx.send(Err(Error::with_message(
ErrorKind::BackendError,
format!("PipeWire: could not acquire registry: {e}"),
)));
return;
src/host/alsa/mod.rs:263
- Calling
signal_ready()before returning still allows the ALSA worker to run (including realtime error reporting and data callbacks) while this function is preempted beforeOk(stream)reaches the caller. This does not fully enforce the “no callbacks before the caller has the handle” guarantee.
stream.signal_ready();
src/host/wasapi/device.rs:167
- This unblocks the WASAPI worker before
build_output_stream_rawreturns to the caller, so a scheduler preemption here can still let the worker invoke the error callback (for example, realtime-priority failure or device-change handling) before the caller receives theStreamhandle.
let stream = Stream::new_output(stream_inner, data_callback, error_callback, monitor)?;
stream.signal_ready();
Ok(stream)
src/host/pulseaudio/mod.rs:440
signal_ready()runs before the stream is returned, so the driver/latency threads can wake and invoke the user callback before the caller receives the handle. This narrows the old race but does not eliminate it.
}?;
stream.signal_ready();
Ok(stream)
src/host/pipewire/device.rs:465
signal_ready()unblocks the PipeWire worker beforeOk(stream)reaches the caller; if the worker is scheduled first, realtime promotion failure or other mainloop callbacks can still invoke the error callback before the caller has the stream handle.
let stream = Stream::new(handle, pw_play_tx, last_quantum, start, stream_ready);
stream.signal_ready();
Ok(stream)
src/host/pipewire/device.rs:624
signal_ready()unblocks the PipeWire worker beforeOk(stream)reaches the caller; if the worker is scheduled first, realtime promotion failure or other mainloop callbacks can still invoke the error callback before the caller has the stream handle.
let stream = Stream::new(handle, pw_play_tx, last_quantum, start, stream_ready);
stream.signal_ready();
Ok(stream)
src/host/aaudio/mod.rs:410
- This comment is inaccurate when
try_emit_errorcannot acquire the error-callback mutex:rt_checkedremains false, so the RT check is retried on later audio callbacks rather than being performed once per stream.
// RT check: performed on the audio thread once per stream.
src/host/asio/stream.rs:513
add_event_callbackcan now fail when the debounce timer thread cannot be spawned, but this happens afterget_or_create_output_streamhas already prepared and stored an ASIO output stream inself.asio_streams. Returning here without rolling that state back can leave a partially-created stream behind after a failed build.
let driver_event_callback_id = self.add_event_callback(
&driver,
error_callback,
Arc::clone(&hardware_output_latency),
false,
Arc::clone(&state),
)?;
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated 12 comments.
Comments suppressed due to low confidence (8)
src/host/wasapi/device.rs:166
- This wakes the WASAPI worker before
build_output_stream_rawhas returned theStreamto the caller. The worker can run and invoke the error callback (for example during realtime promotion) in the window beforeOk(stream)is returned, so the startup race is not fully closed.
stream.signal_ready();
src/host/alsa/mod.rs:263
- This wakes the ALSA worker before the output stream has been returned to the caller. Because the worker may invoke
error_callbackduring its initial realtime-priority setup, the callback-before-handle race remains possible in the window beforeOk(stream).
stream.signal_ready();
src/host/pulseaudio/mod.rs:439
- This signals the PulseAudio worker threads before the output stream is returned to the caller. The driver/latency threads can run and report errors in the window before
Ok(stream), so callbacks are still not strictly gated until after the handle is received.
stream.signal_ready();
src/host/pipewire/device.rs:655
- This releases the PipeWire worker before the output stream is returned. The worker can emit the realtime-promotion error immediately after the gate opens, leaving a window where callbacks fire before the caller receives the
Stream.
stream.signal_ready();
src/host/jack/stream.rs:153
- Changing the state to
Pausedhere happens beforenew_outputreturns theStream. Since JACK notification callbacks are allowed once the state is no longerInitializing, they can still callerror_callbackbefore the caller receives the handle.
StreamState::Paused.store(&state, Ordering::Release);
src/host/asio/stream.rs:817
- On
driver.start()failure this removes callbacks but does not clear the output stream stored byget_or_create_output_stream. Because noStreamis returned to clean it up, subsequent builds on thisDevicecan see stale ASIO stream state and skip buffer preparation for the new driver instance.
if let Err(e) = driver.start() {
driver.remove_event_callback(driver_event_callback_id);
driver.remove_callback(callback_id);
return Err(build_stream_err(e));
src/host/coreaudio/macos/device.rs:945
- The output/default-device monitor is active before this returns the
Stream, so a device-change, disconnect, or sample-rate notification can still invokeerror_callbackbefore the caller receives the handle. Removingaudio_unit.start()avoids data callbacks but does not gate these monitor callbacks.
Ok(Stream::new(inner_arc, monitor))
src/host/coreaudio/ios/mod.rs:239
- The iOS session observers are active before this output stream is returned, and they invoke
error_callbackdirectly on route/media-services notifications. This preserves a callback-before-handle race even though the audio unit no longer starts during construction.
Ok(Stream::new(
StreamInner {
playing: false,
audio_unit,
},
session_manager,
Add a host/latch.rs utility and replace per-backend Arc<AtomicBool> "stream_ready" gates with a Latch/Waiter pair.
Continuation from #1187.