-
Notifications
You must be signed in to change notification settings - Fork 10
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
Rework API to better work with async and sync calls #46
Conversation
var executeStopwatch = Stopwatch.StartNew(); | ||
var status = CommandCompletionStatus.RanToCompletion; | ||
|
||
var cts = timeout.HasValue |
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.
Where are you passing through the Command's CancellationToken? i.e. if one has been explicitly passed into the call.
This might be happening somewhere else, but I can't find it at quick glance.
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.
The GetActualTimeout()
call on L61 should get the right value given configuration and default values - is that what you're referring to? I'm still kind of rethinking this a bit, too, for testing and logging purposes.
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.
Ignore me, I was thinking that there was a user specified CancellationToken passed through here, but actually there's not.....carry on 👀
This looks so much cleaner and easier to debug! |
Timeout tests are finicky and non-deterministic. Working on that.
|
||
// ReSharper disable NotAccessedField.Local | ||
// Don't let these get garbage collected. | ||
private readonly GaugeTimer _timer; | ||
private readonly GaugeTimer _timer2; |
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.
Could we maybe rename this one to _metricsTimer, and the other one to _statsTimer? I was confused about why we had two, until I read the comment below.
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.
Renamed these.
This looks sweet, nice work. |
@@ -12,5 +12,11 @@ internal static class ConcurrentDictionaryExtensions | |||
var lazy = dictionary.GetOrAdd(key, new Lazy<V>(() => valueFactory(key), LazyThreadSafetyMode.PublicationOnly)); |
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.
Maybe call the method below? Also I think we spoke about PublicationOnly
before, should we make the default ExecutionAndPublication
, since the most common use case is to not allow the valueFactory
to be called more than once.
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.
Yeah, I might even get rid of this one altogether and have the caller make a decision about the concurrency they'd like.
General comment: It looks like the new default timeout will be 2000ms, rather than 15000. Should it be clear somewhere in code/release notes that this is changing when you switch to the new CommandInvoker approach? |
@lewislabs Yeah, good call. I added a bullet to the release notes. Any thoughts on the change to 2000 as a default? |
I think it's a good idea to bring it down. Did @marcusdarmstrong have some analysis on timeouts that we could use for a suitable default? |
{ | ||
void Release(); | ||
bool TryEnter(); | ||
int Available { get; } |
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.
Do we want to rename this to CountAvailable
or something indicative of it being a count?
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.
Renamed to CountAvailable
.
This should be okay. Two risks: 1) if locking code ever gets introduced into the actual constructors (unlikely), there could be deadlocks, and 2) we're changing the exception caching mode so that exceptions will be cached and re-thrown on subsequent attempts to initialize the Lazy, which is probably fine. We don't see any initialization errors today.
Rework API to better work with async and sync calls
These changes will all be backwards compatible, so this ought to result in a minor version update where clients can move code from the old
Command
over to the two new types at their leisure. Not sure ifCommand
will get an[Obsolete]
tag yet, though.For an example of some of the API changes, check out
Example.cs
Code Review: Two good entry points for review are
CommandInvoker.cs
andBaseCommand.cs
.CommandInvoker
delegates to a couple other invokers (BulkheadInvoker
andBreakerInvoker
) that could use review as well.Separate sync/async Command APIs
Instead of
Invoke()
andInvokeAsync()
onCommand
, implementations need to inherit fromSyncCommand
orAsyncCommand
. This avoids the forced async-to-sync that's built into the library, and avoids the need for callers to make problematic async-to-sync conversions. Though convenient for callers, it's often a source of confusion, and also means that Mjolnir is more inefficient withasync
than it ought to be.Currently, Commands are used like this:
This PR offers an alternative API:
Throw
andReturn
variantsThe
Return
variants will catch and wrap Exceptions, preferring to always return a result to the caller (even when errors occur). Since Mjolnir's intent is to introduce fast failure to avoid failure cascading in unpredictable ways, it's useful for callers to handle that failure in ways beyond just re-throwing the exception upward. This could be handled by the caller usingtry/catch
, but baking it into the Mjolnir API helps callers realize that they need to think about failure and make conscious decisions about how to handle it.The
Return
variant returns aCommandResult<TResult>
instead of justTResult
. The wrapper contains the result or the causing exception if the command failed. TheThrow
method overrides don't return a wrapped result since it would add an unnecessary "unwrapping" step for callers.Timeout override
Default and configured timeouts still apply, but callers can also provide a per-call timeout if they desire more lenient/rigid SLAs.
Improved testability
This will also make commands more unit-testable, since
ICommandInvoker
will be injectable (where injecting Command behavior is currently a challenge).Use semaphore bulkheads (without queues) by default
I'm not sold on the use of thread pools as a bulkhead. Their strongest advantage is the ability to have a small queue in front that allows for absorbing some latency and bursts. However, there are a number of disadvantages.
async
. Fully async code means that threads are returned to the thread pool until the async work is done. Introducing an entire thread to "wrap" that and limit concurrency feels nonsensical to me.async
work.I'd like to try using semaphores as the default bulkhead. It does mean (for now, at least) getting rid of the queues, but they can be re-introduced if needed in the future.
Better interface for hooking into metrics
The existing interface (
IStats
[docs]) is pretty vague and free-form, and makes it difficult to adapt events into collectors that use standardized metric types like timers and meters.A new interface,
IMetricEvents
, is available that should make that easier. It provides specific methods for events that occur, and recommends a metric type for each. A couple of example methods:The original
IStats
were targeted both at profiling and operational events;IMetricEvents
focuses less on method profiling and more on relevant bulkhead, breaker, and command events. Parity betweenIStats
andIMetricEvents
wasn't a goal, so they're fairly different.The new
BaseCommand
/CommandInvoker
pipeline will not publish the oldIStats
events.The adapter we use will also be made available (once complete).
For Release Notes
A list of items that should end up in the release notes.
Differences between old and new Command
Invoke
INFO log isInvoke
for both sync and async calls (Invoke Command={0} Breaker={1} Pool={2} Timeout={3}
). In the old Command, it'sInvokeAsync
for the async call.CommandFailedException
. Instead, the root cause exception is simply rethrown with some Command-specific data attached to it.mjolnir.command.
The old commands were inconsistent with the rest of the library and just started withcommand.
. Examples:command.my-group.MyCommand.Timeout=1000
mjolnir.command.my-group.MyCommand.Timeout=1000
Possible Remaining Work
[Command]
support for sync and async commandsHudl.Mjolnir.Interceptor
), which would help get rid of a dependency onCastle.Core
.Task
(non-generic) from execute methods