====== Regeln für Mengen von Aggregates ====== Es gibt verschiedene Arten von Mengen basierten Regeln. Im einfachsten Fall baut man einfach ein Aggregate für die Menge, mit Entitäten, die die einzelnen Aggregates referenzieren. Wenn z.B. die Projektnummer eindeutig sein muss, hat das Project Aggregate selbst keine Projektnummer. Es gibt ein ProjectKeyRegistry Aggregate, in dem die Projektnummern eindeutig gehalten werden. Jede Projektnummer im Aggregate ist eine Entität, welche ein Project Aggregate referenziert. \\ \\ Projektionen können nun für das Read Model ein Project aufbauen, indem das Project Aggregate und das ProjectKeyRegistry Aggregate kombiniert werden((bzw. die Ereignisse aus diesen beiden Aggregates)). \\ \\ Problematisch wird es nur, wenn auch das Project Aggregate Regeln bezüglich der Nummer hat. Der Command Handler könnte bei Bedarf die Nummer aus dem Read Model an die Methoden des Project Aggregates übergeben, aber das wäre dann nur noch schließlich konsistent. Zudem möchte man hier und da Änderungen über das Project Aggregate steuern, d.h. wenn der Projektstatus eine Änderung der Projektnummer nicht zulässt, soll man auch nicht über die ProjectKeyRegistry einfach die Nummer ändern können. \\ \\ Also müsste man ein ProjectSystem Aggregate bauen, durch das alles läuft, und Projekte sind immer nur Entitäten. Das wäre die konsistenteste Lösung. Es geht aber darum, im CAP einen vernünftigen Kompromiss zwischen der Konsistenz((__C__onsistency)), der Verfügbarkeit((__A__vailability)) und der Partitionstoleranz((__P__artition tolerance)) zu finden. Wenn man zuviel Gewicht auf die Konsistenz legt, leiden die anderen beiden Punkte. \\ \\ Wenn die Aussage also ist, dass es auf keinen Fall zwei gleiche Projektnummern geben darf, dann muss man für die Projektnummer die Konsistenz auf 100% halten, also die Projektnummernvergabe für alle Projekte serialisieren. Das heißt aber nicht, dass man alles das Projekt betreffend serialisieren muss. Die Projektnummer wird i.d.R. einmal vergeben, vieles Andere wird öfters geändert. \\ \\ Ich möchte als Anwender nun auch mehrere Aktionen das Projekt betreffend abschicken, und erwarte, dass entweder alle oder keine durchgeführt wird. Wie geht so was? ===== Reservation Pattern mit einem kompensierenden Prozess ===== {{ :concepts:projectkeyregistry.png?nolink |}} Das sieht ja erst mal kompliziert aus für solch eine simple Aufgabe. Wie läuft das ab? public class Project: ProjectBase { public void SetProjectKey(string projectKey, IProjectKeyRegistry registry) { // eigene Prüfungen var reservationNumber = registry.Reserve(Id, projectKey); // asynchron, wird also gespeichert ApplyChange(new ProjectKeySet(reservationNumber, projectKey); } } Im Erfolgsfall ist das das //schönste// Modell. Der Anwender schickt einen Befehl, der an das Projekt gerichtet ist ((und nicht an die ProjectKeyRegistry)), und weiß, dass die Projektnummer geändert wurde wenn der Befehl fehlerfrei durchgelaufen ist. ==== Aus der Reservierung eine Registrierung machen ==== Wenn ProjectKeyReserved oder ProjectKeySet erhalten wurde((Das erste Ereignis kommt von der Registry, das zweite vom Projekt. Die Reihenfolge, in der sie erhalten werden, ist also nicht fest)), muss eine SAGA gestartet werden, die für die Project Id läuft. Sobald auch das zweite dieser Ereignisse erhalten wurde, kann die SAGA der ProjectKeyRegistry mitteilen, dass sie aus der Reservierung eine Registrierung machen soll. \\ \\ Womit wird die Projektnummer im Read Model gesetzt? Mit ProjectKeySet vom Projekt, oder mit ProjectKeyRegistered von der Projektnummer-Registratur? \\ \\ Die Registratur ist ja im Grunde nur dafür da, eindeutige Projektnummern zu verifizieren. Was alles andere Betrifft, ist die Projektnummer relevant, die im Projekt steht, und nicht in der Registratur. Das Read Model aller Projekte ist nur schließlich konsistent, d.h. es könnten im Read Model doppelte Projektnummern erscheinen((Wenn Projekt 1 erst die Nummer //123// hat, die dann in //321// geändert wird, und Projekt 2 danach die Nummer //123// erhält - vorausgesetzt die Registratur befreit Projektnummern, die geändert werden. Nun könnte Projekt 1 im Read Model theoretisch noch nicht aktuell sein und wie Projekt 2 auch die //123// anzeigen --> doppelte Projektnummer)). ==== Fehlerfall ==== Es kann passieren, dass im Methodenaufruf von oben die Projektnummer reserviert wird, aber nie verwendet wird((z.B. Infrastrukturfehler)). Die reservierte Projektnummer muss in dem Fall storniert werden, damit sie wieder verwendet werden darf - im Normalfall wird der Client den Befehl nochmals senden. \\ \\ Dafür ist die SAGA da. Sie wurde mit ProjectKeyReserved gestartet((Nicht mit ProjectKeySet, da das Ereignis im Fehlerfall ja gar nicht stattgefunden hat)). Die SAGA wartet nun nicht einfach auf ProjectKeySet, sondern hat einen eingebauten Nerv Faktor(("Sind wir schon da?", "Sind wir schon da?", "Sind wir schon da?")). Sie sendet in dem Fall den Befehl ConfirmProjectKeyReservation an das Projekt. Im Projekt passiert folgendes: public class Project: ProjectBase { public void ConfirmProjectKeyReservation(string reservationNumber) { if (!ProjectKeySetWithReservation(reservationNumber)) ApplyChange(new ProjectKeyReservationNotUsed(reservationNumber)); } } Wenn das Projekt die Reservierungsnummer verwendet hat, macht die Methode nichts. In dem Fall wurde bereits ProjektKeySet mit der Reservierungsnummer durchgeführt, und die SAGA wird das Ereignis früher oder später erhalten, um aus der Reservierung eine Registrierung zu machen. Wenn das Projekt die Reservierungsnummer aber nicht verwendet hat, wird es der SAGA über ProjectKeyReservationNotUsed mitteilen, dass es diese Reservierungsnummer nicht verwendet hat. Daraufhin kann die SAGA der Registratur den Befehl erteilen, die Reservierung zu stornieren. ==== Fehler in der Projektanlage ==== Was aber, wenn die Projektnummer während der Projektanlage reserviert wurde, das Projekt aber nicht angelegt wurde? ConfirmProjectKeyReservation läuft dann ins Leere. In dem Fall kann die Reservierung storniert werden, das Projekt wurde ja nicht angelegt. > FALSCH! Es könnte sein, dass der Handler für die Projektanlage einfach noch nicht fertig ist, zu dem Zeitpunkt, da ConfirmProjectKeyReservation von einem anderen Handler abgearbeitet wird. Wenn jetzt die Reservierung storniert wird, die Projektanlage dann aber doch statt findet, wurde sie fälschlicherweise gelöscht. \\ \\ Wenn der Handler also ConfirmProjectKeyReservation behandeln soll, muss er das Projekt anlegen, wenn er es nicht findet! Nur so kann verhindert werden, das die Reservierung gleichzeitig storniert wird, und verwendet wird. Entweder schlägt die Anlage des Handlers für ConfirmProjectKeyReservation fehl, weil der andere Handler mittlerweile das Projekt mit der Id gespeichert hat, oder die Anlage klappt, und der Handler für die Projektanlage läuft auf einen Fehler. === Handler für die Frage nach der Bestätigung einer Reservierung === public class ProjectCommandHandler: CommandHandler, Handles { public void Handle(ConfirmProjectKeyReservation message) { var rep = new Repository(); var proj = rep.Get(message.Id); if (proj == null) proj = ProjectFactory.CreateVoidProject(message.Id, message.ReservationNumber); proj.ConfirmProjectKeyReservation(message.ReservationNumber); } Die ProjectFactory erzeugt ein Projekt das nichts anderes macht als zu verhindern, dass ein weiteres Projekt mit derselben Id erzeugt werden kann. Dieses Projekt ist nirgendwo sichtbar, kann auch keine anderen Befehle als ConfirmProjectKeyReservation entgegennehmen. Das Ergebnis des Methodenaufrufs ist auch fest, dass die Reservierung nicht verwendet wurde. \\ \\ Da es MainProject und SubProject gibt, die von Project abgeleitet sind, sollte es auch ein VoidProject für diesen Fall geben. Es verwendet denselben Id-Kreis wie die anderen Projekte auch. === Doppelte Id Vergabe === Nun kann es passieren, dass der Handler für die ursprüngliche Projektanlage eine DuplicateAggregateIdException auslöst, wenn inzwischen ein VoidProject erzeugt wurde. In diesem Fall muss der Client für die Projektanlage in der Lage sein, die Anlage erneut zu senden, vorher die Id auszutauschen. Dieser Fall sollte aber so gut wie nie eintreten. === Eine Reservierung pro Projekt Id === Die Registratur für Projektnummern erlaubt keine zwei gleichzeitigen Reservierungen für dieselbe Id. Das System ist nun 100% konsistent. Es kann aber folgendes passieren: - Befehl für neues Projekt mit der Id=123 und der Projektnummer "ABC" wird abgesendet, - Im Konstruktor des Projektes wird eine Reservierung in der Registratur für {123,"ABC"} gemacht, - Das Projekt kann nicht gespeichert werden, - Der Client sendet den Befehl zur Projektanlage erneut ab. Jetzt können zwei Dinge passiert sein. Die SAGA hat den Befehl zur Bestätigung der Reservierung abgeschickt, ein VoidProjekt mit der Id=123 wurde erzeugt, und die Reservierung für {123,"ABC"} wurde storniert. In diesem Fall wird der neue Befehl mit einer DuplicateAggregateIdException abgelehnt und der Client muss den Befehl mit einer neuen Id absenden. \\ \\ Alternativ kann es sein, dass die SAGA noch nichts gemacht hat. In der erneuten Projektanlage wird nun wieder versucht, die Projektnummer "ABC" für die Id 123 zu reservieren. Die Registratur muss in diesem Fall eine PendingProjectKeyReservationException auslösen. Sie kann nicht davon ausgehen, dass sich das Projekt nach ganz bestimmten Regeln verhält. Es ist das Projekt, dass in diesem Fall weiß, dass da was nicht stimmen kann. Entweder gibt es wirklich ein anderes Projekt mit der Id, dass gerade eine Projektnummeränderung vornimmt, oder es gab für die Id einen Fehler, der noch nicht abgearbeitet ist. In beiden Fällen((Also bei der PendingProjectKeyReservationException)) sollte die Anlage mit einer neuen Id versucht werden. \\ \\ Der einzige Fall, der nicht so einfach aufgelöst werden kann ist der, dass die neue Projektanlage abgeschickt wird, die alte Reservierung für die Projektnummer "ABC" noch nicht storniert wurde, und "ABC" im Zuge der Projektanlage nicht reserviert werden kann - obwohl "ABC" gleich freigegeben wird, was das Projekt aber nicht weiß. Die Projektanlage würde mit der Begründung abgelehnt werden, dass es die Projektnummer bereits gibt. \\ \\ Dieser Fall kann aufgelöst werden, indem die Projektanlage bei einer PendingProjectKeyReservationException einfach immer wieder((Mit einer neuen Id - siehe ersten Fehlerfall)) versucht wird. Irgendwann ist die Reservierung weg, weil sie storniert oder bestätigt wurde. \\ \\ So, das sollte alle Fälle abdecken. Zusammengefasst: > Wenn man in der Projektanlage die Projektnummer setzen möchte und eine DuplicateAggregateIdException oder eine PendingProjectKeyReservationException erhält, wiederholt man die Anlage einfach immer wieder bis sie klappt oder eine andere Fehlermeldung erhält((Die Projektnummer kann dafür z.B. bei einem Import als idempotency-Id genutzt werden, denn wenn die Anlage funktioniert hat samt Projektnummernvergabe, würde man bei einem erneuten Versuch eine DuplicateProjectKeyException erhalten)). Man kann aber auch bei einer DuplicateAggregateIdException prüfen, ob es das Projekt überhaupt gibt, und wenn ja, ob es ein VoidProject ist. Dito für die andere Exception. > Wenn man für ein vorhandenes Projekt die Projektnummer setzen möchte, kann es genauso passieren, dass die Reservierung klappt, das Speichern des Projektes aber nicht. In diesem Fall wird ja das Projekt von der SAGA regelmäßig abgefragt. In diesem Fall kann der Befehl zur Projektnummernänderung bei einer PendingProjectKeyReservationException einfach so lange abgeschickt werden, bis der Befehl funktioniert oder aus dem Fehler eine ProjectKeyRegisteredException geworden ist. ===== Alternativlösung ===== Damit der Client eine Projektanlage nicht immer wieder mit einer neuen Id senden muss, kann das Projekt und der Command Handler ein wenig angepasst werden. Der Command Handler kann wie folgt aussehen: public ProjectCommandHandler: CommandHandler, Handles { public void Handle(AddProject message) { var repo = new Repository(); var proj = repo.Get(message.Id); if (proj == null) { proj = ProjectFactory.AddBootstrapped(message.Id); repo.Save(proj); // das Projekt ist nun vorhanden! auch wenn es hiernach nicht mehr weiter geht. } proj.Bootstrap(message.ProjectName, message.ProjectKey, ...); repo.Save(proj); } } Das sieht merkwürdig aus. Jetzt kann der Client bei einer PendingProjectKeyReservationException den Befehl einfach neu senden, muss die Id des Projektes nicht ändern. Ein Projekt, dass mit AddBootstrapped erzeugt wurde, ist im Read Model nicht sichtbar, wird auch nur zwei Befehle akzeptieren: * Bootstrap * ConfirmProjectKeyReservation Bootstrap ist wie eine Factory, und wird vom Projekt nur akzeptiert, solange es den Status //Bootstrapping// hat. ConfirmProjectKeyReservation wird im //Bootstrapping// Status immer ReservedProjectKeyNotUsed veröffentlichen. Wenn ein Projekt also zum Bootstrapping angelegt werden konnte, in der Bootstrap() Methode eine Projektnummer reserviert hat, danach aber ein Fehler passiert ist, kann die Bootstrap() Methode immer wieder aufgerufen werden, genau wie SetProjectKey() in einem vorhandenen Projekt. \\ \\ Es besteht jetzt nur noch die //Gefahr//, dass bei Fehlern nach der Reservierung, oder bei Änderungen an der Projektnummer schnell hintereinander, die Reservierung noch besteht aber weder storniert noch bestätigt wurde. Das System erhält dann eine PendingProjectKeyReservationException von der Registratur. Die Command Handler können den Aufruf in dem Fall einfach immer wieder behandeln, denn sie wissen, dass dieser Fehler schließlich verschwinden muss. \\ \\ Sie sollten dabei ein Time-Out setzten, d.h. dass der Befehl letztendlich abgelehnt wird, wenn der Fehler z.B. nicht nach 30 Sekunden verschwindet. In dem Fall gibt es Probleme im System, die das Messaging daran hindern, Nachrichten rechtzeitig //an den Mann// zu bringen.