/*
* 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.jeri.internal.runtime.ImplRefManager.ImplRef;
import com.sun.jini.logging.Levels;
import com.sun.jini.thread.NewThreadAction;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.rmi.Remote;
import java.rmi.server.ExportException;
import java.rmi.server.Unreferenced;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.security.PrivilegedExceptionAction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.jini.export.ServerContext;
import net.jini.id.Uuid;
import net.jini.id.UuidFactory;
import net.jini.io.MarshalInputStream;
import net.jini.io.UnsupportedConstraintException;
import net.jini.jeri.BasicInvocationDispatcher;
import net.jini.jeri.InvocationDispatcher;
import net.jini.jeri.InboundRequest;
import net.jini.jeri.RequestDispatcher;
import net.jini.jeri.ServerCapabilities;
import net.jini.core.constraint.InvocationConstraints;
import net.jini.security.Security;
import net.jini.security.SecurityContext;
/**
* A table of exported remote objects.
*
* @author Sun Microsystems, Inc.
**/
final class ObjectTable {
private static final Logger logger =
Logger.getLogger("net.jini.jeri.BasicJeriExporter");
private static final Collection dgcDispatcherMethods = new ArrayList(2);
static {
Method[] methods = DgcServer.class.getMethods();
for (int i = 0; i < methods.length; i++) {
final Method m = methods[i];
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
m.setAccessible(true);
return null;
}
});
dgcDispatcherMethods.add(m);
}
}
private static final ServerCapabilities dgcServerCapabilities =
new ServerCapabilities() {
public InvocationConstraints checkConstraints(
InvocationConstraints constraints)
throws UnsupportedConstraintException
{
assert constraints.equals(InvocationConstraints.EMPTY);
return InvocationConstraints.EMPTY;
}
};
/**
* lock to serialize request dispatcher reservation per export, so
* that a partial export will not cause another export to fail
* unnecessarily
**/
private final Object requestDispatchersLock = new Object();
/** table of references to impls exported with DGC */
private final ImplRefManager implRefManager = new ImplRefManager();
/** lock guarding keepAliveCount and keeper */
private final Object keepAliveLock = new Object();
/** number of objects exported with keepAlive == true */
private int keepAliveCount = 0;
/** thread to keep VM alive while keepAliveCount > 0 */
private Thread keeper = null;
/** maps client ID to Lease (lock guards leaseChecker too) */
private final Map leaseTable = new HashMap();
/** thread to check for expired leases */
private Thread leaseChecker = null;
ObjectTable() { }
RequestDispatcher createRequestDispatcher(Unreferenced unrefCallback) {
return new RD(unrefCallback);
}
boolean isReferenced(RequestDispatcher requestDispatcher) {
return getRD(requestDispatcher).isReferenced();
}
Target export(Remote impl,
RequestDispatcher[] requestDispatchers,
boolean allowDGC,
boolean keepAlive,
Uuid id)
throws ExportException
{
RD[] rds = new RD[requestDispatchers.length];
for (int i = 0; i < requestDispatchers.length; i++) {
rds[i] = getRD(requestDispatchers[i]);
}
return new Target(impl, id, rds, allowDGC, keepAlive);
}
private RD getRD(RequestDispatcher requestDispatcher) {
/*
* The following cast will throw a ClassCastException if we were
* passed a RequestDispatcher that was not returned by this class's
* createRequestDispatcher method:
*/
RD rd = (RD) requestDispatcher;
if (!rd.forTable(this)) {
throw new IllegalArgumentException(
"request dispatcher for different object table");
}
return rd;
}
/**
* Increments the count of objects exported with keepAlive true,
* starting a non-daemon thread if necessary.
**/
private void incrementKeepAliveCount() {
synchronized (keepAliveLock) {
keepAliveCount++;
if (keeper == null) {
keeper = (Thread) AccessController.doPrivileged(
new NewThreadAction(new Runnable() {
public void run() {
try {
while (true) {
Thread.sleep(Long.MAX_VALUE);
}
} catch (InterruptedException e) {
// pass away if interrupted
}
}
}, "KeepAlive", false));
keeper.start();
}
}
}
/**
* Decrements the count of objects exported with keepAlive true,
* stopping the non-daemon thread if decremented to zero.
**/
private void decrementKeepAliveCount() {
synchronized (keepAliveLock) {
keepAliveCount--;
if (keepAliveCount == 0) {
assert keeper != null;
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
keeper.interrupt();
return null;
}
});
keeper = null;
}
}
}
/**
* A Target is returned by the export method to represent the object
* exported to this ObjectTable. It can be used to unexport the
* exported object.
*/
final class Target {
private final ImplRef implRef;
private final Uuid id;
private final RD[] requestDispatchers;
private final boolean allowDGC;
private final boolean keepAlive;
private final SecurityContext securityContext;
private final ClassLoader ccl;
/** lock guarding all mutable instance state (below) */
private final Object lock = new Object();
private InvocationDispatcher invocationDispatcher;
private boolean exported = false;
private int callsInProgress = 0;
private final Set referencedSet;
private final Map sequenceTable;
Target(Remote impl,
Uuid id,
RD[] requestDispatchers,
boolean allowDGC,
boolean keepAlive)
throws ExportException
{
this.id = id;
this.requestDispatchers = requestDispatchers;
this.allowDGC = allowDGC;
this.keepAlive = keepAlive;
securityContext = Security.getContext();
ccl = Thread.currentThread().getContextClassLoader();
synchronized (requestDispatchersLock) {
boolean success = false;
int i = 0;
try {
for (i = 0; i < requestDispatchers.length; i++) {
requestDispatchers[i].put(this);
}
success = true;
} finally {
if (!success) {
for (int j = 0; j < i; j++) {
requestDispatchers[i].remove(this, false);
}
}
}
}
implRef = implRefManager.getImplRef(impl, this);
if (allowDGC) {
referencedSet = new HashSet(3);
sequenceTable = new HashMap(3);
} else {
referencedSet = null;
sequenceTable = null;
}
if (keepAlive) {
incrementKeepAliveCount();
}
synchronized (lock) {
exported = true;
}
}
void setInvocationDispatcher(InvocationDispatcher id) {
assert id != null;
synchronized (lock) {
assert invocationDispatcher == null;
invocationDispatcher = id;
}
}
boolean unexport(boolean force) {
synchronized (lock) {
if (!exported) {
return true;
}
if (!force && callsInProgress > 0) {
return false;
}
exported = false;
if (keepAlive && callsInProgress == 0) {
decrementKeepAliveCount();
}
if (allowDGC) {
if (!referencedSet.isEmpty()) {
for (Iterator i = referencedSet.iterator();
i.hasNext();)
{
Uuid clientID = (Uuid) i.next();
unregisterTarget(this, clientID);
}
referencedSet.clear();
}
sequenceTable.clear();
}
}
implRef.release(this);
for (int i = 0; i < requestDispatchers.length; i++) {
requestDispatchers[i].remove(this, false);
}
return true;
}
void collect() {
synchronized (lock) {
if (!exported) {
return;
}
if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE,
"garbage collection of object with id {0}", id);
}
exported = false;
if (keepAlive && callsInProgress == 0) {
decrementKeepAliveCount();
}
if (allowDGC) {
assert referencedSet.isEmpty();
sequenceTable.clear();
}
}
for (int i = 0; i < requestDispatchers.length; i++) {
requestDispatchers[i].remove(this, true);
}
}
Uuid getObjectIdentifier() {
return id;
}
// used by ImplRef for invoking Unreferenced.unreferenced
boolean getEnableDGC() {
return allowDGC;
}
// used by ImplRef for invoking Unreferenced.unreferenced
SecurityContext getSecurityContext() {
return securityContext;
}
// used by ImplRef for invoking Unreferenced.unreferenced
ClassLoader getContextClassLoader() {
return ccl;
}
void referenced(Uuid clientID, long sequenceNum) {
if (!allowDGC) {
return; // ignore if DGC not enabled for this object
}
synchronized (lock) {
if (!exported) {
return;
}
if (logger.isLoggable(Level.FINEST)) {
logger.log(Level.FINEST,
"this={0}, clientID={1}, sequenceNum={2}",
new Object[] {
this, clientID, new Long(sequenceNum)
});
}
/*
* Check current sequence number against the last
* recorded sequence number for the client. If the
* current value is lower, then this is a "late dirty
* call", which should not be processed. Otherwise,
* update the last recorded sequence number.
*/
SequenceEntry entry =
(SequenceEntry) sequenceTable.get(clientID);
if (entry == null) {
// no record: must assume this is not a late dirty call
entry = new SequenceEntry(sequenceNum);
sequenceTable.put(clientID, entry);
} else if (sequenceNum < entry.sequenceNum) {
return; // late dirty call: ignore
} else {
entry.sequenceNum = sequenceNum;
}
if (!referencedSet.contains(clientID)) {
if (referencedSet.isEmpty()) {
Remote impl = implRef.getImpl();
if (impl == null) {
return; // too late if impl was collected
}
implRef.pin(this);
}
referencedSet.add(clientID);
registerTarget(this, clientID);
}
}
}
void unreferenced(Uuid clientID, long sequenceNum, boolean strong) {
if (!allowDGC) {
return; // ignore if DGC not enabled for this object
}
synchronized (lock) {
if (!exported) {
return;
}
if (logger.isLoggable(Level.FINEST)) {
logger.log(Level.FINEST,
"this={0}, clientID={1}, sequenceNum={2}, strong={3}",
new Object[] {
this, clientID, new Long(sequenceNum),
Boolean.valueOf(strong)
});
}
/*
* Check current sequence number against the last
* recorded sequence number for the client. If the
* current value is lower, then this is a "late clean
* call", which should not be processed. Otherwise:
* if this is for a strong clean call, then update the
* last recorded sequence number; if no strong clean
* call has been processed for this client, discard
* its sequence number record.
*/
SequenceEntry entry =
(SequenceEntry) sequenceTable.get(clientID);
if (entry == null) {
// no record: must assume this is not a late clean call
if (strong) {
entry = new SequenceEntry(sequenceNum);
sequenceTable.put(clientID, entry);
entry.keep = true;
}
} else if (sequenceNum < entry.sequenceNum) {
return; // late clean call: ignore
} else if (strong) {
entry.sequenceNum = sequenceNum;
entry.keep = true; // strong clean: retain sequence number
} else if (!entry.keep) {
sequenceTable.remove(clientID);
}
unregisterTarget(this, clientID);
if (referencedSet.remove(clientID) &&
referencedSet.isEmpty())
{
implRef.unpin(this);
}
}
}
void leaseExpired(Uuid clientID) {
assert allowDGC;
synchronized (lock) {
if (!exported) {
return;
}
if (logger.isLoggable(Level.FINEST)) {
logger.log(Level.FINEST,
"this={0}, clientID={1}",
new Object[] { this, clientID });
}
SequenceEntry entry =
(SequenceEntry) sequenceTable.get(clientID);
if (entry != null && !entry.keep) {
/*
* REMIND: We could be removing the sequence number
* for a more recent lease, thus allowing a "late
* clean call" to be inappropriately processed?
* (See 4848840 Comments.)
*/
sequenceTable.remove(clientID);
}
if (referencedSet.remove(clientID) &&
referencedSet.isEmpty())
{
implRef.unpin(this);
}
}
}
void dispatch(InboundRequest request)
throws IOException, NoSuchObject
{
InvocationDispatcher id;
synchronized (lock) {
if (!exported || invocationDispatcher == null) {
if (logger.isLoggable(Level.FINEST)) {
logger.log(Level.FINEST,
"this={0}, not exported", this);
}
throw new NoSuchObject();
}
id = invocationDispatcher; // save for reference outside lock
callsInProgress++;
}
try {
Remote impl = implRef.getImpl();
if (impl == null) {
if (logger.isLoggable(Level.FINEST)) {
logger.log(Level.FINEST,
"this={0}, garbage collected", this);
}
throw new NoSuchObject();
}
dispatch(request, id, impl);
} finally {
synchronized (lock) {
assert callsInProgress > 0;
callsInProgress--;
if (keepAlive && !exported && callsInProgress == 0) {
decrementKeepAliveCount();
}
}
}
}
private void dispatch(final InboundRequest request,
final InvocationDispatcher id,
final Remote impl)
throws IOException, NoSuchObject
{
Thread t = Thread.currentThread();
ClassLoader savedCcl = t.getContextClassLoader();
try {
if (ccl != savedCcl) {
t.setContextClassLoader(ccl);
}
AccessController.doPrivileged(securityContext.wrap(
new PrivilegedExceptionAction() {
public Object run() throws IOException {
dispatch0(request, id, impl);
return null;
}
}), securityContext.getAccessControlContext());
} catch (java.security.PrivilegedActionException e) {
throw (IOException) e.getException();
} finally {
if (ccl != savedCcl || savedCcl != t.getContextClassLoader()) {
t.setContextClassLoader(savedCcl);
}
}
}
private void dispatch0(final InboundRequest request,
final InvocationDispatcher id,
final Remote impl)
throws IOException
{
request.checkPermissions();
OutputStream out = request.getResponseOutputStream();
out.write(Jeri.OBJECT_HERE);
final Collection context = new ArrayList(5);
request.populateContext(context);
ServerContext.doWithServerContext(new Runnable() {
public void run() {
id.dispatch(impl, request, context);
}
}, Collections.unmodifiableCollection(context));
}
public String toString() { // for logging
return "Target@" + Integer.toHexString(hashCode()) +
"[" + id + "]";
}
}
private static final class SequenceEntry {
long sequenceNum;
boolean keep;
SequenceEntry(long sequenceNum) {
this.sequenceNum = sequenceNum;
}
}
void registerTarget(Target target, Uuid clientID) {
synchronized (leaseTable) {
Lease lease = (Lease) leaseTable.get(clientID);
if (lease == null) {
target.leaseExpired(clientID);
} else {
synchronized (lease.notifySet) {
lease.notifySet.add(target);
}
}
}
}
void unregisterTarget(Target target, Uuid clientID) {
synchronized (leaseTable) {
Lease lease = (Lease) leaseTable.get(clientID);
if (lease != null) {
synchronized (lease.notifySet) {
lease.notifySet.remove(target);
}
}
}
}
/**
* RequestDispatcher implementation.
**/
private class RD implements RequestDispatcher {
private final Unreferenced unrefCallback;
private final Map idTable = new HashMap();
private int dgcEnabledCount = 0; // guarded by idTable lock
private final InvocationDispatcher dgcDispatcher;
private final DgcServerImpl dgcServerImpl;
RD(Unreferenced unrefCallback) {
this.unrefCallback = unrefCallback;
try {
dgcDispatcher =
new BasicInvocationDispatcher(
dgcDispatcherMethods, dgcServerCapabilities,
null, null, this.getClass().getClassLoader())
{
protected ObjectInputStream createMarshalInputStream(
Object impl,
InboundRequest request,
boolean integrity,
Collection context)
throws IOException
{
ClassLoader loader = getClassLoader();
return new MarshalInputStream(
request.getRequestInputStream(),
loader, integrity, loader,
Collections.unmodifiableCollection(context));
// useStreamCodebases() not invoked
}
};
} catch (ExportException e) {
throw new AssertionError();
}
dgcServerImpl = new DgcServerImpl();
}
boolean forTable(ObjectTable table) {
return ObjectTable.this == table;
}
boolean isReferenced() {
synchronized (idTable) {
return !idTable.isEmpty();
}
}
Target get(Uuid id) {
synchronized (idTable) {
return (Target) idTable.get(id);
}
}
void put(Target target) throws ExportException {
synchronized (idTable) {
Uuid id = target.getObjectIdentifier();
if (id.equals(Jeri.DGC_ID)) {
throw new ExportException(
"object identifier reserved for DGC");
}
if (idTable.containsKey(id)) {
throw new ExportException(
"object identifier already in use");
}
idTable.put(id, target);
if (target.getEnableDGC()) {
dgcEnabledCount++;
}
}
}
void remove(Target target, boolean gc) {
boolean empty = false;
synchronized (idTable) {
Uuid id = target.getObjectIdentifier();
assert idTable.get(id) == target;
idTable.remove(id);
if (target.getEnableDGC()) {
dgcEnabledCount--;
assert dgcEnabledCount >= 0;
}
if (idTable.isEmpty()) {
empty = true;
}
}
if (gc && empty) {
/*
* We have to be careful to make this callback without holding
* the lock for idTable, because the callback implementation
* will likely be code that calls this object's isReferenced
* method in its own synchronized block.
*/
unrefCallback.unreferenced();
}
}
private boolean hasDgcEnabledTargets() {
synchronized (idTable) {
return dgcEnabledCount > 0;
}
}
public void dispatch(InboundRequest request) {
try {
InputStream in = request.getRequestInputStream();
Uuid id = UuidFactory.read(in);
if (logger.isLoggable(Level.FINEST)) {
logger.log(Level.FINEST, "id={0}", id);
}
try {
/*
* The DGC object identifier is hardwired here,
* rather than install it in idTable; this
* eliminates the need to worry about not counting
* the DGC server as an exported object in the
* table, and it doesn't need all of the machinery
* that Target provides.
*/
if (id.equals(Jeri.DGC_ID)) {
dispatchDgcRequest(request);
return;
}
Target target = (Target) get(id);
if (target == null) {
logger.log(Level.FINEST, "id not in table");
throw new NoSuchObject();
}
target.dispatch(request);
} catch (NoSuchObject e) {
in.close();
OutputStream out = request.getResponseOutputStream();
out.write(Jeri.NO_SUCH_OBJECT);
out.close();
if (logger.isLoggable(Levels.FAILED)) {
logger.log(Levels.FAILED, "no such object: {0}", id);
}
}
} catch (IOException e) {
request.abort();
if (logger.isLoggable(Levels.FAILED)) {
logger.log(Levels.FAILED,
"I/O exception dispatching request", e);
}
}
}
private void dispatchDgcRequest(final InboundRequest request)
throws IOException, NoSuchObject
{
if (!hasDgcEnabledTargets()) {
logger.log(Level.FINEST, "no DGC-enabled targets");
throw new NoSuchObject();
}
OutputStream out = request.getResponseOutputStream();
out.write(Jeri.OBJECT_HERE);
final Collection context = new ArrayList(5);
request.populateContext(context);
ServerContext.doWithServerContext(new Runnable() {
public void run() {
dgcDispatcher.dispatch(dgcServerImpl, request, context);
}
}, Collections.unmodifiableCollection(context));
}
private class DgcServerImpl implements DgcServer {
public long dirty(Uuid clientID,
long sequenceNum,
Uuid[] ids)
{
if (logger.isLoggable(Level.FINEST)) {
logger.log(Level.FINEST,
"clientID={0}, sequenceNum={1}, ids={2}",
new Object[] {
clientID, new Long(sequenceNum), Arrays.asList(ids)
});
}
long duration = Jeri.leaseValue;
synchronized (leaseTable) {
Lease lease = (Lease) leaseTable.get(clientID);
if (lease == null) {
leaseTable.put(clientID,
new Lease(clientID, duration));
if (leaseChecker == null) {
leaseChecker =
(Thread) AccessController.doPrivileged(
new NewThreadAction(new LeaseChecker(),
"DGC Lease Checker", true));
leaseChecker.start();
}
} else {
lease.renew(duration);
}
}
for (int i = 0; i < ids.length; i++) {
Target target = get(ids[i]);
if (target != null) {
target.referenced(clientID, sequenceNum);
}
}
return duration;
}
public void clean(Uuid clientID,
long sequenceNum,
Uuid[] ids,
boolean strong)
{
if (logger.isLoggable(Level.FINEST)) {
logger.log(Level.FINEST,
"clientID={0}, sequenceNum={1}, ids={2}, strong={3}",
new Object[] {
clientID, new Long(sequenceNum),
Arrays.asList(ids), Boolean.valueOf(strong)
});
}
for (int i = 0; i < ids.length; i++) {
Target target = get(ids[i]);
if (target != null) {
target.unreferenced(clientID, sequenceNum, strong);
}
}
}
}
}
private class LeaseChecker implements Runnable {
public void run() {
boolean done = false;
do {
try {
Thread.sleep(Jeri.leaseCheckInterval);
} catch (InterruptedException e) {
// REMIND: shouldn't happen, OK to ignore?
}
long now = System.currentTimeMillis();
Collection expiredLeases = new ArrayList();
synchronized (leaseTable) {
for (Iterator i = leaseTable.values().iterator();
i.hasNext();)
{
Lease lease = (Lease) i.next();
if (lease.hasExpired(now)) {
expiredLeases.add(lease);
i.remove();
}
}
if (leaseTable.isEmpty()) {
leaseChecker = null;
done = true;
}
}
if (expiredLeases.isEmpty()) {
continue;
}
for (Iterator i = expiredLeases.iterator(); i.hasNext();) {
Lease lease = (Lease) i.next();
if (lease.notifySet.isEmpty()) {
continue;
}
for (Iterator i2 = lease.notifySet.iterator();
i2.hasNext();)
{
Target target = (Target) i2.next();
target.leaseExpired(lease.getClientID());
}
}
} while (!done);
}
}
private static class Lease {
private final Uuid clientID;
final Set notifySet = new HashSet(3); // guarded?
private long expiration; // guarded by leaseTable lock
Lease(Uuid clientID, long duration) {
this.clientID = clientID;
expiration = System.currentTimeMillis() + duration;
}
Uuid getClientID() {
return clientID;
}
void renew(long duration) {
long newExpiration = System.currentTimeMillis() + duration;
if (newExpiration > expiration) {
expiration = newExpiration;
}
}
boolean hasExpired(long now) {
return expiration < now;
}
}
private static class NoSuchObject extends Exception { }
}