The way the ScopeManager is currently defined makes the lifecycle of Span objects unclear. This complicates span object recycling aka object pooling.
Recycling objects is a way to reduce the allocation overhead by maintaining a pool of pre-allocated Span instances. Instead of instantiating a new span every time a span is started, a span is taken out of the object pool. When the Span is finished and has been reported out of process, the span instance is reset and put back into the object pool. Using this technique, it is possible to add almost no allocation overhead to the monitored application which means less GC overhead caused by the tracer.
IMO, the lifecycle of a Span should be very straightforward. When finish() is called, no other interactions should be allowed so it is safe to recycle and reuse the whole span instance.
There are two APIs which break this assumption.
Span.context
Quote from The spec of Span.finish:
With the exception of the method to retrieve the Span's SpanContext, none of the below may be called after the Span is finished.
IMO, the SpanContext should be retrieved before the span is finished if it is intended to be used after the span is finished. The implementations are supposed to return an object which is not tied to the span’s lifecycle, for example making a copy of the internal span context.
ScopeManager
Even in the current version of the spec, it “forbids” calls after finish():
With the exception of the method to retrieve the Span's SpanContext, none of the below may be called after the Span is finished.
Scoping a span in a different thread than the one it is finished from, indirectly breaks this requirement. In the second thread, there is no way to know whether the activeSpan() has already finished, as it has no control over the lifecycle of that span. So calling tracer.activeSpan().addTag("foo", "bar") is never legal if the active span is finished in a different thread. The Span reference returned by tracer.activeSpan() might already be recycled and used to record an entirely different operation.
Example scenario:
| Thread A |
Thread B |
|
| Span span1 = startSpan() |
|
|
| Scope scopeSpan1 = scopeManager.activate(span1) |
|
|
| Executor.sumbit(Runnable) |
Runnable.run |
|
| |
Scope scopeSpan1 = scopeManager.activate(span1) |
|
| scopeSpan1.close() |
|
|
| span1.finish() |
|
|
| |
tracer.activeSpan().addTag("foo", "bar") |
Is this span finished or not? The caller has no way to know that. Worst case is that it now represents an entirely different operation. |
| |
Span span2 = startSpan() |
|
| |
span2.finish() |
|
| |
scopeSpan1.finish() |
|
For Java, the damage is probably not as big unless the tracer implements object pooling. I don’t have a lot of experience with C++, but don’t you have to exactly know the lifecycle of spans in order to be able to deallocate the span’s memory?
Suggestion
Change
With the exception of the method to retrieve the Span's SpanContext, none of the below may be called after the Span is finished.
To
After the Span is finished no other interactions are allowed, including activating the Span via the scope manager and retrieving the Span's SpanContext.
- Add method
Scope activate(SpanContext spanContext) to ScopeManager
Scope.span() returns null in this case.
- Add method
SpanContext spanContext() to Scope
- In case
Scope#activate(Span) has been called, acts as a shortcut for Scope.span().context()
For SpanBuilders which did not call SpanBuilder#ignoreActiveSpan, an implicit child_of reference will be created for ScopeManager.active().spanContext() contains a SpanContext. If ScopeManager.active().span() is not null, this is the preferred way to set the reference.
//cc @adriancole @raphw
The way the ScopeManager is currently defined makes the lifecycle of Span objects unclear. This complicates span object recycling aka object pooling.
Recycling objects is a way to reduce the allocation overhead by maintaining a pool of pre-allocated Span instances. Instead of instantiating a new span every time a span is started, a span is taken out of the object pool. When the Span is finished and has been reported out of process, the span instance is reset and put back into the object pool. Using this technique, it is possible to add almost no allocation overhead to the monitored application which means less GC overhead caused by the tracer.
IMO, the lifecycle of a Span should be very straightforward. When finish() is called, no other interactions should be allowed so it is safe to recycle and reuse the whole span instance.
There are two APIs which break this assumption.
Span.context
Quote from The spec of Span.finish:
IMO, the SpanContext should be retrieved before the span is finished if it is intended to be used after the span is finished. The implementations are supposed to return an object which is not tied to the span’s lifecycle, for example making a copy of the internal span context.
ScopeManager
Even in the current version of the spec, it “forbids” calls after
finish():Scoping a span in a different thread than the one it is finished from, indirectly breaks this requirement. In the second thread, there is no way to know whether the
activeSpan()has already finished, as it has no control over the lifecycle of that span. So callingtracer.activeSpan().addTag("foo", "bar")is never legal if the active span is finished in a different thread. The Span reference returned bytracer.activeSpan()might already be recycled and used to record an entirely different operation.Example scenario:
For Java, the damage is probably not as big unless the tracer implements object pooling. I don’t have a lot of experience with C++, but don’t you have to exactly know the lifecycle of spans in order to be able to deallocate the span’s memory?
Suggestion
Change
To
Scope activate(SpanContext spanContext)toScopeManagerScope.span()returnsnullin this case.SpanContext spanContext()toScopeScope#activate(Span)has been called, acts as a shortcut forScope.span().context()For SpanBuilders which did not call
SpanBuilder#ignoreActiveSpan, an implicitchild_ofreference will be created forScopeManager.active().spanContext()contains aSpanContext. IfScopeManager.active().span()is notnull, this is the preferred way to set the reference.//cc @adriancole @raphw