-
Notifications
You must be signed in to change notification settings - Fork 842
core: fix race condition in DefaultCallsite::register
#3401
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
hds
wants to merge
2
commits into
main
Choose a base branch
from
hds/fix-defaultcallsite-register-race
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+255
−25
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
130 changes: 130 additions & 0 deletions
130
tracing-core/tests/missed_register_callsite_global_subscriber.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,130 @@ | ||
| use std::{ | ||
| ptr, | ||
| sync::atomic::{AtomicPtr, Ordering}, | ||
| thread, | ||
| time::Duration, | ||
| }; | ||
|
|
||
| use tracing_core::{ | ||
| callsite::{Callsite as _, DefaultCallsite}, | ||
| dispatcher, | ||
| field::{FieldSet, Value}, | ||
| span, Dispatch, Event, Kind, Level, Metadata, Subscriber, | ||
| }; | ||
|
|
||
| struct TestSubscriber { | ||
| sleep: Duration, | ||
| callsite: AtomicPtr<Metadata<'static>>, | ||
| } | ||
|
|
||
| impl TestSubscriber { | ||
| fn new(sleep_micros: u64) -> Self { | ||
| Self { | ||
| sleep: Duration::from_micros(sleep_micros), | ||
| callsite: AtomicPtr::new(ptr::null_mut()), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl Subscriber for TestSubscriber { | ||
| fn register_callsite(&self, metadata: &'static Metadata<'static>) -> tracing_core::Interest { | ||
| if !self.sleep.is_zero() { | ||
| thread::sleep(self.sleep); | ||
| } | ||
|
|
||
| self.callsite | ||
| .store(metadata as *const _ as *mut _, Ordering::SeqCst); | ||
|
|
||
| tracing_core::Interest::always() | ||
| } | ||
|
|
||
| fn event(&self, event: &tracing_core::Event<'_>) { | ||
| let stored_callsite = self.callsite.load(Ordering::SeqCst); | ||
| let event_callsite: *mut Metadata<'static> = event.metadata() as *const _ as *mut _; | ||
|
|
||
| // This assert is the actual test. | ||
| assert_eq!( | ||
| stored_callsite, event_callsite, | ||
| "stored callsite: {stored_callsite:#?} does not match event \ | ||
| callsite: {event_callsite:#?}. Was `event` called before \ | ||
| `register_callsite`?" | ||
| ); | ||
| } | ||
|
|
||
| fn enabled(&self, _metadata: &Metadata<'_>) -> bool { | ||
| true | ||
| } | ||
| fn new_span(&self, _span: &span::Attributes<'_>) -> span::Id { | ||
| span::Id::from_u64(0) | ||
| } | ||
| fn record(&self, _span: &span::Id, _values: &span::Record<'_>) {} | ||
| fn record_follows_from(&self, _span: &span::Id, _follows: &span::Id) {} | ||
| fn enter(&self, _span: &tracing_core::span::Id) {} | ||
| fn exit(&self, _span: &tracing_core::span::Id) {} | ||
| } | ||
|
|
||
| fn dispatch_event(idx: usize) { | ||
| static CALLSITE: DefaultCallsite = { | ||
| // The values of the metadata are unimportant | ||
| static META: Metadata<'static> = Metadata::new( | ||
| "event ", | ||
| "module::path", | ||
| Level::INFO, | ||
| None, | ||
| None, | ||
| None, | ||
| FieldSet::new(&["message"], tracing_core::callsite::Identifier(&CALLSITE)), | ||
| Kind::EVENT, | ||
| ); | ||
| DefaultCallsite::new(&META) | ||
| }; | ||
| let _interest = CALLSITE.interest(); | ||
|
|
||
| let meta = CALLSITE.metadata(); | ||
| let field = meta.fields().field("message").unwrap(); | ||
| let message = format!("event-from-{idx}", idx = idx); | ||
| let values = [(&field, Some(&message as &dyn Value))]; | ||
| let value_set = CALLSITE.metadata().fields().value_set(&values); | ||
|
|
||
| Event::dispatch(meta, &value_set); | ||
| } | ||
|
|
||
| /// Regression test for missing register_callsite call (#2743) | ||
| /// | ||
| /// This test provides the race condition which causes multiple threads to attempt to register the | ||
| /// same callsite in parallel. Previously, if one thread finds that another thread is already in | ||
| /// The process of registering a callsite, it would continue on to call `enabled` and then possible | ||
| /// `event` or `new_span` which could then be called before ``register_callsite` had completed on | ||
| /// the thread actually registering the callsite. | ||
| /// | ||
| /// Because the test depends on the interaction of multiple dispatchers in different threads, | ||
| /// it needs to be in a test file by itself. | ||
| #[test] | ||
| fn event_before_register() { | ||
| let register_sleep_micros = 100; | ||
| let subscriber = TestSubscriber::new(register_sleep_micros); | ||
| dispatcher::set_global_default(Dispatch::new(subscriber)).unwrap(); | ||
|
|
||
| std::thread::scope(|s| { | ||
| for idx in 0..16 { | ||
| thread::Builder::new() | ||
| .name(format!("event-{idx}")) | ||
| .spawn_scoped(s, move || dispatch_event(idx)) | ||
| .expect("failed to spawn thread"); | ||
| } | ||
| }); | ||
|
|
||
| // let subscriber_1_register_sleep_micros = 100; | ||
| // let subscriber_2_register_sleep_micros = 0; | ||
| // | ||
| // let jh1 = subscriber_thread(1, subscriber_1_register_sleep_micros); | ||
| // | ||
| // // This delay ensures that the event callsite has interest() called first. | ||
| // thread::sleep(Duration::from_micros(50)); | ||
| // let jh2 = subscriber_thread(2, subscriber_2_register_sleep_micros); | ||
| // | ||
| // jh1.join().expect("failed to join thread"); | ||
| // jh2.join().expect("failed to join thread"); | ||
| } | ||
|
|
||
|
|
File renamed without changes.
93 changes: 93 additions & 0 deletions
93
tracing/tests/missed_register_callsite_global_subscriber.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| use std::{ | ||
| ptr, | ||
| sync::atomic::{AtomicPtr, Ordering}, | ||
| thread, | ||
| time::Duration, | ||
| }; | ||
|
|
||
| use tracing::Subscriber; | ||
| use tracing_core::{span, Metadata}; | ||
|
|
||
| struct TestSubscriber { | ||
| creator_thread: String, | ||
| sleep: Duration, | ||
| callsite: AtomicPtr<Metadata<'static>>, | ||
| } | ||
|
|
||
| impl TestSubscriber { | ||
| fn new(sleep_micros: u64) -> Self { | ||
| let creator_thread = thread::current() | ||
| .name() | ||
| .unwrap_or("<unknown thread>") | ||
| .to_owned(); | ||
| Self { | ||
| creator_thread, | ||
| sleep: Duration::from_micros(sleep_micros), | ||
| callsite: AtomicPtr::new(ptr::null_mut()), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl Subscriber for TestSubscriber { | ||
| fn register_callsite(&self, metadata: &'static Metadata<'static>) -> tracing_core::Interest { | ||
| if !self.sleep.is_zero() { | ||
| thread::sleep(self.sleep); | ||
| } | ||
|
|
||
| self.callsite | ||
| .store(metadata as *const _ as *mut _, Ordering::SeqCst); | ||
| println!( | ||
| "{creator} from {thread:?}: register_callsite: {callsite:#?}", | ||
| creator = self.creator_thread, | ||
| callsite = metadata as *const _, | ||
| thread = thread::current().name(), | ||
| ); | ||
| tracing_core::Interest::always() | ||
| } | ||
|
|
||
| fn event(&self, event: &tracing_core::Event<'_>) { | ||
| let stored_callsite = self.callsite.load(Ordering::SeqCst); | ||
| let event_callsite: *mut Metadata<'static> = event.metadata() as *const _ as *mut _; | ||
|
|
||
| println!( | ||
| "{creator} from {thread:?}: event (with callsite): {event_callsite:#?} (stored callsite: {stored_callsite:#?})", | ||
| creator = self.creator_thread, | ||
| thread = thread::current().name(), | ||
| ); | ||
|
|
||
| // This assert is the actual test. | ||
| assert_eq!( | ||
| stored_callsite, event_callsite, | ||
| "stored callsite: {stored_callsite:#?} does not match event \ | ||
| callsite: {event_callsite:#?}. Was `event` called before \ | ||
| `register_callsite`?" | ||
| ); | ||
| } | ||
|
|
||
| fn enabled(&self, _metadata: &Metadata<'_>) -> bool { | ||
| true | ||
| } | ||
| fn new_span(&self, _span: &span::Attributes<'_>) -> span::Id { | ||
| span::Id::from_u64(0) | ||
| } | ||
| fn record(&self, _span: &span::Id, _values: &span::Record<'_>) {} | ||
| fn record_follows_from(&self, _span: &span::Id, _follows: &span::Id) {} | ||
| fn enter(&self, _span: &tracing_core::span::Id) {} | ||
| fn exit(&self, _span: &tracing_core::span::Id) {} | ||
| } | ||
|
|
||
| #[test] | ||
| fn event_before_register() { | ||
| let register_sleep_micros = 100; | ||
| let subscriber = TestSubscriber::new(register_sleep_micros); | ||
| tracing::subscriber::set_global_default(subscriber).unwrap(); | ||
|
|
||
| std::thread::scope(|s| { | ||
| for idx in 0..16 { | ||
| thread::Builder::new() | ||
| .name(format!("event-{idx}")) | ||
| .spawn_scoped(s, move || tracing::info!("Thread {} started", idx)) | ||
| .expect("failed to spawn thread"); | ||
| } | ||
| }); | ||
| } |
File renamed without changes.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we must spin, then at least use
std::hint::spin_loop().There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the tip!
This currently causes a deadlock in the case of reentrant traces from inside a subscriber, so I'm trying to come up with a solution that avoids that issue.