Services und ServiceRegistry

Targets können Micro-Services anbieten.

Dazu registrieren sie sogenannte Services in einer ServiceRegistry.

Die Services haben eine eigene Service-ID (kurz: SID), vom Typ IId.

Im Gegensatz zu den TID sind die SID dokumentiert und wohlbekannt, also konstant. So können auch Services benutzt werden, die in fremden devel.one-Instanzen angeboten werden.

SoftDevel betreibt eine Registratur an bekannten Services; die Verwendung dieser dokumentierten Services wird ausdrücklich empfohlen. Jeder kann Services zur Dokumentation einreichen.

Daher schickt man meist weniger Messages an ein (eher anonymes) Target, sondern an einen Service.

An Services kann man auch Observer registrieren, die jede Nachricht in Kopie bekommen, welche an einen Service gerichtet ist. Aus diesem Grund werden Services auch für das Informations-Management verwendet. Notifications werden einfach an einen dafür eingerichteten Service geschickt, und alle Interessenten bekommen die Mitteilung in Kopie.

Wie spricht man einen Service an?

Nachrichten haben einen Umschlag (Envelope), der u.a. die Empfänger-Adresse enthält. Das ist gewöhnlich die Adresse eines Targets in der Form TID.NID.HID. So eine Adresse ist gerichtet, und die Nachricht wird direkt dem Target zugestellt.

Läßt man aber die Target-ID (TID) weg, so bleibt nur noch die Adresse eines Namespaces über. Diese hat die Form NID.HID. Stößt der Kernel auf eine solche unvollständige Adresse, prüft er, ob ein Service in genau diesem Namespace registriert ist, der die gleiche ID hat wie die Record-ID. Die Record-ID der Nachricht muss also die Service-ID sein.

Hat der Kernel einen solchen Service gefunden, geht eine KOPIE der Nachricht an alle Observer, die an diesem Service angeheftet wurden.

Das kann man nun nutzen als

  • Indirektion: Der Anbieter des Services klebt seine Adresse als Observer an den Service. Er bekommt die an den Service gerichtete Nachricht und antwortet wie gehabt. Der Absender der Nachricht kennt nicht die Adresse des Targets. Es könnte sogar ein Target sein, was in einer anderen D1-Instanz registriert wurde.

  • Notifikation: Der Betreiber (oder ein irgendein anderes Target) triggert den Service, indem es eine Nachricht an den Service schickt (das Triggern geht übrigens auch synchron bei lokalen Services). Alle Observer dieses Services bekommen die KOPIE der Nachricht. So kann man leicht Zustandsänderungen an alle interessierten Targets richten.

  • Datenobjekt: Die letzte Nachricht wird übrigens am Service gespeichert. Klebt nun ein Target seine Adresse als Observer an den Service, bekommt es eine KOPIE der letzten Nachricht. Somit ist es möglich, einen Zustand (im Gegensatz zur Zustandsänderung) an die Observer zu vermitteln.

    Das wird besonders gerne genutzt, um zu signalisieren, ob ein SubSystem schon hochgefahren wurde. Das SubSystem betreibt einen Service “SubSystemAvailable”. Bevor ein Kunden-Target das SubSystem nutzt, klebt es einen Observer an diesen Service und wartet die Nachricht “Available” ab. Erst wenn die Nachricht kommt (mit dem Argument available=true), legt der Kunde los. Würde das Target sofort nach dem StartUp eine Nachricht an das Subsystem richten, würde es Gefahr laufen, dass das SubSystem noch nicht geladen wurde.

Beispiel einer Nachricht an einen Service:

protected void sendCreateNamespace(IId aNid) throws CException
{
    final CEnvelope env = new CEnvelope(CWellKnownNID.SYSTEM);

    final CRecord rec = new CRecord(CRecordCreateNamespace.ID);
    CRecordCreateNamespace.setParamWantedNid(rec, aNid);

    sendRequest(env, rec);
}

Hier wird ein neuer Namespace eingerichtet. Der Envelope erhält eine Empfänger-Adresse, die nur aus einem Namespace (“SYSTEM”) besteht. Das ist natürlich keine vollständige Adresse. Die TID lassen wir weg, weil wir ja einen Service ansprechen wollen. Und die HID können wir weglassen, weil wir hier den lokalen Namespace “SYSTEM” ansprechen wollen. Wollten wir einen Namespace in einer entfernten D1-Instanz anlegen, so sähe diese Zeile so aus:

