Mittwoch, 31. Oktober 2012

Quartz innerhalb von Spring clusterfähig aufsetzen

In meinem derzeitigen Projekt besteht die Anforderung, Statusupdates, deren Versand fehlgeschlagen ist, per Scheduler erneut zu versenden. Da das Projekt schon mit Spring konfiguriert war und auf einem Tomcat läuft, fiel die Wahl auf den Quartz Scheduler (http://www.quartz-scheduler.org). Die von mir verwendeten Versionen der Frameworks sind Spring 3.1.0 und Quartz 2.1.6. Die Versionsnummern sind wichtig, da es sonst im Betrieb zu Inkompatibilitäten kommen kann.

Aus Gründen der Ausfallsicherheit ist das Zielsystem als Cluster mit einem vorgeschalteten Load Balancer aufgesetzt. Die von mir gewählte Basiskonfiguration von Quartz basierte allerdings auf der Annahme, dass es nur eine lauffähige Instanz der Anwendung gibt. So kam es, wie es kommen musste: Die unterschiedlichen Instanzen des Schedulers kamen sich in die Quere und verursachten Locking-Exceptions auf Datenbankebene.
Daraufhin habe ich die Konfiguration von Quartz derart geändert, dass Jobs des Schedulers genau von einer Instanz ausgeführt werden. Um dies zu gewährleisten schreibt Quartz diverse Informationen in verschiedene Datenbanktabellen. Die nötigen Skripte befinden sich in der Quartz-Distribution im Verzeichnis quartz-2.1.6\docs\dbTables.  Da ich auf das ein oder andere Problem gestoßen bin, habe ich beschlossen, meine Lösung in Form dieses Blogposts zu veröffentlichen.

Der Task



 

 
Der resendMessagesTask ist eine einfache Spring Bean, die bei jedem Aufruf des Schedulers ausgeführt werden soll:

 
public class ResendMessagesTask implements Serializable {

    private static final long serialVersionUID = -4193130296397110811L;
    private transient CommonService commonService;

    public void sendMessages() {
            if (commonService == null) {
                commonService = (CommonService) ApplicationContextProvider.getApplicationContext().getBean(
                        "CommonService");
            }
            commonService.handleUndeliveredMessages();
    }
}
Der Task muss serialisiebar sein, damit Quartz ihn in die Datenbank schreiben kann. Da ich in der Klasse eine weitere Spring-Bean verwende, kann diese nicht per @Autowired injiziert werden, da @Autowired nicht serialisierbar ist. Ich habe daher eine Hilfsklasse geschrieben, die das Spring-Interface ApplicationContextAware implementiert und den Kontext zur Laufzeit zur Verfügung stellt.

Der Job


Der resendMessagesJob ist ebenfalls eine Spring-Bean und bündelt verschiedene Tasks (in unserem Fall genau einen) zu einer ausführbaren Einheit:
 
public class ResendMessagesJob extends QuartzJobBean {
    @Autowired
    ResendMessagesTask resendMessagesTask;

    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        resendMessagesTask.sendMessages();
    }
}

Der Trigger


Im cronTrigger wird festgelegt, wie oft der resendMessagesJob laufen soll, in diesem Fall also alle 2 Minuten:

  
  

Die Quartz-Konfiguration

Kommen wir nun zum interessanten Teil der Konfiguration, in dem die Clusterfähigkeit von Quartz konfiguriert wird:
 
 
 
  
    
      
    
  
  
    
      
    
  
  
  
  
  
  
  
   
    someScheduler
    AUTO
    60000
    org.quartz.impl.jdbcjobstore.JobStoreTX
    
    org.quartz.impl.jdbcjobstore.DB2v8Delegate
    
    qrtz_
    true
    org.quartz.simpl.SimpleThreadPool
    2
    5
   
  
 
Die Einträge kurz vorgestellt:
  • applicationContextSchedulerContextKey:  Stellt den ApplicationContext in allen Scheduler-Klassen zur Verfügung.
  • dataSource: Die Datenbankanbindung (Konfiguration habe ich ausgelassen)
  • transactionManager: Die verwendete Spring-Transaktionssteuerung (Konfiguration ebenfalls ausgelassen).
  • overwriteExistingJobs: Legt fest, ob bereits angelegte identische Jobs überschrieben werden dürfen.
  • autoStartup: Legt fest, ob der Job manuell oder automatisch gestartet werden soll.
  • org.quartz.scheduler.instanceName: Der Name der Scheduler-Instanz. Er muss bei allen Knoten des Clusters gleich lauten, da Quartz anhand des Namens festellt, welche Instanzen zusammen gehören.
  • org.quartz.scheduler.instanceId: Jede Instanz bekommt eine eindeutige ID. Steht der Wert auf AUTO, kümmert sich Quartz um die Vergabe.
  • org.quartz.jobStore.misfireThreshold: Zeit in ms, die Quartz wartet, bevor eine Ausführung als fehlerhaft markiert wird.
  • org.quartz.jobStore.class: Die Klasse legt fest, wo Quartz die Informationen ablegt, in unserem Fall in einer Datenbank.
  • org.quartz.jobStore.driverDelegateClass: Der verwendete Datenbanktyp
  • org.quartz.jobStore.tablePrefix: Alle angelegten Tabellen beginnen mit diesem Prefix.
  • org.quartz.jobStore.isClustered: Keine Erklärung notwendig, der Grund für diesen Blogpost.
  • org.quartz.threadPool.class: Der Typ des Threadpools, der zur Verwaltung der einzelnen Threads verwendet wird
  • org.quartz.threadPool.threadCount: Anzahl der gleichzeitig erlaubten Threads. Werte zwischen 1 und 100 sind praktikabel (je mehr Jobs da sind, die gleichzeitig laufen sollen, umso höher sollte der Wert sein). In unserem Fall existiert nur ein einziger Job, weswegen sogar 1 als Angabe gereicht hätte.
  • org.quartz.threadPool.threadPriority: Die Priorität der einzelnen Threads (wie in Java, 1 = niedrig, 10 = hoch).

Keine Kommentare:

Kommentar veröffentlichen