/*
* 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.jeri.internal.runtime;
import com.sun.jini.action.GetLongAction;
import com.sun.jini.thread.NewThreadAction;
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
import java.rmi.ConnectException;
import java.rmi.ConnectIOException;
import java.rmi.NoSuchObjectException;
import java.rmi.RemoteException;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
/**
* AbstractDgcClient implements the client-side behavior of RMI's
* distributed garbage collection system abstractly with respect to
* the types used to represent transport endpoints, object IDs, and
* live remote references. The actual types used for endpoints,
* object IDs, and live references depends on the concrete subclass.
*
* The entry point into the machinery of AbstractDgcClient is the
* "registerRefs" method: when a live reference enters the scope of
* this AbstractDgcClient (the current virtual machine, for example),
* it should be registered with that method in order for it to
* participate in distributed garbage collection.
*
* When the first live reference to a particular remote object is
* registered, a "dirty" call is made to the server-side distributed
* garbage collector at the remote object's endpoint, which, if
* successful, returns a lease guaranteeing that the server-side DGC
* will not collect the remote object for a certain period of time.
* While live references to remote objects at a particular endpoint
* exist, this AbstractDgcClient will continue to make more "dirty"
* calls to renew its lease on the referenced remote objects.
*
* This AbstractDgcClient tracks the local reachability of registered
* live references (using phantom references). When all of the live
* reference instances for a particular remote object become garbage
* collected locally, a "clean" call is made to the server-side
* distributed garbage collector, indicating that the server no longer
* needs to keep the remote object alive for this client.
*
* Internally, AbstractDgcClient holds and manipulates transport
* endpoints, object IDs, and live references with references of type
* java.lang.Object; it is assumed that their actual classes define
* "equals" and "hashCode" in a meaningful way.
*
* Concrete subclasses must provide additional behavior for the actual
* endpoint, object ID, and live reference types by implementing the
* abstract protected methods of this class (see below). In
* particular, the "getDgcProxy" method should return an object that
* implements the actual protocol for DGC "dirty" and "clean" calls
* for the endpoint type being used, the "freeEndpoint" method may
* make use of the indication that a particular endpoint no longer has
* references, and the "getRefEndpoint" and "getRefObjectID" methods
* should return the endpoint and object ID contained in a particular
* live reference object. A concrete subclass should also provide a
* type-safe equivalent of "registerRefs" that delegates to this
* class's "registerRefs" method.
*
* @author Sun Microsystems, Inc.
**/
abstract class AbstractDgcClient {
/** lease duration to request (usually ignored by server) */
private static final long leaseValue = // default 10 minutes
((Long) AccessController.doPrivileged(new GetLongAction(
"com.sun.jini.jeri.dgc.leaseValue", 600000)))
.longValue();
/** maximum interval between retries of failed clean calls */
private static final long cleanInterval = // default 3 minutes
((Long) AccessController.doPrivileged(new GetLongAction(
"com.sun.jini.jeri.dgc.cleanInterval", 180000)))
.longValue();
/** minimum lease duration that we bother to honor */
private static final long minimumDuration = // default 5 seconds
((Long) AccessController.doPrivileged(new GetLongAction(
"com.sun.jini.jeri.dgc.minimumDuration", 5000)))
.longValue();
/** minimum retry count for dirty calls that fail */
private static final int dirtyFailureRetries = 5;
/** retry count for clean calls that fail with ConnectException */
private static final int cleanConnectRetries = 3;
/** constant empty Object array for lease renewal optimization */
private static final Object[] emptyObjectArray = new Object[0];
/** next sequence number for DGC calls (access synchronized on class) */
private static long nextSequenceNum = Long.MIN_VALUE;
/**
* endpoint table: maps generic endpoint to EndpointEntry
* (lock guards endpointTable)
*/
private final Map endpointTable = new HashMap(5);
protected AbstractDgcClient() {
}
/**
* A DgcProxy is a proxy for invoking DGC operations on a server-side
* DGC implementation at a particular endpoint. A DgcProxy instance
* for a given endpoint is obtained from concrete class (using the
* getDgcProxy method) and used by the abstract implementation.
*/
protected interface DgcProxy {
long dirty(long sequenceNum, Object[] ids, long duration)
throws RemoteException;
void clean(long sequenceNum, Object[] ids, boolean strong)
throws RemoteException;
}
/** Returns a proxy for making DGC calls to the given endpoint. */
protected abstract DgcProxy getDgcProxy(Object endpoint);
/** Indicates that resources for the given endpoint may be freed. */
protected abstract void freeEndpoint(Object endpoint);
/** Returns the endpoint in the given live reference. */
protected abstract Object getRefEndpoint(Object ref);
/** Returns the object ID in the given live reference. */
protected abstract Object getRefObjectID(Object ref);
/**
* Registers the live reference instances in the supplied collection to
* participate in distributed garbage collection.
*
* All of the live references in the list must be for remote objects at
* the given endpoint.
*/
protected final void registerRefs(Object endpoint, Collection refs) {
/*
* Look up the given endpoint and register the refs with it.
* The retrieved entry may get removed from the global endpoint
* table before EndpointEntry.registerRefs() is able to acquire
* its lock; in this event, it returns false, and we loop and
* try again.
*/
EndpointEntry epEntry;
do {
epEntry = getEndpointEntry(endpoint);
} while (!epEntry.registerRefs(refs));
}
/**
* Gets the next sequence number to be used for a dirty or clean
* operation from this AbstractDgcClient. This method should only be
* called while synchronized on the EndpointEntry whose data structures
* the operation affects.
*/
private static synchronized long getNextSequenceNum() {
return nextSequenceNum++;
}
/**
* Given the length of a lease and the time that it was granted,
* computes the absolute time at which it should be renewed, giving
* room for reasonable computational and communication delays.
*/
private static long computeRenewTime(long grantTime, long duration) {
/*
* REMIND: This algorithm should be more sophisticated, waiting
* a longer fraction of the lease duration for longer leases.
*/
return grantTime + (duration / 2);
}
/**
* Looks up the EndpointEntry for the given endpoint. An entry is
* created if one does not already exist.
*/
private EndpointEntry getEndpointEntry(Object endpoint) {
synchronized (endpointTable) {
EndpointEntry entry = (EndpointEntry) endpointTable.get(endpoint);
if (entry == null) {
entry = new EndpointEntry(endpoint);
endpointTable.put(endpoint, entry);
/*
* If the endpoint table was previously empty, we are now
* interested in special assistance from the local garbage
* collector for aggressively discovering unreachable live
* remote references (by notifying our phantom references),
* so that DGC "clean" calls can be sent in a timely fashion.
*
* Without guaranteed access to something like the
* sun.misc.GC API, however, we currently have no
* practical way of getting such special assistance.
*/
}
return entry;
}
}
/**
* EndpointEntry encapsulates the client-side DGC information specific
* to a particular endpoint. Of most significance is the table that
* maps live reference objects to RefEntry objects and the renew/clean
* thread that handles asynchronous client-side DGC operations.
*/
private final class EndpointEntry {
/** the endpoint that this EndpointEntry is for */
private final Object endpoint;
/** synthesized reference to the remote server-side DGC */
private final DgcProxy dgcProxy;
/** renew/clean thread for handling lease renewals and clean calls */
private final Thread renewCleanThread;
/** reference queue for phantom references */
private final ReferenceQueue refQueue = new ReferenceQueue();
/* mutable instance state (below) is guarded by this object's lock */
/** true if this entry has been removed from the global table */
private boolean removed = false;
/** table of refs held for endpoint: maps object ID to RefEntry */
private final Map refTable = new HashMap(5);
/** set of RefEntry instances from last (failed) dirty call */
private Set invalidRefs = new HashSet(5);
/** absolute time to renew current lease to this endpoint */
private long renewTime = Long.MAX_VALUE;
/** absolute time current lease to this endpoint will expire */
private long expirationTime = Long.MIN_VALUE;
/** count of recent dirty calls that have failed */
private int dirtyFailures = 0;
/** absolute time of first recent failed dirty call */
private long dirtyFailureStartTime;
/** (average) elapsed time for recent failed dirty calls */
private long dirtyFailureDuration;
/** true if renew/clean thread may be interrupted */
private boolean interruptible = false;
/** set of clean calls that need to be made */
private final Set pendingCleans = new HashSet(5);
private EndpointEntry(final Object endpoint) {
this.endpoint = endpoint;
dgcProxy = getDgcProxy(endpoint);
renewCleanThread = (Thread) AccessController.doPrivileged(
new NewThreadAction(new RenewCleanThread(),
"RenewClean-" + endpoint, true));
renewCleanThread.start();
}
/**
* Registers the live reference instances in the supplied list to
* participate in distributed garbage collection.
*
* This method returns false if this entry was removed from the
* global endpoint table (because it was empty) before these refs
* could be registered. In that case, a new EndpointEntry needs
* to be looked up.
*
* This method must NOT be invoked while synchronized on this
* EndpointEntry.
*/
boolean registerRefs(Collection refs) {
assert !Thread.holdsLock(this);
Set refsToDirty = null; // entries for refs needing dirty
long sequenceNum; // sequence number for dirty call
synchronized (this) {
if (removed) {
return false;
}
Iterator iter = refs.iterator();
while (iter.hasNext()) {
Object ref = iter.next();
assert getRefEndpoint(ref).equals(endpoint);
Object objectID = getRefObjectID(ref);
RefEntry refEntry = (RefEntry) refTable.get(objectID);
if (refEntry == null) {
refEntry = new RefEntry(objectID);
refTable.put(objectID, refEntry);
if (refsToDirty == null) {
refsToDirty = new HashSet(5);
}
refsToDirty.add(refEntry);
}
refEntry.addInstanceToRefSet(ref);
}
if (refsToDirty == null) {
return true;
}
refsToDirty.addAll(invalidRefs);
invalidRefs.clear();
sequenceNum = getNextSequenceNum();
}
makeDirtyCall(refsToDirty, sequenceNum);
return true;
}
/**
* Removes the given RefEntry from the ref table. If that makes
* the ref table empty, remove this entry from the global endpoint
* table.
*
* This method must ONLY be invoked while synchronized on this
* EndpointEntry.
*/
private void removeRefEntry(RefEntry refEntry) {
assert Thread.holdsLock(this);
assert !removed;
assert refTable.containsKey(refEntry.getObjectID());
refTable.remove(refEntry.getObjectID());
invalidRefs.remove(refEntry);
if (refTable.isEmpty()) {
synchronized (endpointTable) {
endpointTable.remove(endpoint);
freeEndpoint(endpoint);
/*
* If the endpoint table is now empty, we are no longer
* interested in special assistance from the local garbage
* collector for aggressively discovering unreachable
* live remote references, if we had been getting such
* special assistance in the first place.
*/
}
removed = true;
}
}
/**
* Makes a DGC dirty call to this entry's endpoint, for the
* object IDs corresponding to the given set of refs and with
* the given sequence number.
*
* This method must NOT be invoked while synchronized on this
* EndpointEntry.
*/
private void makeDirtyCall(Set refEntries, long sequenceNum) {
assert !Thread.holdsLock(this);
Object[] ids;
if (refEntries != null) {
ids = createObjectIDArray(refEntries);
} else {
ids = emptyObjectArray;
}
long startTime = System.currentTimeMillis();
try {
long duration = dgcProxy.dirty(sequenceNum, ids, leaseValue);
synchronized (this) {
dirtyFailures = 0;
if (duration < 0) {
setRenewTime(Long.MAX_VALUE);
invalidRefs.addAll(refTable.values());
} else {
setRenewTime(
computeRenewTime(startTime,
Math.max(duration,
minimumDuration)));
expirationTime = startTime + duration;
}
}
} catch (NoSuchObjectException e) {
synchronized (this) {
setRenewTime(Long.MAX_VALUE);
invalidRefs.addAll(refTable.values());
}
} catch (Exception e) {
long endTime = System.currentTimeMillis();
synchronized (this) {
dirtyFailures++;
if (dirtyFailures == 1) {
/*
* If this was the first recent failed dirty call,
* reschedule another one immediately, in case there
* was just a transient network problem, and remember
* the start time and duration of this attempt for
* future calculations of the delays between retries.
*/
dirtyFailureStartTime = startTime;
dirtyFailureDuration = endTime - startTime;
setRenewTime(endTime);
} else {
/*
* For each successive failed dirty call, wait for a
* (binary) exponentially increasing delay before
* retrying, to avoid network congestion.
*/
int n = dirtyFailures - 2;
if (n == 0) {
/*
* Calculate the initial retry delay from the
* average time elapsed for each of the first
* two failed dirty calls. The result must be
* at least 1000ms, to prevent a tight loop.
*/
dirtyFailureDuration =
Math.max((dirtyFailureDuration +
(endTime - startTime)) >> 1, 1000);
}
long newRenewTime =
endTime + (dirtyFailureDuration << n);
/*
* Continue if the last known held lease has not
* expired, or else at least a fixed number of times,
* or at least until we've tried for a fixed amount
* of time (the default lease value we request).
*/
if (newRenewTime < expirationTime ||
dirtyFailures < dirtyFailureRetries ||
newRenewTime < dirtyFailureStartTime + leaseValue)
{
setRenewTime(newRenewTime);
} else {
/*
* Give up: postpone lease renewals until next
* ref is registered for this endpoint.
*/
setRenewTime(Long.MAX_VALUE);
}
}
if (refEntries != null) {
/*
* Add all of these refs to the set of refs for this
* endpoint that may be invalid (this AbstractDgcClient
* may not be in the server's referenced set), so that
* we will attempt to explicitly dirty them again in
* the future.
*/
invalidRefs.addAll(refEntries);
/*
* Record that a dirty call has failed for all of these
* refs, so that clean calls for them in the future
* will be strong.
*/
Iterator iter = refEntries.iterator();
while (iter.hasNext()) {
RefEntry refEntry = (RefEntry) iter.next();
refEntry.markDirtyFailed();
}
}
/*
* If the last known held lease will have expired before
* the next renewal, all refs might be invalid.
*/
if (renewTime >= expirationTime) {
invalidRefs.addAll(refTable.values());
}
}
}
}
/**
* Sets the absolute time at which the lease for this entry should
* be renewed.
*
* This method must ONLY be invoked while synchronized on this
* EndpointEntry.
*/
private void setRenewTime(long newRenewTime) {
assert Thread.holdsLock(this);
if (newRenewTime < renewTime) {
renewTime = newRenewTime;
if (interruptible) {
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
renewCleanThread.interrupt();
return null;
}
});
}
} else {
renewTime = newRenewTime;
}
}
/**
* RenewCleanThread handles the asynchronous client-side DGC activity
* for this entry: renewing the leases and making clean calls.
*/
private class RenewCleanThread implements Runnable {
public void run() {
do {
long timeToWait;
RefEntry.PhantomLiveRef phantom = null;
boolean needRenewal = false;
Set refsToDirty = null;
long sequenceNum = Long.MIN_VALUE;
synchronized (EndpointEntry.this) {
/*
* Calculate time to block (waiting for phantom
* reference notifications). It is the time until the
* lease renewal should be done, bounded on the low
* end by 1 ms so that the reference queue will always
* get processed, and if there are pending clean
* requests (remaining because some clean calls
* failed), bounded on the high end by the maximum
* clean call retry interval.
*/
long timeUntilRenew =
renewTime - System.currentTimeMillis();
timeToWait = Math.max(timeUntilRenew, 1);
if (!pendingCleans.isEmpty()) {
timeToWait = Math.min(timeToWait, cleanInterval);
}
/*
* Set flag indicating that it is OK to interrupt this
* thread now, such as if a earlier lease renewal time
* is set, because we are only going to be blocking
* and can deal with interrupts.
*/
interruptible = true;
}
try {
/*
* Wait for the duration calculated above for any of
* our phantom references to be enqueued.
*/
phantom = (RefEntry.PhantomLiveRef)
refQueue.remove(timeToWait);
} catch (InterruptedException e) {
}
synchronized (EndpointEntry.this) {
/*
* Set flag indicating that it is NOT OK to interrupt
* this thread now, because we may be undertaking I/O
* operations that should not be interrupted (and we
* will not be blocking arbitrarily).
*/
interruptible = false;
Thread.interrupted(); // clear interrupted state
/*
* If there was a phantom reference enqueued, process
* it and all the rest on the queue, generating
* clean requests as necessary.
*/
if (phantom != null) {
processPhantomRefs(phantom);
}
/*
* Check if it is time to renew this entry's lease.
*/
long currentTime = System.currentTimeMillis();
if (currentTime > renewTime) {
needRenewal = true;
if (currentTime >= expirationTime) {
invalidRefs.addAll(refTable.values());
}
if (!invalidRefs.isEmpty()) {
refsToDirty = invalidRefs;
invalidRefs = new HashSet(5);
}
sequenceNum = getNextSequenceNum();
}
}
if (needRenewal) {
makeDirtyCall(refsToDirty, sequenceNum);
}
if (!pendingCleans.isEmpty()) {
makeCleanCalls();
}
} while (!removed || !pendingCleans.isEmpty());
}
}
/**
* Processes the notification of the given phantom reference and any
* others that are on this entry's reference queue. Each phantom
* reference is removed from its RefEntry's ref set. All ref
* entries that have no more registered instances are collected
* into up to two batched clean call requests: one for refs
* requiring a "strong" clean call, and one for the rest.
*
* This method must ONLY be invoked while synchronized on this
* EndpointEntry.
*/
private void processPhantomRefs(RefEntry.PhantomLiveRef phantom) {
assert Thread.holdsLock(this);
Set strongCleans = null;
Set normalCleans = null;
do {
RefEntry refEntry = phantom.getRefEntry();
refEntry.removeInstanceFromRefSet(phantom);
if (refEntry.isRefSetEmpty()) {
if (refEntry.hasDirtyFailed()) {
if (strongCleans == null) {
strongCleans = new HashSet(5);
}
strongCleans.add(refEntry);
} else {
if (normalCleans == null) {
normalCleans = new HashSet(5);
}
normalCleans.add(refEntry);
}
removeRefEntry(refEntry);
}
} while ((phantom =
(RefEntry.PhantomLiveRef) refQueue.poll()) != null);
if (strongCleans != null) {
pendingCleans.add(
new CleanRequest(getNextSequenceNum(),
createObjectIDArray(strongCleans),
true));
}
if (normalCleans != null) {
pendingCleans.add(
new CleanRequest(getNextSequenceNum(),
createObjectIDArray(normalCleans),
false));
}
}
/**
* Makes all of the clean calls described by the clean requests in
* this entry's set of "pending cleans". Clean requests for clean
* calls that succeed are removed from the "pending cleans" set.
*
* This method must NOT be invoked while synchronized on this
* EndpointEntry.
*/
private void makeCleanCalls() {
assert !Thread.holdsLock(this);
Iterator iter = pendingCleans.iterator();
while (iter.hasNext()) {
CleanRequest request = (CleanRequest) iter.next();
try {
dgcProxy.clean(request.sequenceNum, request.objectIDs,
request.strong);
iter.remove();
} catch (NoSuchObjectException e) {
iter.remove();
} catch (Exception e) {
if (e instanceof ConnectException ||
e instanceof ConnectIOException)
{
/*
* If we get a ConnectException, the target DGC likely
* has gone away, in which case we shouldn't bother
* retrying this clean request forever. Then again,
* the server could just be heavily loaded, so we will
* give a finite number of retry opportunities to
* clean requests that fail this way.
*
* A similar (but different) argument can be made for
* ConnectIOException.
*/
if (++request.connectFailures >= cleanConnectRetries) {
iter.remove();
}
} else {
// possible transient failure, retain clean request
}
}
}
}
/**
* Creates an array of object IDs (needed for the DGC remote calls)
* from the ids in the given set of refs.
*/
private Object[] createObjectIDArray(Set refEntries) {
Object[] ids = new Object[refEntries.size()];
Iterator iter = refEntries.iterator();
for (int i = 0; i < ids.length; i++) {
ids[i] = ((RefEntry) iter.next()).getObjectID();
}
return ids;
}
/**
* RefEntry encapsulates the client-side DGC information specific to
* a particular object ID of an endpoint (a unique live reference
* value).
*
* In particular, it contains a set of phantom references to all of
* the live reference instances for the given object ID and endpoint
* in this VM that have been registered with this AbstractDgcClient
* (but not yet garbage collected locally).
*/
private class RefEntry {
/**
* the object ID that this RefEntry is for (the endpoint is
* implied by the outer EndpointEntry instance)
*/
private final Object objectID;
/*
* mutable instance state (below) is guarded by outer
* EndpointEntry's lock
*/
/** set of phantom references to registered instances */
private final Set refSet = new HashSet(5);
/** true if a dirty call containing this ref has failed */
private boolean dirtyFailed = false;
RefEntry(Object objectID) {
this.objectID = objectID;
}
/**
* Returns the object ID that this entry is for.
*/
Object getObjectID() {
return objectID;
}
/**
* Adds a live reference to the set of registered instances for
* this entry.
*
* This method must ONLY be invoked while synchronized on this
* RefEntry's EndpointEntry.
*/
void addInstanceToRefSet(Object ref) {
assert Thread.holdsLock(EndpointEntry.this);
assert getRefObjectID(ref).equals(objectID);
/*
* Only keep a phantom reference to the registered instance,
* so that it can be garbage collected normally (and we can be
* notified when that happens).
*/
refSet.add(new PhantomLiveRef(ref));
}
/**
* Removes a PhantomLiveRef from the set of registered instances.
*
* This method must ONLY be invoked while synchronized on this
* RefEntry's EndpointEntry.
*/
void removeInstanceFromRefSet(PhantomLiveRef phantom) {
assert Thread.holdsLock(EndpointEntry.this);
assert refSet.contains(phantom);
refSet.remove(phantom);
}
/**
* Returns true if there are no registered live reference
* instances for this entry still reachable in this VM.
*
* This method must ONLY be invoked while synchronized on this
* RefEntry's EndpointEntry.
*/
boolean isRefSetEmpty() {
assert Thread.holdsLock(EndpointEntry.this);
return refSet.size() == 0;
}
/**
* Records that a dirty call that explicitly contained this
* entry's ref value has failed.
*
* This method must ONLY be invoked while synchronized on this
* RefEntry's EndpointEntry.
*/
void markDirtyFailed() {
assert Thread.holdsLock(EndpointEntry.this);
dirtyFailed = true;
}
/**
* Returns true if a dirty call that explicitly contained this
* entry's ref value has failed (and therefore a clean call for
* the ref value needs to be marked "strong").
*
* This method must ONLY be invoked while synchronized on this
* RefEntry's EndpointEntry.
*/
boolean hasDirtyFailed() {
assert Thread.holdsLock(EndpointEntry.this);
return dirtyFailed;
}
/**
* PhantomLiveRef is a PhantomReference to a live reference
* instance, used to detect when the particular live reference
* becomes permanently unreachable in this VM.
*/
class PhantomLiveRef extends PhantomReference {
PhantomLiveRef(Object ref) {
super(ref, EndpointEntry.this.refQueue);
}
RefEntry getRefEntry() {
return RefEntry.this;
}
}
}
}
/**
* CleanRequest holds the data for the arguments of a clean call
* that needs to be made.
*/
private static class CleanRequest {
long sequenceNum;
Object[] objectIDs;
boolean strong;
/** how many times this request has failed with ConnectException */
int connectFailures = 0;
CleanRequest(long sequenceNum, Object[] objectIDs, boolean strong) {
this.sequenceNum = sequenceNum;
this.objectIDs = objectIDs;
this.strong = strong;
}
}
}