/*
This file is part of Cyclos (www.cyclos.org).
A project of the Social Trade Organisation (www.socialtrade.org).
Cyclos is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
Cyclos 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with Cyclos; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package nl.strohalm.cyclos.utils.tasks;
import java.io.Serializable;
import java.util.Calendar;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.Callable;
import nl.strohalm.cyclos.scheduling.tasks.ScheduledTask;
import nl.strohalm.cyclos.utils.HazelcastHelper;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.time.DateUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContextAware;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.core.ILock;
/**
* A {@link TaskRunner} which runs tasks correctly in a cluster
*
* @author luis
*/
public class HazelcastTaskRunner extends TaskRunnerImpl implements InitializingBean, ApplicationContextAware {
public static enum KeyType {
INITIALIZATION, SCHEDULED_TASK, POLLING_TASK, DB_INIT
}
public static class LockKey implements Serializable {
private static final long serialVersionUID = 3480718144050023229L;
private final KeyType type;
private final String name;
private LockKey(final KeyType type, final String name) {
this.type = type;
this.name = name;
}
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof LockKey)) {
return false;
}
LockKey key = (LockKey) obj;
return key.type == key.type && name.equals(key.name);
}
@Override
public int hashCode() {
return 13 * type.hashCode() * name.hashCode();
}
@Override
public String toString() {
return type + " " + name;
}
}
private Map<String, String> initializationControl;
private Map<String, Calendar> scheduledTaskControl;
private HazelcastInstance hazelcastInstance;
@Override
public void afterPropertiesSet() throws Exception {
hazelcastInstance = HazelcastHelper.getHazelcastInstance(applicationContext);
initializationControl = hazelcastInstance.getMap("cyclos.initializationControl");
scheduledTaskControl = hazelcastInstance.getMap("cyclos.scheduledTaskControl");
}
@Override
public void runInitializations(final Collection<String> beanNames) {
// As some other node could be running some initialization, make sure that everything is initialized before returning
for (boolean firstTime = true; !allInitializationsExecuted(beanNames); firstTime = false) {
if (!firstTime) {
// When not the first time, sleep a bit, to give other nodes time to finish the initializations
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
}
// Ensure all bean names are in the initialization control
super.runInitializations(beanNames);
}
}
@Override
protected void doHandleDatabaseInitialization(final Runnable runnable) {
LockKey lockKey = new LockKey(KeyType.DB_INIT, StringUtils.EMPTY);
ILock lock = hazelcastInstance.getLock(lockKey);
// Sleep until the lock is acquired
lock.lock();
try {
super.doHandleDatabaseInitialization(runnable);
} finally {
HazelcastHelper.release(lock);
}
}
@Override
protected void doRunInitialization(final String beanName) {
LockKey lockKey = new LockKey(KeyType.INITIALIZATION, beanName);
// Try to get the initialization lock
ILock lock = hazelcastInstance.getLock(lockKey);
if (lock.tryLock()) {
// No one else is trying to run this initialization right now. Check if it was already ran by someone else
if (!initializationControl.containsKey(beanName)) {
try {
// This initialization was never executed. Run it and mark it as executed
super.doRunInitialization(beanName);
initializationControl.put(beanName, beanName);
} finally {
HazelcastHelper.release(lock);
}
}
} else {
if (LOG.isDebugEnabled()) {
LOG.debug("Not running initialization for bean " + beanName + " because some other node is currently running it");
}
}
}
@Override
protected boolean doRunPollingTask(final String key, final Callable<Boolean> task) {
LockKey lockKey = new LockKey(KeyType.POLLING_TASK, key);
ILock lock = hazelcastInstance.getLock(lockKey);
// Ensure multiple nodes can't run a polling task simultaneously
if (lock.tryLock()) {
try {
return super.doRunPollingTask(key, task);
} finally {
HazelcastHelper.release(lock);
}
} else {
// Force a sleep, as couldn't get the lock for this polling task
if (LOG.isDebugEnabled()) {
LOG.debug("Some other cluster node is running the " + key + " polling task. Leaving.");
}
return false;
}
}
@Override
protected void doRunScheduledTask(final String taskName, final Calendar time) {
// Scheduled tasks won't run twice for the same hour in the entire cluster.
LockKey lockKey = new LockKey(KeyType.SCHEDULED_TASK, taskName);
ILock lock = hazelcastInstance.getLock(lockKey);
if (lock.tryLock()) {
// No other node is trying to execute this scheduled task
try {
// Determine whether the task is daily
ScheduledTask scheduledTask = getSchedulingHandler().getTask(taskName);
boolean daily = !scheduledTask.isEveryHour();
int field = daily ? Calendar.DAY_OF_MONTH : Calendar.HOUR_OF_DAY;
// Check the last hour this task was performed
Calendar lastRun = scheduledTaskControl.get(taskName);
if (lastRun != null) {
lastRun = DateUtils.truncate(lastRun, field);
}
Calendar thisRun = DateUtils.truncate(time, field);
// Fill all the gaps between the last run and this run.
// In normal execution, this loop will be evaluated only once.
while (lastRun == null || lastRun.before(thisRun)) {
if (lastRun == null) {
// Never executed: run as this time
lastRun = thisRun;
} else {
// Increment the field (either hour or day)
lastRun.add(field, 1);
}
// Run the task
super.doRunScheduledTask(taskName, lastRun);
// Store the task hour, no other node will run it on this hour again
scheduledTaskControl.put(taskName, lastRun);
}
} finally {
HazelcastHelper.release(lock);
}
}
}
/**
* Returns whether all initializations have been already executed
*/
private boolean allInitializationsExecuted(final Collection<String> beanNames) {
for (String beanName : beanNames) {
if (!initializationControl.containsKey(beanName)) {
return false;
}
}
return true;
}
}