Follow my new blog

Posts mit dem Label Application Space werden angezeigt. Alle Posts anzeigen
Posts mit dem Label Application Space werden angezeigt. Alle Posts anzeigen

Donnerstag, 3. Juni 2010

Remote Communication mit Event-Based Components und Application Space

Wie Asynchronizität über “Zwischenstücke” in eine EBC-Architektur eingebaut werden kann, habe ich in einem früheren Blogposting beschrieben. Jetzt ist in der myCsharp.de Community die Frage aufgetaucht, wie denn eine verteilte Architektur mit EBCs realisiert werden könnte.  Die einfache Antwort: genauso ;-)

Damit meine ich, dass das, was zum Aspekt remote communication gehört, in eine EBC Standardkomponente verpackt werden sollte. Die Domänenlogik-Komponenten bekommen davon nichts mit. Hier als Beispiel eine simple Echo-Kommunikation: ein Client schickt eine Nachricht an einen Service, der sie (fast) unverändert wieder zurückschickt.

image

Die Kontrakte für die Komponenten sehen so aus:

public interface IClientEBC
{
    event Action<string> Out_RequestTextProcessing;

    void In_ProcessedText(string text);
}

public interface IServiceEBC
{
    void In_EchoText(string text);

    event Action<string> Out_Echo;
}

Und das sollte sich auch nicht ändern, nur weil sie nicht im selben Prozess laufen. Die Event- bzw. Nachrichtenorientierung der EBCs ist dafür eine gute Voraussetzung.

Wie die Implementationen für die Kontrakte aussieht, ist eigentlich uninteressant. Ein Blick auf die Verdrahtung lohnt allerdings. Hier der Host für beide Komponenten, solange sie im selben Prozess laufen:

namespace Servent
{
    class Program
    {
        static void Main(string[] args)
        {
            var service = new ServiceEBC();
            var client = new ClientEBC();

            client.Out_RequestTextProcessing += service.In_EchoText;
            service.Out_Echo += client.In_ProcessedText;

            Application.Run(client);
        }
    }
}

(Ich habe die Client-Komponente als WinForms-Formular ausgelegt; deshalb ruft der Host am Ende Application.Run() auf.)

Wie zu erwarten werden Instanzen beider Komponenten direkt zusammengesteckt. Der Client kommuniziert ohne Umwege mit dem Service.

Remoting zwischenstecken

Der Trick bei EBCs ist, dass Kommunikation über “Drähte” verläuft. Während Objekte normalerweise sozusagen zusammengeschweißt sind, gibt es bei EBCs immer eine Indirektion. Zwischen Client und Service sitzt ein Delegat.

Wie ich in einem früheren Blogposting gezeigt habe, ist das der Schlüssel zu großer Flexibilität. Denn wo sich Client und Service nicht wirklich “berühren”, ist es eigentlich egal, welche Distanz sie haben bzw. was zwischen ihnen sitzt.

Das nutze ich für´s Remoting nun aus. Zwischen Client und Service schiebe ich einfach zwei Standardkomponenten:

 

image

Die Client-Komponente kommuniziert nun nicht mehr direkt mit dem Service, sondern mit einem Proxy für ihn. Und der Service wird nicht mehr von der Client-Komponente angesprochen, sondern von einem Stub.

Ganz wichtig: Für Client und Service macht das keinen Unterschied! In WCF müssen Sie vorausschauen und für einen Service einen Service-Kontrakt definieren. Bei .NET Remoting müssen Sie einen Service durch Ableitung von MarshalByRefObject kennzeichnen. Immer, wenn Sie Funktionalität entfernt betreiben wollen, müssen Sie also speziellen Code dafür entwickeln.

Das ist hier nun anders. Der Code der Standardkomponente ist immer gleich. Sonst wäre es ja auch keine Standardkomponente ;-) Mit EBCs müssen Sie nie mehr Code für die Verteilung entwickeln. (Oder höchstens einmal für das Kommunikationsmedium Ihrer Wahl.)

