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
Adding locks where context is accessed #528
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -99,6 +99,9 @@ func (s *Span) SetTag(key string, value interface{}) opentracing.Span { | |
return s.setTagInternal(key, value, true) | ||
} | ||
|
||
// setTagInternal sets tags in a thread-safe manner if lock param is set to true. | ||
// lock param can be set to false if concurrent access in not expected like when span is created. | ||
// The caller shouldn't obtain any lock on the span while calling this as it will lead to a deadlock. | ||
func (s *Span) setTagInternal(key string, value interface{}, lock bool) opentracing.Span { | ||
s.observer.OnSetTag(key, value) | ||
if key == string(ext.SamplingPriority) && !setSamplingPriority(s, value) { | ||
|
@@ -120,8 +123,8 @@ func (s *Span) setTagInternal(key string, value interface{}, lock bool) opentrac | |
|
||
// SpanContext returns span context | ||
func (s *Span) SpanContext() SpanContext { | ||
s.Lock() | ||
defer s.Unlock() | ||
s.RLock() | ||
defer s.RUnlock() | ||
return s.context | ||
} | ||
|
||
|
@@ -345,7 +348,7 @@ func (s *Span) FinishWithOptions(options opentracing.FinishOptions) { | |
decision := s.tracer.sampler.OnFinishSpan(s) | ||
s.applySamplingDecision(decision, true) | ||
} | ||
if s.context.IsSampled() { | ||
if s.SpanContext().IsSampled() { | ||
s.Lock() | ||
s.fixLogsIfDropped() | ||
if len(options.LogRecords) > 0 || len(options.BulkLogData) > 0 { | ||
|
@@ -366,8 +369,8 @@ func (s *Span) FinishWithOptions(options opentracing.FinishOptions) { | |
|
||
// Context implements opentracing.Span API | ||
func (s *Span) Context() opentracing.SpanContext { | ||
s.Lock() | ||
defer s.Unlock() | ||
s.RLock() | ||
defer s.RUnlock() | ||
return s.context | ||
} | ||
|
||
|
@@ -425,12 +428,15 @@ func (s *Span) serviceName() string { | |
return s.tracer.serviceName | ||
} | ||
|
||
// applySamplingDecision modifies sampling state in a thread-safe manner if lock param is set to true. | ||
// lock param can be set to false if concurrent access in not expected like when span is created. | ||
// The caller shouldn't obtain any lock on the span while calling this as it will lead to a deadlock. | ||
func (s *Span) applySamplingDecision(decision SamplingDecision, lock bool) { | ||
if !decision.Retryable { | ||
s.context.samplingState.setFinal() | ||
s.SpanContext().samplingState.setFinal() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The presence of the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah rwmutex is not re-entrable, but so far this is not called in places where the caller already obtains a lock, the method is thread-safe in itself, so the caller shouldn't need to obtain locks, added documentation for that. lock param is set to false only in the start span method where too no locks are obtained. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Personally, I think it's dangerous to ignore the lock parameter even if right now there doesn't happen to be a codepath that deadlocks. Perhaps access Alternatively the way it's been changed we're always locking and we've already taken the performance hit. We could just remove the lock param. Depending on benchmarks this may be an option. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah that is true, I started making changes that way to apply lock while calling all these methods, but this method also calls sampler and observer callbacks like We can call locks around these call backs, the code is going to lock and unlock twice at-least in each method, one before and after sampler callbacks. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more. This is how some three methods in span.go would look, if this looks good we can go with this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. some benchmarks can tell which approach is better, I will add some micro benchmarks over the weekend |
||
} | ||
if decision.Sample { | ||
s.context.samplingState.setSampled() | ||
s.SpanContext().samplingState.setSampled() | ||
if len(decision.Tags) > 0 { | ||
if lock { | ||
s.Lock() | ||
|
@@ -445,12 +451,12 @@ func (s *Span) applySamplingDecision(decision SamplingDecision, lock bool) { | |
|
||
// Span can be written to if it is sampled or the sampling decision has not been finalized. | ||
func (s *Span) isWriteable() bool { | ||
state := s.context.samplingState | ||
state := s.SpanContext().samplingState | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
return !state.isFinal() || state.isSampled() | ||
} | ||
|
||
func (s *Span) isSamplingFinalized() bool { | ||
return s.context.samplingState.isFinal() | ||
return s.SpanContext().samplingState.isFinal() | ||
} | ||
|
||
// setSamplingPriority returns true if the flag was updated successfully, false otherwise. | ||
|
@@ -469,26 +475,25 @@ func setSamplingPriority(s *Span, value interface{}) bool { | |
if !ok { | ||
return false | ||
} | ||
ctx := s.SpanContext() | ||
if val == 0 { | ||
s.context.samplingState.unsetSampled() | ||
s.context.samplingState.setFinal() | ||
ctx.samplingState.unsetSampled() | ||
ctx.samplingState.setFinal() | ||
return true | ||
} | ||
if s.tracer.options.noDebugFlagOnForcedSampling { | ||
s.context.samplingState.setSampled() | ||
s.context.samplingState.setFinal() | ||
ctx.samplingState.setSampled() | ||
ctx.samplingState.setFinal() | ||
return true | ||
} else if s.tracer.isDebugAllowed(s.operationName) { | ||
s.context.samplingState.setDebugAndSampled() | ||
s.context.samplingState.setFinal() | ||
ctx.samplingState.setDebugAndSampled() | ||
ctx.samplingState.setFinal() | ||
return true | ||
} | ||
return false | ||
} | ||
|
||
// EnableFirehose enables firehose flag on the span context | ||
func EnableFirehose(s *Span) { | ||
s.Lock() | ||
defer s.Unlock() | ||
s.context.samplingState.setFirehose() | ||
s.SpanContext().samplingState.setFirehose() | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -363,7 +363,6 @@ func TestSpan_References(t *testing.T) { | |
} | ||
|
||
func TestSpanContextRaces(t *testing.T) { | ||
t.Skip("Skipped: test will panic with -race, see https://github.com/jaegertracing/jaeger-client-go/issues/526") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might be useful to add a few more mutators to this test, namely those that affect samplingState and flags. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah added a few, and also called span.Finish in the end, and found a few more data race conditions in the reportSpan() method and modified it |
||
tracer, closer := NewTracer("test", NewConstSampler(true), NewNullReporter()) | ||
defer closer.Close() | ||
|
||
|
@@ -395,6 +394,16 @@ func TestSpanContextRaces(t *testing.T) { | |
go accessor(func() { | ||
span.BaggageItem("k") | ||
}) | ||
go accessor(func() { | ||
ext.SamplingPriority.Set(span, 0) | ||
}) | ||
go accessor(func() { | ||
EnableFirehose(span) | ||
}) | ||
go accessor(func() { | ||
span.SpanContext().samplingState.setFlag(flagDebug) | ||
}) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Added a comment to the main thread with some final thoughts. |
||
time.Sleep(100 * time.Millisecond) | ||
span.Finish() | ||
close(end) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -439,7 +439,7 @@ func (t *Tracer) emitNewSpanMetrics(sp *Span, newTrace bool) { | |
func (t *Tracer) reportSpan(sp *Span) { | ||
if !sp.isSamplingFinalized() { | ||
t.metrics.SpansFinishedDelayedSampling.Inc(1) | ||
} else if sp.context.IsSampled() { | ||
} else if sp.SpanContext().IsSampled() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. store the value of |
||
t.metrics.SpansFinishedSampled.Inc(1) | ||
} else { | ||
t.metrics.SpansFinishedNotSampled.Inc(1) | ||
|
@@ -448,7 +448,7 @@ func (t *Tracer) reportSpan(sp *Span) { | |
// Note: if the reporter is processing Span asynchronously then it needs to Retain() the span, | ||
// and then Release() it when no longer needed. | ||
// Otherwise, the span may be reused for another trace and its data may be overwritten. | ||
if sp.context.IsSampled() { | ||
if sp.SpanContext().IsSampled() { | ||
t.reporter.Report(sp) | ||
} | ||
|
||
|
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 are a number of other places that hold
.Lock()
when only.RLock()
is needed. Seeing.StartTime()
,.Duration()
,.Tags()
for instance. Probably not in the scope of this fix though.