User Tools

Site Tools


technology:domainmodel:processmanager

Process Manager

Für mich ist ein Process Manager1) keine Saga. Der Process Manager verwendet aber Sagas, kann aber auch weitere Dinge enthalten wie z.B. Aggregates, usw. Ein Prozess wird also dadurch definiert, wie er ausgelöst wird, und was die einzelnen Prozess Schritte sind. Wenn ein Prozess implizit ausgelöst wird, besteht er i.d.R. nur aus einer Saga, wenn er aber explizit ausgelöst wird, besteht er aus einem Aggregate, welches vom Client verwendet wird, um den Prozess explizit auszulösen. Das kann am besten an einem Beispiel erklärt werden.

Benutzerkonten mit eindeutigen Benutzernamen

Die Aufgabe ist es, mit CQRS und Event Sourcing sicher zu stellen, dass Benutzernamen nicht doppelt erstell werden. Das ist gar nicht so einfach. Viele erfolglose Versuche2), technische Lösungen für die einhudert prozentige Einhaltung von Regeln zu finden, die mehrere Aggregate umfassen, waren zum Scheitern verurteilt.

Entweder läßt man zu, dass die Regeln nur mit einem gewissen Grad an Sicherheit eingehalten werden, und findet Lösungen in der Sprache des Businesses, um die Ausnahmefälle zu lösen, oder aber man muss Aggregates definieren, um die Regeln einzuhalten3). Das ist auch immer ein trade-off zwischen garantierter Datenkonsistenz und dem Aufwand/die Kosten, diese Konsistenz einzuhalten vs. der Kosten, um die Fälle zu lösen, in denen die Regel mal nicht eingehalten wird4).

Hier nun meine Abhandlung zu der Aufgabe5)

public class Account: AggregateRoot
{
  public Account(Guid id, string password)
  {
      ApplyChange(new AccountAdded(id, password));
  }
}
public class UniqueUserNames: AggregateRoot
{
  private IDictionary<Guid, string> _names = new Dictionary<Guid, string>();
 
  public UniqueUserNames(Guid id)
  {
      if (!id.Equals(Guid.Empty)) throw new ArgumentException("Illegal UniqueUsernames Aggregate ID"); // probable better ways to make it "singleton", i.e. a global rule over all usernames. Works with "real" ids just as well for non-global rules
      ApplyChange(new UniqueUserNamesAdded(id));
  }
 
  public void AddUserName(Guid id, string name)
  {
    if (_names.ContainsKey(id)) throw new DuplicateAccountIdException(); // this really should not happen, because accounts with unique ids are always created before the name is added...
    if (_names.Values.Contains(name)) throw new DuplicateUserNameException();
    ApplyChange(new UniqueUserNameAdded(id, name)); // (the event is straight forward, no need to show its declaration in this example)
  }
 
  public void RemoveUserName(Guid id)
  {
    ApplyChange(new UniqueUserNameRemoved(id)); // (the event is straight forward, no need to show its declaration in this example)
  }
 
  private void Apply(UniqueUserNameAdded @event)
  {
    _names.Add(@event.AccountId, @event.Username);
  }
 
  private void Apply(UniqueUserNameRemoved @event)
  {
    _names.Remove(@event.AccountId);
  }
}

To add an account you need to start an AccountCreationProcess. I see a process as an AggregateRoot together with a SAGA. The creation event of the process AggregateRoot will start the SAGA, which then coordinates the process. The following AggregateRoot is needed:

public class AccountCreationProcess: AggregateRoot
{
  public AccountCreationProcess(Guid id, string username, string password)
  {
      ApplyChange(new AccountCreationProcessInitiated(id, username, password));
  }
}

The AccountCreationSaga will be started by an event handler of AccountCreationProcessInitiated. The first steps in the process will be6):

{
  // excuse my terminology, i am not using a bus myself, but it should work something like this, i guess
  Bus.Publish(new AddAccount(message.id, message.password));
  Bus.Publish(new AddUniqueUserName(Guid.Empty, message.id, message.username));
}

In case of a success of both commands you have a new Account and a new username added to UniqueUserName. An event handler for AccountAdded and UniqueUserNameAdded can denormalize the combination into a view holding the id of the Account, together with the username. Only if both events are handled will the complete account exist, it can be kept out of sight of the front-end until that has happened. If that's not good enough, include the reservation pattern to make sure the Account cannot be used until the Username has been added, for example.

The Sagas work is not yet done, though. It needs to react to the success or failure of the two commands it sent:

  • If both commands succeed, the saga has done its work. It could send a command to the AccountCreationProcess to tell it that it was successful.
  • If AccountAdded was the first received event, and AddUniqueUsernameRejected was received, the saga sends a command to remove the account. A second command can be sent to the AccountCreationProcess to let it know that it failed.
  • On the other hand, if UniqueUsernameAdded was the first event, and AddAccountRejected was observed, the saga sends a command to remove the username, and possibly a failure command to the AccountCreationProcess.

The Saga needs no timeout. It needs to be sure that both commands were published and received. If neccessary, it will send them as often as neccessary until it knows for sure that they were both received 7). Now the Saga can be certain that it will (eventually) observe either an acceptance or a rejection of the events, i.e. one of the 3 possible cases above will happen with certainty and the process will come to an end.

