Skip to content

Commit 4c89a68

Browse files
committed
Fix crash on shutdown when using global recording stream variables
1 parent 801decc commit 4c89a68

File tree

3 files changed

+53
-9
lines changed

3 files changed

+53
-9
lines changed

crates/top/rerun_c/src/lib.rs

+41-2
Original file line numberDiff line numberDiff line change
@@ -340,11 +340,50 @@ pub extern "C" fn rr_recording_stream_new(
340340
}
341341
}
342342

343+
/// See `THREAD_LIFE_TRACKER` for more information.
344+
struct TrivialTypeWithDrop;
345+
346+
impl Drop for TrivialTypeWithDrop {
347+
fn drop(&mut self) {
348+
// Try to ensure that drop doesn't get optimized away.
349+
std::hint::black_box(self);
350+
}
351+
}
352+
353+
thread_local! {
354+
/// It can happen that we end up in here during a thread shutdown.
355+
/// This happens either when:
356+
/// * the application shuts down, causing the destructor of globally defined recordings to be invoked
357+
/// -> Not an issue, we likely already destroyed the recording list.
358+
/// * the user stored their C++ recording in a thread local variable, and then shut down the thread.
359+
/// -> More problematic, since we can't access `RECORDING_STREAMS` now, meaning we leak the recording.
360+
/// (we can't access it because we use channels internally which in turn use thread-local storage)
361+
///
362+
/// So how do we figure out that our thread is shutting down?
363+
/// As of writing `std::thread::current()` panics if there's nothing on `std::sys_common::thread_info::current_thread()`.
364+
/// Unfortunately, `std::sys_common` is a private implementation detail!
365+
/// So instead, we try accessing a thread local variable and see if that's still possible.
366+
/// If not, then we assume that the thread is shutting down.
367+
///
368+
/// Just any thread local variable will not do though!
369+
/// We need something that is guaranteed to be dropped with the thread shutting down.
370+
/// A simple integer value won't do that, `Box` works but seems wasteful, so we use a trivial type with a drop implementation.
371+
#[allow(clippy::unnecessary_box_returns)]
372+
pub static THREAD_LIFE_TRACKER: TrivialTypeWithDrop = TrivialTypeWithDrop;
373+
}
374+
343375
#[allow(unsafe_code)]
344376
#[no_mangle]
345377
pub extern "C" fn rr_recording_stream_free(id: CRecordingStream) {
346-
if let Some(stream) = RECORDING_STREAMS.lock().remove(id) {
347-
stream.disconnect();
378+
if THREAD_LIFE_TRACKER.try_with(|_v| {}).is_ok() {
379+
if let Some(stream) = RECORDING_STREAMS.lock().remove(id) {
380+
stream.disconnect();
381+
}
382+
} else {
383+
// Yes, at least as of writing we can still log things in this state!
384+
re_log::debug!(
385+
"rr_recording_stream_free called on a thread that is shutting down and can no longer access thread locals. We can't handle this and have to ignore this call."
386+
);
348387
}
349388
}
350389

rerun_cpp/tests/recording_stream.cpp

+11-6
Original file line numberDiff line numberDiff line change
@@ -493,17 +493,13 @@ SCENARIO("Recording stream handles serialization failure during logging graceful
493493

494494
THEN("calling log with an array logs the serialization error") {
495495
check_logged_error(
496-
[&] {
497-
stream.log(path, std::array{component, component});
498-
},
496+
[&] { stream.log(path, std::array{component, component}); },
499497
expected_error.code
500498
);
501499
}
502500
THEN("calling log with a vector logs the serialization error") {
503501
check_logged_error(
504-
[&] {
505-
stream.log(path, std::vector{component, component});
506-
},
502+
[&] { stream.log(path, std::vector{component, component}); },
507503
expected_error.code
508504
);
509505
}
@@ -653,3 +649,12 @@ SCENARIO("Deprecated log_timeless still works", TEST_TAG) {
653649

654650
RR_POP_WARNINGS // For `RR_DISABLE_DEPRECATION_WARNING`.
655651
}
652+
653+
SCENARIO("Global RecordingStream doesn't cause crashes", TEST_TAG) {
654+
// This caused a crash on Mac & Linux due to issues with cleanup order of global variables
655+
// in Rust vs C++.
656+
// See:
657+
// * https://github.com/rerun-io/rerun/issues/6852
658+
// * https://github.com/rerun-io/rerun/issues/5260
659+
static rerun::RecordingStream global_stream("global");
660+
}

tests/python/plot_dashboard_stress/main.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ def main() -> None:
193193

194194
total_num_scalars += scalars_per_tick
195195
total_elapsed = time.time() - total_start_time
196-
if total_elapsed >= 1.0:
196+
if total_elapsed >= 1.0
197197
print(
198198
f"logged {total_num_scalars} scalars over {round(total_elapsed, 3)}s \
199199
(freq={round(total_num_scalars / total_elapsed, 3)}Hz, expected={round(expected_total_freq, 3)}Hz, \

0 commit comments

Comments
 (0)