remoteHid = CHidFactory.create("a78d5028-79dd-4315-b47b-4266c35e7ab7");
final CEnvelope env = new CEnvelope(CWellKnownNID.SYSTEM, remoteHid);

In den Record gehören die Nutzdaten, also speichern wir dort die gewünschte Namespace-ID. Anschließend versenden wir die Envelope und Record, welche zusammen die Nachricht ergeben.

Der Service CRecordCreateNamespace.ID wird von einem System-Target betrieben, welcher die abgeschickte Nachricht erhält, den Request ausführt und das Ergebnis in die Nachricht als Antwort hineinschreibt. Der Kernel wird dann die Nachricht zum Absender zurücktransportieren, damit das Ergebnis geprüft werden kann.

Wir sehen an dem Beispiel auch, dass es keinen Unterschied macht, einen Service in einem lokalen wie auch in einem entfernten System anzusprechen. Das macht devel.one einzigartig.

Wie richte ich als Target einen Service ein?

Hier gibt es viele Möglichkeiten. Wenn man die Services und andere Nachrichten mit XML beschreibt (sehr zu empfehlen), so werden die Information gleich beim Start des Namespaces ausgewerten und die Services automatisch im System registriert.

Den weiterführenden Artikel zum Record-Generator findest du hier: Überblick.

Gleichwohl kann man aber auch alles mit der ServiceRegistry selber regeln:

final IId serviceId = CIdFactory.create("2d6cb8e5-80a5-4676-b88a-a5baaf7a995d");
final IServiceRegistry sr = getNamespace().getServiceRegistry();
sr.registerService(serviceId, "AddMonitorPanel");

Hier wird eine Service-ID aus einer UUID erzeugt. Dann wird die ServiceRegistry des lokalen Namespaces geholt und anschließend der Service registriert. Das zweite Argument dient lediglich dem erleichterten Debuggen, indem der SID ein sprechender Namen zugewiesen wird.

Für entfernte Systeme muss der Service mittels einer Nachricht registriert werden.

Nun fehlt aber noch etwas: Das Target erhält noch keine Nachrichten, da es noch keinen Observer an den Service geheftet hat. Das holen wir gleich nach:

sr.addObserver(serviceId, getAddress());

Und noch etwas fehlt: Damit wir die Nachricht auffangen können, brauchen wir einen Message-Handler:

addMessageHandler(CRecordMonitorAddPanel.ID, new CMessageHandler(this, "AddMonitorPanel")
{
    @Override
    public boolean handleMessage(final CEnvelope aEnvelope,
                                 final CRecord aRecord) throws Exception
    {
        final Object paramPanelRef = CRecordMonitorAddPanel.getParamPanelref(aRecord, null);
        final String paramTabName = CRecordMonitorAddPanel.getParamTabname(aRecord, null);
        final int paramTabOrder = CRecordMonitorAddPanel.getParamTaborder(aRecord, 0);

        if (paramPanelRef == null)
        {
            final String err = "PanelRef is null.";
            LOG.error(err);
            aEnvelope.setResult(CResultCode.INVALID_ARGUMENT, err);
        }
        else if (paramPanelRef instanceof JPanel == false)
        {
            final String err = "PanelRef is no JPanel.";
            LOG.error(err);
            aEnvelope.setResult(CResultCode.INVALID_ARGUMENT, err);
        }
        else if (CUtilString.isEmpty(paramTabName))
        {
            final String err = "PanelName is empty.";
            LOG.error(err);
            aEnvelope.setResult(CResultCode.INVALID_ARGUMENT, err);
        }
        else
        {
            final JPanel panel = (JPanel) paramPanelRef;

            ...
        }

        aEnvelope.setResult(null);
        return true;
    }
});

Hier ist er, ein abgespeckter original Message-Handler aus dem Monitor. Er sorgt dafür, das PlugIns weitere Tabulatoren in dem TAB-Control des Monitors einhängen können, wie zum Beispiel das Sequence-Panel dieses macht.

Entfernt wird ein Service über die folgende Methode:

public void deregisterService(final IId aSID) throws CException;

Wie erfahre ich, ob ein Service existiert?

In einem asynchron arbeitenden System ist nicht immer gewährleistet, dass alle Services von Beginn an registriert sind. Manche PlugIns werden nach dem eigenen Code geladen, oder es kommen sehr viel später weitere Services dazu. Wir brauchen dazu Instrumente, um die Existenz von Services zu erkennen.