If you do want the Saga to be able to timeout, the Aggregates the Commands are sent to must be able to reserve the requested changes, and only once all reservations have been made, the Saga will send commands to confirm the reservations. The timeout will only be allowed to happen during the reservation process, not during the confirmation process, and the confirmation commands must be guaranteed to succeed - if either one of these constraints is not met, you could end up with an Account without a Username or vice versa, and again, you would have to prepare the domain for that :-(

By the way, once the process succeeded, you do not need to use the UniqueUserName aggregate every time you access the account. Only once when creating it. If you want to authenticate a username and password, query a view of UniqueUsername using the username to obtain the AccountId, then retrieve the Account and do something like account.Authenticate(password).

Alternative Lösung

Es gibt in diesem einfachen Fall auch eine Lösung ohne Saga. Folgendes kann gemacht werden wenn ein Konto angelegt werden soll:

  1. Befehl zur Kontoanlage absenden. Der Befehl/das Konto kann den Wunsch-Usernamen enthalten - für den Fall, dass das UI das Konto schon zur Anzeige hat, den Usernamen aber noch nicht hat. Der Denormalisierer schreibt dann in eine Tabelle den Wunschnamen8) + den Tatsächlichen9). Das UI kann den Wunschnamen samt Sternchen(*) anzeigen, falls der richtige noch nicht da ist10),11),
  2. Eine gewisse Zeit auf das Ergebnis warten/pollen.
    1. Wenn das Konto angelegt ist, den Befehl zur Usernamenanlage absenden,
      1. Wenn dieser fehl schlägt, ist eine Guid für ein nicht verwendbares Konto verwendet worden. Das muss nicht korrigiert werden. Das Konto kann dem Anwender ja auch ohne Passwort angezeigt werden, und er kann das Passwort dann nochmal angeben, bzw. ein neues probieren.
    2. Wenn das Konto nicht angelegt werden konnte ist die Aktion zu Ende, ohne das irgend etwas angelegt wurde.
  3. Wenn das Zeitlimit überschritten ist, bzw. irgend eine unerwartete Exception passiert, Abbruch. Leeres Konto mit Guid braucht nicht korrigiert werden, bzw. kann es dem Anwender angezeigt werden, und er kann die Passwortangabe erneut versuchen.

Die Alternative Lösung basiert darauf, dass ein Konto ohne Passwort nicht schadet, und auch keine Daten blockiert - eine verwendete GUID. Entweder wird es dem Anwender gar nicht erst angezeigt, damit nichts weiteres mit dem Konto passieren kann, oder es wird dem Anwender angezeigt damit er das Passwort nachträglich eintragen kann.

Das ist ein simpler Fall. Aber auch das ist eine Erfahrung: jeder Fall mag anders sein, und es gibt nicht immer die Lösung. Wenn man aber die Prozesse analysiert sollte man immer zu einer Lösung finden.

Ändern des Benutzernamen

Das ist noch viel einfacher. Es muss nur ein Befehl an das UniqueUserNames Aggregate geschickt werden, um den Benutzernamen zu ändern. Das wird entweder erlaubt oder abgelehnt.

Hier ist wichtig zu verstehen, dass eine Änderung am Benutzernamen, zusammen mit dem Passwort, nicht transaktional geschehen kann - und es auch gar nicht muss. Damit der Anwender gar keine anderen Erwartungen hat, gibt es eine Befehlsschaltfläche zum Ändern des Passwortes, und eine für den Benutzernamen. Er wird gar nicht erst beide zum Ändern gemeinsam eingeben können.

1) oder auch Workflow Manager
2) eigene und wie man nachlesen kann auch von anderen
3) das kann ich nicht genug betonen. Ohne Aggregates gibt es keine Möglichkeit Regeln sicher einzuhalten. Das kann nicht funktionieren, egal was für ausgefeilte Lösungen man findet, mit Sagas, Reservierungsmustern, usw. Ich habe es wieder und wieder versucht, bitte nie wieder!
4) mein Problem ist, dass die Wahrscheinlichkeiten der Regelverletzungen - vor allem durch Eventual Consistency - , und somit die Kosten überhaupt nicht einschätzbar sind
5) In Englisch - weil es eine Antwort in der DDDCQRS Gruppe werden sollte
6) the process must listen to the result of the events right away, i.e. a result for the first command may be received before the second one has been sent
7) it should make the commands idempotent, of course, so they will not be handled more than once
8) wenn AccountAdded behandelt wird
9) wenn UsernameAdded behandelt wird
10) also UsernameAdded noch nicht erhalten wurde
11) in diesem Fall würde sogar ein AddAccount Befehl vom UI reichen, und ein Handler, der das Ereignis AccountAdded abfängt und AddUsername abschickt - es enthält dann ja die Konto-ID + den Wunschnamen, um zu versuchen, einen Usernamen anzulegen, also um den Befehl AddUsername abzuschicken. Zudem kann der Denormalisierer in die Tabelle von oben rein schreiben, wenn ein Name vergeben ist, das UI könnte dann das Konto samt Wunschnamen anzeigen plus zwei Sternchen oder sonst was, um darauf hinzuweisen, dass der Name nicht geklappt hat
technology/domainmodel/processmanager.txt · Last modified: 2013/01/12 08:56 by rtavassoli