Wie gesagt, Client und Service ändern sich nicht dadurch, dass Sie verteilt betrieben werden. Der Code jedoch, der die Komponenten verdrahtet, sieht anders aus. Er muss die Remoting-Infrastruktur starten und Client bzw. Service mit ihr verdrahten. Der Host für den Client sieht dann z.B. so aus:

namespace Client
{
    static class Program
    {
        [STAThread]
        static void Main()
        {
            using (var host = new RemotingHost(0)) // #1
            {
                var proxy = host.CreateProxy<string, string>(
                                     "localhost:9000/EchoService", // #2
                                     ProxyResponseHandlingModes.Sync); // #3
                var client = new ClientEBC();

                client.Out_RequestTextProcessing += proxy.In_Send;
                proxy.Out_Received += client.In_ProcessedText;

                Application.Run(client);
            } 
        }
    }
}

Der Komponenten-Host – eine Konsolenanwendung – startet die Remoting-Infrastruktur (#1) und erzeugt dann darüber einen Proxy für den Service statt des Service selbst. Wo der Service läuft, gibt eine URL an (#2). Wie die Kommunikation zwischen Proxy und Service läuft, kann dem Client egal sein. Hier kommen der Einfachheit halber aber TCP-Sockets zum Einsatz. Die Option ProxyResponseHandlingModes.Sync (#3) legt fest, dass Antworten im Synchronization Context des Client ankommen sollen; so gibt es kein Problem in WinForms-Anwendungen, denn durch das Remoting sind garantiert mehrere Threads im Spiel.

Hervorhebenswert: Der Proxy ist generisch. Er kann in jede Kommunikationsstrecke eingesetzt werden, die seinem Format folgt. Hier ist das eine bidirektionale Kommunikation auf zwei “Drähten”.

Ein Client-Host unterscheidet sich, wie Sie sehen, nur marginal von dem, der Client und Service gehostet hat. So soll es sein.

Und wie sieht es beim Service aus? Genauso simpel:

namespace Service
{
    class Program
    {
        static void Main(string[] args)
        {
            using(var host = new RemotingHost(9000)) // #1
            {
                host.CreateStub<string, string, ServiceEBC>(
                        "EchoService",
                        s => s.In_EchoText, 
                        (s, c) => s.Out_Echo += c);

                …
            } 
        }
    }
}

Der Service-Host startet dieselbe Remoting-Infrastruktur – allerdings unter Angabe eines TCP-Ports (#1). Die finden Sie beim Client in der Adresse des Service wieder (#2 im Listing davor). Der Client-Host übergibt an die Infrastruktur 0 als Port, weil ihm der Port egal ist. Er muss keinen Endpunkt definieren, weil er nicht explizit adressiert wird.

Dann erzeugt der Service-Host einen Stub für den Service. Das ist der Kommunikationsendpunkt, bei dem Nachrichten vom Proxy ankommen. Er leitet sie weiter an den eigentlichen Service. Für den Service ist der Stub der Client. Und wieder ist die Infrastruktur generisch und der eigentliche Service merkt nichts davon, dass er nun entfernt von Clients betrieben wird.

Achtung: Der Service wird hier nicht instanziert! Sein Typ und zwei Lambda Ausdrücke werden allerdings an die Factory-Methode übergeben. Die reicht sie weiter an eine ServiceFactory (s.u.). Das ermöglicht eine Erzeugung des Service für jede Nachricht wie beim Single-Call-Modus von .NET Remoting. Services zustandslos zu machen ist daher ein kleines Zugeständnis an die Verteilung. Allerdings ist das nicht zwangsläufig nötig; in diesem Fall ist es nur etwas einfacher, um die Diskussion um Remoting nicht noch mit Aspekten der asynchronen Verarbeitung zu belasten.

Das war´s. So einfach kann Remoting mit EBCs sein.

Fragt sich nur, wie es unter der Haube funktioniert :-) Ich habe es mit dem Xcoordination Application Space implementiert. Ihn habe ich in der dotnetpro beschrieben und in Grundzügen auch in meinem englischen Blog. An dieser Stelle halte ich deshalb meine Erklärungen knapp:

Host und Proxy der Remoting Infrastruktur

Der Remoting Host selbst ist einfach. Er startet eigentlich nur einen Application Space und dient als Factory für Proxy und Stub:

namespace ebc.patterns
{
    public class RemotingHost : IRemotingHost
    {
        private readonly IXcoAppSpace space;

        public RemotingHost(int tcpPort)
               : this(string.Format("tcp.port={0}", tcpPort)) { }
        public RemotingHost(string configString)
        {
            this.space = new XcoAppSpace(configString);
        }

        public void Dispose()
        {
            this.space.Dispose();
        }
    …

Ein Proxy ist schnell erzeugt:

public IRemotingProxy<TRequest, TResponse> CreateProxy<TRequest, TResponse>(
       string serviceAddress,
       ProxyResponseHandlingModes mode)
{
    var remoteStub = this.space.ConnectWorker<Port<Request<TRequest, TResponse>>>(serviceAddress);
    return new RemotingProxy<TRequest, TResponse>(remoteStub, mode);
}

Dazu nimmt der Remoting Host Kontakt mit einem generischen Service-Worker auf, dessen Adresse ihm der Client übergibt. Und der RemotingProxy übernimmt die Aufgabe, EBC-Nachrichten in AppSpace Nachrichten zu übersetzen:

namespace ebc.patterns
{
    public class RemotingProxy<TRequest, TResponse> : IRemotingProxy<TRequest, TResponse>
    {
        private readonly Port<Request<TRequest, TResponse>> remoteWorker;
        private readonly Port<TResponse> responses;

        private readonly Port<Exception> exceptions;
        private readonly SynchronizationContext ctx;

        internal RemotingProxy(Port<Request<TRequest, TResponse>> remoteWorker, ProxyResponseHandlingModes mode)
        {
            this.remoteWorker = remoteWorker;

            if (mode == ProxyResponseHandlingModes.Sync)
                this.ctx = SynchronizationContext.Current;

            // Antwort vom Service verarbeiten
            this.responses = new Port<TResponse>();
            Arbiter.Activate(
                new DispatcherQueue(),
                Arbiter.Receive(true, this.responses, msg => this.ProcessMessageInSyncContext(msg, this.Out_Received))
                );

            // Exceptions vom Service verarbeiten
            this.exceptions = new Port<Exception>();
            Arbiter.Activate(
                new DispatcherQueue(),
                Arbiter.Receive(true, this.exceptions, ex => this.ProcessMessageInSyncContext(ex, this.Out_Exception))
                );

            this.Out_Exception += ex => { };
        }

        void ProcessMessageInSyncContext<T>(T msg, Action<T> messageHandler)
        {
            if (this.ctx != null)
                this.ctx.Send(x => messageHandler(msg), null);
            else
                messageHandler(msg);
        }

        public void In_Send(TRequest message)
        {
            var c = new Causality("ex", this.exceptions);
            Dispatcher.AddCausality(c);
            {
                var req = new Request<TRequest, TResponse> {
                                  Data = message,
                                  Response = responses};
                this.remoteWorker.Post(req);
            }
            Dispatcher.RemoveCausality(c);
        }

        public event Action<TResponse> Out_Received;
        public event Action<Exception> Out_Exception;
    }
}

Außerdem übernimmt der Proxy auch die Exception-Verarbeitung. Das habe ich bisher der Einfachheit unterschlagen. Wenn Exceptions vom Service kommen, dann leitet der Proxy sie auf einem speziellen Output-Pin weiter. Damit ist die Behandlung gleich, egal ob der Client Antworten vom Service in seinem Synchronization Context empfangen will oder nicht.

image

Stub der Remoting Infrastruktur

Auf der Service-Seite sieht es etwas kniffliger aus. Die ist nämlich grundsätzlich multi-threaded. Derselbe Stub wird für viele Anfragen und Antworten benutzt. Daher müssen Antworten vom Service, der nichts von solchen Feinheiten weiß, mit Anfragen korreliert werden. Nur so können sie an den, der die Anfrage geschickt hat, zurückgesandt werden.  Deshalb ist der Stub nicht fest verdrahtet mit einer Service-Instanz (was allerdings möglich wäre). Das obige Bild von Stub und Service ist also eine Vereinfachung. In Wirklichkeit sind die Verhältnisse so:

image

Das ändert zum Glück nichts daran, dass der Service nichts von seinem Glück wissen muss, remote betrieben zu werden.

Wie funktioniert das Ganze nun? Also…

1. Eine Anfrage kommt vom RemotingProxy über ein Transportmedium im RemotingStub beim StubWorker an. Der StubWorker ist ein AppSpace Worker, der die Übersetzung von CCR Port-Kommunikation auf EBC Events übernimmt. Da er asynchron arbeitet, braucht er eine Möglichkeit, Antworten Anfragen zuzuordnen. Deshalb enhält sein Nachrichtenaustausch mit einer angeschlossenen EBC KorrelationsIDs.

namespace ebc.patterns
{
    internal class StubWorker<TRequest, TResponse>
                   : Port<Request<TRequest, TResponse>>
    {
        private readonly Dictionary<Guid, IPort> responsePorts = new Dictionary<Guid, IPort>();

        [XcoConcurrent]
        public void ProcessRequest(Request<TRequest, TResponse> req)
        {
            var msg = new CorrelatableMessage<TRequest>(req.Data);
            lock (this.responsePorts)
            {
                this.responsePorts.Add(msg.CorrelationId, req.Response);
            }
            this.Out_Received(msg);
        }

        public void In_Reply(CorrelatableMessage<TResponse> msg)
        {
            IPort response;
            lock (this.responsePorts)
            {
                response = this.responsePorts[msg.CorrelationId];
                this.responsePorts.Remove(msg.CorrelationId);
            }
            response.PostUnknownType(msg.Data);
        }

        public event Action<CorrelatableMessage<TRequest>> Out_Received;
    }
}

2. Der Kommunikationspartner des Workers kann wg. der KorrelationsIDs nicht der Service sein. Der hat von solcherlei Dingen keine Ahnung. Stattdessen reicht der Worker die Anfrage weiter an die ServiceFactory. Die erzeugt dafür eine neue Service-Instanz – und schaltet ihr eine Standardkomponente vor, die Nachrichten mit KorrelationsID in solche ohne umwandeln kann (und umgekehrt).

namespace ebc.patterns
{
    internal class ServiceFactory<TRequest, TResponse, TService>
                   : IServiceFactory<TRequest, TResponse>
        where TService : new()
    {
        private readonly Func<TService, Action<TRequest>> inputPin;
        private readonly Action<TService, Action<TResponse>> connectOutputPin;

        public ServiceFactory(
                Func<TService, Action<TRequest>> inputPin,
                Action<TService, Action<TResponse>> connectOutputPin
            )
        {
            this.inputPin = inputPin;
            this.connectOutputPin = connectOutputPin;
        }

        public void In_Request(CorrelatableMessage<TRequest> request)
        {
            var corr = new SyncCorrelator<TRequest, TResponse>();
            var service = new TService();

            corr.Out_Received += this.inputPin(service);
            this.connectOutputPin(service, corr.In_Correlate);

            corr.Out_Reply += r => this.Out_Response(r);

            corr.In_DeCorrelate(request);
        }

        public event Action<CorrelatableMessage<TResponse>> Out_Response;
    }
}

Die ServiceFactory bastelt also dynamisch für jede Anfrage einen EBC-Platineninhalt zusammen, den sie mit ihren eigenen Pins verbindet.

Das ist kein Hexenwerk, aber doch ein bisschen umständlich. Zum Glück muss es nur einmal implementiert werden und ist dann für alle Kommunikationen gleich. Einfacher wäre es allerdings, der Service wäre darauf ausgelegt, asynchron und thread-safe zu arbeiten. Dann käme er selbst mit KorrelationsIDs zurecht und müsste nicht immer neu erzeugt werden. Dann wäre das Bild tatsächlich so einfach wie oben: Stub spricht mit Service. Wie Sie sehen, geht´s aber auch so, ohne Vorüberlegungen. Das ist das schöne an EBCs.

Im folgenden Bild stelle ich die beiden alternativen mal nebeneinander:

image

Der Quellcode, den Sie in einem Mercurial Google Projekt hier finden, spiegel das allerdings nicht wider. Er entspricht noch dem Bild, bei dem der RemotingStub die ServiceFactory enthält. Sauberer finde ich allerdings diese letzte abgebildete Variante. Sie entzerrt die Verantwortlichkeiten. Der RemotingStub ist nur für die Kommunikation zuständig – auch wenn das bedeutet, dass er Nachrichten mit KorrelationsIDs erzeugt/konsumiert. Darauf kann sich die bei Bedarf einstellen: Ist der Service nicht damit vertraut, muss er pro Nachricht neu instanziert werden. Dazu verdrahten sie ihn passend auf einer eigenen Platine.

Fazit

Client und Service müssen nicht speziell auf eine Verteilung vorbereitet werden. Und Sie müssen für eine Verteilung auch nichts extra programmieren, keine Service- oder Datenkontrakte sind nötig. Die Remoting-Infrastruktur kann generisch sein. Sie stecken sich also zusammen, was Sie brauchen.

Das bedeutet nicht, dass Sie keinen Gedanken verschwenden sollen, ob Services lokal oder entfernt betrieben werden. Die grundsätzliche Asynchronizität (oder auch mal Unverfügbarkeit ;-) eines entfernten Dienstes können seine Implementation beeinflussen. An der grundsätzlichen Einfachheit des Zusammensteckens ändert das jedoch nichts. Mal stecken Sie nur weniger, mal mehr zusammen.

Und nun: Basteln Sie schön :-) Arbeiten Sie mit der Remoting-Infrastruktur dieses Beispiels oder bauen Sie eine eigene auf Basis von .NET Remoting oder WCF oder was Sie wollen. Bei EBCs ist das noch erlaubt, weil sie so neu sind :-)

Mittwoch, 10. Juni 2009

Asynchronizität und Verteilung üben - Szenarien für verteilte Anwendungen

Wer was Neues lernen will, tut das am besten zunächst mit Übungen. Chirurgen lernen neue Techniken erst an toten und/oder nicht menschlichen Lebewesen, Piloten lernen im Simulator. Und ich will den neuen Application Space ausprobieren oder allgemeiner asynchrone und verteilte Architekturen üben. Was sind aber Übungsaufgaben, an denen ich mich versuchen kann? Einen asynchronen und verteilten Service aufzusetzen ist ja trivial. Von dem Service dann auch noch Notifikationen zu bekommen oder Pub/Sub einzurichten, das ist auch trivial. Jeweils für sich genommen sind diese Dinge einfach - aber wie füge ich diese Bausteine zu etwas Größerem, Realistisch(er)em zusammen? Erst in einem umfassenderen Szenarion, das nicht von der Technik ausgeht, sondern von "Kundenanforderungen" kann ich auch feststellen, was einer Technologie wie dem Application Space noch fehlen mag (oder wo sie besonders geeignet ist).

Hier möchte ich nun einige Szenarien zusammentragen, die mir als Übungen für Verteilung und Asynchronizität erscheinen. Sie sind mehr oder weniger komplex, aber immer irgendwie "zum Anfassen". Jedes bietet für die zu übende oder evaluierende Technologie eine andere Herausforderung. Ich werde sie mit dem Application Space implementieren, wer mag, kann aber natürlich WCF pur oder mit Azure oder Jabber pur oder MassTransit oder NServiceBus oder Rhino Service Bus oder MSMQ pur oder TCP Sockets pur oder noch ganz andere Technologien damit ausprobieren. Ganz im Sinne der School of .NET Diskussion sehe ich diese Szenarien auch als Chancen für ganzheitliches Lernen. Clean Code Development, Komponentenorientierung, .NET Framework Grundlagen, TDD... all das und mehr kann man auch einfließen lassen.

Szenario 1: Stammdatenverwaltung

Aller Anfang sollte einfach und typisch sein. Deshalb ist mein erstes Szenario eines, mit dem viele Entwickler immer wieder konfrontiert werden: die Stammdatenverwaltung oder "forms over data". Ein Anwender verwaltet mit seinem Client Daten in einer Datenbank mit den üblichen CRUD-Funktionen: Create, Read, Update, Delete. Zusätzlich kann er einen serverseitigen Datenimport anstoßen.

Ob sich für dieses Szenario eine Verteilung überhaupt lohnt, sei einmal dahingestellt. Allemal, wenn aus anderen Gründen eine Anwendung verteilt werden soll, muss auch die Stammdatenverwaltung auf eine solche Architektur abgebildet werden.

Um das Datenmodell einfach zu halten, reicht es aus, wenn das Szenario sich nur um Personen mit ihrer Adresse dreht.

Datenmodell:

  • Person(Nachname, Vorname, Straße, PLZ, Ort, Land, Tel, Soundex)

Featureliste:

  • Der Anwender kann nach Personen suchen; der Server liefert eine Liste von passenden Personen zurück.
  • Der Anwender kann eine gefundene Person bearbeiten und speichern.
  • Der Anwender kann eine neue Person anlegen.
  • Der Anwender kann eine Person löschen.
  • Der Anwender kann gefundene Adressen in eine CSV-Datei exportieren. Der Export kann clientseitig erfolgen.
  • Der Anwender kann den Import von Personen aus einer CSV-Datei veranlassen. Dazu muss er dem Server mitteilen, in welcher Datei die Daten liegen. Der Server importiert, meldet zwischendurch den Fortschritt und liefert am Ende ein Importresultat.
  • Dublettenprüfung: Jede Person soll nur einmal in der Datenbank stehen. Mehrere Sätze mit denselben Daten sind zu vermeiden, um bei Mailings nicht mehrere Briefe an dieselbe Person zu senden. Um den Vergleich von Personen zu vereinfachen, können sie mit einem Soundex-Wert ausgestattet werden. Wannimmer eine Person gespeichert werden soll (nach Bearbeitung, nach Neuanlage, beim Import) und schon mit einem anderen Datensatz in der Datenbank vertreten ist, wird die Operation verweigert und der Anwender informiert. Die Dublettenprüfung kann in Schritten implementiert werden:
    • Dubletten bei Neuanlage prüfen
    • Dubletten beim Import prüfen
    • Dubletten nach Bearbeitung prüfen

Klingt doch einfach, oder? Hat aber natürlich seine Tücken, denn es gilt ja, diese Funktionalität asynchron und verteilt zu realisieren. Wie kommunizieren Client und Server im Sinne einer solchen Stammdatenverwaltung miteinander?

image

Herausforderungen:

  • Wie werden asynchrone Operationen wie Speichern oder Import im UI repräsentiert?
  • Wie meldet der Server den Fortschritt beim Im/Export an den Client (Notifikationen)?

Szenario 2: Referentenfeedback (Heckle Service)

Christian Weyer hat ein schon älteres Szenario in seinem dotnetpro-Artikel "Schnuppern an Azure" (5/2009)mit den aktuellen Technologien neu implementiert. In der dotnetpro 7/2009 greife ich das auf und realisiere es mit dem Application Space.

Die Idee ist einfach: Zuschauer eines Vortrags auf einer Konferenz sollen dem Referenten live Feedback geben können. Sie sollen sozusagen elektronisch zwischenrufen können (engl. to heckle). Dazu hat jeder Teilnehmer einen Client, mit dem er kurze Textnachrichten an den Referenten senden kann, der sie in einem eigenen Frontend auflaufen sieht.

Datenmodell:

  • Nachricht(Absendername, Nachrichtentext, Eingangszeitstempel) - Jede Nachricht gehört natürlich zu einem Referenten. Ob das allerdings in der Nachricht vermerkt werden muss, soll hier nicht festgelegt werden.

Featureliste:

  • Teilnehmer senden Zwischenrufe an den Referenten.
  • Teilnehmer sehen sich die Liste der letzten n Zwischenrufe an.
  • Der Referent bekommt jeden Zwischenruf automatisch angezeigt.
  • Falls der Referent sein Frontend - aus welchen Gründen auch immer - neu startet, bekommt er die Liste aller bisher eingegangenen Zwischenrufe angezeigt.
  • Der Referent identifiziert sich irgendwie, so dass die Teilnehmer ihm und keinem anderen ihre Zwischenrufe senden. Die Teilnehmer müssen einen Referenten also beim Zwischenrufen adressieren. Potenziell kann die Heckle-Anwendung ja gleichzeitig in vielen Vorträgen zum Einsatz kommen.
  • Der Veranstalter der Vorträge kann die Zwischenruflisten aller Referenten jederzeit einsehen.

image

Herausforderungen:

  • Wie nehmen Teilnehmer mit dem Referenten Kontakt auf? Direkt, indem sie seinen Rechner adressieren oder indirekt via eines Discovery-Servers?
  • Wo werden die Nachrichten vorgehalten, damit vor allem Teilnehmer und Veranstalter sich jederzeit einen Überblick verschaffen können?
  • Wie wird insb. der Referent automatisch über neue Nachrichten informiert?

Szenario 3: Tic Tac Toe

Es ist zwar kein typisches Geschäftsanwendungsszenario, aber es macht Spaß: ein Spiel realisieren. Bei Tic Tac Toe (TTT) sind die Regeln simpel, so dass man sich auf die verteilte Implementation konzentrieren kann.

Zwei Spieler spielen gegeneinander auf einem TTT-Brett. Jeder sitzt an seinem PC und sieht den gemeinsamen Spielstand.

Datenmodell:

  • Spielfeld mit 3x3 Spielfeldern in den Zuständen O, X und leer. Zusätzlich sollte das Spielfeld noch einen Spielzustand haben wie Spiel begonnen, Spiel beendet, Gewinner ist Spieler 1, Gewinner ist Spieler 2.

Featureliste:

  • Ein Spieler bietet sich zum Spiel an.
  • Ein Spieler nimmt Kontakt mit einem anderen auf und sie beginnen eine Partie.
  • Spieler machen Züge.
  • Ob und welcher Spieler gewinnt, wird automatisch festgestellt.
  • Ein Spieler beendet eine Partie vorzeitig.

Dies ist während des Spiels natürlich ein Peer-to-Peer-Szenario. Ob intern die Rollen aber auch gleich sind oder nicht vielleicht doch ein Spieler ein Partienserver ist, hängt von der Implementation ab.

image

Herausforderungen:

  • Wie nehmen die Spieler Kontakt miteinander auf?
  • Wo wird der Partienzustand gehalten?
  • Wie erfahren die Spieler über den nächsten Zug?
  • Wie wird den Spielern das Spielende mitgeteilt?

Szenario 4: Starbucks

Gregor Hohpe hat in einem Blogbeitrag deutlich gemacht, wie wenig praktikabel die bisher so beliebten 2-Phase-Commit-Transaktionen in der realen Welt, d.h. in asynchronen (und verteilten) Szenarien sind. MassTransit und Rhino Service Bus haben das aufgenommen und versucht, mit ihren Mitteln das Szenario abzubilden. Es ist einfach eine schöne Fingerübung für jeden, der in die verteilte und asynchrone Programmierung einsteigen will.

Bei Starbucks kommen Kunde, Kassierer und Barista zusammen. Der Kunde bestellt ein Getränk, der Kassierer nennt den Preis und nimmt das Geld entgegen. Währenddessen bereitet der Barista schon das Getränk zu und serviert es, wenn die Zahlung geklappt hat.

Datenmodell:

  • Bestellung(Getränkeart, Bechergröße, Menge)
  • Zahlungsaufforderung(Gesamtpreis einer Bestellung)
  • Bezahlung(Betrag, Zahlmittel) - Zahlmittel könnten Barzahlung oder Kreditkarte sein

Featureliste:

  • Kunde bestellt ein Getränk beim Kassierer. Variation: Kunde bestellt mehrere und verschiedene Getränke beim Kassierer.
  • Kassierer nennt den Gesamtpreis
  • Kunden bezahlt
  • Kassierer nimmt Bezahlung entgegen und prüft den Betrag. Wenn ok, dann schließt er die Bestellung ab.
  • Barista bereitet bestellte Getränke vor.
  • Wenn Bezahlung abgeschlossen, stellt der Barista die Getränke zur Abholung bereit.

Der Kunde kann hier als interaktiver Client realisiert werden. Kassierer und Barista hingegen sind automatische Dienste. Um die reale Welt nachzustellen, können ihre Funktionen über Pausen (Thread.Sleep()) eine wahrnehmbare Dauer bekommen.

image

Herausforderungen:

  • Wie nehmen die Beteiligten Kontakt miteinander auf?
  • Wie wird der Dialog zwischen Kunde und Kassierer geführt?
  • Wie erfährt der Kunde über das fertiggestellte Getränk?
  • Was passiert mit einem schon zubereiteten Getränk, wenn die Zahlung nicht erfolgreich ist?

Szenario 5: Arbeitsteilung

Das MSDN Magazine Juni/2009 beschreibt in "A Peer-To-Peer Work Processing App With WCF" ein Szenario, dass sich auch zur Übung zu realisieren lohnt. Mehrere sog. Worker stehen da bereit, um Aufträge von sog. Usern anzunehmen. Der Artikel nutzt zur Arbeitsverteilung das P2P-Protokoll von WCF - aber man kann es auch anders machen.

Datenmodell:

  • Arbeitsauftrag(Id, Dauer)

Featureliste:

  • User vergeben Aufträge in einen Pool von Workern hinein.
  • Worker übernehmen einen oder mehrere Arbeitsaufträge.
  • Worker können dem Pool beitreten oder ihn verlassen.
  • Ob ein Worker Aufträge annimmt hängt von seiner Last ab. Die gesamte Auftragslast soll natürlich möglichst gleichmäßig auf die Worker verteilt werden.
  • Wie die Arbeit "so läuft" können sog. Spectators beobachten; sie dienen der Instrumentierung des Systems.

In dieser Featureliste fehlen einige Aspekte, die der Artikel beschreibt, z.B. "Work Item Backup" oder "Work Sharing". Ich habe sie nicht aufgenommen, weil sie mir abhängig scheinen vom gewählten Lösungsweg (hier: P2P-Kommunikation). Meine Szenarien sollen aber keine Lösungswege oder Technologien nahelegen (z.B. Einsatz eines Busses oder Sagas oder P2P-Kommunikation).

Die schlanken Aufträge habe ich ebenfalls deshalb übernommen. Es geht ja nicht um eine konkrete Problemdomäne. So müssen die Aufträge nur eine Dauer haben, um Lastverteilung beobachten zu können.

image

Herausforderungen:

  • Wie wird die Auftragslast möglichst gleichmäßig (oder "gerecht") auf die Worker verteilt?
  • Wie werden weitere Worker möglichst unmittelbar in die Auftragsbearbeitung einbezogen? Oder allgemeiner: Wie kann der Worker-Pool "atmen"?
  • Wie werden User über Auftragsergebnisse (oder gar Fortschritte) informiert?
  • Wie kann ein Spectator die Arbeit(sverteilung) beobachten?
  • Müssen Aufträge nach Vergabe noch "gesichert" werden können, falls Worker noch nicht zu ihrer Verarbeitung gekommen sind?
  • Wie können die Worker möglichst ortsunabhängig verfügbar gemacht werden?