Eine Möglichkeit ist natürlich, einfach eine Nachricht an den Service zu schicken. Kann dieser nicht gefunden werden, kommt die Nachricht mit einem Fehlercode zurück.

Für lokale Services ist es einfacher, die Existenz per synchronem Methodenaufruf zu prüfen:

boolean exist = getNamespace().getServiceRegistry().existService(sid);
Und was machen, wenn er nicht gefunden wurde?
Also lieber gleich einen Monitor verwenden::
boolean exist = existService(sid, getAddress());

Wenn hier der Service NICHT gefunden wurde, wird ein Monitor registriert. Man bekommt dann Nachrichten, wenn sich der Zustand dieses Services ändert:

  • CRecordNotifyServiceRegistered.ID, wenn der Service registriert wurde
  • CRecordNotifyServiceDeregistered.ID, wenn der Service deregistriert wurde
  • CRecordNotifyServiceObserverRegistered.ID, wenn ein Observer registriert wurde
  • CRecordNotifyServiceObserverDeregistered.ID, wenn ein Observer deregistriert wurde

Für das Registrieren (und dem Abmelden) von Monitoren gibt es auch direkte Methoden:

public void registerMonitor(final IId aSID,
                            final ITargetAddress aObserver);
public void deregisterMonitor(final IId aSID,
                              final ITargetAddress aObserver);

Observer Methoden

Folgende Observer-Methoden werden von der ServiceRegistry unterstützt:

// Ein Observer anheften
public void addObserver(final IId aSID,
                        final ITargetAddress aAddress) throws CException;

// An all diese Services ein Observer anheften
public void addObservers(final IId[] aSID,
                         final ITargetAddress aAddress) throws CException;

// Die Anzahl der Observer an einem bestimmten Service feststellen
public int getObserverCount(final IId aSID);

// Alle Observer eines Services entfernen (Vorsicht!)
public void removeAllObserver(final IId aSID) throws CException;

// einen eigenen Observer von einem Service entfernen
public void removeObserver(final IId aSID,
                           final ITargetAddress aObserver) throws CException;

// Alle eigenen Observer von einem Service entfernen.
public void removeObservers(final ITargetAddress aObserver) throws CException;

Die Methoden sind vermutlich selbsterklärend.

Trigger

Gewöhnlich schickt man an einen Service Nachrichten, die dann die Observer bekommen. Diesen Trigger kann man auch synchron auslösen:

// den Service synchron triggern (Die Nachrichten werden asynchron verschickt)
public void triggerObserver(final IId aSID,
                            final CMessage aTemplate) throws CException;

Hier bekommen die Observer eine Kopie der Nachricht aTemplate zugeschickt.

Möchte man den letzten Trigger wiederholen, so kann man auf folgende zwei Methoden zurückgreifen:

public void recallTrigger(final IId aSID) throws CException;

public void recallTrigger(final IId aSID,
                          final ITargetAddress aAddress) throws CException;

Während die erste Methode alle vorhandenen Observer dieses Services berücksichtigt, schickt die Letztere die Nachricht des letzten Triggers an die angegebene Adresse.

Weitere Methoden

Folgende Methoden werden noch unterstützt:

// Gibt den Namen eines Services
public String getName(final IId aSID);

// Gibt Informationen über den Service
public void getServiceInfo(final IId aSID,
                           final CRecord aRecord) throws CException;

// Wieviel Services sind in diesem Namespace registriert?
public int size();

Services der ServiceRegistry

Folgende Services dienen dem asynchronen Zugang zur Service-Registry eines jeden Namespaces. Sie können per Message genutzt werden. Eine genauere Beschreibung kannst du dem JavaDoc entnehmen.

  • CRecordRegisterService.ID
  • CRecordDeregisterService.ID
  • CRecordGetServiceInfo.ID
  • CRecordNotifyServiceDeregistered.ID
  • CRecordNotifyServiceObserverDeregistered.ID
  • CRecordNotifyServiceObserverRegistered.ID
  • CRecordNotifyServiceRegistered.ID
  • CRecordTriggerRecall.ID

Services der D1-Instanz

Services werden in den Namespaces registriert, für die sie gelten sollen. Wenn man aber Micro-Services anbieten möchte, die für die ganze Instanz relevant sind und nicht nur für einen Namespace, so sollte man die Verwendung des SYSTEM-Namespaces erwägen.