wtorek, 10 kwietnia 2012

Camel, Quartz i zmienne harmonogramy

Zupełnie nie tak dawno miałem okazję przygotowywać integrację kilku usług, która miała być uruchamiana w/g harmonogramu ze zmiennym cyklem. Kluczowym było to, że w procesie przygotowania żądania do usługi konieczne było wyliczenie zakresu na osi czasu, który miałby odpowiadać dokładnej długości interwału. Suma wszystkich zakresów z całej doby miała wynosić 24h. Dodatkową trudność stanowiło wymaganie, które wymuszało zbieranie żądań weekendowych celem ich przetworzenia na początku kolejnego tygodnia (czyli w poniedziałek od godziny 0:00).

Teoretycznie wszystko proste, ale taki Quartz nie pozwala łączyć wyrażeń CRON'a. To znaczy, że jeśli chciałbym wywoływać jakieś zadanie ze zmiennym cyklem, to jestem zmuszony definiować je tyle razy, ile jest różnych cykli. Udało mi się rozwiązać te niedogodność stosując trasy Camel'a. A w zasadzie tyle tras, ile jest cykli, a każda kończąca się w tym samym miejscu, czyli w kolejce żądań do przetworzenia. Jest to o tyle wygodne, że dodanie nowego cyklu wiąże się tylko z dodaniem nowej trasy do RouteBuilder'a bez wnikania w dalsze szczegóły przetwarzania.

Kolejka żądań powinna być trwała, to znaczy opierać się na czymś a'la JMS, bo ewentualna awaria nie powinna usunąć z pamięci nieprzetworzonych żądań (wymaganie ciągłości zakresu).

A jak zatrzymywać przetwarzanie na czas weekendu? Okazuje się, że z pomocą przychodzi jedna z implementacji RoutePolicy, a mianowicie CronScheduledRoutePolicy, która umożliwia zatrzymywanie i uruchamianie wybranej trasy w określonym czasie.

Czas na przykład. Poniżej jest schematyczny RouteBuilder, który dla większej przejrzystości pozbawiony został logiki wywołań usług, a tym samym generacji żądań (na trasie wędruje tylko Exchange wygenerowany z Quartz). Trzy różne Endpoint'y (day|night|weekend) reprezentują trzy odrębne cykle harmonogramu. Wszystkie zbiegają się w queueEndpoint, skąd są przetwarzane ostatnią trasą i trafiają w finalEndpoint.
package example.schedule;

import org.apache.camel.Endpoint;
import org.apache.camel.EndpointInject;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.spi.RoutePolicy;

public class ExampleRouteBuilder extends RouteBuilder {

    @EndpointInject
    protected Endpoint dayCronEndpoint;
    @EndpointInject
    protected Endpoint nightCronEndpoint;
    @EndpointInject
    protected Endpoint weekendCronEndpoint;
    @EndpointInject
    protected Endpoint queueEndpoint;
    @EndpointInject
    protected Endpoint finalEndpoint;

    private RoutePolicy scheduleRoutePolicy;

    public void setScheduleRoutePolicy(RoutePolicy scheduleRoutePolicy) {
        this.scheduleRoutePolicy = scheduleRoutePolicy;
    }

    @Override
    public void configure() throws Exception {
        from(dayCronEndpoint)
                .routeId("day-cron")
                .to(queueEndpoint);

        from(nightCronEndpoint)
                .routeId("night-cron")
                .to(queueEndpoint);

        from(weekendCronEndpoint)
                .routeId("weekend-cron")
                .to(queueEndpoint);

        from(queueEndpoint)
                .routeId("process")
                .routePolicy(scheduleRoutePolicy)
                .to(finalEndpoint);
    }
}
Konfiguracja całości sprowadza się do uzupełnienia definicji Endpoint'ów o poprawne URI i wstrzyknięcia odpowiednio skonfigurowanej RoutePolicy do ExampleRouteBuildera:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:camel="http://camel.apache.org/schema/spring"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://camel.apache.org/schema/spring
       http://camel.apache.org/schema/spring/camel-spring.xsd">

    <camel:camelContext xmlns="http://camel.apache.org/schema/spring" trace="true">
        <routeBuilder ref="exampleRouteBuilder"/>

        <endpoint id="dayCronEndpoint"     uri="quartz://d/?cron=0+*/15+9-16+?+*+MON-FRI"/>
        <endpoint id="nightCronEndpoint"   uri="quartz://n/?cron=0+*/30+17-8+?+*+MON-FRI"/>
        <endpoint id="weekendCronEndpoint" uri="quartz://w/?cron=0+0+*+?+*+SAT-SUN"/>
        <endpoint id="queueEndpoint"       uri="seda:queue"/>
        <endpoint id="finalEndpoint"       uri="log:example?level=INFO"/>
    </camel:camelContext>

    <bean id="exampleRouteBuilder" class="example.schedule.ExampleRouteBuilder">
        <property name="scheduleRoutePolicy">
            <bean class="org.apache.camel.routepolicy.quartz.CronScheduledRoutePolicy">
                <property name="routeStartTime" value="0 0 0 ? * MON"/>
                <property name="routeStopTime"  value="0 0 0 ? * SAT"/>
            </bean>
        </property>
    </bean>

</beans>
Sam przykład jest nieco bezużyteczny, ale z powodzeniem obrazuje jak solidne rozwiązania można tworzyć przy pomocy kilku linii kodu i dobrze dobranych komponentów.