[Issue 3112] Use the configured transaction provider when no override is supplied#3525
Conversation
…ovider if an override is not supplied, plus throw an exception if a transaction provider is supplied with a different transaction type from the one configured.
…an override is not supplied.
…r to the CommandProcessor
… wrapping exceptions thrown inside invocations
There was a problem hiding this comment.
Gates Failed
Prevent hotspot decline
(1 hotspot with Code Duplication)
Enforce advisory code health rules
(1 file with Code Duplication)
Gates Passed
2 Quality Gates Passed
See analysis details in CodeScene
Reason for failure
| Prevent hotspot decline | Violations | Code Health Impact | |
|---|---|---|---|
| CommandProcessor.cs | 1 rule in this hotspot | 8.28 → 7.33 | Suppress |
| Enforce advisory code health rules | Violations | Code Health Impact | |
|---|---|---|---|
| CommandProcessor.cs | 1 advisory rule | 8.28 → 7.33 | Suppress |
Quality Gate Profile: Clean Code Collective
Want more control? Customize Code Health rules or catch issues early with our IDE extension and CLI tool.
iancooper
left a comment
There was a problem hiding this comment.
I would like to see if we can move away from a spy here, and use our InMemoryOutbox and possibly add an InMemoryTransaction and InMemoryTransactionProvider
|
|
||
| namespace Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles | ||
| { | ||
| internal class SpyOutbox : |
There was a problem hiding this comment.
Could we use the InMemoryOutbox? We are trying to avoid Test Doubles where the in-memory version could substitute. If we need to query the state of the outbox, is that something that is all outboxed, or should the in-memory outbox be exposed?
There was a problem hiding this comment.
The issue with using the InMemoryOutbox is that it's the default implementation, and the scenario being tested here is when the user uses something other than the default, since that was what led to this issue being found in the first place (I also wouldn't have found that we should be clearing the bound MethodInfo's when we clear the outbox mediator from the CommandProcessor).
To do that without a test-only Spy implementation we would need to either have an additional type of in memory outbox in the core package (which feels redundant?), or pull in one of the other outbox implementations (and then require all the associated infrastructure in order to be able to run the core tests). This feels like the cleanest solution to me.
There was a problem hiding this comment.
Ah yes, that makes sense in this context. Closed.
|
|
||
| namespace Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; | ||
|
|
||
| public class SpyTransaction |
There was a problem hiding this comment.
Similar to in-memory, does this imply that perhaps we need to swap from CommittableTransacton to our own InMemoryTransaction to use?
|
|
||
| namespace Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; | ||
|
|
||
| public class SpyTransactionProvider : IAmABoxTransactionProvider<SpyTransaction> |
There was a problem hiding this comment.
Again, does this imply an InMemoryTransactionProvider?
| private static readonly ConcurrentDictionary<string, MethodInfo> s_boundDepositCalls = new(); | ||
| private static readonly ConcurrentDictionary<string, MethodInfo> s_boundDepositCallsAsync = new(); | ||
| private static readonly ConcurrentDictionary<string, MethodInfo> s_boundBulkDepositCalls = new(); | ||
| private static readonly ConcurrentDictionary<string, MethodInfo> s_boundBulkDepositCallsAsync = new(); | ||
| private static readonly ConcurrentDictionary<string, MethodInfo> s_boundMediatorMethods = new(); |
There was a problem hiding this comment.
Since you are changing this, what about using Delegate rather than MethodInfo.Invoke?
private static readonly ConcurrentDictionary<string, Func<....>> s_boundDepositCallsAsync = new();
....
var del =method.CreateDelegate<Func<...>>();
s_boundDepositCallsAsync .Add(key, del);
...
var del = s_boundDepositCallsAsync[key];
del(.....);There was a problem hiding this comment.
There are a few reasons not to use Delegates here. If you look at how the code would change, you'd be going from this:
deposit = depositMethod?.MakeGenericMethod(actualRequestType, transactionType)!;
return (deposit?.Invoke(this, [actualRequest, amABoxTransactionProvider, requestContext, dictionary, batchId]) as string)!;
To this:
deposit = depositMethod?.MakeGenericMethod(actualRequestType, transactionType)!;
var delegate = deposit.CreateDelegate(
typeof(Func<IRequest, IAmABoxTransactionProvider, RequestContext, Dictionary<string, object>, string, Type, string>),
this);
return (delegate.DynamicInvoke(actualRequest, amABoxTransactionProvider, requestContext, dictionary, batchId) as string)!;
By storing the methods as MethodInfo's, we can also store multiple methods with different signatures in the same collection, as we do with the the bound mediator methods in s_boundMediatorMethods. If they were delegates, each mediator method would need a separate member variable. So overall, using delegates makes the code more verbose without any real benefit in terms of clarity.
Lastly, using Delegate.DynamicInvoke is actually much slower than MethodInfo.Invoke, and given we're at the heart of things here we want it to be as performant as possible.
There was a problem hiding this comment.
I should also say that MethodInfo.CreateDelegate<T> doesn't exist in netstandard2.0, which we still support at the moment. Hence the need for Delegate.DynamicInvoke.
# Conflicts: # src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs # src/Paramore.Brighter/CommandProcessor.cs # src/Paramore.Brighter/CommandProcessorBuilder.cs
There was a problem hiding this comment.
Gates Passed
4 Quality Gates Passed
See analysis details in CodeScene
Quality Gate Profile: Clean Code Collective
Want more control? Customize Code Health rules or catch issues early with our IDE extension and CLI tool.
There was a problem hiding this comment.
Gates Passed
4 Quality Gates Passed
See analysis details in CodeScene
Quality Gate Profile: Clean Code Collective
Want more control? Customize Code Health rules or catch issues early with our IDE extension and CLI tool.
|
I think we could merge this in if you want @dhickie? |
… is supplied (BrighterCommand#3525) * Update DepositPost and DepositPostAsync to use default transaction provider if an override is not supplied, plus throw an exception if a transaction provider is supplied with a different transaction type from the one configured. * Update Post and PostAsync to use the default transaction provider if an override is not supplied. * Flow the transaction provider through from the CommandProcessorBuilder to the CommandProcessor * Fix binding generic methods of the wrong type to invocation targets & wrapping exceptions thrown inside invocations * Add DepositPost tests * Add Post tests & clear bound CommandProcessor methods when clearing the mediator * Fix broken CommandProcessor test setup
This PR fixes issue #3112, in which an
InvalidCastExceptionwas thrown when callingPostorPostAsyncon theCommandProcessorif a persistent outbox was registered.It does this by switching to a reflection based approach when calling methods on the
OutboxProducerMediator, removing the need for the problematic cast. The transaction type for the generic method call is taken from the registered transaction provider during construction, and defaults toCommittableTransactionif no transaction provider is registered.In an effort to simplify usage of the
CommandProcessor, it also makes a couple of other changes:DepositPostorDepositPostAsyncthat doesn't take a transaction provider, it uses the one that was registered along with the persistent outbox (if any).DepositPostorDepositPostAsyncthat does take a transaction provider, but the transaction type for the supplied transaction provider doesn't match the type that was registered with the persistent outbox (if any), anInvalidOperationExceptionis thrown.