Skip to content
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

Remove Message base class #685

Closed
wants to merge 1 commit into from

Conversation

slang25
Copy link
Member

@slang25 slang25 commented May 3, 2020

A draft to start discussion around removing the JS dependency on domain messages.

I think the api would be more intuitive if we go down the route of message envelopes. You get to keep your domain message clean, and still feel like the apis are steering you with types, where as with an unbounded TMessage if feels like you could easily put any parameter in there and not find out till runtime you messed up.

@slang25 slang25 force-pushed the remove-message-base branch from cba521d to eea63cf Compare May 3, 2020 16:34
@slang25
Copy link
Member Author

slang25 commented May 3, 2020

If the resulting API feels weird (and it does) it's cause at the top of the publish api we don't have a generic IMessagePublisher, that would require rework of the builder apis, but would reduce a bunch of the method level generics, and where we switch to and from object & TMessage.

@@ -1,13 +1,12 @@
using System;
using JustSaying.Models;

namespace JustSaying.Messaging.MessageSerialization
{
public interface IMessageSerializer
Copy link
Member Author

@slang25 slang25 May 3, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole interface should be generic and object killed from the api surface.

@@ -1,5 +1,3 @@
using JustSaying.Models;

namespace JustSaying.Messaging.MessageSerialization
{
public interface IMessageSerializationRegister
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole interface should be generic and object killed from the api surface.

@slang25 slang25 force-pushed the remove-message-base branch from eea63cf to c8c8546 Compare May 3, 2020 17:55
@@ -32,7 +34,7 @@ internal sealed class ExactlyOnceHandler<T> : IHandlerAsync<T> where T : Message

public async Task<bool> Handle(T message)
{
string lockKey = $"{message.UniqueKey()}-{_lockSuffixKeyForHandler}";
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So here UniqueKey is a virtual method that defaults to Id.ToString(), I've changed it so we can optionally pass a function when we register the handler, maybe not the right place to surface the api.

@@ -155,8 +154,7 @@ private async Task<bool> CallMessageHandler(Message message)
watch.Stop();

_logger.LogTrace(
"Handled message with Id '{MessageId}' of type {MessageType} in {TimeToHandle}.",
message.Id,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So here it is, the one place where we actually do something with a property on Message (ok, there was UniqueKey() too I guess). It's on a trace log, we arguably would want to use the SQS message id for this level of debugging, but tying it back to the domain event is going to be the challenge.

@slang25
Copy link
Member Author

slang25 commented May 3, 2020

There's a failing test because the uniqueKeySelector is missing from the new fluent api, I think it needs addressing by a different design, it's too error prone otherwise.

@@ -49,14 +48,19 @@ internal sealed class MessageHandlerWrapper
throw new Exception("IMessageLock is null. You need to specify an implementation for IMessageLock.");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should change this to InvalidOperationException(?)

@@ -49,14 +48,19 @@ internal sealed class MessageHandlerWrapper
throw new Exception("IMessageLock is null. You need to specify an implementation for IMessageLock.");
}

if (uniqueKeySelector == null)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should throw this in the caller before we start to do any work, rather than in the delegate further along the call path.

@@ -93,7 +92,7 @@ public SqsNotificationListener WithMessageProcessingStrategy(IMessageProcessingS
return this;
}

public void AddMessageHandler<T>(Func<IHandlerAsync<T>> futureHandler) where T : Message
public void AddMessageHandler<T>(Func<IHandlerAsync<T>> futureHandler, Func<T, string> uniqueKeySelector = default) where T : class
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is optional, how does it avoid causing the ArgumentNullException later? Or is this causing the test failure you mentioned?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow. I didn't know there was a default for Func<T, string>!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There isn't, well there is, it's just null.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, haha. I don't know why I assumed otherwise.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

default is just syntactic sugar added in C# 8 (at least I think it was 8, maybe a minor revision of 7) for default(Type). It's null for any reference type and the "zero" value for value types (0, 0l, etc.). The same behaviour you'd get from LINQ's FirstOrDefault()`.

@@ -11,6 +10,6 @@ public class SnsWriteConfiguration
/// Extension point enabling custom error handling on a per notification basis, including ability handle raised exceptions.
/// </summary>
/// <returns>Boolean indicating whether the exception has been handled</returns>
public Func<Exception, Message, bool> HandleException { get; set; }
public Func<Exception, object, bool> HandleException { get; set; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we'd need to bite the bullet and introduce the generics you mentioned? Otherwise, using the error handler is going to be quite bleurgh from a caller having to cast/pattern-match the type all the time.

Copy link
Member Author

@slang25 slang25 May 4, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this is the bit that persuaded me we need to do better. As it stands there are islands of generic type information, we can fix that by having more generic apis at the top level and flowing it down. A big change with this approach would be IMessagePublisher<T> and reworking the builder APIs (yet again).

Or we do the message envelope thing, which is looking more appealing. It would contain Id & Timestamp (perhaps) and a TMessage property, but other than that, is really just a way of allowing people to cleanly "pick out" their message types by defining a concrete envelope, then they get the nice typed guide rails they'd come to expect.

The fiddly bit for us with this approach is then splitting out the properties on (de)serializing, but shouldn't be too tricky.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quite the conundrum! I'm not such a fan of the idea of IMessagePublisher<T>, although if we did choose to go down that route, we could probably easily have some sort of IAnyMessagePublisher.

I think it's worth thinking about what use there was in having this typed as Message in the first place. What sort of error handling code would we like to be able to write? If you want to write some generic error handling code then you can't write that in terms of any specific domain message type anyway. In that case you'd rather want to write a try-catch in your handler for that specific message.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's my thoughts too @shaynevanasperen, I think a message envelope concept doesn't solve this problem, it acts as a potential way to separate the non-domain stuff from the transporty stuff.

When the base class was Message, you were either:

  • using Id, Tenant etc... on Message
  • ignoring the message and doing some generic error reporting (I think this is the primary use case)
  • upcasting to the known inheriting types

You could still do those last two with a message of type object, it just looks bad.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I just realised that this is for handling SNS publishing errors! I thought it was for errors in implementations of IHandlerAsync<T>. I've had a look around the code now and I see that the equivalent of this on the other side (SQS) is in SqsReadConfiguration. Over there, we work exclusively with the Amazon.SQS.Model.Message type (with no strong typing of our domain message).

So why not have symmetry with that approach and just do the equivalent here? That would mean using Amazon.SimpleNotificationService.Model.PublishRequest making the signature public Func<Exception, PublishRequest, bool> HandleException { get; set; }. The PublishRequest object has the message as a serialized string, so the error logging can easily use that. This abstraction seems like it should remain at the raw transport level, and if consumers want more control they can always wrap the IMessagePublisher this library returns with some decorator that adds custom error logging and possibly even things like a retry mechanism etc.

While looking around at the code which uses this configuration delegate, I noticed something else that we might want to do there. The error handling there only handles exceptions but won't call our error handler if the response.HttpStatusCode is not 200. I haven't checked this though (maybe the Client.PublishAsync call does throw for non-success status codes?).

{
throw new ArgumentNullException(nameof(handler));
}
var wrappedHandler = new Func<Exception, object, bool>((ex, message) => handler(ex, (T)message));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like here 😄


namespace JustSaying
{
public interface IPublishConfiguration
{
int PublishFailureReAttempts { get; set; }
TimeSpan PublishFailureBackoff { get; set; }
Action<MessageResponse, Message> MessageResponseLogger { get; set; }
Action<MessageResponse, object> MessageResponseLogger { get; set; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly here.

@@ -93,7 +92,7 @@ public SqsNotificationListener WithMessageProcessingStrategy(IMessageProcessingS
return this;
}

public void AddMessageHandler<T>(Func<IHandlerAsync<T>> futureHandler) where T : Message
public void AddMessageHandler<T>(Func<IHandlerAsync<T>> futureHandler, Func<T, string> uniqueKeySelector = default) where T : class
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow. I didn't know there was a default for Func<T, string>!

@@ -23,19 +22,19 @@ internal sealed class MessageHandlerWrapper
_loggerFactory = loggerFactory;
}

public Func<Message, Task<bool>> WrapMessageHandler<T>(Func<IHandlerAsync<T>> futureHandler) where T : Message
public Func<object, Task<bool>> WrapMessageHandler<T>(Func<IHandlerAsync<T>> futureHandler, Func<T, string> uniqueKeySelector = default) where T : class
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it not possible to just use the SNS/SQS message ID for the unique key? Why does it have to be a value in the domain message?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original thinking behind this was that only the developer knows what determines the messages identity, and what is a duplicate. Perhaps the event publisher has OrderId and that is the real identity of the message, or a composite of OrderId and Status. Maybe it's a hash of all of the properties on the message?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. And with the benefit of hindsight, has that thinking paid off? Perhaps it's better to remove this "exactly once" functionality from this library and force consumers to make their solutions inherently idempotent in the domain rather than only at the message bus layer?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@@ -11,6 +10,6 @@ public class SnsWriteConfiguration
/// Extension point enabling custom error handling on a per notification basis, including ability handle raised exceptions.
/// </summary>
/// <returns>Boolean indicating whether the exception has been handled</returns>
public Func<Exception, Message, bool> HandleException { get; set; }
public Func<Exception, object, bool> HandleException { get; set; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quite the conundrum! I'm not such a fan of the idea of IMessagePublisher<T>, although if we did choose to go down that route, we could probably easily have some sort of IAnyMessagePublisher.

I think it's worth thinking about what use there was in having this typed as Message in the first place. What sort of error handling code would we like to be able to write? If you want to write some generic error handling code then you can't write that in terms of any specific domain message type anyway. In that case you'd rather want to write a try-catch in your handler for that specific message.

@slang25
Copy link
Member Author

slang25 commented May 22, 2020

After some discussion, while this is possible it's not something we are prioritising at the moment.

Having a little pain involved in sharing types as contracts will nudge people in the direction we want, which is to declare their own type (share contracts not types).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants