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

Sync command dispatch #82

Closed
slashdotdash opened this issue Sep 25, 2017 · 8 comments
Closed

Sync command dispatch #82

slashdotdash opened this issue Sep 25, 2017 · 8 comments
Assignees

Comments

@slashdotdash
Copy link
Member

slashdotdash commented Sep 25, 2017

Dispatching a command returns an :ok response when successfully handled by the registered command handler or aggregate.

:ok = Router.dispatch(%RegisterAccount{uuid: UUID.uuid4(), username: "slashdotdash"})

This provides a strong consistency guarantee for the write side (in a CQRS application). You are guaranteed that the write succeeded once you receive the :ok.

Problem description

Event handlers in Commanded are asynchronous when processing persisted events. This allows parallelisation of handlers for both performance and independence so that one slow handler doesn't negatively affect any other. The downside of this approach is eventual consistency for your read models; after successful command dispatch you have no way of knowing when the handlers have processed the events created by the command .

The current solution to eventual consistency in the read model is to use one of the following approaches: "fake" the response by using data from the command; poll the read model until it has been updated; use pub/sub from the read model to notify subscribers when the event has been processed.

This requires you to write boilerplate code for every command dispatch that needs to subsequently query the read model that will be updated by events created from the command. Most web apps use the POST/Redirect/GET pattern that is affected by this problem. As an example, after publishing a blog post the user should be redirected to view their new post.

Proposal

The proposed solution is to allow "synchronous" command dispatch and allow you to configure which event handlers should run with strong, not eventual, consistency.

Example

Add a consistency option to the command dispatch function:

:ok = Router.dispatch(%RegisterAccount{uuid: UUID.uuid4(), username: "slashdotdash"}, consistency: :strong)

The supported consistency options would be :eventual (the current behaviour and default) and :strong.

For each event handler, and process manager, you would be able to specify its consistency guarantee.

Example

Article read model projection (an event handler) requests :strong consistency:

defmodule ArticleProjector do
  use Commanded.Event.Handler, 
    name: "UserProjector",
    consistency: :strong

  def handle(%ArticlePublished{title: title, body: body}) do
    # ... update read model
  end
end

An email sending event handler can be configured for :eventual consistency since it can safely run async:

defmodule SendWelcomeEmail do
  use Commanded.Event.Handler, 
    name: "SendWelcomeEmail",
    consistency: :eventual

  def handle(%UserRegistered{name: name, email: email}) do
    # ... send welcome email async
  end
end

It would also be possible to define the consistency for process managers in exactly the same manner.

When dispatching a command using consistency: :strong the dispatch will block until all of the strongly consistent event handlers and process managers have handled all events created by the command. This guarantees that when you receive the :ok response from dispatch your strongly consistent read models will have been updated and can safely be queried.

Summary

This proposal would be backwards compatible with any existing usage. The default consistency guarantee will be eventual, as it already is. But you will now be allowed to opt-in to strong consistency when dispatching a command.

  • Strong consistency offers up-to-date data but at the cost of high latency.
  • Eventual consistency offers low latency but may reply to read requests with stale data since they may not have processed the persisted events and updated their data.
@slashdotdash slashdotdash self-assigned this Sep 25, 2017
@florish
Copy link

florish commented Sep 25, 2017

@slashdotdash Great write-up! This looks like a great addition to commanded, which for me personally would solve some eventual consistency issues I'm currently encountering in test suite runs.

Some feedback:

(1) The method name dispatch_sync does not really connect me to consistency: strong on first read. I'm not sure I have a better name... dispatch_with_strong_consistency doesn't really sound like an improvement to me, but it does point a little bit more to what the method does. Maybe only offering the consistency: strong option to dispatch is OK for now, without adding a new method?

