/*
* (C) Copyright 2007-2010 Nuxeo SA (http://nuxeo.com/) and contributors.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Lesser General Public License
* (LGPL) version 2.1 which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/lgpl.html
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* Contributors:
* Florent Guillaume
*/
package org.nuxeo.ecm.core.scheduler;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.net.URL;
import java.text.ParseException;
import java.util.Map;
import java.util.Properties;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.runtime.RuntimeServiceEvent;
import org.nuxeo.runtime.RuntimeServiceListener;
import org.nuxeo.runtime.model.ComponentContext;
import org.nuxeo.runtime.model.DefaultComponent;
import org.nuxeo.runtime.model.Extension;
import org.nuxeo.runtime.model.RuntimeContext;
import org.quartz.CronTrigger;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.ObjectAlreadyExistsException;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.impl.StdSchedulerFactory;
/**
* Schedule service implementation.
*
* Since the cleanup of the quartz job is done when service is activated, ( see
* see https://jira.nuxeo.com/browse/NXP-7303 ) in cluster mode, the schedules
* contributions MUST be the same on all nodes. Due the fact that all jobs are
* removed when service starts on a node it may be a short period with no
* schedules in quartz table even other node is running.
*
*/
public class SchedulerServiceImpl extends DefaultComponent implements
SchedulerService, RuntimeServiceListener {
private static final Log log = LogFactory.getLog(SchedulerServiceImpl.class);
protected RuntimeContext bundle;
protected Scheduler scheduler;
protected final ScheduleExtensionRegistry registry = new ScheduleExtensionRegistry();
@Override
public void activate(ComponentContext context) throws Exception {
log.debug("Activate");
bundle = context.getRuntimeContext();
}
protected void setupScheduler(ComponentContext context) throws IOException,
SchedulerException {
StdSchedulerFactory schedulerFactory = new StdSchedulerFactory();
URL cfg = context.getRuntimeContext().getResource(
"config/quartz.properties");
if (cfg != null) {
InputStream stream = cfg.openStream();
try {
schedulerFactory.initialize(stream);
} finally {
stream.close();
}
} else {
// use default config (unit tests)
Properties props = new Properties();
props.put("org.quartz.scheduler.instanceName", "Quartz");
props.put("org.quartz.scheduler.threadName", "Quartz_Scheduler");
props.put("org.quartz.scheduler.instanceId", "NON_CLUSTERED");
props.put("org.quartz.scheduler.makeSchedulerThreadDaemon", "true");
props.put("org.quartz.scheduler.skipUpdateCheck", "true");
props.put("org.quartz.threadPool.class",
"org.quartz.simpl.SimpleThreadPool");
props.put("org.quartz.threadPool.threadCount", "1");
props.put("org.quartz.threadPool.threadPriority", "4");
props.put("org.quartz.threadPool.makeThreadsDaemons", "true");
schedulerFactory.initialize(props);
}
scheduler = schedulerFactory.getScheduler();
scheduler.start();
// server = MBeanServerFactory.createMBeanServer();
// server.createMBean("org.quartz.ee.jmx.jboss.QuartzService",
// quartzObjectName);
// clean up all nuxeo jobs
// https://jira.nuxeo.com/browse/NXP-7303
String[] jobs = scheduler.getJobNames("nuxeo");
for (String job : jobs) {
unschedule(job);
}
for (Schedule each:registry.getSchedules()) {
registerSchedule(each);
}
log.info("scheduler started");
}
protected void shutdownScheduler() {
if (scheduler == null) {
return;
}
try {
scheduler.shutdown();
} catch (SchedulerException cause) {
log.error("Cannot shutdown scheduler", cause);
} finally {
scheduler = null;
}
}
@Override
public void deactivate(ComponentContext context) throws Exception {
log.debug("Deactivate");
shutdownScheduler();
}
@Override
public void applicationStarted(ComponentContext context) throws Exception {
setupScheduler(context);
}
@Override
public boolean hasApplicationStarted() {
return scheduler != null;
}
@Override
public void registerExtension(Extension extension) {
Object[] contribs = extension.getContributions();
for (Object contrib : contribs) {
registerSchedule((Schedule)contrib);
}
}
@Override
public void unregisterExtension(Extension extension) {
// do nothing to do ;
// clean up will be done when service is activated
// see https://jira.nuxeo.com/browse/NXP-7303
}
public RuntimeContext getContext() {
return bundle;
}
@Override
public void registerSchedule(Schedule schedule) {
registerSchedule(schedule, null);
}
@Override
public void registerSchedule(Schedule schedule,
Map<String, Serializable> parameters) {
registry.addContribution(schedule);
if (scheduler == null) {
return;
}
Schedule contributed = registry.getSchedule(schedule);
if (contributed != null) {
schedule(contributed, parameters);
} else {
unschedule(schedule.getId());
}
}
protected void schedule(Schedule schedule,
Map<String, Serializable> parameters) {
log.info("Registering " + schedule);
JobDetail job = new JobDetail(schedule.getId(), "nuxeo", EventJob.class);
JobDataMap map = job.getJobDataMap();
map.put("eventId", schedule.getEventId());
map.put("eventCategory", schedule.getEventCategory());
map.put("username", schedule.getUsername());
if (parameters != null) {
map.putAll(parameters);
}
Trigger trigger;
try {
trigger = new CronTrigger(schedule.getId(), "nuxeo",
schedule.getCronExpression());
} catch (ParseException e) {
log.error(String.format(
"invalid cron expresion '%s' for schedule '%s'",
schedule.getCronExpression(), schedule.getId()), e);
return;
}
// This is useful when testing to avoid multiple threads:
// trigger = new SimpleTrigger(schedule.getId(), "nuxeo");
try {
scheduler.scheduleJob(job, trigger);
} catch (ObjectAlreadyExistsException e) {
log.trace("Overriding scheduler with id: " + schedule.getId());
// when jobs are persisted in a database, the job should already
// be there
// remove existing job and re-schedule
boolean unregistred = unregisterSchedule(schedule.getId());
if (unregistred) {
try {
scheduler.scheduleJob(job, trigger);
} catch (SchedulerException e1) {
log.error(String.format(
"failed to schedule job with id '%s': %s",
schedule.getId(), e.getMessage()), e);
}
}
} catch (SchedulerException e) {
log.error(String.format("failed to schedule job with id '%s': %s",
schedule.getId(), e.getMessage()), e);
}
}
@Override
public boolean unregisterSchedule(String id) {
log.info("Unregistering schedule with id" + id);
Schedule schedule = registry.getSchedule(id);
if (schedule == null) {
return false;
}
registry.removeContribution(schedule, true);
return unschedule(id);
}
protected boolean unschedule(String jobId) {
try {
return scheduler.deleteJob(jobId, "nuxeo");
} catch (SchedulerException e) {
log.error(String.format("failed to unschedule job with '%s': %s",
jobId, e.getMessage()), e);
}
return false;
}
@Override
public boolean unregisterSchedule(Schedule schedule) {
return unregisterSchedule(schedule.getId());
}
@Override
public void handleEvent(RuntimeServiceEvent event) {
if (!event.getEventName().equals(RuntimeServiceEvent.RUNTIME_ABOUT_TO_STOP)) {
return;
}
shutdownScheduler();
}
}