Recently in the development of conference case with Enode, encountered a problem, write a story to share.
Issue background
The Conference case is a system for creating meetings online (like Qcon, a global developer conference), managing the location of meetings online, and booking a meeting online. Specifically, you can see Microsoft's homepage for this project: Http://cqrsjourney.github.io.
We then designed a conference aggregation root, which corresponds to the domain concept of meeting in the field. Below the conference aggregation root, there are some positional information seattype. A meeting aggregation root below you can add different types of locations, and each type of location can specify quantity and price. So, conference is the aggregate root, conference itself has some basic properties that we care about, and it aggregates some seattype child entities internally. Each seattype contains two information about the price and quantity of the location.
Then, at the UI level, we have the following interface boundaries to manage all the location information for a meeting.
Lists two types of locations for a meeting, quota represents the number of quotas for a location, and when we want to modify a location, you can click the link and then appear as shown:
There are four edit boxes, and we can modify any one of the boxes. By clicking Save when you're done, we'll be able to update some type of location information. We then designed two domain event in domain, representing changes in location basic information and the number of location quotas.
Why do we have to independently change the number of domain event? This amount also changes when the user orders the location in the foreground. That is, the number of positions may change individually. Therefore, we consider defining a domain event individually for the change in the number of positions.
Then, our current code is that when you click Save, the first update will update the location of the basic information, and then determine whether the number of changes, if not changed, only the location of the basic information changes in the domain event, if there is a change, it also produces a change in the location of the domain event. The specific implementation of conference aggregation root-related methods is as follows:
The general meaning of the above code is to find out the type of position that needs to be modified from within the aggregation, throw the exception if it does not exist, and if it exists, first the change event of the position basic information, then determine whether the quantity has changed, if there is change, continue to judge whether the current input is too small, if too small is not allowed
For example, if the number of user input is 10, but the current type of position has been 11 have been scheduled, it can not be changed to 10, but must be at least 11. Finally, if everything is legal, a seattypequantitychanged event is generated, indicating that the number of positions of a type has changed, along with the number of the remaining positions that can be predetermined in the event.
Then read the library and we'll update it based on the above two events.
The question now is, if all two events occur, how will the library be updated (in one transaction)? One of our event handler can handle only one event, that is, we will have two event handler to handle the corresponding event respectively. Because domain aggregate is a one-time atom, it generates two domain event at once. So, we want to make sure that two event handler either update successfully, or not update the success, this problem has not been considered before, let's think of ways to do.
Solution Ideas 1
Find a way to wrap these two event handler in one transaction, but this requires that the framework supports such a transaction mechanism spanning multiple event handler, and that the requirements of the framework are a bit larger and more complex and less feasible. Because the framework has to consider the problem is more general, for example, once the introduction of transactions, may also introduce distributed transactions and other issues. And this approach, performance is not high, the violation of Enode was originally designed for high concurrency.
Idea 2
Do not design two domain event in the requirement domain, use a domain event to solve; This event contains all the changes to the information, including the number of modifications. This approach works, but requires the model to make compromises and concessions. What if one day we encounter a model that has to produce multiple events? So, this idea is still running away from the problem.
Idea 3
Instead of using transactions, we use optimistic locking + sequential control + power to solve the problem. The idea is that the framework calls these two event handler sequentially, and the order of the calls is consistent with the order of the two events; two event handler are allowed in a transaction.
The problem is that if the first event is handled successfully, then the machine loses power and the second event is not processed. That is to do, the next time a machine restarts, the second event can be processed. Then, because the entire schema is distributed, the first event is also likely to be repeated, and when the framework calls event handler, for performance reasons, it will only try to ensure that the same event will not be repeated by the same event handler, and will not be absolutely guaranteed But the framework provides a mechanism for developers to resolve the problem of duplicate processing within the event handler by relying on the version number. So, to summarize, we need to deal with the following 3 issues:
- Need to ensure that any event handler within its own absolute power, such as the framework to provide support;
- It is necessary to ensure that any event is processed at least once, even in the event of a power outage at any time;
- It is necessary to guarantee the events in the same event flow, and the order of processing should be processed in the order of the event flow.
In order to achieve the above 3 points, I have done a perfect enode, is to introduce a sub-version of the concept of the number of events.
Is that when the aggregation root changes every time, regardless of the number of domain event generated, the domain event is in an event stream, each event stream has a version number, and then each domain The major version number of the event is the version number of the event stream in which it resides. For example, a change in an aggregation root produces 2 domain event, which is guaranteed to be in an event stream, and if the event stream has a version number of 10, then the major version number of each domain event is also 10 This enode framework can be guaranteed. Where did the version number of the event stream come from? is from the aggregation root, because each aggregate root maintains the current version number of its own, in version, the next generation of the event stream version number is version+1.
The above explains what the major version number of the event is. Let's talk about what is the child version number of the event. The sub-version number is simple, that is, if an event stream contains 2 events, the first event's child version number is 1, the second is 2; So, the child version number is the sequence number of the event in the event stream.
Then, there is the concept of the event's major and child version numbers. We can do the above 3 requirements. In the 2nd, Equeue will make sure that any message is processed at least once, and this does not unfold. 1th, 3 points, we use the following code to combine analysis and discussion.
In order to code effect better, I directly through the way, blog Park after the official provision of a set of such code template it, hehe. @ Cricket, the last time you told me that template, I later forgot to use:)
In the above code, each event handler has a transaction inside, why does it need a transaction? Since we are now updating the aggregate root, the child entity (location information) is part of the aggregation root, so it is natural to update the aggregation root itself when reading the library update. Only the version number of the aggregate root needs to be updated here.
First event handler, we start a transaction first, and then update the main version number of the aggregate root, as well as the minor version number, if the database conference record the current major version number is 10, the minor version number is 1, then this evnt. Version is 11,evnt. Sequence is 1,sequence is the minor version number. Then, with the first update SQL, we can update the major and minor version numbers of the aggregate root. Because a single update SQL is an atomic transaction (no concurrency problem), we only have to determine whether the update affects the number of rows that are 1. If it is 1, then the update is successful, then the location record can be updated. Then, since these two update statements are in one transaction, either they are all done, or nothing is done, not half the case.
The second event handler, again, we start a transaction first. Then the difference is, because we know that the Seattypequantitychanged event and the Seattypeupdate event always occur in an event stream, and that it is always in the second order. Therefore, when this event handler is executed, the main version number of the aggregate root must already be 11, and the child version number is 1. So, in the second event handler, we only need to update the child version number to 2 for the aggregate root. Is the first UPDATE statement. Then the same judgment affects whether the number of rows is 1. If it is, the number of locations to update and the number of available, and if not 1, do nothing.
There is a question, when will appear not 1? This is the time when the event handler is executed repeatedly. In this case, we can ignore it. Because we're going to do the power of update.
Basically, it's almost there. But there is a major premise to be explained. That's it. As you can see, in the first event handler, when you update the major version number of the aggregate root, the Where condition will determine the current version number of the aggregate root record is evnt.version-1; this is to ensure that when reading the library updates, always follow the domain The event order is updated sequentially, and cannot be skipped or scrambled. Otherwise, the final data of the read library is inconsistent. So, the event handler internal to make such judgments, to ensure that such a thing will never happen. But the light event handler internal judgment is not enough. The Enode framework also ensures that the order in which the event stream messages are processed is in order, otherwise the number of rows affected by the aggregation root update in the event handler may never be 1.
Enode has been aware of this problem, so it has helped us make such a guarantee!
Summarize
The last scenario above, I think is a more general solution. The framework does not require transactions that support cross-event handler, and the changes are relatively small. It also guarantees the performance of the Read library update and, in addition, ensures that events are handled when power is lost.
In short, everything is for high performance, in order to ensure eventual consistency. Also spent an article to share a little bit of design, hehe.
Enode 2.6-Schema design for updating read libraries when aggregating roots produce multiple domain events at a time