/* * 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.util.Hashtable; import java.util.Set; import java.util.WeakHashMap; import java.util.logging.Level; import java.util.logging.Logger; import net.jini.core.entry.UnusableEntryException; import net.jini.core.transaction.CannotJoinException; import net.jini.core.transaction.server.TransactionConstants; /** * <code>EntryHolder</code>s hold all the entries of a exact given * class. <code>OutriggerServerImpl</code> has one * <code>EntryHolder</code> for each entry class it knows about. A * simple implementation is used that simply stores the entries in a * list. * * @author Sun Microsystems, Inc. */ class EntryHolder implements TransactionConstants { /** The list that holds the handles */ private final FastList contents = new FastList(); /** * The map of cookies to handles, shared with the * <code>EntryHolderSet</code> and every other * <code>EntryHolder</code>. */ private final Hashtable idMap; /** The server we are working for */ private final OutriggerServerImpl space; /** Logger for logging information about entry matching */ private static final Logger matchingLogger = Logger.getLogger(OutriggerServerImpl.matchingLoggerName); /** Logger for logging information about iterators */ private static final Logger iteratorLogger = Logger.getLogger(OutriggerServerImpl.iteratorLoggerName); /** * Create a new <code>EntryHolder</code> with the shared * <code>idMap</code>, and which will hold classes of the given * <code>className</code>. The <code>idMap</code> is shared with * <code>EntryHolderSet</code> so that there is one table that can * map ID to <code>EntryRep</code> */ EntryHolder(OutriggerServerImpl space, Hashtable idMap) { this.space = space; this.idMap = idMap; } /** * Return an <code>EntryHandle</code> object that matches the given * template, or <code>null</code> if none does. Optionally * removes (perhaps provisionally) the found entry. * * @param tmplRep The template to match against * @param txnMgr If non-null the transaction (represented as * a <code>TransactableMgr</code> to perform * the operation under. May be <code>null</code> * if the operation is not to be done under * a transaction. * @param takeIt <code>true</code> if <code>hasMatch</code> should * remove the matching entry. * @param conflictSet If non-null the <code>TransactableMgr</code> * objects of any transactions that prevent * a non-null value from being retured will * be added to <code>conflictSet</code>. May * be <code>null</code> in which case * conflicting transaction will not be recorded. * This method assumes that any concurrent access * is being arbitrated by the set or by the caller. * @param lockedEntrySet If non-null the ID of any entries that * can't be retured because of conflicting * transaction will be added to * <code>lockedEntrySet</code>. May be * <code>null</code> in which case unavailable * entries will not be recorded. This method * assumes that any concurrent access is being * arbitrated by the set or by the caller. * @param provisionallyRemovedEntrySet If the entry can not be * read/taken because it has been provisionally * removed then its handle will be placed in the * passed <code>WeakHashMap</code> as a key (with * null as the value). May be <code>null</code> in * which case provisionally removed entries will not * be recorded. This method assumes that any * concurrent access is being arbitrated by the set * or by the caller. * @throws CannotJoinException if a match is found and * the operation is to be performed under a transaction, * but the transaction is no longer active. * @see #attemptCapture */ EntryHandle hasMatch(EntryRep tmpl, TransactableMgr txn, boolean takeIt, Set conflictSet, Set lockedEntrySet, WeakHashMap provisionallyRemovedEntrySet) throws CannotJoinException { matchingLogger.entering("EntryHolder", "hasMatch"); EntryHandle handle = (EntryHandle) contents.head(); if (handle == null) return null; // Nothing here final EntryHandleTmplDesc desc = EntryHandle.descFor(tmpl, handle.rep().numFields()); final long startTime = System.currentTimeMillis(); for (;handle != null; handle = (EntryHandle) handle.next()) { if (handle.removed()) continue; // Quick reject -- see the if handle mask is incompatible if ((handle.hash() & desc.mask) != desc.hash) continue; final EntryRep rep = handle.rep(); if (!tmpl.matches(rep)) continue; final boolean available = confirmAvailabilityWithTxn(rep, handle, txn, takeIt, startTime, conflictSet, lockedEntrySet, provisionallyRemovedEntrySet); if (available) return handle; } return null; } /** * Debug method: Dump out the state of this holder, printing out * the name of the dump first. */ void dump(String name) { try { System.out.println(name); for (EntryHandle handle = (EntryHandle) contents.head(); handle != null; handle = (EntryHandle) handle.next()) { EntryRep rep = handle.rep(); System.out.println(" " + rep + ", " + rep.entry()); } } catch (UnusableEntryException e) { e.printStackTrace(); } } /** * Atomically check to see if the passed entry can be read/taken by * the specified operation using the specified transaction and if * it can read/take it and return <code>true</code>, otherwise * return <code>false</code>. If the entry is removed. Note, * if the entry is removed, removal is logged. * @param entry The <code>EntryHandle</code> of the entry * the caller wants to read/take. * @param txn If non-null the transaction to perform * this operation under. Note, if non-null and * <code>txn</code> is not active <code>false</code> * will be returned. * @param takeIt <code>true</code> if the caller is trying * take the passed entry, <code>false</code> * otherwise. * @param conflictSet If non-null and the entry can not be * read/taken because of transaction conflicts the * conflicting transaction(s) will be added to this set. * This method assumes that any concurrent access is * being arbitrated by the set or by the caller. * @param lockedEntrySet If the entry can not be read/taken * because of a transaction conflict, the ID of the * entry will be added to this set. This method * assumes that any concurrent access is being arbitrated * by the set or by the caller. * @param provisionallyRemovedEntrySet If the entry can not be * read/taken because it has been provisionally * removed then its handle will be placed in the * passed <code>WeakHashMap</code> as a key (with * null as the value). May be <code>null</code> in * which case provisionally removed entries will not * be recorded. This method assumes that any * concurrent access is being arbitrated by the set * or by the caller. * @param now an estimate of the current time in milliseconds * since the beginning of the epoch. * @return <code>true</code> if the entry could be read/taken and * <code>false</code> otherwise. * @throws NullPointerException if entry is <code>null</code>. */ boolean attemptCapture(EntryHandle handle, TransactableMgr txn, boolean takeIt, Set conflictSet, Set lockedEntrySet, WeakHashMap provisionallyRemovedEntrySet, long now) { try { return confirmAvailabilityWithTxn(handle.rep(), handle, txn, takeIt, now, conflictSet, lockedEntrySet, provisionallyRemovedEntrySet); } catch (CannotJoinException e) { return false; } } /* Now that we know we have a match, make sure that the the * item in question is still in the space and hasn't been * subject to other transactional-interference... */ private boolean confirmAvailabilityWithTxn(EntryRep rep, EntryHandle handle, TransactableMgr txnMgr, boolean takeIt, long time, Set conflictSet, Set lockedEntrySet, WeakHashMap provisionallyRemovedEntrySet) throws CannotJoinException { // Now that we know we have a match, make sure that the the // item in question is still in the space and hasn't been // subject to other transactional-interference... // Lock the txn first (since that what everybody else does that) // to ensure that the locking always happens in A cannonical // order. We lock the transaction (txn) object to ensure that the // transaction hasn't been altered behind our backs. Other, cheaper // actions with the space (write, etc.) are handled nicely in // OutriggerServerImpl. But, this one (reading/taking) was expensive // enough that we wanted to do it here, where the window is // smallest. // Bug 4394263... final Txn txn = (Txn)txnMgr; try { if (txn != null) txn.ensureActive(); return confirmAvailability(rep, handle, txn, takeIt, time, conflictSet, lockedEntrySet, provisionallyRemovedEntrySet); } finally { if (txn != null) txn.allowStateChange(); } } /** * With the EntryRep <code>rep</code> passed in, verify that the * entry hasn't been taken by someone else, hasn't expired, etc. * Also, verify that the entry is really (legally) visible to this * transaction at this time. If this is a <code>take</code>, it it * is removed or provisionally removed. If this operation is under * a transaction, the entry is locked appropriately. * @see grab -- a helper routine */ private boolean confirmAvailability(EntryRep rep, EntryHandle handle, TransactableMgr txn, boolean takeIt, long time, Set conflictSet, Set lockedEntrySet, WeakHashMap provisionallyRemovedEntrySet) { if (handle.removed()) return false; synchronized (handle) { // get rid of stale entries if (isExpired(time, handle)) return false; if (handle.removed()) // oh, well -- someone got it first return false; if (handle.isProvisionallyRemoved()) { if (provisionallyRemovedEntrySet != null) provisionallyRemovedEntrySet.put(handle, null); return false; } int op = (takeIt ? TransactableMgr.TAKE : TransactableMgr.READ); if (!handle.canPerform(txn, op)) { // Before deciding this is a conflict, we have to // make sure we are not conflicting with ourselves. if (handle.onlyMgr(txn)) // We are just conflicting with ourselves, this entry // must have been taken by this txn already ... oh well return false; // Must be a real conflict, log if we were told too if (matchingLogger.isLoggable(Level.FINER)) { matchingLogger.log(Level.FINER, "match, but can''t " + "perform {0}; handle.knownMgr(txn) == {1}", new Object[] {new Integer(op), new Boolean(handle.knownMgr(txn))}); } if (conflictSet != null) { handle.addTxns(conflictSet); } if (lockedEntrySet != null) { lockedEntrySet.add(rep.id()); } return false; } if (grab(handle, txn, op, takeIt, false)) return true; else throw new AssertionError("entry became non-available while locked"); } } /** * Given an entry that we want to return as the result of a query * and we have confirmed we can return it, make the results of the * query visible to the rest of the service. We must either [a] * remove it from the holder (if the operation is a take under the * same transaction the entry was written under), [b] mark it as * provisionally removed pending commiting the operation to disk * (if the operation is a take without a transaction), [c] * lock it under the transaction passed into this method (if the * operation is a read or take under a transaction), or [d] do * nothing (read not under a transaction) as appropriate. * <p> * Also used during log recovery to recover takes. * * @param handle The handle attached to the particular EntryRep * @param txn The Txn object * @param op TAKE or READ (as a TransactableMgr constant) * @param takeIt Is this a TAKE (or is it [false] a READ) * @param recovery <code>true</code> if being called as * part of store recovery. * @return <code>true</code> if the entry could be grabbed. * @throws NullPointerException if <code>handle</code> is * <code>null</code>. */ private boolean grab(EntryHandle handle, TransactableMgr txn, int op, boolean takeIt, boolean recovery) { assert op==(takeIt?TransactableMgr.TAKE:TransactableMgr.READ); assert !recovery || takeIt; /* * If this manager (txn) is known to the handle, then txn * is a transaction which can operate directly on the * existing handle. If so, then we can operate on it * directly. Note that if txn is null and the handle is * not managed, then txn is "known" in this sense. * * Else, this operation is new relative to the * known transactions (possibly null) and txn. */ if (handle.knownMgr(txn)) { // added or read in this txn if taking // take it if we have to (otherwise it must be a read // there is no house keeping to be done since must already // be read locked) if (takeIt) { // is this an entry that was written under this // transaction or another? if (handle.managed()) { // It is locked by a transaction and it must // be the one this take is under. if (!handle.promoteToTakeIfNeeded()) { // this transaction wrote the entry (note txn // must be non-null, otherwise either // handle.managed or handle.knownMgr(txn) // would have returned null), just need to try // and remove it assert txn != null; // It is ok to call remove before committing this // op to disk since a crash will cause the original // write to be undone and the entry will still // end up being removed. if (!remove(handle, recovery)) // Someone got to it first return false; } // Otherwise it was read locked (now take // locked and needs to still be semi-visible // to ifExists queries outside of the // transaction, leave it in place with its new // lock type (aka "state") } else { // Nether the entry nor this take are under a // transaction (txn must be null because the // that is the only way knownMgr return true, and // managed() false) assert txn == null; if (handle.removed() || handle.isProvisionallyRemoved()) // Someone got to it first return false; handle.provisionallyRemove(); } } } else { if (txn != null) { // We need to add this txn to this handle and // add this handle to this txn. handle.add(txn, op, this); // add manager to handle's list txn.add(handle); // add handle to mgr's list } } return true; } /** * Recover a logged take. * @param handle The <code>EntryHandle</code> of the entry who's * take is being logged. * @param txn If non-null the transaction the take was performed * under. * @throws NullPointerException if <code>handle</code> is * <code>null</code>. */ void recoverTake(EntryHandle handle, Txn txn) { if (!grab(handle, txn, TransactableMgr.TAKE, true, true)) throw new AssertionError("match not found while recovering take"); } /** * Return <code>true</code> if the entry held by the given * <code>handle</code> has expired by the time in <code>now</code>. */ private boolean isExpired(long now, EntryHandle handle) { /* Some callers already own the lock on handle when * they call us, but the general feeling is that * re-synchronizing is not costly and it is important * to only call remove if we own the lock. */ synchronized (handle) { final EntryRep rep = handle.rep(); synchronized (rep) { if (rep.getExpiration() > now) { // Not expired return false; } /* Expired, set expiration to before the beginning of * time so renew() (which does not lock on the handle, * but the rep...) can't renew the lease once we leave * this block, even it already has a ref to the * rep. Everyone else locks on the handle so * and makes sure the handle has not been removed * before doing anything important. */ rep.setExpiration(Long.MIN_VALUE); } // If we are here the lease must have expired if (matchingLogger.isLoggable(Level.FINER)) { matchingLogger.log(Level.FINER, "expired {0} at {1} (now {2})", new Object[] {rep.id(), new Long(rep.getExpiration()), new Long(now)}); } if (!handle.isProvisionallyRemoved() && remove(handle, false)) { /* If we got to do the remove, schedule * logging of the removal, otherwise * someone else already did it, or will do it */ space.scheduleCancelOp(rep.id()); } return true; } } /** * Add new new entry to the holder. Assumes the lock * on the handle is held if there is a possibility * of concurrent access. * @param handle The <code>EntryHandle</code> for the * entry being added. * @param txn If the add is being done under a * transaction the <code>TransactableMgr</code> for * that transaction. * @throws NullPointerException if <code>handle</code> is * <code>null</code>. */ void add(EntryHandle handle, TransactableMgr txn) { final EntryRep rep = handle.rep(); /* Make sure info duplicated across all the handles in this * holder is shared. * * Because this thread will usually hold the lock on handle, * we must call contents.head before calling contents.add * otherwise during the head call this thread will attempt to * obtain a lock on a 2nd FastList node in the same FastList, * and make that attempt in the wrong order. In particular * this could lead to deadlocks when SimpleRepEnum or * ContinuingQuery call into FastList.Node.restart ( restart * locks the node its called on (which can be the same node * head locks) and then tries to lock the tail (which can be * handle if add has already been called)). By calling head * before calling add we have locks on two FastList.Node * objects, but they are not in the same list so we are ok. * * We make the head call before calling txn.add because * it seems like better hygiene to fix reps state before * exposing it to others (even though shareWith will * not change handle's state materially). */ final EntryHandle head = (EntryHandle) contents.head(); if (head != null && head != handle) rep.shareWith(head.rep()); if (txn != null) txn.add(handle); contents.add(handle); idMap.put(rep.getCookie(), handle); } /** * Return an array of the class names of the super classes of the * entries in this holder, or <code>null</code> if the holder is * empty. */ String[] supertypes() { final EntryHandle head = (EntryHandle)contents.head(); if (head == null) return null; return head.rep().superclasses(); } /** * Return an enumerator over the contents of this space that are visible * from the given mgr. */ RepEnum contents(TransactableMgr mgr) { return new SimpleRepEnum(mgr); } /** * The class that implements <code>RepEnum</code> for this class. */ private class SimpleRepEnum implements RepEnum { /** The last node we saw */ private EntryHandle handle; private TransactableMgr mgr; private long startTime; int count = 0; SimpleRepEnum(TransactableMgr mgr) { this.mgr = mgr; handle = (EntryHandle) contents.head(); startTime = System.currentTimeMillis(); } // inherit doc comment from superclass public EntryRep nextRep() { iteratorLogger.entering("SimpleRepEnum", "nextRep"); /* Since we might be accessing our iteration from a * different thread than the one that began the traversal, * we need to inform the FastList that this thread should * be allowed to assume that traversal without throwing * an exception. FastList must therefore update its data * structures to accommodate the traversal from this new * thread. [It would be better if did this once per a thread...] */ if (handle != null) handle.restart(); /* Skip over handles which are either removed or unable * to perform a READ operation. */ while (handle != null && (!handle.canPerform(mgr, TransactableMgr.READ) || isExpired(startTime, handle) || handle.removed() )) { handle = (EntryHandle) handle.next(); iteratorLogger.log(Level.FINEST, "advanced current handle to {0}", handle); } if (handle == null) return null; EntryHandle h = handle; handle = (EntryHandle) h.next(); return h.rep(); } } /** * Return an object that can be used to perform a query that can * return multiple matches and be restarted in another thread. * * @param tmpls An array of templates. Query will yield any * entry that matches one or more of the templates. * @param txnMgr Transaction that should be used with the query. * May be <code>null</code>. If * non-<code>null</code> any entries yielded by the * query will be locked under the transaction. * @param takeThem If <code>true</code> any entries yielded by * the query should be removed. * @param now Estimate of current time used to weed out * expired entries, ok if old * @return a new ContinuingQuery object. */ ContinuingQuery continuingQuery(EntryRep[] tmpls, TransactableMgr txn, boolean takeThem, long now) { return new ContinuingQuery(tmpls, txn, takeThem, now); } /** * Object that can be used to perform a query that can * return multiple matches and be restarted in another thread. * Assumes that is being invoked from only one thread * at a time, but does handle synchronization between concurrent * queries on the parent <code>EntryHolder</code>. */ class ContinuingQuery { /** Templates being used for the query */ final private EntryRep[] tmpls; /** Transaction (if any) being used for the query */ final private TransactableMgr txn; /** * <code>true</code> if entries yielded by the query should * removed. */ final private boolean takeThem; /** <code>EntryHandleTmplDesc</code> for the templates */ final private EntryHandleTmplDesc[] descs; /** Time used to weed out expired entries, ok if old */ long now; /** * Current position in parent <code>EntryHolder</code>'s * <code>contents</code> */ private EntryHandle handle; /** * Create a new <code>ContinuingQuery</code> object. * * @param tmpls An array of templates. Query will yield any * entry that matches one or more of the templates. * @param txnMgr Transaction that should be used with the query. * May be <code>null</code>. If * non-<code>null</code> any entries yielded by the * query will be locked under the transaction. * @param takeThem If <code>true</code> any entries yielded by * the query should be removed. * @param now Estimate of current time used to weed out * expired entries, ok if old * @return a new ContinuingQuery object. */ private ContinuingQuery(EntryRep[] tmpls, TransactableMgr txn, boolean takeThem, long now) { this.tmpls = tmpls; this.txn = txn; this.takeThem = takeThem; this.now = now; handle = (EntryHandle)contents.head(); if (handle == null) { // nothing to search - first next will return null descs = null; // keep compiler happy return; } descs = new EntryHandleTmplDesc[tmpls.length]; for (int i=0; i<tmpls.length; i++) { descs[i] = EntryHandle.descFor(this.tmpls[i], handle.rep().numFields()); } } /** * <code>EntryHolder</code> queries have thread local state * that get clobbered if another query (on a different or same * <code>EntryHolder</code>, including <code>hasMatch</code> * calls) is started in the same thread. The state also needs * to be restored if it the query is continued in another * thread. The <code>restart</code> method must be called to * restore the thread local state. * @param now Estimate of current time used to weed out * expired entries, ok if old */ void restart(long now) { if (handle == null) return; this.now = now; handle.restart(); } /** * Return the next matching entry. Returns <code>null</code> * if there are no matches remaining. Call {@link #restart * restart} first if query is being used in a different thread * from the last <code>next</code> call and/or the current * thread has done any form of <code>EntryHolder</code> query * on any holder since the last next call. * * @param conflictSet If non-null the <code>TransactableMgr</code> * objects of any transactions that prevent * a entry from being retured will * be added to <code>conflictSet</code>. May * be <code>null</code> in which case * conflicting transaction will not be recorded. * This method assumes that any concurrent access * is being arbitrated by the set or by the caller. * @param lockedEntrySet If non-null the ID of any entries that * can't be retured because of conflicting * transaction will be added to * <code>lockedEntrySet</code>. May be * <code>null</code> in which case unavailable * entries will not be recorded. This method * assumes that any concurrent access is being * arbitrated by the set or by the caller. * @param provisionallyRemovedEntrySet If the entry can not be * read/taken because it has been provisionally * removed then its handle will be placed in the * passed <code>WeakHashMap</code> as a key (with * null as the value). May be <code>null</code> in * which case provisionally removed entries will not * be recorded. This method assumes that any * concurrent access is being arbitrated by the set * or by the caller. * @return a matching entry or <code>null</code>. * @throws CannotJoinException if a match is found and * the operation is to be performed under a transaction, * but the transaction is no longer active. */ EntryHandle next(Set conflictSet, Set lockedEntrySet, WeakHashMap provisionallyRemovedEntrySet) throws CannotJoinException { matchingLogger.entering("ContinuingQuery", "next"); if (handle == null) return null; // done for (; handle != null; handle = (EntryHandle)handle.next()) { if (handleMatch()) { final boolean available = confirmAvailabilityWithTxn(handle.rep(), handle, txn, takeThem, now, conflictSet, lockedEntrySet, provisionallyRemovedEntrySet); if (available) { // Don't want to return this guy twice. final EntryHandle rslt = handle; handle = (EntryHandle)handle.next(); return rslt; } } } return null; } /** * Returns <code>true</code> if handle has not been removed * and matches one or more of the templates */ private boolean handleMatch() { if (handle.removed()) return false; for (int i=0; i<tmpls.length; i++) { final EntryRep tmpl = tmpls[i]; final EntryHandleTmplDesc desc = descs[i]; // Quick reject if ((handle.hash() & desc.mask) != desc.hash) continue; if (!tmpl.matches(handle.rep())) continue; return true; } return false; } } /** * Remove the given handle from this holder and the <code>idMap</code>. * If the handle isn't in this holder, this does nothing. * @param h the <code>EntryHandle</code> to remove. * @param recovery <code>true</code> if being called as part * of log recovery. * @return <code>true</code> if this call removed <code>h</code> and * <code>false</code> otherwise. */ boolean remove(EntryHandle h, boolean recovery) { assert (recovery || Thread.holdsLock(h)); final boolean ok = contents.remove(h); h.removalComplete(); if (ok) { idMap.remove(h.rep().getCookie()); /* This may cause an ifExists query to be resolved, * even if this entry was not locked under a transaction */ if (!recovery) { space.recordTransition( new EntryTransition(h, null, false, false, false)); } } return ok; } /** * Return the handle for the given <code>EntryRep</code> object. * This is done via lookup in the <code>idMap</code>. The * <code>idMap</code> is doing double duty here. The * <code>LeaseDesc</code> associated with an <code>EntryRep</code> * is also the rep's <code>EntryHandle</code>. */ private EntryHandle handleFor(EntryRep rep) { return (EntryHandle) idMap.get(rep.getCookie()); } /** * Reap the expired elements (and the underlying FastList) */ void reap() { // Examine each of the elements within the FastList to determine if // any of them have expired. If they have, ensure that they are // removed ("reaped") from the collection. long now = System.currentTimeMillis(); for (EntryHandle handle = (EntryHandle) contents.head(); handle != null; handle = (EntryHandle) handle.next()) { // Don't try to remove things twice if (handle.removed()) { continue; } // Calling isExpired() will both make the check and remove it // if necessary. isExpired(now, handle); } // This provides the FastList with an opportunity to actually // excise the items identified as "removed" from the list. contents.reap(); } }