/* (c) 2014 Open Source Geospatial Foundation - all rights reserved
* (c) 2001 - 2013 OpenPlans
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.geoserver.platform.GeoServerExtensions;
import org.geotools.util.logging.Logging;
/**
* The global configuration lock. At the moment it is called by coarse grained request level
* callbacks to lock both the GUI and the REST configuration so that concurrent access does not end
* up causing issues (like corrupt configuration and the like).
* <p>
* The locking code can be disabled by calling
* {@link GeoServerConfigurationLock#setEnabled(boolean)} or by setting the system variable
* {code}-DGeoServerConfigurationLock.enabled=false{code}
*
* @author Andrea Aime - GeoSolution
*
*/
public class GeoServerConfigurationLock {
/** DEFAULT_TRY_LOCK_TIMEOUT_MS */
public static long DEFAULT_TRY_LOCK_TIMEOUT_MS =
(GeoServerExtensions.getProperty("CONFIGURATION_TRYLOCK_TIMEOUT") != null ?
Long.valueOf(GeoServerExtensions.getProperty("CONFIGURATION_TRYLOCK_TIMEOUT")) : 30000);
private static final Level LEVEL = Level.FINE;
private static final Logger LOGGER = Logging.getLogger(GeoServerConfigurationLock.class);
private static final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(true);
private static final ThreadLocal<LockType> currentLock = new ThreadLocal<>();
public static enum LockType {
READ, WRITE
};
private boolean enabled;
public GeoServerConfigurationLock() {
String pvalue = System.getProperty("GeoServerConfigurationLock.enabled");
if (pvalue != null) {
enabled = Boolean.parseBoolean(pvalue);
} else {
enabled = true;
}
LOGGER.info("GeoServer configuration lock is " + (enabled ? "enabled" : "disabled"));
}
/**
* Opens a lock in the specified mode. To avoid deadlocks make sure the corresponding unlock
* method is called as well before the code exits
*
* @param type
*/
public void lock(LockType type) {
if (!enabled) {
return;
}
Lock lock = getLock(type);
lock.lock();
currentLock.set(type);
if (LOGGER.isLoggable(LEVEL)) {
LOGGER.log(LEVEL, "Thread " + Thread.currentThread().getId() + " got the lock in mode "
+ type);
}
}
/**
* Tries to open a lock in the specified mode. Acquires the lock if it is available and returns immediately with the value true.
* If the lock is not available then this method will return immediately with the value false.
*
* A typical usage idiom for this method would be:
*
* <pre>
* Lock lock = ...;
* if (lock.tryLock()) {
* try {
* // manipulate protected state
* } finally {
* lock.unlock();
* }
* } else {
* // perform alternative actions
* }}
* </pre>
*
* This usage ensures that the lock is unlocked if it was acquired, and doesn't try to unlock if the lock was not acquired.
*
* @param type
* @return true if the lock was acquired and false otherwise
*/
public boolean tryLock(LockType type) {
if (!enabled) {
return true;
}
Lock lock = getLock(type);
boolean res = false;
try {
res = lock.tryLock(DEFAULT_TRY_LOCK_TIMEOUT_MS, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
LOGGER.log(Level.WARNING, "Thread " + Thread.currentThread().getId() + " thrown an InterruptedException on GeoServerConfigurationLock TryLock.", e);
res = false;
} finally {
if(res) {
currentLock.set(type);
}
}
if (LOGGER.isLoggable(LEVEL)) {
if (res) {
LOGGER.log(LEVEL, "Thread " + Thread.currentThread().getId() + " got the lock in mode "
+ type);
} else {
LOGGER.log(LEVEL, "Thread " + Thread.currentThread().getId() + " could not get the lock in mode "
+ type);
}
}
return res;
}
/**
* Tries to upgrade the current read lock to a write lock. If the current lock is not a read
* one, it will throw an {@link IllegalStateException}
* @return True if the lock upgrade succeeded, false in case it failed. In case of failure the
* previously owned read lock is also lost.
*/
public void tryUpgradeLock() {
LockType lock = currentLock.get();
if(lock == null) {
throw new IllegalStateException("No lock currently held");
} else if(lock == LockType.WRITE) {
throw new IllegalStateException("Already owning a write lock");
} else {
// core java does not have a notion of lock upgrade, one has to release the
// read lock and get a write one
unlock();
if(tryLock(LockType.WRITE)) {
currentLock.set(LockType.WRITE);
} else {
currentLock.set(null);
throw new RuntimeException("Failed to upgrade lock from read to write "
+ "state, please re-try the configuration operation");
}
}
}
/**
* Unlocks a previously acquired lock. The lock type must match the previous
* {@link #lock(LockType)} call
*
* @param type
*/
public void unlock() {
if (!enabled) {
return;
}
final LockType type = getCurrentLock();
if(type == null) {
return;
}
try {
Lock lock = getLock(type);
if (LOGGER.isLoggable(LEVEL)) {
LOGGER.log(LEVEL, "Thread " + Thread.currentThread().getId()
+ " releasing the lock in mode " + type);
}
lock.unlock();
} finally {
currentLock.set(null);
}
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
/**
* @param type
* @return
*/
private Lock getLock(LockType type) {
Lock lock;
if (type == LockType.WRITE) {
lock = readWriteLock.writeLock();
} else {
lock = readWriteLock.readLock();
}
if (LOGGER.isLoggable(LEVEL)) {
LOGGER.log(LEVEL, "Thread " + Thread.currentThread().getId() + " locking in mode "
+ type);
}
return lock;
}
/**
* Returns the lock type owned by the current thread (could be {@code null} for no lock)
* @return
*/
public LockType getCurrentLock() {
return currentLock.get();
}
}