(2) I'm not sure whether the addition of :consistency on the read side is the right approach. Is it always possible to say this in an Event Handler, or does this depend on the context from where the command is being dispatched? E.g. for a specific case, a CreateBlogpost command dispatch may want to wait for a BlogPost read model projection to be updated, but in other cases, maybe not. And maybe sometimes, you do want to wait for an e-mail to have been sent, but in other cases not.

An alternative could be to add a list of event handlers to wait for on command dispatch. Example:

:ok = Router.dispatch(%CreateArticle{...}, consistency: :strong, wait_for: [ArticleProjector])
# or only with :wait_for (consistency: :strong is assumed then)
:ok = Router.dispatch(%CreateArticle{...}, wait_for: [ArticleProjector])

(3) In the current approach, it's not entirely clear to me what happens if only dispatch(consistency: :strong) is used, without adding the same option to any event handlers. Does commanded default to waiting for all event handlers, or does it default to none? That's just a decision to make, I guess, there's no absolute right or wrong here.

The above are just some ideas, the original proposal would be great in itself without any changes!

@florish
Copy link

florish commented Sep 25, 2017

@slashdotdash Additional thought on (3): if the event handlers ultimately decide whether or not strong consistency is enforced, do we actually need a dispatch_sync and/or dispatch(consistency: :strong) option? I guess not.

This still leaves point (2) above open for discussion: in practice, is it a good idea to have Event Handlers have the final say in this, or should this better be handled on dispatch?

@slashdotdash
Copy link
Member Author

@florish Allowing dispatch with strong or eventual consistency allows the consumer to dictate their consistency requirements. Typically commands dispatched by event handlers or process managers can always run with eventual consistency since they are "background" tasks. In this scenario there's no need to incur the overhead of collecting & waiting for the strongly consistent event handlers.

@slashdotdash
Copy link
Member Author

slashdotdash commented Sep 25, 2017

@florish Think I agree with you that adding dispatch_sync is unnecessary with the consistency option for dispatch. I've removed it from the proposal.

Dispatching a command with :strong consistency, but no handlers configured for the option, would wait for none (same as existing behaviour or using eventual consistency). Which needs to be stated in the documentation.

@astery
Copy link
Contributor

astery commented Sep 25, 2017

@slashdotdash, I agree with @florish projections should not care whenever they used for strong consistency or not. It's should be decided when we dispatch command.

@astery
Copy link
Contributor

astery commented Sep 25, 2017

Also I want to discuss about a more complex case when we need to wait multiple projections of same kind after dispatching a command.

For example I have a command %CreateOrder{items: [item_uuid1, item_uuid2]}, and I want to wait until both projections from list will be ready %OrderItemProjection{id: item_uuid1} and %OrderItemProjection{id: item_uuid2}

Or similar, I have a %CreateUser{roles: [%Customer{}, %Passenger{}]}, this command emits three events: UserCreated, RoleGranted, RoleGranted, so my projection updates three times.

I think something like:

dispatch(%CreateOrder{items: [item_uuid1, item_uuid2]}, wait_for: [%OrderItemProjectionUpdated{id: item_uuid1}, %OrderItemProjectionUpdated{id: item_uuid2}])`
dispatch(%CreateUser{id: user_id, roles: [%Customer{}, %Passenger{}]}, wait_for: [%UserProjectionUpdated{id: user_id}, %UserProjectionUpdated{id: user_id}]), %UserProjectionUpdated{id: user_id}])]`

Can be helpful, but it's more complex, and in such cases you maybe prefer more refined control on events order and error output.

@astery
Copy link
Contributor

astery commented Sep 25, 2017

This gist of CreateUserSync maybe can be some kind helpful.

@slashdotdash
Copy link
Member Author

@astery By using dispatch with consistency :strongit would wait until the strongly consistent event handlers have processed all events created by the command being dispatched. The events for a single aggregate are persisted as a batch. So in your create user command example, the dispatch would block until all three events have been handled (user created, role granted x 2).

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

No branches or pull requests

3 participants