-
Notifications
You must be signed in to change notification settings - Fork 440
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
Support for multiple processors #692
Conversation
Codecov Report
@@ Coverage Diff @@
## main #692 +/- ##
==========================================
- Coverage 94.81% 94.72% -0.09%
==========================================
Files 214 216 +2
Lines 9750 9895 +145
==========================================
+ Hits 9244 9373 +129
- Misses 506 522 +16
|
} | ||
} | ||
|
||
void AddProcessor(std::unique_ptr<SpanProcessor> &&processor) |
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 the intention is to allow processors to be added at runtime, we will need to cover thread safety.
For example, this is thread-safe in a single-writer-multi-reader situation (e.g. the processors chain is handling spans from multiple threads, while one thread is trying to add a new processor).
Given here we're using STL vector, I guess we'll run into race condition - if one thread is trying to add a processor while other threads are sending spans to the processor chain, we might get broken state.
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.
Good comment. My earlier code did have mutex guard against all vector operations in this class, somehow missed out to checkin that. The doubly linked list (as done in dotnet) looks like a good option, would be good to do some benchmarking to select the optimal one of two.
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.
I can share the thinking from the .NET implementation:
STL vector + mutex would give the ultimate thread-safety in multi-reader-multi-writer situation, the potential downside is that it will introduce contention to the readers (in this case, the readers are the threads that push spans to the processor chain), which might be something to avoid.
Doubly linked list only gives single-writer-multi-reader thread-safety, however it is well aligned with the general .NET API guideline (that if a class method doesn't specify thread safety, by default it is not supposed to be called concurrently). And if we need the ultimate thread safety later, we can introduce a lock object which is shared among writers (but not readers), in this way we have the minimum contention on the hot path.
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.
Ok. now I realize why I removed the mutex guard - as MultiSpanProcessor
is an atomic instance in TracerContext
:
opentelemetry::sdk::common::AtomicUniquePtr<SpanProcessor> processor_; |
And so multiple threads shouldn't be able to access it simultaneously.
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.
Atomic just means that you can compare-and-swap the pointer. Users can still access it simultaneously (after reading the pointer in memory).
if you have AddProcessor
return a new MultiProcessor instance and compare-and-swap the Atomic Ptr then I think you'd be ok, but as is, you still have a (possible) threading issue if more than thread calls AddProcessor. It's unlikely but still possible.
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.
OK, I think I got your point, in that case, we should have Atomic Ptr for Span Processor, and create new instance of MultiProcessor everytime AddProcessor() is called. It is already documented that AddProcessor() is not thread safe, and shouldn't be called simultaneously. I will do these changes.
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.
Ah, I missed that AddProcessor was marked not thread-safe as well. This means my point is not important.
If the changes to make AddProcessor thread-safe aren't too painful, I think it's worth it. I think it's a low-frequency method call, so it's ok to be "expensive" to AddProcessor as long as the "read" hot path is fast (which you already have AFAICT). Removing my requested changes. I'd prefer to see AddProcessor thread-safe but this is fine as-is.
Co-authored-by: Reiley Yang <reyang@microsoft.com>
examples/multi_processor/README.md
Outdated
@@ -0,0 +1,12 @@ | |||
|
|||
# Simple Trace Example |
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.
Remove empty first line and change the title to indicate this is for multiple processors?
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.
Good point, have changed the title, and the description too.
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.
Method name improvements LGTM for Processor / AddProcessor.
+1 to Reiley's comments on thread safety, will re-review when that's updated, but like this approach!
// Default is an always-on sampler. | ||
auto context = std::make_shared<sdktrace::TracerContext>(std::move(processor)); | ||
auto context = std::make_shared<sdktrace::TracerContext>(std::move(processors)); |
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.
Should there be a convenience constructor that just takes on processor?
Alternatively, a static helper method to make TracerProvider from a processor?
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.
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.
Consider taking a look at the other language clients that reached 1.0 stable and see what's the best practice.
One more example from Python https://opentelemetry-python.readthedocs.io/en/latest/getting-started.html.
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.
This specific example is using TracerContext
. This is how it looks like if we directly use TracerProvider
to add processors:
( which would be what application developer be doing most of the times ):
auto provider = nostd::shared_ptr<opentelemetry::trace::TracerProvider>(std:move(processor1), resource, sampler, id_generator);
provider.AddProcessor(std::move(processor2));
provider.AddProcessor(std::move(processor3));
opentelemetry::trace::Provider::SetTracerProvider(provider);
Do we want this for TracerContext
too ?
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.
I have modified the example NOT to use TracerContext
, instead directly use TracerProvider
to pass processor(s) as an argument to constructor. TracerProvider
constructor takes either of single processor, or vector of processors as an argument as shown in example in above comment ( and in PR description ).
As suggested by @reyang , the following changes are done
|
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.
LGTM.
const std::unique_ptr<Recordable> &GetRecordable(const SpanProcessor &processor) const noexcept | ||
{ | ||
// TODO - return nullptr ref on failed lookup? | ||
static std::unique_ptr<Recordable> empty(nullptr); |
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.
nit: this empty could be moved to the line right above return empty
? We don't need initialize this value if recordable is found.
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.
Or we could make the empty
variable as class static as it is also useful in ReleaseRecordable
?
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.
this empty could be moved to the line right above return empty? We don't need initialize this value if recordable is found.
done
Or we could make the empty variable as class static as it is also useful in ReleaseRecordable?
That won't help as ReleaseRecordable needs to return unique_ptr, not it's a reference so class static can't be used. But control would never reach both places as we don't support removing processors once configured.
Co-authored-by: Tom Tan <lilotom@gmail.com>
Will see if there are comments from @jsuereth, @pyohannes, and others before closing it, |
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.
I think there's still a threading issue around AddProcessor.
You could possible solve this via fully-copying the processors array in the "AddProcessor" method of TracerContext (and then use a compare-and-swap to flip instances) meaning no threads would have an inconsistent processor. However, the code, as it stands, has a very unlikely threading issue.
|
||
const std::unique_ptr<Recordable> &GetRecordable(const SpanProcessor &processor) const noexcept | ||
{ | ||
// TODO - return nullptr ref on failed lookup? |
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.
Nit: remove the TODO
} | ||
} | ||
|
||
void AddProcessor(std::unique_ptr<SpanProcessor> &&processor) |
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.
Atomic just means that you can compare-and-swap the pointer. Users can still access it simultaneously (after reading the pointer in memory).
if you have AddProcessor
return a new MultiProcessor instance and compare-and-swap the Atomic Ptr then I think you'd be ok, but as is, you still have a (possible) threading issue if more than thread calls AddProcessor. It's unlikely but still possible.
Thank you @jsuereth for the comments. Just to have a better understanding of the issue - the current approach of switching to the linked-list structure still has race condition around |
} | ||
} | ||
|
||
void AddProcessor(std::unique_ptr<SpanProcessor> &&processor) |
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.
Ah, I missed that AddProcessor was marked not thread-safe as well. This means my point is not important.
If the changes to make AddProcessor thread-safe aren't too painful, I think it's worth it. I think it's a low-frequency method call, so it's ok to be "expensive" to AddProcessor as long as the "read" hot path is fast (which you already have AFAICT). Removing my requested changes. I'd prefer to see AddProcessor thread-safe but this is fine as-is.
Yeah, I think you have two options:
|
FYI - a slightly different perspective for consideration, in C# we chose to make AddProcessor not thread safe (see my explanation here) for couple reasons:
Additional thoughts: Maybe we should clarify this in the spec. |
That's a logical reason to enforce non-thread-safe behavior for AddProcessor |
@reyang @jsuereth - Thanks for the comments. I am merging it in the double linked-list based approach as per the discussions here. Atm ptr is not used as AddProcessor() is not thread-safe. Will make it explicit in sdk document. Also will create a ticket to discuss and revisit its design in the future if we need to. |
Fixes #664
Changes
MultiSpanProcessor
- ImplementsSpanProcessor
and internally stores configuredSpanProcessors
.MultiRecordable
- implements the Recordable interface, and encapsulates several regular recordables.TracerContext
internally maintains theMultiSpanProcessor
instance.TracerContext
constructor accepts vector ofSpanProcessors
to be configured and provide methods to add newSpanProcessors
. SpanProcessor once configured cannot be deleted.Example:
(Using
TracerProvider
):(Using
TracerContext
):For significant contributions please make sure you have completed the following items:
CHANGELOG.md
updated for non-trivial changes