/*
* (C) Copyright 2007-2015 Nuxeo SA (http://nuxeo.com/) and others.
*
* Licensed 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.
*
* Contributors:
* Florent Guillaume
* Thierry Martins
*/
package org.nuxeo.ecm.core.scheduler;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.net.URL;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.ecm.core.api.NuxeoException;
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.CronScheduleBuilder;
import org.quartz.JobBuilder;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.JobKey;
import org.quartz.ObjectAlreadyExistsException;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.impl.StdSchedulerFactory;
import org.quartz.impl.matchers.GroupMatcher;
/**
* 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 {
private static final Log log = LogFactory.getLog(SchedulerServiceImpl.class);
protected RuntimeContext context;
protected Scheduler scheduler;
protected final ScheduleExtensionRegistry registry = new ScheduleExtensionRegistry();
/**
* @since 7.10
*/
private Map<String, JobKey> jobKeys = new HashMap<String, JobKey>();
@Override
public void activate(ComponentContext context) {
log.debug("Activate");
this.context = context.getRuntimeContext();
}
protected void setupScheduler() throws IOException, SchedulerException {
StdSchedulerFactory schedulerFactory = new StdSchedulerFactory();
URL cfg = context.getResource("config/quartz.properties");
if (cfg != null) {
try (InputStream stream = cfg.openStream()) {
schedulerFactory.initialize(stream);
}
} 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
GroupMatcher<JobKey> matcher = GroupMatcher.jobGroupEquals("nuxeo");
Set<JobKey> jobs = scheduler.getJobKeys(matcher);
scheduler.deleteJobs(new ArrayList<JobKey>(jobs));
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) {
log.debug("Deactivate");
shutdownScheduler();
}
@Override
public void applicationStarted(ComponentContext context) {
try {
setupScheduler();
} catch (IOException | SchedulerException e) {
throw new NuxeoException(e);
}
}
@Override
public void applicationStopped(ComponentContext context, Instant deadline) {
try {
scheduler.standby();
} catch (SchedulerException cause) {
log.error("Cannot put scheduler in stand by mode", cause);
}
}
@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 context;
}
@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);
JobDataMap map = new JobDataMap();
if (parameters != null) {
map.putAll(parameters);
}
JobDetail job = JobBuilder.newJob(EventJob.class)
.withIdentity(schedule.getId(), "nuxeo")
.usingJobData(map)
.usingJobData("eventId", schedule.getEventId())
.usingJobData("eventCategory", schedule.getEventCategory())
.usingJobData("username", schedule.getUsername())
.build();
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity(schedule.getId(), "nuxeo")
.withSchedule(CronScheduleBuilder.cronSchedule(schedule.getCronExpression()))
.build();
// This is useful when testing to avoid multiple threads:
// trigger = new SimpleTrigger(schedule.getId(), "nuxeo");
try {
scheduler.scheduleJob(job, trigger);
jobKeys.put(schedule.getId(), job.getKey());
} 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(jobKeys.get(jobId));
} 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());
}
}