/*
* Copyright 2008-2009 LinkedIn, Inc
*
* 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.
*/
package voldemort.common.service;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import javax.management.MBeanOperationInfo;
import org.apache.log4j.Logger;
import voldemort.VoldemortException;
import voldemort.annotations.jmx.JmxGetter;
import voldemort.annotations.jmx.JmxManaged;
import voldemort.annotations.jmx.JmxOperation;
import voldemort.utils.Time;
import com.google.common.collect.Lists;
/**
* The voldemort scheduler
*
*
*/
@SuppressWarnings("unchecked")
@JmxManaged(description = "A service that runs scheduled jobs.")
public class SchedulerService extends AbstractService {
private static final Logger logger = Logger.getLogger(SchedulerService.class);
private boolean mayInterrupt;
private static final String THREAD_NAME_PREFIX = "voldemort-scheduler-service";
private static final AtomicInteger schedulerServiceCount = new AtomicInteger(0);
private final String schedulerName = THREAD_NAME_PREFIX + schedulerServiceCount.incrementAndGet();
private class ScheduledRunnable {
private Runnable runnable;
private Date delayDate;
private long intervalMs;
ScheduledRunnable(Runnable runnable, Date delayDate, long intervalMs) {
this.runnable = runnable;
this.delayDate = delayDate;
this.intervalMs = intervalMs;
}
ScheduledRunnable(Runnable runnable, Date delayDate) {
this(runnable, delayDate, 0);
}
Runnable getRunnable() {
return this.runnable;
}
Date getDelayDate() {
return this.delayDate;
}
long getIntervalMs() {
return this.intervalMs;
}
}
private final ScheduledThreadPoolExecutor scheduler;
private final Time time;
private final Map<String, ScheduledFuture> scheduledJobResults;
private final Map<String, ScheduledRunnable> allJobs;
public SchedulerService(int schedulerThreads, Time time) {
this(schedulerThreads, time, true);
}
public SchedulerService(int schedulerThreads, Time time, boolean mayInterrupt) {
super(ServiceType.SCHEDULER);
this.time = time;
this.scheduler = new SchedulerThreadPool(schedulerThreads, schedulerName);
this.scheduledJobResults = new ConcurrentHashMap<String, ScheduledFuture>();
this.allJobs = new ConcurrentHashMap<String, ScheduledRunnable>();
this.mayInterrupt = mayInterrupt;
}
@Override
public void startInner() {
// TODO note that most code does not do this. so scheduler.isStarted()
// returns false even after you have submitted some tasks and they are
// running fine.
}
@Override
public void stopInner() {
this.scheduler.shutdownNow();
try {
this.scheduler.awaitTermination(5, TimeUnit.SECONDS);
} catch(Exception e) {
logger.info("Error waiting for termination of scheduler service", e);
}
}
@JmxOperation(description = "Disable a particular scheduled job", impact = MBeanOperationInfo.ACTION)
public void disable(String id) {
if(allJobs.containsKey(id) && scheduledJobResults.containsKey(id)) {
ScheduledFuture<?> future = scheduledJobResults.get(id);
boolean cancelled = future.cancel(false);
if(cancelled == true) {
logger.info("Removed '" + id + "' from list of scheduled jobs");
scheduledJobResults.remove(id);
}
}
}
@JmxOperation(description = "Terminate a particular scheduled job", impact = MBeanOperationInfo.ACTION)
public void terminate(String id) {
if(allJobs.containsKey(id) && scheduledJobResults.containsKey(id)) {
ScheduledFuture<?> future = scheduledJobResults.get(id);
boolean cancelled = future.cancel(this.mayInterrupt);
if(cancelled == true) {
logger.info("Removed '" + id + "' from list of scheduled jobs");
scheduledJobResults.remove(id);
}
}
}
@JmxOperation(description = "Enable a particular scheduled job", impact = MBeanOperationInfo.ACTION)
public void enable(String id) {
if(allJobs.containsKey(id) && !scheduledJobResults.containsKey(id)) {
ScheduledRunnable scheduledRunnable = allJobs.get(id);
logger.info("Adding '" + id + "' to list of scheduled jobs");
if(scheduledRunnable.getIntervalMs() > 0) {
schedule(id,
scheduledRunnable.getRunnable(),
scheduledRunnable.getDelayDate(),
scheduledRunnable.getIntervalMs());
} else {
schedule(id, scheduledRunnable.getRunnable(), scheduledRunnable.getDelayDate());
}
}
}
@JmxGetter(name = "getScheduledJobs", description = "Returns names of jobs in the scheduler")
public List<String> getScheduledJobs() {
return Lists.newArrayList(scheduledJobResults.keySet());
}
public List<String> getAllJobs() {
return Lists.newArrayList(allJobs.keySet());
}
public boolean getJobEnabled(String id) {
if(allJobs.containsKey(id)) {
return scheduledJobResults.containsKey(id);
} else {
throw new VoldemortException("Job id "+id + " does not exist.");
}
}
public void scheduleNow(Runnable runnable) {
scheduler.execute(runnable);
}
public void schedule(String id, Runnable runnable, Date timeToRun) {
ScheduledFuture<?> future = scheduler.schedule(runnable,
delayMs(timeToRun),
TimeUnit.MILLISECONDS);
if(!allJobs.containsKey(id)) {
allJobs.put(id, new ScheduledRunnable(runnable, timeToRun));
}
scheduledJobResults.put(id, future);
}
public void schedule(String id, Runnable runnable, Date nextRun, long periodMs) {
schedule(id, runnable, nextRun, periodMs, false);
}
public void schedule(String id,
Runnable runnable,
Date nextRun,
long periodMs,
boolean scheduleAtFixedRate) {
ScheduledFuture<?> future = null;
if(scheduleAtFixedRate)
future = scheduler.scheduleAtFixedRate(runnable,
delayMs(nextRun),
periodMs,
TimeUnit.MILLISECONDS);
else
future = scheduler.scheduleWithFixedDelay(runnable,
delayMs(nextRun),
periodMs,
TimeUnit.MILLISECONDS);
if(!allJobs.containsKey(id)) {
allJobs.put(id, new ScheduledRunnable(runnable, nextRun, periodMs));
}
scheduledJobResults.put(id, future);
}
private long delayMs(Date runDate) {
return Math.max(0, runDate.getTime() - time.getMilliseconds());
}
/**
* A scheduled thread pool that fixes some default behaviors
*/
private static class SchedulerThreadPool extends ScheduledThreadPoolExecutor {
public SchedulerThreadPool(int numThreads, final String schedulerName) {
super(numThreads, new ThreadFactory() {
private AtomicInteger threadCount = new AtomicInteger(0);
/**
* This function is overridden in order to activate the daemon mode as well as
* to give a human readable name to threads used by the {@link SchedulerService}.
*
* Previously, this function would set the thread's name to the value of the passed-in
* {@link Runnable}'s class name, but this is useless since it always ends up being a
* java.util.concurrent.ThreadPoolExecutor$Worker
*
* Instead, a generic name is now used, and the thread's name can be set more
* precisely during {@link voldemort.server.protocol.admin.AsyncOperation#run()}.
*
* @param r {@link Runnable} to execute
* @return a new {@link Thread} appropriate for use within the {@link SchedulerService}.
*/
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setDaemon(true);
thread.setName(schedulerName + "-t" + threadCount.incrementAndGet());
return thread;
}
});
}
}
@JmxGetter(name = "numActiveTasks", description = "Returns number of tasks executing currently")
public long getActiveTasksCount() {
return this.scheduler.getActiveCount();
}
@JmxGetter(name = "numQueuedTasks",
description = "Returns number of tasks queued for execution")
public long getQueuedTasksCount() {
return this.scheduler.getQueue().size();
}
}