/*
* 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 java.io.IOException;
import java.rmi.RemoteException;
import java.util.Map;
import java.util.List;
import java.util.Iterator;
import java.util.logging.Logger;
import net.jini.core.transaction.server.TransactionManager;
import net.jini.core.transaction.server.ServerTransaction;
import net.jini.security.ProxyPreparer;
import com.sun.jini.logging.Levels;
/**
* Keeps a mapping from {@link TransactionManager}/id pairs, to
* {@link Txn} objects. Some <code>Txn</code>s may be
* <i>broken</i>, that is the <code>TransactionManager</code> they are
* associated with can not be unmarshalled and prepared, and thus
* can't currently be used (but may be usable in the future).
*
* @author Sun Microsystems, Inc.
* @since 2.0
*/
class TxnTable {
/**
* Key class for the primary map (from manager/id pairs to
* <code>Txn</code>s. We use a new class instead of
* <code>ServerTransaction</code> objects so we can make
* sure we don't call <code>equals</code> on unprepared
* managers.
*/
private class Key {
/** The manager for the transaction */
private final TransactionManager manager;
/** The id for the transaction */
private final long id;
/**
* True if it has been asserted that <code>manager</code> has been
* prepared. Note, we only put Keys in the table that have
* this flag set.
*/
private final boolean prepared;
/**
* Create a new key from the specified manager and id.
* @param manager the manager for the transaction
* @param id the id for the transaction
* @param prepared should be <code>true</code> if the manager has
* been prepared and false otherwise
* @throws NullPointerException if manager is <code>null</code>.
*/
private Key(TransactionManager manager, long id, boolean prepared) {
if (manager == null)
throw new NullPointerException("manager must be non-null");
this.manager = manager;
this.id = id;
this.prepared = prepared;
}
// Inherit doc from super
public int hashCode() {
return (int)id ^ manager.hashCode();
}
// Inherit doc from super
public boolean equals(Object other) {
if (!(other instanceof Key))
return false;
final Key o = (Key)other;
if (id != o.id)
return false;
/* Either this or o should have prepared == true, call
* manager.equals on the one who has been prepared.
*/
if (o.prepared)
// Common case, usually the passed in object is in the map
return o.manager.equals(manager);
else if (prepared)
return manager.equals(o);
else
throw new
AssertionError("TxnTable.Key equals call with two " +
"unprepared managers");
}
}
/**
* Map of manager,id pairs (represented as <code>Keys</code>s)
* to non-broken <code>Txn</code>s. Only <code>Key</code>s that
* have their prepared flag set should go in this Map.
*/
final private Map txns = new java.util.HashMap();
/**
* Map of transaction ids to the <code>List</code> of broken
* <code>Txn</code> objects that have the id. <code>null</code>
* if there are no broken <code>Txn</code>s.
*/
private Map brokenTxns = null;
/**
* <code>ProxyPreparer</code> to use when unpacking
* transactions, may be <code>null</code>.
*/
private final ProxyPreparer proxyPreparer;
/** An array of type <code>Txn</code> to pass to <code>toArray</code> */
private static final Txn[] txnArray = new Txn[0];
/** The logger to use */
static private final Logger logger =
Logger.getLogger(OutriggerServerImpl.txnLoggerName);
/**
* Create a new <code>TxnTable</code>.
* @param proxyPreparer the proxy preparer to use
* on recovered <code>TransactionManager</code>s.
*/
TxnTable(ProxyPreparer proxyPreparer) {
this.proxyPreparer = proxyPreparer;
}
/**
* Given a <code>TransactionManager</code>, <code>manager</code>,
* and a transaction id, return the associated <code>Txn</code>,
* or <code>null</code> if there is no <code>Txn</code> for
* this manager/id pair. The only method this method will
* call on <code>manager</code> is <code>hashCode</code>.
* The returned <code>Txn</code> object will not be broken.
*
* @param manager a <code>TransactionManager</code>.
* @param id a transaction id.
* @return the <code>Txn</code> for the specified pair
* or null if none can be found.
* @throws IOException if there was one or more
* possible matches, but they are broken
* and the attempt to unpack them yielded
* an <code>IOException</code>.
* @throws ClassNotFoundException if there was one or more
* possible matches, but they are broken
* and the attempt to unpack them yielded
* a <code>ClassNotFoundException</code>.
* @throws SecurityException if there was one or more
* possible matches, but they are broken
* and the attempt to unpack them yielded
* a <code>SecurityException</code>.
*/
Txn get(TransactionManager manager, long id)
throws IOException, ClassNotFoundException
{
final Long idAsLong;
final Txn brokenTxnsForId[];
// Try the table of non-broken txns first
synchronized (this) {
final Txn r = (Txn)txns.get(new Key(manager, id, false));
if (r != null)
return r;
// Check broken txns
if (brokenTxns == null)
// No broken Txns so txns is definitive
return null;
idAsLong = new Long(id);
final List txnsForId = (List)brokenTxns.get(idAsLong);
if (txnsForId == null)
/* Broken Txns, but none with the right ID
* so txns is definitive for this manager/id pair.
*/
return null;
/* If we are here there are broken Txns with the
* specified id, we need to try and fix each one
* and check to see if there is a match. Make
* a copy of the list so we don't need to
* hold a lock while making fix attempts (which
* in general involve remote communications).
*/
brokenTxnsForId = (Txn[])txnsForId.toArray(txnArray);
}
/* try and fix each element of brokenTxnsForId until
* we find the right Txn, or until we run out of elements
*/
Txn match = null;
Throwable t = null; // The first throwable we get, if any
final List fixed = new java.util.LinkedList(); // fixed Txns
for (int i=0; i<brokenTxnsForId.length; i++) {
try {
final ServerTransaction st =
brokenTxnsForId[i].getTransaction(proxyPreparer);
/* We fixed a Txn, remember so we can move it
* to txns.
*/
fixed.add(brokenTxnsForId[i]);
/* Did this match? Pass unprepared manager to
* prepared one!
*/
if (st.mgr.equals(manager)) {
// bingo!
match = brokenTxnsForId[i];
break;
}
} catch (Throwable tt) {
if (logger.isLoggable(Levels.FAILED))
logger.log(Levels.FAILED, "Encountered " + tt + "while " +
"recovering/re-preparing transaction, will retry" +
"latter", tt);
if (t == null)
t = tt;
}
}
/* Now that all the remote operations are done, re-acquire lock
* so we can move anything that was fixed.
*/
if (!fixed.isEmpty()) {
synchronized (this) {
if (brokenTxns != null) {
final List txnsForId = (List)brokenTxns.get(idAsLong);
if (txnsForId != null) {
// Remove from brokenTxns
txnsForId.removeAll(fixed);
// can we get rid of txnsForId and brokenTxns?
if (txnsForId.isEmpty()) {
brokenTxns.remove(idAsLong);
if (brokenTxns.isEmpty())
brokenTxns = null;
}
// Put in txns
for (Iterator i=fixed.iterator(); i.hasNext();) {
put((Txn)i.next());
}
} else {
/* if the list for this id is gone the fixed Txns
* must have already been moved by someone else
*/
}
} else {
/* if brokenTxns is gone the fixed Txns must have already
* been moved by someone else
*/
}
}
}
// Did we run out of candidates, or did we find a match?
if (match != null)
return match;
/* Was there some Txn that may have been a match
* but is still broken so we don't know?
* [this could happen if :
* o the managers codebase changed so
* old proxies can't be unmarshalled (but new ones can),
* o the log is corrupted,
* o a bad preparer has been supplied,
* o etc., etc.
* ]
*/
if (t != null) {
/* We may have some recored of this manager/id pair,
* but we can't tell since one or more of the broken
* Txns that could be a match could not be fixed.
* Throw an exception to indicate an ambiguous result.
*/
if (t instanceof IOException)
throw (IOException)t;
else if (t instanceof ClassNotFoundException)
throw (ClassNotFoundException)t;
else if (t instanceof Error)
throw (Error)t;
else if (t instanceof RuntimeException)
throw (RuntimeException)t;
else
throw new AssertionError(t);
}
/* If we are here there were one or more
* broken Txns with the same ID, but all them
* could be fixed and none of them had a manager that
* matched. Definitively return null, we have no
* record of this manager/id pair.
*/
return null;
}
/**
* Atomically test if there is a <code>Txn</code> for the
* specified <code>ServerTransaction</code> in the table, creating
* a new <code>Txn</code>, and place in table if there is not. If
* there is already a <code>Txn</code> for the specified
* <code>ServerTransaction</code> return the existing one,
* otherwise return the new one. Does not check for matches
* against broken txns.
* @param tr <code>ServerTransaction</code> to add to the table.
* The contained manager proxy should already been prepared
* @return the <code>Txn</code> for <code>tr</code>.
*/
Txn put(ServerTransaction tr) {
final Key k = new Key(tr.mgr, tr.id, true);
final long internalID = OutriggerServerImpl.nextID();
// Atomic test and set
synchronized (this) {
Txn r = (Txn)txns.get(k);
if (r == null) {
// not in table, put a new one in and return it.
r = new Txn(tr, internalID);
txns.put(k, r);
}
return r;
}
}
/**
* Used to put a formally broken <code>Txn</code> in the main table.
* Only puts it in the table if it is not already their.
* @param txn the <code>Txn</code> being moved, should have
* been prepared
*/
private void put(Txn txn) {
final Key k = new Key(txn.getManager(), txn.getTransactionId(), true);
synchronized (this) {
final Txn r = (Txn)txns.get(k);
if (r == null) {
txns.put(k, txn);
}
}
}
/**
* Restore a <code>Txn</code> in the table as
* part of log recovery. An attempt will be made
* unpack and prepare the transaction manager.
* The <code>Txn</code> may be broken.
* This method is not synchronized.
* @param txn the <code>Txn</code> being recovered.
*/
void recover(Txn txn) {
// Try to get the transaction
ServerTransaction st = null;
try {
st = txn.getTransaction(proxyPreparer);
} catch (Throwable t) {
// log, but otherwise ignore
if (logger.isLoggable(Levels.FAILED))
logger.log(Levels.FAILED, "Encountered " + t + " while " +
"recovering/re-preparing transaction, will retry latter",
t);
}
if (st == null) {
// txn is broken, put in brokenTxns
if (brokenTxns == null)
brokenTxns = new java.util.HashMap();
final Long id = new Long(txn.getTransactionId());
List txnsForId = (List)brokenTxns.get(id);
if (txnsForId == null) {
txnsForId = new java.util.LinkedList();
brokenTxns.put(id, txnsForId);
}
txnsForId.add(txn);
} else {
// Txn is ok, put it in txns
final Key k = new Key(st.mgr, st.id, true);
txns.put(k, txn);
}
}
/**
* Remove the mapping for the given
* <code>TransactionManager</code>, id pair. Will
* not remove broken <code>Txn</code>s.
* @param manager the <code>TransactionManager</code> for
* transaction being removed.
* @parma id the manager assigned to the transaction being
* removed.
*/
void remove(TransactionManager manager, long id) {
final Key k = new Key(manager, id, false);
synchronized (this) {
txns.remove(k);
}
}
}