/*
* Copyright 2007-2010 Sun Microsystems, Inc.
*
* This file is part of Project Darkstar Server.
*
* Project Darkstar Server is free software: you can redistribute it
* and/or modify it under the terms of the GNU General Public License
* version 2 as published by the Free Software Foundation and
* distributed hereunder to you.
*
* Project Darkstar Server 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 this program. If not, see <http://www.gnu.org/licenses/>.
*
* --
*/
package com.sun.sgs.impl.kernel;
import com.sun.sgs.app.TransactionAbortedException;
import com.sun.sgs.app.TransactionConflictException;
import com.sun.sgs.app.TransactionTimeoutException;
import com.sun.sgs.impl.profile.ProfileCollectorHandle;
import com.sun.sgs.impl.service.transaction.TransactionCoordinator;
import com.sun.sgs.impl.service.transaction.TransactionCoordinatorImpl;
import com.sun.sgs.impl.sharedutil.LoggerWrapper;
import static com.sun.sgs.impl.sharedutil.Objects.checkNull;
import com.sun.sgs.impl.sharedutil.PropertiesWrapper;
import com.sun.sgs.impl.util.lock.LockConflict;
import com.sun.sgs.impl.util.lock.LockConflictType;
import com.sun.sgs.impl.util.lock.LockManager;
import com.sun.sgs.impl.util.lock.LockRequest;
import com.sun.sgs.impl.util.lock.TxnLockManager;
import com.sun.sgs.impl.util.lock.TxnLocker;
import com.sun.sgs.kernel.AccessCoordinator;
import com.sun.sgs.kernel.AccessReporter;
import com.sun.sgs.kernel.AccessReporter.AccessType;
import com.sun.sgs.kernel.AccessedObject;
import com.sun.sgs.profile.AccessedObjectsDetail;
import com.sun.sgs.profile.AccessedObjectsDetail.ConflictType;
import com.sun.sgs.service.Transaction;
import com.sun.sgs.service.TransactionInterruptedException;
import com.sun.sgs.service.TransactionListener;
import com.sun.sgs.service.TransactionProxy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.logging.Level;
import static java.util.logging.Level.CONFIG;
import static java.util.logging.Level.FINER;
import java.util.logging.Logger;
/**
* An implementation of {@link AccessCoordinator} that uses locking to handle
* conflicts. <p>
*
* This implementation checks for deadlock whenever an access request is
* blocked due to a conflict. It selects the youngest transaction as the
* deadlock victim, determining the age using the originally requested start
* time for the task associated with the transaction. The implementation does
* not deny requests that would not result in deadlock. When requests block,
* it services the requests in the order that they arrive. <p>
*
* The methods that this class provides to implement {@code AccessReporter} are
* not thread safe, and should either be called from a single thread or else
* protected with external synchronization. <p>
*
* The {@link #LockingAccessCoordinator constructor} supports the following
* configuration properties: <p>
*
* <dl style="margin-left: 1em">
*
* <dt> <i>Property:</i> <b>{@value #LOCK_TIMEOUT_PROPERTY}</b> <br>
* <i>Default:</i> {@value #DEFAULT_LOCK_TIMEOUT_PROPORTION} times the
* value of the {@code com.sun.sgs.txn.timeout} property, if specified,
* otherwise times the value of the default transaction timeout.
*
* <dd style="padding-top: .5em">The maximum number of milliseconds to wait for
* obtaining a lock. The value must be greater than {@code 0}, and should
* be less than the transaction timeout. <p>
*
* <dt> <i>Property:</i> <b>{@value #NUM_KEY_MAPS_PROPERTY}</b> <br>
* <i>Default:</i> {@value #NUM_KEY_MAPS_DEFAULT}
*
* <dd style="padding-top: .5em">The number of maps to use for associating keys
* and maps. The number of maps controls the amount of concurrency, and
* should typically be set to a value to support concurrent access by the
* number of active threads. The value must be greater than {@code
* 0}. <p>
*
* </dl> <p>
*
* This class uses the {@link Logger} named {@code
* com.sun.sgs.impl.kernel.LockingAccessCoordinator} to log information at the
* following logging levels: <p>
*
* <ul>
* <li> {@link Level#CONFIG CONFIG} - Creating an instance
* <li> {@link Level#FINER FINER} - Beginning and ending transactions
* </ul>
*/
public class LockingAccessCoordinator extends AbstractAccessCoordinator {
/** The class name. */
private static final String CLASS =
"com.sun.sgs.impl.kernel.LockingAccessCoordinator";
/**
* The property for specifying the maximum number of milliseconds to wait
* for obtaining a lock.
*/
public static final String LOCK_TIMEOUT_PROPERTY =
CLASS + ".lock.timeout";
/**
* The proportion of the transaction timeout to use for the lock timeout if
* no lock timeout is specified.
*/
public static final double DEFAULT_LOCK_TIMEOUT_PROPORTION = 0.1;
/**
* The property for specifying the number of maps to use for associating
* keys and maps. The number of maps controls the amount of concurrency.
*/
public static final String NUM_KEY_MAPS_PROPERTY =
CLASS + ".num.key.maps";
/** The default number of key maps. */
public static final int NUM_KEY_MAPS_DEFAULT = 8;
/** The logger for this class. */
static final LoggerWrapper logger = new LoggerWrapper(
Logger.getLogger(LockingAccessCoordinator.class.getName()));
/** Maps transactions to lockers. */
private final ConcurrentMap<Transaction, LockerImpl> txnMap =
new ConcurrentHashMap<Transaction, LockerImpl>();
/** The lock manager. */
private final TxnLockManager<Key> lockManager;
/* -- Public constructor -- */
/**
* Creates an instance of this class.
*
* @param properties the configuration properties
* @param txnProxy the transaction proxy
* @param profileCollectorHandle the profile collector handle
* @throws IllegalArgumentException if the values of the configuration
* properties are illegal
*/
public LockingAccessCoordinator(
Properties properties,
TransactionProxy txnProxy,
ProfileCollectorHandle profileCollectorHandle)
{
super(txnProxy, profileCollectorHandle);
PropertiesWrapper wrappedProps = new PropertiesWrapper(properties);
long txnTimeout = wrappedProps.getLongProperty(
TransactionCoordinator.TXN_TIMEOUT_PROPERTY,
TransactionCoordinatorImpl.BOUNDED_TIMEOUT_DEFAULT);
long defaultLockTimeout = Math.max(
1L, (long) (txnTimeout * DEFAULT_LOCK_TIMEOUT_PROPORTION));
long lockTimeout = wrappedProps.getLongProperty(
LOCK_TIMEOUT_PROPERTY, defaultLockTimeout, 1, Long.MAX_VALUE);
int numKeyMaps = wrappedProps.getIntProperty(
NUM_KEY_MAPS_PROPERTY, NUM_KEY_MAPS_DEFAULT, 1, Integer.MAX_VALUE);
lockManager = new TxnLockManager<Key>(lockTimeout, numKeyMaps);
if (logger.isLoggable(CONFIG)) {
logger.log(CONFIG,
"Created LockingAccessCoordinator with properties:" +
"\n txn timeout: " + txnTimeout +
"\n lock timeout: " + lockTimeout +
"\n num key maps: " + numKeyMaps);
}
}
/* -- Implement AccessCoordinator -- */
/** {@inheritDoc} */
public <T> AccessReporter<T> registerAccessSource(
String sourceName, Class<T> objectIdType)
{
checkNull("objectIdType", objectIdType);
return new AccessReporterImpl<T>(sourceName);
}
/**
* {@inheritDoc} <p>
*
* This implementation does not record information about completed
* transactions, so it always returns {@code null}.
*/
public Transaction getConflictingTransaction(Transaction txn) {
checkNull("txn", txn);
return null;
}
/* -- Implement AccessCoordinatorHandle -- */
/** {@inheritDoc} */
public void notifyNewTransaction(
Transaction txn, long requestedStartTime, int tryCount)
{
if (tryCount < 1) {
throw new IllegalArgumentException(
"The tryCount must not be less than 1");
}
LockerImpl locker =
new LockerImpl(lockManager, txn, requestedStartTime);
LockerImpl existing = txnMap.putIfAbsent(txn, locker);
if (existing != null) {
throw new IllegalStateException("Transaction already started");
}
if (logger.isLoggable(FINER)) {
logger.log(FINER, "begin {0}, requestedStartTime:{1,number,#}",
locker, requestedStartTime);
}
txn.registerListener(new TxnListener(txn));
}
/* -- Other methods -- */
/**
* Returns the locker associated with a transaction.
*
* @param txn the transaction
* @return the locker
* @throws IllegalArgumentException if the transaction is not active
*/
LockerImpl getLocker(Transaction txn) {
checkNull("txn", txn);
LockerImpl locker = txnMap.get(txn);
if (locker == null) {
throw new IllegalArgumentException(
"Transaction not active: " + txn);
}
return locker;
}
/**
* Releases the locks for the transaction and reports object accesses to
* the profiling system.
*
* @param txn the finished transaction
*/
private void endTransaction(Transaction txn) {
LockerImpl locker = getLocker(txn);
logger.log(FINER, "end {0}", locker);
locker.releaseAll();
txnMap.remove(txn);
profileCollectorHandle.setAccessedObjectsDetail(locker);
}
/* -- Other classes -- */
/**
* Define a locker that records information about the transaction
* requesting locks, and descriptions.
*/
public static class LockerImpl extends TxnLocker<Key>
implements AccessedObjectsDetail
{
/** The lock requests made by this transaction. */
private final List<AccessedObjectImpl> requests =
new ArrayList<AccessedObjectImpl>();
/** A map from keys to descriptions, or {@code null}. */
private Map<Key, Object> keyToDescriptionMap = null;
/**
* Whether the transaction has ended. Used when assertions are enabled
* to check the thread safety of accesses to the requests field.
* Synchronize on the requests field, rather than the locker object
* itself, when accessing this field, to avoid lock ordering problems.
*/
private boolean ended = false;
/**
* Creates an instance of this class.
*
* @param lockManager the lock manager for this locker
* @param txn the associated transaction
* @param requestedStartTime the time milliseconds that the task
* associated with the transaction was originally
* requested to start
* @throws IllegalArgumentException if {@code requestedStartTime}
* is less than {@code 0}
*/
LockerImpl(TxnLockManager<Key> lockManager,
Transaction txn,
long requestedStartTime)
{
super(lockManager, txn, requestedStartTime);
}
/**
* {@inheritDoc} <p>
*
* This implementation records the new request and uses a local
* class.
*/
@Override
protected LockRequest<Key> newLockRequest(
Key key, boolean forWrite, boolean upgrade)
{
assert !getEnded();
AccessedObjectImpl request =
new AccessedObjectImpl(this, key, forWrite, upgrade);
requests.add(request);
return request;
}
/** Release all locks. */
void releaseAll() {
assert setEnded();
LockManager<Key> lockManager = getLockManager();
for (LockRequest<Key> request : requests) {
lockManager.releaseLock(this, request.getKey());
}
}
/**
* Returns a string representation of this object. This implementation
* prints the associated transaction, for debugging.
*
* @return a string representation of this object
*/
@Override
public String toString() {
return txn.toString();
}
/* -- Implement AccessedObjectsDetail -- */
/** {@inheritDoc} */
public List<AccessedObject> getAccessedObjects() {
return Collections.<AccessedObject>unmodifiableList(requests);
}
/** {@inheritDoc} */
public ConflictType getConflictType() {
LockConflict<Key> conflict = getConflict();
if (conflict == null) {
return ConflictType.NONE;
} else if (conflict.getType() == LockConflictType.DEADLOCK) {
return ConflictType.DEADLOCK;
} else {
return ConflictType.ACCESS_NOT_GRANTED;
}
}
/** {@inheritDoc} */
public byte[] getConflictingId() {
LockConflict<Key> conflict = getConflict();
if (conflict != null) {
LockerImpl conflictingLocker =
(LockerImpl) conflict.getConflictingLocker();
return conflictingLocker.getTransaction().getId();
} else {
return null;
}
}
/* -- Other methods -- */
/**
* Sets the description associated with a key for this locker. The
* description should not be {@code null}. Does not replace an
* existing description.
*
* @param key the key
* @param description the description
*/
void setDescription(Key key, Object description) {
assert key != null;
assert description != null;
if (keyToDescriptionMap == null) {
keyToDescriptionMap = new HashMap<Key, Object>();
}
if (!keyToDescriptionMap.containsKey(key)) {
keyToDescriptionMap.put(key, description);
}
}
/**
* Gets the description associated with a key for this locker.
*
* @param key the key
* @return the description or {@code null}
*/
Object getDescription(Key key) {
return (keyToDescriptionMap == null)
? null : keyToDescriptionMap.get(key);
}
/**
* Sets the specified conflict if this locker does not have a conflict
* set.
*/
synchronized void setConflictIfNeeded(LockConflict<Key> conflict) {
if (getConflict() == null) {
setConflict(conflict);
}
}
/**
* Marks the transaction as ended.
*
* @return whether the transaction is newly ended
*/
private boolean setEnded() {
synchronized (requests) {
if (ended) {
return false;
} else {
ended = true;
return true;
}
}
}
/** Returns whether the transaction has ended. */
private boolean getEnded() {
synchronized (requests) {
return ended;
}
}
}
/** Implement {@code AccessedObject}. */
private static class AccessedObjectImpl extends LockRequest<Key>
implements AccessedObject
{
/**
* Creates an instance of this class.
*
* @param locker the locker that requested the lock
* @param key the key identifying the lock
* @param forWrite whether a write lock was requested
* @param upgrade whether an upgrade was requested
*/
AccessedObjectImpl(LockerImpl locker,
Key key,
boolean forWrite,
boolean upgrade)
{
super(locker, key, forWrite, upgrade);
}
/* -- Implement AccessedObject -- */
/** {@inheritDoc} */
public String getSource() {
return getKey().source;
}
/** {@inheritDoc} */
public Object getObjectId() {
return getKey().objectId;
}
/** {@inheritDoc} */
public AccessType getAccessType() {
return getForWrite() ? AccessType.WRITE : AccessType.READ;
}
/** {@inheritDoc} */
public Object getDescription() {
return ((LockerImpl) getLocker()).getDescription(getKey());
}
/**
* Two instances are equal if they are instances of this class, and
* have the same source, object ID, and access type.
*
* @param object the object to compare with
* @return whether this instance equals the argument
*/
@Override
public boolean equals(Object object) {
if (object == this) {
return true;
} else if (object instanceof AccessedObjectImpl) {
AccessedObjectImpl other = (AccessedObjectImpl) object;
return getKey().equals(other.getKey()) &&
getForWrite() == other.getForWrite();
} else {
return false;
}
}
@Override
public int hashCode() {
return getKey().hashCode() ^ (getForWrite() ? 1 : 0);
}
/* -- Other methods -- */
/** Print fields, for debugging. */
@Override
public String toString() {
return "AccessedObjectImpl[" + getLocker() + ", " +
getKey() + ", " +
(getForWrite() ? "WRITE" : getUpgrade() ? "UPGRADE" : "READ") +
"]";
}
}
/** Represents an object as identified by a source and an object ID. */
private static final class Key {
/** The source. */
final String source;
/** The object ID. */
final Object objectId;
/**
* Creates an instance of this class.
*
* @param source the source of the object
* @param objectId the object ID of the object
*/
Key(String source, Object objectId) {
checkNull("source", source);
checkNull("objectId", objectId);
this.source = source;
this.objectId = objectId;
}
/* -- Compare source and object ID -- */
@Override
public boolean equals(Object object) {
if (object == this) {
return true;
} else if (object instanceof Key) {
Key key = (Key) object;
return source.equals(key.source) &&
objectId.equals(key.objectId);
} else {
return false;
}
}
@Override
public int hashCode() {
return source.hashCode() ^ objectId.hashCode();
}
/** Print fields, for debugging. */
@Override
public String toString() {
return source + ":" + objectId;
}
}
/** Implement {@link AccessReporter}. */
private class AccessReporterImpl<T> extends AbstractAccessReporter<T> {
/**
* Creates an instance of this class.
*
* @param source the source of the objects managed by this
* reporter
*/
AccessReporterImpl(String source) {
super(source);
}
/* -- Implement AccessReporter -- */
/** {@inheritDoc} */
public void reportObjectAccess(
Transaction txn, T objectId, AccessType type, Object description)
{
checkNull("type", type);
LockerImpl locker = getLocker(txn);
Key key = new Key(source, objectId);
if (description != null) {
locker.setDescription(key, description);
}
LockConflict<Key> conflict =
lockManager.lock(locker, key, type == AccessType.WRITE);
if (conflict != null) {
locker.setConflictIfNeeded(conflict);
String descriptionMsg = "";
if (description != null) {
try {
descriptionMsg = ", description:" + description;
} catch (RuntimeException e) {
}
}
String accessMsg = "Access txn:" + txn +
", type:" + type +
", source:" + source +
", objectId:" + objectId +
descriptionMsg +
" failed: ";
LockerImpl conflictingLocker =
(LockerImpl) conflict.getConflictingLocker();
String conflictMsg = ", with conflicting transaction " +
conflictingLocker.getTransaction();
TransactionAbortedException exception;
switch (conflict.getType()) {
case TIMEOUT:
exception = new TransactionTimeoutException(
accessMsg + "Transaction timed out" + conflictMsg);
break;
case DENIED:
exception = new TransactionConflictException(
accessMsg + "Access denied" + conflictMsg);
break;
case INTERRUPTED:
exception = new TransactionInterruptedException(
accessMsg + "Transaction interrupted" + conflictMsg);
break;
case DEADLOCK:
exception = new TransactionConflictException(
accessMsg + "Transaction deadlock" + conflictMsg);
break;
default:
throw new AssertionError(
"Should not be " + conflict.getType());
}
txn.abort(exception);
throw exception;
}
}
/** {@inheritDoc} */
public void setObjectDescription(
Transaction txn, T objectId, Object description)
{
LockerImpl locker = getLocker(txn);
if (description == null) {
checkNull("objectId", objectId);
} else {
locker.setDescription(new Key(source, objectId), description);
}
}
}
/**
* A transaction listener that calls {@link #endTransaction} when called
* after the transaction completes. Use a listener instead of a
* transaction participant to make sure that locks are released only after
* all of the transaction participants have finished their work.
*/
private class TxnListener implements TransactionListener {
/** The transaction. */
private final Transaction txn;
/**
* Creates an instance of this class.
*
* @param txn the transaction we're listening for
*/
TxnListener(Transaction txn) {
this.txn = txn;
}
/**
* {@inheritDoc} <p>
*
* This implementation does nothing.
*/
public void beforeCompletion() { }
/**
* {@inheritDoc} <p>
*
* This implementation calls {@link #endTransaction}.
*/
public void afterCompletion(boolean committed) {
endTransaction(txn);
}
/** {@inheritDoc} */
public String getTypeName() {
return TxnListener.class.getName();
}
}
}