Make `Span` and `Transaction` disposable? - Agent API design
See original GitHub issueThis issue is about a design decision and for discussion about this specific decision. Relates to #26.
To save time/space in the discussion we focus on Span
, but the same discussion is valid for Transaction
.
Description of the current situation
The default pattern to wrap a particular piece of code into a span is this:
var span = transaction.StartSpan("SampleSpan", Span.TYPE_DB);
try
{
//code you want to capture as a span
}
catch(Exception e)
{
span.CaptureException(e);
throw; //don't use `throw e` to preserve the call stack.
}
finally
{
span.End();
}
This is very similar to the Java Agent API.
Currently both ISpan
and ITransaction
offer a convenient method to start, stop spans/transactions and also to automatically report unhandled exceptions. The intention is to avoid having to write too many additional lines just to capture something as a span or transaction.
So, the equivalent of the code from above with the convenient API is this:
transaction.CaptureSpan("TestSpan", Span.TYPE_DB, () =>
{
//code you want to capture as a span
});
There are overloads for async methods, and also for methods with return value.
Alternative solution, IDisposable
Lots of similar tools, like MiniProfiler solve this by implementing IDisposable
, and not by having a method that takes Action
or Func
as we do. Therefore some users may expect that our Span
and Transaction
types also implement IDisposable
and disposing those would do basically the same as calling the End()
method.
With this the code would be something like this:
using(var span = transaction.StartSpan("SampleSpan", Span.TYPE_DB))
{
//code you want to capture as a span
//+ capturing exceptions somehow
}
Therefore: Should our Span
and Transaction
types implement IDisposable
?
Pros:
- Other tools do this (OpenTracing C# API, MiniProfiler).
- Wrapping these kings of things into a
using
block seems very natural. using
avoids closures, which we have with the methods taking a lambda. This can cause overhead in perf. critical scenarios due to allocations. (On the other hand theStartX()
andEnd()
combination still can be used to avoid this problem).
Cons:
- The
using
block only partially solves the problem: it can automatically callEnd()
, but it does not capture unhandled exceptions. Actually tools that typically useIDisposable
for similar things don’t have acatch(Exception e){capture(e)}
part in the pattern that they simplify with theusing
block. So having theusing
block plus an additional exception handling logic would make the API more noisy than the plain, original API from the beginning of the discussion. - In the original API ending a span happens by calling the
End()
method, and this is common across all agents. If we implementIDisposable
then we will call theEnd()
method from theDispose()
method and from that point we basically have 2 methods that end a span, and that does not feel right. The current API does not have this confusion. - Adding
IDisposable
is a pattern to wrap unmanaged resources. We don’t really wrap unmanaged resources here. So automatically calling theEnd()
method is a similar problem to the problem thatIDisposable
solved, but it’s not the same. SeeIDisposable
documentation : “Provides a mechanism for releasing unmanaged resources.” That’s not what we do by callingEnd()
.
Currently we don’t implement IDisposable
and that seems to be the better decision, but this can be of course revisited. Happy to hear opinions!
Issue Analytics
- State:
- Created 5 years ago
- Comments:9 (8 by maintainers)
Top GitHub Comments
We discussed this offline and currently we don’t plan to add
IDisposable
to our spans and transactions.I close this now, but we can reopen this and revisit this decision later.
I am brand new to using this, but it definitely felt very off this wasn’t using
using
.Another pro
I’ve used this with great effect in the past adding MiniProfiler to a legacy application: I was specifically interested in profiling some slow, multi-hundred-line, badly-indented monstrosities of functions, and this let me create a readable pull request. Wrapping in braces would have meant a ton of whitespace noise in the PR plus having my name forever stuck on the
git blame
of that awful code.Arguments against cons
I’d still argue it’s less noisy, because it’s one less line of code:
In the case of nested spans – especially if you are doing this in nested functions, which is the natural use case – you can ignore the exception while letting it be caught by a higher span or at the transaction level.
There’s also a trick to detect exceptions, though it feels a bit hacky.
The logic in the
Dispose()
handler could basically be:As a .NET developer, I don’t understand why this would be confusing, as it’s quite a common pattern in many objects that implement IDiposable – for example System.Data.SqlClient.SqlConnection.Close. The comments there state “Close and Dispose are functionally equivalent” and adding that to the docs would help.
See also in my code sample above, I added an
Ended
property to track ifEnd()
was called manually and not call it twice.Arguing semantics, I think it is what this does. A “Span” is pretty much an unmanaged resource – even if it’s not consuming an actual resource like a socket or file handle.
Practically speaking, it’s a widely-used convention in .NET, and frankly, not having it makes this feel like an API from the early 2000’s. This answer links to a lot more debate (both for and against) than I can cover.