/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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 com.sun.jini.outrigger;
import com.sun.jini.constants.TxnConstants;
import com.sun.jini.logging.Levels;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.rmi.RemoteException;
import java.util.List;
import java.util.Iterator;
import java.util.Collections;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.jini.core.transaction.CannotJoinException;
import net.jini.core.transaction.server.ServerTransaction;
import net.jini.core.transaction.server.TransactionConstants;
import net.jini.core.transaction.server.TransactionManager;
import net.jini.space.InternalSpaceException;
import net.jini.security.ProxyPreparer;
/**
* This class represents a space's state in a single transaction.
*
* Object of this class represent Jini transactions within outrigger.
* These transactions hold "Transactables" -- things that represent
* the actions that have been taken under this transaction. For example,
* if this transaction were to be cancelled, the Transactables are
* examined and serve as the list of things to roll back in order to
* restore the state of the Space status quo ante.
*
* This is achieved by having the transactables notified of state changes
* to this transaction such as preparing, commit, etc. The
* transactables themselves are responsible for "doing the right thing."
*
* NB: some--but not all--of the actions one can take with a transaction
* are managed internally by these objects. That is, all of the important
* methods objects of these types are synchronized. Therefore, two
* simultaneous calls to abort() and commit() are arbitrated properly.
*
* However, care must be taken when add()ing a transactable. Even
* though the add() method is synchronized if you check the state of
* the transaction to ensure that is active and then call add() the
* transaction could have left the active state between the check and
* the add() call unless the call obtains the appropriate locks. This
* is even more likely if other work needs to be done in addition to
* calling add() (e.g. persisting state, obtaining locks, etc.). The
* caller of add() should lock the associated transaction object and
* ensure that the transaction is still considered ACTIVE, do whatever
* work is necessary to complete while the transaction is in the ACTIVE
* state (including calling call add()) and then release the lock.
* This can be done by :
* <ul>
* <li> holding the lock on this object while checking the
* state and carrying out the operation (including calling add()), or
* <li> calling ensureActive() to check the state
* and obtain a non-exclusive lock, carrying out the operation
* (including calling add()), and then calling allowStateChange() to
* release the lock.
* </ul>
* The pair of ensureActive() and allowStateChange() allows for more
* concurrency if the operation is expected to take a long time, in
* that it will allow for other operations to be performed under the
* same transaction and let aborts prevent other operations from
* being started.
*
* @author Sun Microsystems, Inc.
*/
class Txn implements TransactableMgr, TransactionConstants, StorableObject {
/** The internal id Outrigger as assigned to the transaction */
final private long id;
/** What state we think the transaction is in */
private int state;
/**
* The transaction manager associated with the transaction
* this object is fronting for.
*/
private StorableReference trm;
/**
* Cached <code>ServerTransaction</code> object for
* the transaction this object is fronting for.
*/
private ServerTransaction tr;
/** The id the transaction manager assigned to this transaction */
private long trId;
/**
* The list of <code>Transactable</code> participating in
* this transaction.
*/
final private List txnables = new java.util.LinkedList();
/**
* The task responsible for monitoring to see if this
* transaction has been aborted with us being told, or
* null if no such task as been allocated.
*/
private TxnMonitorTask monitorTask;
/** Count of number of threads holding a read lock on state */
private int stateReaders = 0;
/**
* <code>true</code> if there is a blocked state change. Used
* to give writers priority.
*/
private boolean stateChangeWaiting = false;
/** Logger for logging transaction related information */
private static final Logger logger =
Logger.getLogger(OutriggerServerImpl.txnLoggerName);
/**
* Create a new <code>Txn</code> that represents our state in the
* given <code>ServerTransaction</code>.
*/
Txn(ServerTransaction tr, long id) {
this(id);
trId = tr.id;
this.tr = tr;
this.trm = new StorableReference(tr.mgr);
state = ACTIVE;
if (logger.isLoggable(Level.FINER)) {
logger.log(Level.FINER, "creating txn for transaction mgr:" +
"{0}, id:{1}, state:{2}",
new Object[]{tr, new Long(trId), TxnConstants.getName(state)});
}
}
/** Used in recovery */
Txn(long id) {
this.id = id; // the txn id is not persisted
}
/**
* Get the id for this txn. Note that this id is NOT the same as
* the ID of the transaction. Since that ID is not unique (it must
* be qualified with the <code>ServerTransaction</code> object) we create
* our own internal id to make txns unique. This is needed since we
* may not have the <code>Transaction</code> unmarshalled.
*/
Long getId() {
return new Long(id);
}
/**
* We keep the transaction ID around because we may need it
* to identify a broken transaction after recovery.
*/
long getTransactionId() {
return trId;
}
/**
* Return our local view of the current state. Need to be holding
* the lock on this object or have called <code>ensureActive</code>
* to get the current value.
*/
int getState() {
return state;
}
/**
* Atomically checks that this transaction is in the active
* state and locks the transaction in the active state.
* The lock can be released by calling <code>allowStateChange</code>.
* Each call to this method should be paired with a call to
* <code>allowStateChange</code> in a finally block.
* @throws CannotJoinException if the transaction
* is not active or a state change is pending.
*/
synchronized void ensureActive() throws CannotJoinException {
if (state != ACTIVE || stateChangeWaiting) {
final String msg = "transaction mgr:" + tr + ", id:" + trId +
" not active, in state " + TxnConstants.getName(state);
final CannotJoinException e = new CannotJoinException(msg);
logger.log(Levels.FAILED, msg, e);
throw e;
}
assert stateReaders >= 0;
stateReaders++;
}
/**
* Release the read lock created by an <code>ensureActive</code>
* call. Does nothing if the transaction is not active or there is
* a state change pending and thus is safe to call even if the
* corresponding <code>ensureActive</code> call threw
* <code>CannotJoinException</code>.
*/
synchronized void allowStateChange() {
if (state != ACTIVE || stateChangeWaiting)
return;
stateReaders--;
assert stateReaders >= 0;
notifyAll();
}
/**
* Prevents new operations from being started under this
* transaction and blocks until in process operations are
* completed.
*/
synchronized void makeInactive() {
stateChangeWaiting = true;
assert stateReaders >= 0;
while (stateReaders != 0) {
try {
wait();
} catch (InterruptedException e) {
throw new AssertionError(e);
}
assert stateReaders >= 0;
}
}
/**
* Prepare for transaction commit. <code>makeInactive</code> must have
* been called on this transaction first.
*/
synchronized int prepare(OutriggerServerImpl space) {
assert stateChangeWaiting : "prepare called before makeInactive";
assert stateReaders == 0 : "prepare called before makeInactive completed";
if (logger.isLoggable(Level.FINER)) {
logger.log(Level.FINER, "prepare: transaction mgr:{0}, id:{1}, " +
"state:{2}",
new Object[]{tr, new Long(trId), TxnConstants.getName(state)});
}
switch (state) {
case ABORTED: // previously aborted
return ABORTED;
case COMMITTED: // previously committed
throw new IllegalStateException(); // "cannot happen"
case NOTCHANGED: // previously voted NOTCHANGED
case PREPARED: // previously voted PREPARED
return state; // they are idempotent, and
// and we have nothing to do
// so return
case ACTIVE: // currently active
boolean changed = false; // did this txn change
// anything?
if (logger.isLoggable(Level.FINEST)) {
logger.log(Level.FINEST, "prepare:preparing transaction mgr:" +
"{0}, id:{1}, state:{2}",
new Object[]{tr, new Long(trId), TxnConstants.getName(state)});
}
// loop through Transactable members of this Txn
final Iterator i = txnables.iterator();
int c=0; // Counter for debugging message
while (i.hasNext()) {
// get this member's vote
final Transactable transactable = (Transactable)i.next();
final int prepState = transactable.prepare(this, space);
if (logger.isLoggable(Level.FINEST)) {
logger.log(Level.FINEST, "prepare:prepared " +
"transactable {0} for transaction mgr:{1}, id:{2}," +
" transactable now in state {3}",
new Object[]{transactable, tr, new Long(trId),
TxnConstants.getName(prepState)});
}
switch (prepState) {
case PREPARED: // has prepared state
changed = true; // this means a change
continue;
case ABORTED: // has to abort
abort(space); // abort this txn (does cleanup)
state = ABORTED;
return state; // vote aborted
case NOTCHANGED: // no change
i.remove(); // Won't need to call again
continue;
default: // huh?
throw new
InternalSpaceException("prepare said " + prepState);
}
}
if (changed) {
state = PREPARED;
// have to watch this since it's now holding permanent
// resources
space.monitor(Collections.nCopies(1, this));
} else {
state = NOTCHANGED;
}
break;
default:
throw new IllegalStateException("unknown Txn state: " + state);
}
return state;
}
/**
* Abort the transaction. This must be callable from
* <code>prepare</code> because if a <code>Transactable</code>
* votes <code>ABORT</code>, this method is called to make that
* happen. <code>makeInactive</code> must have been called on this
* transaction first.
*/
synchronized void abort(OutriggerServerImpl space) {
assert stateChangeWaiting : "abort called before makeInactive";
assert stateReaders == 0 : "abort called before makeInactive completed";
if (logger.isLoggable(Level.FINER)) {
logger.log(Level.FINER, "abort: transaction mgr:{0}, id:{1}, " +
"state:{2}",
new Object[]{tr, new Long(trId), TxnConstants.getName(state)});
}
switch (state) {
case ABORTED: // already aborted
case NOTCHANGED: // nothing to abort
break;
case COMMITTED: // "cannot happen"
throw new IllegalStateException("aborting a committed txn");
case ACTIVE:
case PREPARED:
if (logger.isLoggable(Level.FINEST)) {
logger.log(Level.FINEST, "abort:aborting transaction mgr:" +
"{0}, id:{1}, state:{2}",
new Object[]{tr, new Long(trId), TxnConstants.getName(state)});
}
final Iterator i = txnables.iterator();
while (i.hasNext())
((Transactable) i.next()).abort(this, space);
state = ABORTED;
cleanup();
break;
default:
throw new IllegalStateException("unknown Txn state: " + state);
}
}
/**
* Having prepared, roll the changes
* forward. <code>makeInactive</code> must have been called on
* this transaction first.
*/
synchronized void commit(OutriggerServerImpl space) {
assert stateChangeWaiting : "commit called before makeInactive";
assert stateReaders == 0 : "commit called before makeInactive completed";
//!! Need to involve mgr here
if (logger.isLoggable(Level.FINER)) {
logger.log(Level.FINER, "commit: transaction mgr:{0}, id:{1}, " +
"state:{2}",
new Object[]{tr, new Long(trId), TxnConstants.getName(state)});
}
switch (state) {
case ABORTED: // "cannot happen" stuff
case ACTIVE:
case NOTCHANGED:
throw new IllegalStateException("committing "
+ TxnConstants.getName(state) + " txn");
case COMMITTED: // previous committed, that's okay
return;
case PREPARED: // voted PREPARED, time to finish up
final Iterator i = txnables.iterator();
if (logger.isLoggable(Level.FINEST)) {
logger.log(Level.FINEST, "commit:committing transaction mgr:" +
"{0}, id:{1}, state:{2}",
new Object[]{tr, new Long(trId), TxnConstants.getName(state)});
}
while (i.hasNext())
((Transactable) i.next()).commit(this, space);
state = COMMITTED;
cleanup();
return;
default:
throw new IllegalStateException("unknown Txn state: " + state);
}
}
/**
* Caution: see locking discussion at the class level.
*/
public synchronized Transactable add(Transactable t) {
txnables.add(t);
return t;
}
// inherit doc comment
public ServerTransaction getTransaction(ProxyPreparer preparer)
throws IOException, ClassNotFoundException
{
if (tr == null) {
final TransactionManager mgr =
(TransactionManager)trm.get(preparer);
tr = new ServerTransaction(mgr, trId);
}
return tr;
}
/**
* Return the manager associated with this transaction.
* @return the manager associated with this transaction.
* @throws IllegalStateException if this <code>Txn</code>
* is still broken.
*/
TransactionManager getManager() {
if (tr == null)
throw new IllegalStateException("Txn is still broken");
return tr.mgr;
}
/**
* Return the monitor task for this object. Note, this
* method is unsynchronized because it (and
* <code>monitorTask(TxnMonitorTask)</code> are both called
* from the same thread.
*/
TxnMonitorTask monitorTask() {
return monitorTask;
}
/**
* Set the monitor task for this object. Note, this method is
* unsynchronized because it (and <code>monitorTask()</code> are
* both called from the same thread.
*/
void monitorTask(TxnMonitorTask task) {
monitorTask = task;
}
/**
* Clean up any state when the transaction is finished.
*/
private void cleanup() {
if (monitorTask != null)
monitorTask.cancel(); // stop doing this
}
// -----------------------------------
// Methods required by StorableObject
// -----------------------------------
// inherit doc comment
public void store(ObjectOutputStream out) throws IOException {
/* There is a bunch of stuff we don't need to write. The
* Txn id not stored since it is handed back during
* recovery. The content is rebuilt txnables by the various
* recoverWrite and recoverTake calls. state is not written
* because it is always ACTIVE when we write, and always
* needs to be PREPARED when we read it back.
*/
out.writeObject(trm);
out.writeLong(trId);
}
// inherit doc comment
public void restore(ObjectInputStream in)
throws IOException, ClassNotFoundException
{
/* Only transactions that got prepared and not committed or
* aborted get recovered
*/
state = PREPARED;
trm = (StorableReference)in.readObject();
trId = in.readLong();
}
}