/**
* Licensed to JumpMind Inc under one or more contributor
* license agreements. See the NOTICE file distributed
* with this work for additional information regarding
* copyright ownership. JumpMind Inc licenses this file
* to you under the GNU General Public License, version 3.0 (GPLv3)
* (the "License"); you may not use this file except in compliance
* with the License.
*
* You should have received a copy of the GNU General Public License,
* version 3.0 (GPLv3) along with this library; if not, see
* <http://www.gnu.org/licenses/>.
*
* 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.jumpmind.symmetric.service.impl;
import static org.jumpmind.symmetric.service.ClusterConstants.FILE_SYNC_PULL;
import static org.jumpmind.symmetric.service.ClusterConstants.FILE_SYNC_PUSH;
import static org.jumpmind.symmetric.service.ClusterConstants.FILE_SYNC_SHARED;
import static org.jumpmind.symmetric.service.ClusterConstants.FILE_SYNC_TRACKER;
import static org.jumpmind.symmetric.service.ClusterConstants.HEARTBEAT;
import static org.jumpmind.symmetric.service.ClusterConstants.INITIAL_LOAD_EXTRACT;
import static org.jumpmind.symmetric.service.ClusterConstants.PULL;
import static org.jumpmind.symmetric.service.ClusterConstants.PURGE_DATA_GAPS;
import static org.jumpmind.symmetric.service.ClusterConstants.PURGE_INCOMING;
import static org.jumpmind.symmetric.service.ClusterConstants.PURGE_OUTGOING;
import static org.jumpmind.symmetric.service.ClusterConstants.PURGE_STATISTICS;
import static org.jumpmind.symmetric.service.ClusterConstants.PUSH;
import static org.jumpmind.symmetric.service.ClusterConstants.ROUTE;
import static org.jumpmind.symmetric.service.ClusterConstants.STAGE_MANAGEMENT;
import static org.jumpmind.symmetric.service.ClusterConstants.STATISTICS;
import static org.jumpmind.symmetric.service.ClusterConstants.SYNCTRIGGERS;
import static org.jumpmind.symmetric.service.ClusterConstants.TYPE_CLUSTER;
import static org.jumpmind.symmetric.service.ClusterConstants.TYPE_EXCLUSIVE;
import static org.jumpmind.symmetric.service.ClusterConstants.TYPE_SHARED;
import static org.jumpmind.symmetric.service.ClusterConstants.WATCHDOG;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.time.DateUtils;
import org.jumpmind.db.sql.ConcurrencySqlException;
import org.jumpmind.db.sql.ISqlRowMapper;
import org.jumpmind.db.sql.Row;
import org.jumpmind.db.sql.UniqueKeyException;
import org.jumpmind.symmetric.common.ParameterConstants;
import org.jumpmind.symmetric.common.SystemConstants;
import org.jumpmind.symmetric.db.ISymmetricDialect;
import org.jumpmind.symmetric.model.Lock;
import org.jumpmind.symmetric.service.IClusterService;
import org.jumpmind.symmetric.service.IParameterService;
import org.jumpmind.util.AppUtils;
/**
* @see IClusterService
*/
public class ClusterService extends AbstractService implements IClusterService {
private String serverId = null;
private Map<String, Map<String, Set<String>>> initializedLockActionsByChannelForNode = new HashMap<String, Map<String,Set<String>>>();
public ClusterService(IParameterService parameterService, ISymmetricDialect dialect) {
super(parameterService, dialect);
setSqlMap(new ClusterServiceSqlMap(symmetricDialect.getPlatform(),
createSqlReplacementTokens()));
}
public void init() {
clearAllLocks();
Map<String, Lock> allLocks = findLocks();
for (String action : new String[] { ROUTE, PULL, PUSH, HEARTBEAT, PURGE_INCOMING, PURGE_OUTGOING, PURGE_STATISTICS, SYNCTRIGGERS,
PURGE_DATA_GAPS, STAGE_MANAGEMENT, WATCHDOG, STATISTICS, FILE_SYNC_PULL, FILE_SYNC_PUSH, FILE_SYNC_TRACKER,
INITIAL_LOAD_EXTRACT }) {
if (allLocks.get(action) == null) {
initLockTable(action, TYPE_CLUSTER);
}
}
for (String action : new String[] { FILE_SYNC_SHARED }) {
if (allLocks.get(action) == null) {
initLockTable(action, TYPE_SHARED);
}
}
}
public void initLockTable(final String action) {
initLockTable(action, TYPE_CLUSTER);
}
public void initLockTable(final String action, final String lockType) {
try {
sqlTemplate.update(getSql("insertLockSql"), new Object[] { action, lockType });
log.debug("Inserted into the LOCK table for {}, {}", action, lockType);
} catch (UniqueKeyException ex) {
log.debug("Failed to insert to the LOCK table for {}, {}. Must be initialized already.",
action, lockType);
}
}
public void clearAllLocks() {
sqlTemplate.update(getSql("initLockSql"), new Object[] { getServerId() });
sqlTemplate.update(getSql("initNodeChannelLockSql"), new Object[] { getServerId() });
}
private void initNodeChannelActionLockInitialized(String action, String nodeId, String channelId) {
Map<String, Set<String>> nodesByChannel = initializedLockActionsByChannelForNode.get(action);
if (nodesByChannel == null) {
nodesByChannel = new HashMap<String, Set<String>>();
initializedLockActionsByChannelForNode.put(action, nodesByChannel);
}
Set<String> nodeIds = nodesByChannel.get(channelId);
if (nodeIds == null) {
nodeIds = new HashSet<String>();
nodesByChannel.put(channelId, nodeIds);
}
if (!nodeIds.contains(nodeId)) {
nodeIds.add(nodeId);
try {
sqlTemplate.update(getSql("insertNodeChannelLockSql"), action, nodeId, channelId);
} catch (UniqueKeyException ex) {
log.debug(
"Failed to insert to the node channel lock table for action: {}, node: {}, and channel: {}. Must be initialized already.",
action, nodeId, channelId);
}
}
}
public boolean lockNodeChannel(String action, String nodeId, String channelId) {
initNodeChannelActionLockInitialized(action, nodeId, channelId);
final Date timeToBreakLock = DateUtils.addMilliseconds(new Date(),
(int) -parameterService.getLong(ParameterConstants.CLUSTER_LOCK_TIMEOUT_MS));
return 1 == sqlTemplate.update(getSql("acquireNodeChannelClusterLockSql"), serverId,
new Date(), action, nodeId, channelId, timeToBreakLock, serverId);
}
public void unlockNodeChannel(String action, String nodeId, String channelId) {
String lastLockingServerId = serverId.equals(Lock.STOPPED) ? null : serverId;
if (sqlTemplate.update(getSql("releaseNodeChannelClusterLockSql"), new Object[] { new Date(), lastLockingServerId, action, serverId,
nodeId, channelId }) == 0) {
log.warn("Failed to release lock for action:{} server:{} nodeId:{} channelId:{}", new Object[] {action, getServerId(), nodeId, channelId});
}
}
public boolean lock(String action, final String lockType) {
if (lockType.equals(TYPE_CLUSTER)) {
return lock(action);
} else if (lockType.equals(TYPE_SHARED)) {
return lockShared(action);
} else if (lockType.equals(TYPE_EXCLUSIVE)) {
return lockExclusive(action);
} else {
throw new UnsupportedOperationException("Lock type of " + lockType + " is not supported");
}
}
public boolean lock(final String action, final String lockType, long waitMillis) {
if (lockType.equals(TYPE_SHARED) || lockType.equals(TYPE_EXCLUSIVE)) {
return lockWait(action, lockType, waitMillis);
} else {
throw new UnsupportedOperationException("Lock type of " + lockType + " is not supported");
}
}
public boolean lock(final String action) {
final Date timeout = DateUtils.addMilliseconds(new Date(),
(int) -parameterService.getLong(ParameterConstants.CLUSTER_LOCK_TIMEOUT_MS));
return lockCluster(action, timeout, new Date(), getServerId());
}
protected boolean lockCluster(String action, Date timeToBreakLock, Date timeLockAcquired,
String serverId) {
try {
return sqlTemplate.update(getSql("acquireClusterLockSql"), new Object[] { serverId,
timeLockAcquired, action, TYPE_CLUSTER, timeToBreakLock, serverId }) == 1;
} catch (ConcurrencySqlException ex) {
log.debug("Ignoring concurrency error and reporting that we failed to get the cluster lock: {}", ex.getMessage());
return false;
}
}
protected boolean lockShared(final String action) {
final Date timeout = DateUtils.addMilliseconds(new Date(),
(int) -parameterService.getLong(ParameterConstants.LOCK_TIMEOUT_MS));
return sqlTemplate.update(getSql("acquireSharedLockSql"), new Object[] {
TYPE_SHARED, getServerId(), new Date(), action, TYPE_SHARED, timeout }) == 1;
}
protected boolean lockExclusive(final String action) {
final Date timeout = DateUtils.addMilliseconds(new Date(),
(int) -parameterService.getLong(ParameterConstants.LOCK_TIMEOUT_MS));
return sqlTemplate.update(getSql("acquireExclusiveLockSql"), new Object[] {
TYPE_EXCLUSIVE, getServerId(), new Date(), action, TYPE_SHARED, timeout }) == 1;
}
protected boolean lockWait(final String action, final String lockType, long waitMillis) {
boolean isLocked = false;
long endTime = System.currentTimeMillis() + waitMillis;
long sleepMillis = parameterService.getLong(ParameterConstants.LOCK_WAIT_RETRY_MILLIS);
do {
if (lockType.equals(TYPE_SHARED)) {
isLocked = lockShared(action);
} else if (lockType.equals(TYPE_EXCLUSIVE)) {
isLocked = lockExclusive(action);
if (!isLocked) {
sqlTemplate.update(getSql("disableSharedLockSql"), new Object[] { action, TYPE_SHARED });
}
}
if (isLocked) {
break;
}
AppUtils.sleep(sleepMillis);
} while (waitMillis == 0 || System.currentTimeMillis() < endTime);
return isLocked;
}
public Map<String, Lock> findLocks() {
final Map<String, Lock> locks = new HashMap<String, Lock>();
sqlTemplate.query(getSql("findLocksSql"), new ISqlRowMapper<Lock>() {
public Lock mapRow(Row rs) {
Lock lock = new Lock();
lock.setLockAction(rs.getString("lock_action"));
lock.setLockType(rs.getString("lock_type"));
lock.setLockingServerId(rs.getString("locking_server_id"));
lock.setLockTime(rs.getDateTime("lock_time"));
lock.setSharedCount(rs.getInt("shared_count"));
lock.setSharedEnable(rs.getBoolean("shared_enable"));
lock.setLastLockingServerId(rs.getString("last_locking_server_id"));
lock.setLastLockTime(rs.getDateTime("last_lock_time"));
locks.put(lock.getLockAction(), lock);
return lock;
}
});
return locks;
}
/**
* Get a unique identifier that represents the JVM instance this server is
* currently running in.
*/
public String getServerId() {
if (StringUtils.isBlank(serverId)) {
serverId = parameterService.getString(ParameterConstants.CLUSTER_SERVER_ID);
if (StringUtils.isBlank(serverId)) {
serverId = System.getProperty(SystemConstants.SYSPROP_CLUSTER_SERVER_ID, null);
}
if (StringUtils.isBlank(serverId)) {
// JBoss uses this system property to identify a server in a
// cluster
serverId = System.getProperty("bind.address", null);
}
if (StringUtils.isBlank(serverId)) {
// JBoss uses this system property to identify a server in a
// cluster
serverId = System.getProperty("jboss.bind.address", null);
}
if (StringUtils.isBlank(serverId)) {
try {
serverId = AppUtils.getHostName();
} catch (Exception ex) {
serverId = "unknown";
}
}
log.info("This node picked a server id of {}", serverId);
}
return serverId;
}
public void unlock(final String action, final String lockType) {
if (lockType.equals(TYPE_CLUSTER)) {
unlock(action);
} else if (lockType.equals(TYPE_SHARED)) {
unlockShared(action);
} else if (lockType.equals(TYPE_EXCLUSIVE)) {
unlockExclusive(action);
} else {
throw new UnsupportedOperationException("Lock type of " + lockType + " is not supported");
}
}
public void unlock(final String action) {
if (!unlockCluster(action, getServerId())) {
log.warn("Failed to release lock for action:{} server:{}", action, getServerId());
}
}
protected boolean unlockCluster(String action, String serverId) {
String lastLockingServerId = serverId.equals(Lock.STOPPED) ? null : serverId;
return sqlTemplate.update(getSql("releaseClusterLockSql"), new Object[] { new Date(), lastLockingServerId, action,
TYPE_CLUSTER, serverId }) > 0;
}
protected boolean unlockShared(final String action) {
return sqlTemplate.update(getSql("releaseSharedLockSql"), new Object[] {
new Date(), getServerId(), action, TYPE_SHARED }) == 1;
}
protected boolean unlockExclusive(final String action) {
return sqlTemplate.update(getSql("releaseExclusiveLockSql"), new Object[] {
new Date(), getServerId(), action, TYPE_EXCLUSIVE }) == 1;
}
public boolean isInfiniteLocked(String action) {
Map<String, Lock> locks = findLocks();
Lock lock = locks.get(action);
if (lock != null && lock.getLockTime() != null && new Date().before(lock.getLockTime())
&& Lock.STOPPED.equals(lock.getLockingServerId())) {
return true;
} else {
return false;
}
}
public void aquireInfiniteLock(String action) {
int tries = 600;
Date futureTime = DateUtils.addYears(new Date(), 100);
while (tries > 0) {
if (!lockCluster(action, new Date(), futureTime, Lock.STOPPED)) {
AppUtils.sleep(50);
tries--;
} else {
tries = 0;
}
}
}
public void clearInfiniteLock(String action) {
Map<String, Lock> all = findLocks();
Lock lock = all.get(action);
if (lock != null && Lock.STOPPED.equals(lock.getLockingServerId())) {
unlockCluster(action, Lock.STOPPED);
}
}
}