Montag, 28. November 2011

Message Driven Beans anhalten und starten

Message Driven Beans (MDBs) werden meistens schon aktiv (d.h. vom Container aufgerufen, wenn über JMS Nachrichten für die Bean vorhanden sind), während das Deployment der Anwendung noch läuft. Das ist normalerweise unkritisch, aber es gibt Situationen, in denen man sicherstellen möchte/muss, dass die MDBs erst nach vollständigem Deployment (oder irgend einem anderen definierten Zeitpunkt) aktiv werden.

Nahezu jedes Messaging-System (JMS-Implementierung) bietet die Möglichkeit, über produktspezifische API oder externe Kommandos eine Queue anzuhalten und später wieder zu starten. Das betrifft aber die komplette Queue, d.h. es ist dann auch nicht mehr möglich, Nachrichten in die Queue zu schreiben.

Bei einigen Messaging-Systeme kann man daher via JMX die MBean einer MDB ermitteln und damit tatsächlich nur die MDB anhalten – also die Zustellung aus der Queue unterbinden. Das Schreiben in die Queue wäre dann immer noch möglich. (Das Problem, wie damit die MDB gleich zu Beginn des Deployments gestoppt wird, lassen wir mal außen vor.)

Leider sind beide Lösungen produktspezifisch. Eine portable Lösung ist bis inkl. Java EE 6 für Message Driven Beans nicht vorgesehen. Wie könnte eine selbst gebaute, portable Lösung aussehen?

Java SE 5 hat im Paket java.util.concurrent viele neue Klassen rund um Thread-Synchronisation und Sperren in die Java-Welt gebracht. Für unsere Zwecke ist ein CountDownLatch geeignet:

import java.util.concurrent.CountDownLatch;

@Singleton
public class AppStatus {

private static final CountDownLatch startSignal
= new CountDownLatch(1);

public void waitForAppInitialized() {
try {
startSignal.await();
} catch (InterruptedException e) {
throw new RuntimeException(
"Fehler beim Warten auf Sperren-Freigabe", e);
}
}

public void setAppIsInitialized() {
startSignal.countDown();
}

public boolean isAppInitialized() {
return (startSignal.getCount() == 0L);
}
}

Der CountDownLatch ist zu Beginn gesperrt. Zur Freigabe muss die Anwendung zu einem geeigneten Zeitpunkt die Methode setAppIsInitialized() aufrufen, beispielsweise in einem ServletContextListener oder in einem EJB-Timer.

Die zu blockierende MDB wartet dann bei jedem Aufruf zunächst darauf, ob die Sperre schon freigegeben wurde:

@MessageDriven
public class StartbareMDB implements MessageListener {

@EJB
private AppStatus status;

public void onMessage(Message msg) {

status.waitForAppInitialized();

// ... der eigentliche MDB-Code ...
}
}
Eine einfache, funktionierende Lösung mit Java-Bordmitteln.

Nachteil dieser Lösung ist, dass hier Threads des Containers blockiert werden. Man muss aufpassen, dass die Maximalzahl der blockierten MDB-Threads den Threadpool nicht überfordert (was ein Grund ist, warum man in Java EE-Containern nicht mit Threads herumspielen soll).

Außerdem ist der Performanceverlust nicht riesig, aber messbar. Für Höchstlastsysteme ist das also evtl. nicht geeignet. Zumal dort oft Clustering zum Einsatz kommt – und obige Lösung ist nur clusterfähig, wenn man sicherstellen kann, dass die Freigabe-Methode auf jedem Cluster-Knoten aufgerufen wird.

Als weitere (bewusste) Einschränkung ist zu beachten, dass der CountDownLatch nur einmalig nutzbar ist, er dient also wirklich nur als Start-Signal. Wenn man Message Driven Beans beliebig anhalten und starten möchte, muss man andere Sperren verwenden.

Fazit: Die hier vorgestellte Lösung funktioniert in vielen praktischen Situationen gut. Aber gibt es andere Ideen / Lösungen, die ähnlich einfach zu realisieren sind, die gezeigten Einschränkungen aber nicht aufweisen? Oder müssen wir auf Java EE 7 (oder eher 8) warten?