/*
* 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.Set;
import java.util.WeakHashMap;
import net.jini.core.transaction.TransactionException;
import net.jini.space.InternalSpaceException;
/**
* Subclass of <code>QueryWatcher</code> for and transactional
* <code>readIfExists</code> queries. Resolves with the first
* matching transition where the entry is visible to the associated
* transaction and the entry is still available, or of the locked
* entry set goes empty.
*/
class TransactableReadIfExistsWatcher extends SingletonQueryWatcher
implements IfExistsWatcher, Transactable
{
/**
* The set of entries that would match but are currently
* unavailable (e.g. they are locked). We only keep
* the ids, not the entries themselves.
*/
private final Set lockedEntries;
/**
* Set <code>true</code> once the query thread is
* done processing the backlog. Once this is
* <code>true</code> it is ok to resolve if
* <code>lockedEntries</code> is empty.
*/
private boolean backlogFinished = false;
/**
* The transaction this query is
* being performed under.
*/
private final Txn txn;
/**
* Set of entries (represented by <code>EntryHolder</code>s) that
* we would have liked to return, but have been provisionally
* removed.
*/
private final WeakHashMap provisionallyRemovedEntrySet;
/**
* Create a new <code>TransactableReadIfExistsWatcher</code>.
* @param expiration the initial expiration time
* for this <code>TransitionWatcher</code> in
* milliseconds since the beginning of the epoch.
* @param timestamp the value that is used
* to sort <code>TransitionWatcher</code>s.
* @param startOrdinal the highest ordinal associated
* with operations that are considered to have occurred
* before the operation associated with this watcher.
* @param lockedEntries Set of entries (by their IDs)
* that match but are unavailable. Must be non-empty.
* Keeps a reference to this object.
* @param provisionallyRemovedEntrySet If the watcher encounters
* an entry that can not be read/taken because it has been
* provisionally removed then its handle will be placed in
* this <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. Ensures that object is only accessed by one
* thread at a time
* @param txn If the query is being performed under
* a transaction the <code>Txn</code> object
* associated with that transaction.
* @throws NullPointerException if <code>lockedEntries</code> or
* <code>txn</code> is <code>null</code>.
*/
TransactableReadIfExistsWatcher(long expiration, long timestamp,
long startOrdinal, Set lockedEntries,
WeakHashMap provisionallyRemovedEntrySet, Txn txn)
{
super(expiration, timestamp, startOrdinal);
if (lockedEntries == null)
throw new NullPointerException("lockedEntries must be non-null");
if (txn == null)
throw new NullPointerException("txn must be non-null");
this.lockedEntries = lockedEntries;
this.txn = txn;
this.provisionallyRemovedEntrySet = provisionallyRemovedEntrySet;
}
boolean isInterested(EntryTransition transition, long ordinal) {
/* If we are unresolved pretty much all transitions are
* interesting because we may need to update
* lockedEntries. The only exception is read locks being
* resolved. It is important that transitions triggered by the
* release of read locks get filtered out - otherwise process
* could end up adding and removing elements to lockedEntries
* when it shouldn't.
*
* Note, !isResolved() without the lock will result only in
* false positives, not false negatives - it will only
* cause isInterested() to return false if we are resolved,
* we may still return true if we are resolved though.
*/
if (!transition.isVisible() && transition.isAvailable()) {
/* must be a transition triggered by a read lock release,
* wont change anything so ignore it.
*/
return false;
}
return (ordinal>startOrdinal) && !isResolved();
}
synchronized void process(EntryTransition transition, long now) {
if (isResolved())
return; // Already done.
final EntryHandle handle = transition.getHandle();
final EntryRep rep = handle.rep();
final boolean isVisible = transition.isVisible();
final TransactableMgr transitionTxn = transition.getTxn();
/* If it at one time it was available to our transaction
* it may still be, try to get it.
*/
if (isVisible &&
((null == transitionTxn) || txn == transitionTxn)) {
/* Is it still available? */
if (getServer().attemptCapture(handle, txn, false, null,
provisionallyRemovedEntrySet, now, this))
{
// Got it
resolve(handle, null);
} else {
/* Must not have been able to get it. Either
* locked under a conflicting lock, in which
* case it should be in our lockedEntries set, or
* it has been removed, in which it still needs
* to be in our lockedEntries since it may have
* been replaced before being removed.
*/
lockedEntries.add(rep.id());
}
} else if (isVisible) { // but it is not visible to txn
/* If we are here then at one time it must have been was
* visible but not visible to us, implying that it was an
* entry written under a transaction and is interesting to
* us, but not yet visible. We need to add it lockedEntries
* even if has been removed since it could have gotten
* replaced before it was removed. If we did not
* add it we would be acting on the future.
*/
lockedEntries.add(rep.id());
} else {
/* Must not be available, transition must mark
* the resolution of the transaction in such away
* that the entry has been removed, remove it
* from the set and see if that makes the set empty.
*/
lockedEntries.remove(rep.id());
if (backlogFinished && lockedEntries.isEmpty())
resolve(null, null);
}
}
synchronized boolean catchUp(EntryTransition transition, long now) {
if (isResolved())
return true; // Already done.
final EntryHandle handle = transition.getHandle();
final EntryRep rep = handle.rep();
final boolean isVisible = transition.isVisible();
final TransactableMgr transitionTxn = transition.getTxn();
/* Was this the resolution of a read lock? if so ignore */
if (!isVisible && transition.isAvailable())
return false;
/* If it at one time it was available to our transaction
* it may still be, try to get it.
*/
if (isVisible &&
((null == transitionTxn) || txn == transitionTxn)) {
/* Is it still visible? Try to get it. attemptCapture will
* add the entry to lockedEntries for us if we could not
* get it and it is still in the space (but locked).
* Nothing will be added if it has been removed outright.
* This is ok even though we are peaking into the future -
* we won't act on that information until the future
* comes to pass.
*/
if (getServer().attemptCapture(handle, txn, false,
lockedEntries, provisionallyRemovedEntrySet, now, this))
{
// Got it
resolve(handle, null);
return true;
}
// did not resolve
return false;
}
if (isVisible) { // but it is not visible to txn
/* If we are here then at one time it must have been was
* visible but not visible to us, implying that it was
* an entry written under a transaction and is interesting
* to us, but not yet visible. We only add if it has not
* already been removed. It might have gotten replaced
* before removal, but since we won't let this query get
* resolved with a definitive null before we get past the
* point in the journal where it was removed it is ok to
* never put it in (and if we did put it in it might never
* get removed since process() may have already processed
* the removal). We don't need to check to see it the
* entry has been provisionally removed since if has been
* provisional removal does not put in entries in the
* journal and if it is provisionally removed it has not
* yet been removed so the remove recored has not yet been
* created (must less processed).
*/
synchronized (handle) {
if (!handle.removed()) {
lockedEntries.add(rep.id());
}
/* If it has been removed, there is no way it
* will be interesting to us again, ever.
*/
}
// Either way, still not resolved.
return false;
}
/* Must not be visible (and because of the first test can't be
* available either - that is transition can't just be the
* release of a read lock), transition must mark the
* resolution of the transaction in such away that the entry
* has been removed, remove it from the set (don't need to
* check for empty because we haven't gotten to the point
* where we can resolve with a definitive null.)
*/
lockedEntries.remove(rep.id());
return false;
}
/**
* Once the backlog is complete we can resolve if
* lockedEntries is/becomes empty.
*/
public synchronized void caughtUp() {
backlogFinished = true;
if (isResolved())
return; // Don't much mater.
if (lockedEntries.isEmpty())
resolve(null, null);
}
public synchronized boolean isLockedEntrySetEmpty() {
if (!isResolved())
throw new IllegalStateException("Query not yet resolved");
return lockedEntries.isEmpty();
}
/**
* If a transaction ends in the middle of a query we want
* to throw an exception to the client making the query
* not the <code>Txn</code> calling us here.)
*/
public synchronized int prepare(TransactableMgr mgr,
OutriggerServerImpl space)
{
// only throw an exception if we are not resolved.
if (!isResolved()) {
// Query still in progress, kill it
resolve(null, new TransactionException("completed while " +
"operation in progress"));
}
// If this object has made changes they have been recorded elsewhere
return NOTCHANGED;
}
/**
* This should never happen since we always return
* <code>NOTCHANGED</code> from <code>prepare</code>.
*/
public void commit(TransactableMgr mgr, OutriggerServerImpl space) {
throw new InternalSpaceException("committing a blocking query");
}
/**
* If a transaction ends in the middle of a query we want
* to throw an exception to the client making the query
* (not the <code>Txn</code> calling us here.)
*/
public void abort(TransactableMgr mgr, OutriggerServerImpl space) {
// prepare does the right thing, and should forever
prepare(mgr, space);
}
}