/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.falcon.notification.service.impl; import org.apache.falcon.FalconException; import org.apache.falcon.entity.EntityUtil; import org.apache.falcon.entity.v0.Frequency; import org.apache.falcon.exception.NotificationServiceException; import org.apache.falcon.execution.NotificationHandler; import org.apache.falcon.execution.SchedulerUtil; import org.apache.falcon.notification.service.FalconNotificationService; import org.apache.falcon.notification.service.event.TimeElapsedEvent; import org.apache.falcon.notification.service.request.NotificationRequest; import org.apache.falcon.notification.service.request.AlarmRequest; import org.apache.falcon.state.ID; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import org.quartz.CalendarIntervalTrigger; import org.quartz.DateBuilder; import org.quartz.Job; import org.quartz.JobDataMap; import org.quartz.JobDetail; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.quartz.JobKey; import org.quartz.Scheduler; import org.quartz.SchedulerException; import org.quartz.TriggerKey; import org.quartz.impl.StdSchedulerFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.TimeZone; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import static org.quartz.CalendarIntervalScheduleBuilder.calendarIntervalSchedule; import static org.quartz.JobBuilder.newJob; import static org.quartz.TriggerBuilder.newTrigger; /** * This notification service notifies {@link NotificationHandler} when requested time * event has occurred. The class users to subscribe to frequency based, cron based or some calendar based time events. */ public class AlarmService implements FalconNotificationService { private static final Logger LOG = LoggerFactory.getLogger(AlarmService.class); private Map<ID, TriggerKey> notifications = new HashMap<ID, TriggerKey>(); private static ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(10); private Scheduler scheduler; @Override public void init() throws FalconException { try { scheduler = StdSchedulerFactory.getDefaultScheduler(); scheduler.start(); } catch (SchedulerException e) { throw new FalconException(e); } } @Override public void register(NotificationRequest notificationRequest) throws NotificationServiceException { LOG.info("Registering alarm notification for " + notificationRequest.getCallbackId()); AlarmRequest request = (AlarmRequest) notificationRequest; DateTime currentTime = DateTime.now(); DateTime nextStartTime = request.getStartTime(); DateTime endTime; if (request.getEndTime().isBefore(currentTime)) { endTime = request.getEndTime().minusMinutes(1); } else { endTime = currentTime; } // Handle past events. // TODO : Quartz doesn't seem to support running jobs for past events. // TODO : Remove the handling of past events when that support is added. if (request.getStartTime().isBefore(currentTime)) { List<Date> instanceTimes = EntityUtil.getInstanceTimes(request.getStartTime().toDate(), request.getFrequency(), request.getTimeZone(), request.getStartTime().toDate(), endTime.toDate()); if (instanceTimes != null && !instanceTimes.isEmpty()) { Date lastInstanceTime = instanceTimes.get(instanceTimes.size() - 1); nextStartTime = new DateTime(lastInstanceTime.getTime() + SchedulerUtil.getFrequencyInMillis(new DateTime(lastInstanceTime), request.getFrequency())); // Introduce some delay to allow for rest of the registration to complete. LOG.debug("Triggering events for past from {} till {}", instanceTimes.get(0), lastInstanceTime); executor.schedule(new CatchupJob(request, instanceTimes), 1, TimeUnit.SECONDS); } } // All past events have been scheduled. Nothing to schedule in the future. if (endTime.isBefore(nextStartTime)) { return; } LOG.debug("Scheduling to trigger events from {} to {} with frequency {}", nextStartTime, request.getEndTime(), request.getFrequency()); // Schedule future events using Quartz CalendarIntervalTrigger trigger = newTrigger() .withIdentity(notificationRequest.getCallbackId().toString(), "Falcon") .startAt(nextStartTime.toDate()) .endAt(request.getEndTime().toDate()) .withSchedule( calendarIntervalSchedule() .withInterval(request.getFrequency().getFrequencyAsInt(), getTimeUnit(request.getFrequency().getTimeUnit())) .withMisfireHandlingInstructionFireAndProceed()) .build(); // define the job and tie it to our Job class JobDetail job = newJob(FalconProcessJob.class) .withIdentity(getJobKey(notificationRequest.getCallbackId().toString())) .setJobData(getJobDataMap((AlarmRequest) notificationRequest)) .build(); notifications.put(notificationRequest.getCallbackId(), trigger.getKey()); // Tell quartz to run the job using our trigger try { scheduler.scheduleJob(job, trigger); } catch (SchedulerException e) { LOG.error("Error scheduling entity {}", trigger.getKey()); throw new NotificationServiceException(e); } } // Maps the timeunit in entity specification to the one in Quartz DateBuilder private DateBuilder.IntervalUnit getTimeUnit(Frequency.TimeUnit timeUnit) { switch (timeUnit) { case minutes: return DateBuilder.IntervalUnit.MINUTE; case hours: return DateBuilder.IntervalUnit.HOUR; case days: return DateBuilder.IntervalUnit.DAY; case months: return DateBuilder.IntervalUnit.MONTH; default: throw new IllegalArgumentException("Invalid time unit " + timeUnit.name()); } } private JobKey getJobKey(String key) { return new JobKey(key, "Falcon"); } private JobDataMap getJobDataMap(AlarmRequest request) { JobDataMap jobProps = new JobDataMap(); jobProps.put("request", request); return jobProps; } @Override public void unregister(NotificationHandler handler, ID listenerID) throws NotificationServiceException { try { LOG.info("Removing time notification for handler {} with callbackID {}", handler, listenerID); scheduler.unscheduleJob(notifications.get(listenerID)); notifications.remove(listenerID); } catch (SchedulerException e) { throw new NotificationServiceException("Unable to deregister " + listenerID, e); } } @Override public RequestBuilder createRequestBuilder(NotificationHandler handler, ID callbackID) { return new AlarmRequestBuilder(handler, callbackID); } @Override public String getName() { return "AlarmService"; } @Override public void destroy() throws FalconException { try { scheduler.shutdown(); } catch (SchedulerException e) { LOG.warn("Quartz Scheduler shutdown failed.", e); } } // Generates a time elapsed event and invokes onEvent on the handler. private static void notifyHandler(AlarmRequest request, DateTime instanceTime) throws NotificationServiceException { TimeElapsedEvent event = new TimeElapsedEvent(request.getCallbackId(), request.getStartTime(), request.getEndTime(), instanceTime); try { LOG.info("Sending notification to {} with nominal time {} ", request.getCallbackId(), event.getInstanceTime()); request.getHandler().onEvent(event); } catch (FalconException e) { LOG.error("Unable to onEvent " + request.getCallbackId() + " for nominal time, " + instanceTime, e); throw new NotificationServiceException(e); } } /** * The Job that runs when a time trigger happens. */ public static class FalconProcessJob implements Job { public FalconProcessJob() { } @Override public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { LOG.debug("Quartz job called at : {}, Next fire time: {}", jobExecutionContext.getFireTime(), jobExecutionContext.getNextFireTime()); AlarmRequest request = (AlarmRequest) jobExecutionContext.getJobDetail() .getJobDataMap().get("request"); DateTime instanceTime = new DateTime(jobExecutionContext.getScheduledFireTime(), DateTimeZone.forTimeZone(request.getTimeZone())); try { notifyHandler(request, instanceTime); } catch (NotificationServiceException e) { throw new JobExecutionException(e); } } } // Quartz doesn't seem to be able to schedule past events. This job specifically handles that. private static class CatchupJob implements Runnable { private final AlarmRequest request; private final List<Date> instanceTimes; public CatchupJob(AlarmRequest request, List<Date> triggerTimes) { this.request = request; this.instanceTimes = triggerTimes; } @Override public void run() { if (instanceTimes == null) { return; } // Immediate notification for all past events. for(Date instanceTime : instanceTimes) { DateTime nominalDateTime = new DateTime(instanceTime, DateTimeZone.forTimeZone(request.getTimeZone())); try { notifyHandler(request, nominalDateTime); } catch (NotificationServiceException e) { throw new RuntimeException(e); } } } } /** * Builder that builds {@link AlarmRequest}. */ public static class AlarmRequestBuilder extends RequestBuilder<AlarmRequest> { private DateTime startTime; private DateTime endTime; private Frequency frequency; private TimeZone timeZone; public AlarmRequestBuilder(NotificationHandler handler, ID callbackID) { super(handler, callbackID); } /** * @param start of the timer * @return This instance */ public AlarmRequestBuilder setStartTime(DateTime start) { this.startTime = start; return this; } /** * @param end of the timer * @return This instance */ public AlarmRequestBuilder setEndTime(DateTime end) { this.endTime = end; return this; } /** * @param freq of the timer * @return This instance */ public AlarmRequestBuilder setFrequency(Frequency freq) { this.frequency = freq; return this; } /** * @param timeZone */ public void setTimeZone(TimeZone timeZone) { this.timeZone = timeZone; } @Override public AlarmRequest build() { if (callbackId == null || startTime == null || endTime == null || frequency == null) { throw new IllegalArgumentException("Missing one or more of the mandatory arguments:" + " callbackId, startTime, endTime, frequency"); } return new AlarmRequest(handler, callbackId, startTime, endTime, frequency, timeZone); } } }