/*
* 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.impl.profile.ProfileCollectorHandle;
import com.sun.sgs.impl.sharedutil.Objects;
import com.sun.sgs.kernel.AccessCoordinator;
import com.sun.sgs.kernel.AccessedObject;
import com.sun.sgs.kernel.AccessReporter;
import com.sun.sgs.kernel.AccessReporter.AccessType;
import com.sun.sgs.profile.AccessedObjectsDetail;
import com.sun.sgs.profile.AccessedObjectsDetail.ConflictType;
import com.sun.sgs.service.NonDurableTransactionParticipant;
import com.sun.sgs.service.Transaction;
import com.sun.sgs.service.TransactionProxy;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Queue;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* A package-private implementation of {@code AccessCoordinator} that is
* used by the system to track access to objects and handle any possible
* conflict. This implementation is also responsible for reporting
* the access detail to the profiling system.
* <p>
* This implementation provides the option to keep detail on a backlog of
* past transactions to discover what may have caused conflict. This is
* currently only useful for {@code ProfileListener}s that wish to diplay
* this detail. By default this backlog tracking is disabled. To enable,
* set the {@code com.sun.sgs.impl.kernel.TrackingAccessCoordinator.queue.size}
* property to some positive value indicating the length of backlog to use.
* Note that with each transaction failure this backlog will be scanned
* to find a conflicting transaction, so a larger backlog may provide more
* detail about failure but will also be more compute-intensive.
*/
class TrackingAccessCoordinator extends AbstractAccessCoordinator
implements NonDurableTransactionParticipant
{
/**
* The map from active transactions to associated detail
*/
private final ConcurrentMap<Transaction, AccessedObjectsDetailImpl> txnMap =
new ConcurrentHashMap<Transaction, AccessedObjectsDetailImpl>();
// TODO: there may need to be maps for the active locks...these may
// eventually move (in whole or in part) to the conflict resolver,
// but this seems like the right place at least to start them out.
// At any rate, these aren't needed until conflict is being managed,
// since active transactions will not be the cause of failure
// NOTE: this is here as an experiment, as it's not clear if it needs
// to be in the coordinator, or could be moved to the profiling code,
// or put somewhere else altogether
/**
* An optional backlog of completed transactions used for
* identifying a past conflict. The queue is bounded if by the
* value set in the {@link #BACKLOG_QUEUE_PROPERTY}, or {@code
* null} by default.
*/
private final Queue<AccessedObjectsDetailImpl> backlog;
/**
* The property to set the size of the backlog and enable
* transaction conflict detail reporting. The value set by this
* property must be non-negative.
*/
static final String BACKLOG_QUEUE_PROPERTY =
TrackingAccessCoordinator.class.getName() + ".queue.size";
/**
* Creates an instance of {@code TrackingAccessCoordinator}.
*
* @throws IllegalArgumentException if the requested backlog queue size
* is not a valid number greater than 0
*/
TrackingAccessCoordinator(Properties properties, TransactionProxy txnProxy,
ProfileCollectorHandle profileCollector)
{
super(txnProxy, profileCollector);
Objects.checkNull("properties", properties);
String backlogProp = properties.getProperty(BACKLOG_QUEUE_PROPERTY);
if (backlogProp != null) {
try {
backlog = new LinkedBlockingQueue
<AccessedObjectsDetailImpl>(Integer.parseInt(backlogProp));
} catch (NumberFormatException nfe) {
throw new IllegalArgumentException("Backlog size must be a " +
"positive number: "
+ backlogProp);
}
} else {
backlog = null;
}
}
/*
* Implement AccessCoordinator interface.
*/
/**
* {@inheritDoc}
*/
public <T> AccessReporter<T> registerAccessSource(String sourceName,
Class<T> objectIdType)
{
Objects.checkNull("sourceName", sourceName);
Objects.checkNull("objectIdType", objectIdType);
return new AccessReporterImpl<T>(sourceName);
}
/**
* {@inheritDoc}
*/
public Transaction getConflictingTransaction(Transaction txn) {
Objects.checkNull("txn", txn);
// given that we're not actively managing contention yet (which
// means that there aren't many active conflicts) and the scheduler
// isn't trying to optimize using this interface, we don't try
// to track these cases yet
return null;
}
/**
* TODO: Notes for when we look at conflict reolution...
* The idea here is that a resolver needs to know who else might be
* interested in a given object...but is this the right approach? The
* reasoning here is that the Coordinator should implement basic
* conflict and deadlock detection so that each resolver doesn't have
* to, but deadlock may depend on how resolution is done. So, maybe
* the resolver is responsible for this, in which case it needs to
* know about each access, not just possible conclit cases. Does
* this suggest that the Coordinator just tracks access, reports
* profiling, etc. but passes on all access requests to the resolver?
* ...
* Maybe all requests get pased on, but the Coordinator implements a
* utility method to see if there is basic conflict happening?
*/
//public List<> getCurrentReaders(Object obj);
//public List<> getCurrentWriters(Object obj);
/*
* Implement AccessCoordinatorHandle
*/
/**
* {@inheritDoc}
*/
public void notifyNewTransaction(
Transaction txn, long requestedStartTime, int tryCount)
{
if (requestedStartTime < 0) {
throw new IllegalArgumentException(
"The requestedStartTime must not be less than 0");
}
if (tryCount < 1) {
throw new IllegalArgumentException(
"The tryCount must not be less than 1");
}
if (txnMap.containsKey(txn)) {
throw new IllegalStateException("Transaction already started");
}
txn.join(this);
txnMap.put(txn, new AccessedObjectsDetailImpl(txn));
}
/*
* Implement NonDurableTransactionParticipant interface.
*/
/**
* {@inheritDoc}
*/
public boolean prepare(Transaction txn) {
AccessedObjectsDetailImpl detail = txnMap.get(txn);
// TODO: this is the last chance to abort because of conflict,
// when we're actually managing the contention
detail.markPrepared();
return false;
}
/**
* {@inheritDoc}
*/
public void commit(Transaction txn) {
reportDetail(txn, true);
}
/**
* {@inheritDoc}
*/
public void prepareAndCommit(Transaction txn) {
reportDetail(txn, true);
}
/**
* {@inheritDoc}
*/
public void abort(Transaction txn) {
reportDetail(txn, false);
}
/**
* {@inheritDoc}
*/
public String getTypeName() {
return getClass().getName();
}
/**
* Codifies the results of the provided transaction and reports
* the information to the profiling system.
*
* @param txn a finished transaction
* @param succeeded whether {@code txn} was successful (i.e. did
* not abort).
*/
private void reportDetail(Transaction txn, boolean succeeded) {
AccessedObjectsDetailImpl detail = txnMap.remove(txn);
// if the task failed try to determine why
if (!succeeded) {
// mark the type with an initial guess of unknown and then
// try to refine it.
detail.conflictType = ConflictType.UNKNOWN;
// NOTE: in the current system we don't really see transactions
// fail because of conflict with another active transaction,
// so currently this is only for looking through a backlog
if (backlog != null) {
// look through the backlog for a conflict
for (AccessedObjectsDetailImpl oldDetail : backlog) {
if (detail.conflictsWith(oldDetail)) {
detail.setConflict(ConflictType.ACCESS_NOT_GRANTED,
oldDetail);
break;
}
}
}
}
// if we're keeping a backlog, then add the reported detail...if
// the backlog is full, then evict old data until there is room
if (backlog != null) {
while (!backlog.offer(detail)) {
backlog.poll();
}
}
profileCollectorHandle.setAccessedObjectsDetail(detail);
}
/*
* Class implementations.
*/
/** Private implementation of {@code AccessedObjectsDetail}. */
private static class AccessedObjectsDetailImpl
implements AccessedObjectsDetail
{
// the id of the transaction for this detail
private final byte [] txnId;
/**
* The ordered set of accesses for all sources, which includes
* both reads and writes
*/
private final LinkedHashSet<AccessedObject> accessList =
new LinkedHashSet<AccessedObject>();
/**
* The list of write accesses that occurred during the
* transaction. We use a {@code List} here to improve
* iteration efficiency in the {@code conflictsWith} method.
* This list is guaranteed not to have any duplicates in it.
*/
private final List<AccessedObject> writes =
new ArrayList<AccessedObject>();
/**
* The mapping from a source to a second mapping from all of
* that sources's Ids to their descriptions. This secondary
* mapping is used to prevent collisions between multiple
* sources that use the same Id.
*/
private final Map<String, Map<Object, Object>>
sourceToObjIdAndDescription =
new HashMap<String, Map<Object, Object>>();
// whether the transaction has already proceeded past prepare
private final AtomicBoolean prepared = new AtomicBoolean(false);
// information about why the transaction failed, if it failed
private ConflictType conflictType = ConflictType.NONE;
private byte [] idOfConflictingTxn = null;
/** Creates an instance for the given transaction. */
AccessedObjectsDetailImpl(Transaction txn) {
this.txnId = txn.getId();
}
/** Implement AccessObjectsDetail. */
/** {@inheritDoc} */
public List<AccessedObject> getAccessedObjects() {
return new ArrayList<AccessedObject>(accessList);
}
/** {@inheritDoc} */
public ConflictType getConflictType() {
return conflictType;
}
/** {@inheritDoc} */
public byte [] getConflictingId() {
return idOfConflictingTxn;
}
/** Adds an {@code AccessedObject} to the list of accessed objects. */
void addAccess(AccessedObject accessedObject) {
boolean added = accessList.add(accessedObject);
// if this is the first time the object has been accessed,
// we may need to add its id to the set of descriptions,
// and if it was a write, add it to the list.
if (added) {
String source = accessedObject.getSource();
Map<Object, Object> idToDescription =
sourceToObjIdAndDescription.get(source);
if (idToDescription == null) {
idToDescription = new HashMap<Object, Object>();
sourceToObjIdAndDescription.put(source, idToDescription);
}
// if we didn't already have a description set for
// this object, put its Id in the map so we have a
// recorded access of it. We use the keyset of this
// map when checking for conflicts, so every access
// needs to be recorded in it
Object objId = accessedObject.getObjectId();
if (!idToDescription.containsKey(objId)) {
idToDescription.put(objId, null);
}
// keep track of the write accesses in case we later
// need to determine whether this detail conflicted
// with another.
if (accessedObject.getAccessType().equals(AccessType.WRITE)) {
writes.add(accessedObject);
}
}
}
/** Maps the provided object Id to a description. */
void setDescription(String source, Object objId, Object description) {
Map<Object, Object> objIdToDescription =
sourceToObjIdAndDescription.get(source);
if (objIdToDescription == null) {
objIdToDescription = new HashMap<Object, Object>();
sourceToObjIdAndDescription.put(source, objIdToDescription);
}
// if the Id didn't already have a description, add one
if (objIdToDescription.get(objId) == null) {
objIdToDescription.put(objId, description);
}
}
/** Sets the cause and source of conflict for this access detail. */
void setConflict(ConflictType conflictReason,
AccessedObjectsDetailImpl conflicting) {
conflictType = conflictReason;
this.idOfConflictingTxn = conflicting.txnId;
}
/** Returns a given object's annotation or {@code null}. */
Object getDescription(String source, Object objId) {
Map<Object, Object> objIdToDescription =
sourceToObjIdAndDescription.get(source);
return (objIdToDescription == null)
? null : objIdToDescription.get(objId);
}
/** Marks this detail as having progressed past prepare(). */
void markPrepared() {
prepared.set(true);
}
/** Reports whether the associated transaction has prepared. */
boolean isPrepared() {
return prepared.get();
}
/** Checks if the given detail conflicts with this detail. */
boolean conflictsWith(AccessedObjectsDetailImpl other) {
if (other == null) {
return false;
}
// A conflict occurs if two details have write sets that
// intersect, or if the write set of either set intersects
// with the other's read set. We therefore iterate over
// each detail's write set and check if the second detail
// had a source that accessed an object with the same key
// as the write access from the first detail.
for (AccessedObject o : writes) {
Map<Object, Object> objIdToDescription =
other.sourceToObjIdAndDescription.get(o.getSource());
if (objIdToDescription != null &&
objIdToDescription.containsKey(o.getObjectId())) {
return true;
}
}
for (AccessedObject o : other.writes) {
Map<Object, Object> objIdToDescription =
sourceToObjIdAndDescription.get(o.getSource());
if (objIdToDescription != null &&
objIdToDescription.containsKey(o.getObjectId())) {
return true;
}
}
return false;
}
}
/**
* Private implementation of {@code AccessedObject}.
*/
private static class AccessedObjectImpl implements AccessedObject {
private final Object objId;
private final AccessType type;
private final String source;
private final AccessedObjectsDetailImpl parent;
/** Creates an instance of {@code AccessedObjectImpl}. */
AccessedObjectImpl(Object objId, AccessType type, String source,
AccessedObjectsDetailImpl parent) {
Objects.checkNull("objId", objId);
Objects.checkNull("type", type);
Objects.checkNull("source", source);
Objects.checkNull("parent", parent);
this.objId = objId;
this.type = type;
this.source = source;
this.parent = parent;
}
/* Implement AccessedObject. */
/** {@inheritDoc} */
public Object getObjectId() {
return objId;
}
/** {@inheritDoc} */
public AccessType getAccessType() {
return type;
}
/** {@inheritDoc} */
public Object getDescription() {
return parent.getDescription(source, objId);
}
/** {@inheritDoc} */
public String getSource() {
return source;
}
/* Support comparison checking. */
/**
* Returns {@code true} if the other object is an instance of
* {@code AccessedObjectImpl} and has the same object Id, access
* type and source.
*/
public boolean equals(Object o) {
if ((o != null) && (o instanceof AccessedObjectImpl)) {
AccessedObjectImpl a = (AccessedObjectImpl) o;
return objId.equals(a.objId) && type.equals(a.type) &&
source.equals(a.source);
}
return false;
}
/**
* Returns the hash code of the object Id xor'd with the hash
* code of the access type and the hash code of the source.
*/
public int hashCode() {
return objId.hashCode() ^ type.hashCode() ^ source.hashCode();
}
}
/**
* Private implementation of {@code AccessNotifier}. Note that once we
* start managing conflict then the {@code notifyObjectAccess} calls
* will need to start tracking access and check that this doesn't cause
* some transaction to fail.
*/
private class AccessReporterImpl<T> extends AbstractAccessReporter<T> {
/** Creates an instance of {@code AccessReporter}. */
AccessReporterImpl(String source) {
super(source);
}
/** {@inheritDoc} */
public void reportObjectAccess(Transaction txn, T objId,
AccessType type, Object description) {
Objects.checkNull("txn", txn);
Objects.checkNull("objId", objId);
Objects.checkNull("type", type);
AccessedObjectsDetailImpl detail = txnMap.get(txn);
if (detail == null) {
throw new IllegalArgumentException("Unknown transaction: " +
txn);
}
detail.addAccess(new AccessedObjectImpl(objId, type, source,
detail));
if (description != null) {
detail.setDescription(source, objId, description);
}
}
/** {@inheritDoc} */
public void setObjectDescription(Transaction txn, T objId,
Object description) {
Objects.checkNull("txn", txn);
Objects.checkNull("objId", objId);
AccessedObjectsDetailImpl detail = txnMap.get(txn);
if (detail == null) {
throw new IllegalArgumentException("Unknown transaction: " +
txn);
}
if (description != null) {
detail.setDescription(source, objId, description);
}
}
}
}