com.googlecode.mobilityrpc.session.impl.MobilitySessionImpl Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of mobility-rpc Show documentation
Show all versions of mobility-rpc Show documentation
A high performance and easy to use library for Code Mobility and RPC on the Java platform.
The newest version!
/**
* Copyright 2011, 2012 Niall Gallagher
*
* Licensed 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.googlecode.mobilityrpc.session.impl;
import com.googlecode.mobilityrpc.controller.MobilityController;
import com.googlecode.mobilityrpc.controller.impl.MobilityControllerInternal;
import com.googlecode.mobilityrpc.network.ConnectionId;
import com.googlecode.mobilityrpc.protocol.pojo.*;
import com.googlecode.mobilityrpc.quickstart.EmbeddedMobilityServer;
import com.googlecode.mobilityrpc.serialization.Serializer;
import com.googlecode.mobilityrpc.serialization.impl.KryoSerializer;
import java.util.UUID;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* @author Niall Gallagher
*/
public class MobilitySessionImpl implements MobilitySessionInternal {
/**
* How long in millis threads waiting for an execution response to arrive should wait before giving up.
*/
private static final long DEFAULT_EXECUTION_RESPONSE_TIMEOUT_MILLIS = 60000;
private final Logger logger = Logger.getLogger(getClass().getName());
private final UUID sessionId;
private final MobilityControllerInternal mobilityController;
private final SessionClassLoader sessionClassLoader;
private final Serializer defaultSerializer;
private final SerializationFormat defaultSerializationFormat;
private final ConcurrentMap futureExecutionResponses = new ConcurrentHashMap();
private final AtomicInteger numRemoteThreadsExecutingInThisSession = new AtomicInteger(); // TODO: ..use Semaphore instead?
private volatile boolean sessionReleaseRequested = false;
public MobilitySessionImpl(UUID sessionId, MobilityControllerInternal mobilityController) {
this.sessionId = sessionId;
this.mobilityController = mobilityController;
this.sessionClassLoader = new SessionClassLoader(mobilityController, sessionId);
this.defaultSerializer = new KryoSerializer(sessionClassLoader);
this.defaultSerializationFormat = SerializationFormat.KRYO;
}
@Override
public UUID getSessionId() {
return sessionId;
}
@Override
public void execute(String address, Runnable runnable) {
execute(new ConnectionId(address, EmbeddedMobilityServer.DEFAULT_PORT), runnable);
}
@Override
public void execute(String address, long executionResponseTimeoutMs, Runnable runnable) {
execute(new ConnectionId(address, EmbeddedMobilityServer.DEFAULT_PORT), ExecutionMode.RETURN_RESPONSE, executionResponseTimeoutMs, runnable);
}
@Override
public void execute(ConnectionId connectionId, Runnable runnable) {
execute(connectionId, ExecutionMode.RETURN_RESPONSE, runnable);
}
@Override
public void execute(ConnectionId connectionId, ExecutionMode executionMode, Runnable runnable) {
execute(connectionId, executionMode, DEFAULT_EXECUTION_RESPONSE_TIMEOUT_MILLIS, runnable);
}
@Override
public void execute(ConnectionId connectionId, ExecutionMode executionMode, long executionResponseTimeoutMs, Runnable runnable) {
// Serialize the object...
final byte[] serializedExecutableObject = serialize(runnable, defaultSerializationFormat);
// Prepare an ExecutionRequest object which we will send to remote machine...
RequestIdentifier requestIdentifier = new RequestIdentifier(sessionId, UUID.randomUUID(), null);
ExecutionRequest outgoingRequest = new ExecutionRequest(
serializedExecutableObject,
defaultSerializationFormat,
executionMode,
requestIdentifier
);
switch (executionMode) {
case FIRE_AND_FORGET:
// No need to block waiting for response.
// Send execution request to remote machine, and then return without blocking...
try {
mobilityController.sendOutgoingMessage(connectionId, outgoingRequest);
}
catch (Exception e) {
// This exception is unlikely, should only occur if our outgoing queue to machine specified is full...
throw new IllegalStateException("Failed to submit Runnable object in FIRE_AND_FORGET mode for execution on remote machine: " + connectionId, e);
}
break;
case RETURN_RESPONSE:
// Send request and block waiting for response from remote machine.
final ExecutionResponse executionResponse;
try {
// Register a FutureExecutionResponse object in the map, which the thread processing a response to this
// request can later look up to notify this thread of the outcome of executing the request...
FutureExecutionResponse futureExecutionResponse = new FutureExecutionResponse(requestIdentifier);
futureExecutionResponses.put(requestIdentifier, futureExecutionResponse);
// Send the execution request to the remote machine...
mobilityController.sendOutgoingMessage(connectionId, outgoingRequest);
// Now block this thread until we get a response, or we time out...
executionResponse = futureExecutionResponse.getResponse(executionResponseTimeoutMs, TimeUnit.MILLISECONDS);
}
catch (Exception e) {
throw new IllegalStateException("Failed to receive response for execution request sent to remote machine in RETURN_RESPONSE mode for request identifier: " + requestIdentifier + ", connection id: " + connectionId, e);
}
// Decipher the execution response and return control normally to the client,
// or throw exception as necessary...
final ExecutionResponse.ExecutionOutcome executionOutcome = executionResponse.getExecutionOutcome();
// Indicate to the class loader that should this thread require classes when deserializing
// the response that they can be obtained from this remote machine...
sessionClassLoader.setThreadLocalConnectionId(connectionId);
try {
switch (executionOutcome) {
case VOID_RETURNED:
// Return normally...
return;
case FAILURE:
// The code threw an exception on the remote machine.
// Deserialize the exception and throw it to the caller on this machine...
Object responseObject = deserialize(executionResponse.getSerializedReturnObject(), executionResponse.getSerializationFormat());
// Sanity check to validate that indeed an exception was serialized as expected...
if (!(responseObject instanceof Throwable)) {
throw new IllegalStateException("Unexpected response object returned for execution outcome FAILURE: " + responseObject);
}
throw new IllegalStateException("An exception was thrown by the Runnable object when executed on the remote machine: " + connectionId, (Throwable)responseObject);
case VALUE_RETURNED:
// A Runnable does not return a value, this would indicate a problem with the framework...
throw new IllegalStateException("Unexpected ExecutionOutcome returned: " + executionMode);
default:
throw new IllegalStateException("Unexpected ExecutionOutcome returned: " + executionMode);
}
}
finally {
// Null-out the connection id for this calling thread,
// now that response has been deserialized...
sessionClassLoader.setThreadLocalConnectionId(null);
}
default:
throw new IllegalStateException("Unexpected ExecutionMode specified: " + executionMode);
}
}
@Override
public T execute(String address, Callable callable) {
return execute(new ConnectionId(address, EmbeddedMobilityServer.DEFAULT_PORT), callable);
}
@Override
public T execute(String address, long executionResponseTimeoutMs, Callable callable) {
return execute(new ConnectionId(address, EmbeddedMobilityServer.DEFAULT_PORT), ExecutionMode.RETURN_RESPONSE, executionResponseTimeoutMs, callable);
}
@Override
public T execute(ConnectionId connectionId, Callable callable) {
return execute(connectionId, ExecutionMode.RETURN_RESPONSE, callable);
}
@Override
public T execute(ConnectionId connectionId, ExecutionMode executionMode, Callable callable) {
return execute(connectionId, executionMode, DEFAULT_EXECUTION_RESPONSE_TIMEOUT_MILLIS, callable);
}
@Override
public T execute(ConnectionId connectionId, ExecutionMode executionMode, long executionResponseTimeoutMs, Callable callable) {
// Serialize the object...
final byte[] serializedExecutableObject = serialize(callable, defaultSerializationFormat);
// Prepare an ExecutionRequest object which we will send to remote machine...
RequestIdentifier requestIdentifier = new RequestIdentifier(sessionId, UUID.randomUUID(), null);
ExecutionRequest outgoingRequest = new ExecutionRequest(
serializedExecutableObject,
defaultSerializationFormat,
executionMode,
requestIdentifier
);
switch (executionMode) {
case FIRE_AND_FORGET:
// No need to block waiting for response.
// Send execution request to remote machine, and then return without blocking...
try {
mobilityController.sendOutgoingMessage(connectionId, outgoingRequest);
}
catch (Exception e) {
// This exception is unlikely, should only occur if our outgoing queue to machine specified is full...
throw new IllegalStateException("Failed to submit Callable object in FIRE_AND_FORGET mode for execution on remote machine: " + connectionId, e);
}
return null;
case RETURN_RESPONSE:
// Send request and block waiting for response from remote machine.
final ExecutionResponse executionResponse;
try {
// Register a FutureExecutionResponse object in the map, which the thread processing a response to this
// request can later look up to notify this thread of the outcome of executing the request...
FutureExecutionResponse futureExecutionResponse = new FutureExecutionResponse(requestIdentifier);
futureExecutionResponses.put(requestIdentifier, futureExecutionResponse);
// Send the execution request to the remote machine...
mobilityController.sendOutgoingMessage(connectionId, outgoingRequest);
// Now block this thread until we get a response, or we time out...
executionResponse = futureExecutionResponse.getResponse(executionResponseTimeoutMs, TimeUnit.MILLISECONDS);
}
catch (Exception e) {
throw new IllegalStateException("Failed to receive response for execution request sent to remote machine in RETURN_RESPONSE mode for request identifier: " + requestIdentifier + ", connection id: " + connectionId, e);
}
// Decipher the execution response and return control normally to the client,
// or throw exception as necessary...
final ExecutionResponse.ExecutionOutcome executionOutcome = executionResponse.getExecutionOutcome();
// Indicate to the class loader that should this thread require classes when deserializing
// the response that they can be obtained from this remote machine...
sessionClassLoader.setThreadLocalConnectionId(connectionId);
try {
switch (executionOutcome) {
case VOID_RETURNED:
// Return normally...
return null;
case FAILURE:
// The code threw an exception on the remote machine.
// Deserialize the exception and throw it to the caller on this machine...
Object throwable = deserialize(executionResponse.getSerializedReturnObject(), executionResponse.getSerializationFormat());
// Sanity check to validate that indeed an exception was serialized as expected...
if (!(throwable instanceof Throwable)) {
throw new IllegalStateException("Unexpected response object returned for execution outcome FAILURE: " + throwable);
}
throw new IllegalStateException("An exception was thrown by the Callable object when executed on the remote machine: " + connectionId, (Throwable)throwable);
case VALUE_RETURNED:
// The callable returned an object when executed on the remote machine, return it to
// the caller of this method...
@SuppressWarnings({"unchecked", "UnnecessaryLocalVariable"})
T objectReturned = (T) deserialize(executionResponse.getSerializedReturnObject(), executionResponse.getSerializationFormat());
return objectReturned;
default:
throw new IllegalStateException("Unexpected ExecutionOutcome returned: " + executionMode);
}
}
finally {
// Null-out the connection id for this calling thread,
// now that response has been deserialized...
sessionClassLoader.setThreadLocalConnectionId(null);
}
default:
throw new IllegalStateException("Unexpected ExecutionMode specified: " + executionMode);
}
}
public void receiveIncomingExecutionRequest(ConnectionId connectionId, ExecutionRequest executionRequest) {
// Indicate to the class loader that should this thread require classes when processing this request
// that the classes can be requested via the connection from which we received the request...
sessionClassLoader.setThreadLocalConnectionId(connectionId);
// Outer try-catch to catch and log all exceptions
// so as not to kill a processing thread on a bad request...
try {
numRemoteThreadsExecutingInThisSession.incrementAndGet();
// Inner try-catch to catch unexpected exceptions
// and add additional context information to exception messages...
try {
// Deserialize the Runnable or Callable object sent by the client,
// using a (de)serializer appropriate to the format indicated in the request...
byte[] serializeExecutableObject = executionRequest.getSerializedExecutableObject();
final SerializationFormat serializationFormat = executionRequest.getSerializationFormat();
final Object executableObject = deserialize(serializeExecutableObject, serializationFormat);
Throwable exceptionThrown = null;
Object objectReturned = null;
try {
// Set the current session details into a thread-local variable, so code can access its own session...
MobilityContextInternal.setCurrentSession(this);
MobilityContextInternal.setCurrentConnectionId(connectionId);
// Determine if object is Runnable or Callable...
if (executableObject instanceof Runnable) {
// Execute as Runnable...
Runnable runnable = (Runnable) executableObject;
runnable.run();
}
else if (executableObject instanceof Callable) {
Callable callable = (Callable) executableObject;
objectReturned = callable.call();
}
else {
throw new IllegalStateException("Unexpected type of deserialized executable object, expected Runnable or Callable: " + (executableObject == null ? null : executableObject.getClass().getName()));
}
}
catch (Throwable e) {
// Catch Throwable, because we have no idea what client-supplied code might throw...
exceptionThrown = e;
}
finally {
// Unset current session details from the thread-local variable...
MobilityContextInternal.setCurrentSession(null);
MobilityContextInternal.setCurrentConnectionId(null);
}
final ExecutionResponse executionResponse;
switch (executionRequest.getExecutionMode()) {
case FIRE_AND_FORGET:
// No need to send response to client.
if (logger.isLoggable(Level.FINER)) {
logger.log(Level.FINER, "Processed execution task and skipped sending response to client, for connection id: " + connectionId + ", execution request: " + executionRequest);
}
break;
case RETURN_RESPONSE:
if (objectReturned != null) {
executionResponse = new ExecutionResponse(
ExecutionResponse.ExecutionOutcome.VALUE_RETURNED,
serialize(objectReturned, defaultSerializationFormat),
defaultSerializationFormat,
executionRequest.getRequestIdentifier()
);
}
else if (exceptionThrown != null) {
executionResponse = new ExecutionResponse(
ExecutionResponse.ExecutionOutcome.FAILURE,
serialize(exceptionThrown, defaultSerializationFormat),
defaultSerializationFormat,
executionRequest.getRequestIdentifier()
);
}
else {
executionResponse = new ExecutionResponse(
ExecutionResponse.ExecutionOutcome.VOID_RETURNED,
serialize(null, defaultSerializationFormat),
defaultSerializationFormat,
executionRequest.getRequestIdentifier()
);
}
mobilityController.sendOutgoingMessage(connectionId, executionResponse);
if (logger.isLoggable(Level.FINER)) {
logger.log(Level.FINER, "Processed execution task and sent response to client, for connection id: " + connectionId + ", execution request: " + executionRequest);
}
break;
default:
throw new IllegalStateException("Unexpected execution mode specified in request: " + executionRequest.getExecutionMode());
}
}
catch (Exception e) {
// Catch any exceptions above and simply re-throw them with additional context information...
throw new IllegalStateException("Failed to process execution request, for connection id: " + connectionId + ", execution request: " + executionRequest, e);
}
finally {
if (numRemoteThreadsExecutingInThisSession.decrementAndGet() == 0 && sessionReleaseRequested) {
// This is the last (or only) thread in this session processing a request from a remote machine.
// At least one of the mobile objects executed called MobilitySession.release(), which set the
// sessionReleaseRequested flag to true.
// Only now that the last remote thread is about to finish, do we release the session...
doRelease();
sessionReleaseRequested = false;
if (logger.isLoggable(Level.FINER)) {
logger.log(Level.FINER, "Processed deferred release of session, for connection id: " + connectionId + ", execution request: " + executionRequest);
}
}
else if (sessionReleaseRequested && logger.isLoggable(Level.FINER)) {
logger.log(Level.FINER, "Deferred release of session to another thread, for connection id: " + connectionId + ", execution request: " + executionRequest);
}
}
}
catch (Exception e) {
// Note: Exceptions caught here are not exceptions thrown by the client-supplied code.
// Exceptions thrown by client-supplied code are caught explicitly and returned to the client above.
// Exceptions caught here would therefore indicate problems with the framework itself.
// Log such exceptions at SEVERE level...
logger.log(Level.SEVERE, "Unexpected exception processing execution task, for connection id: " + connectionId + ", execution request: " + executionRequest, e);
}
// Null-out the connection id for this thread, which we set earlier above...
sessionClassLoader.setThreadLocalConnectionId(null);
}
public void receiveExecutionResponse(ExecutionResponse executionResponse) {
RequestIdentifier requestIdentifier = executionResponse.getRequestIdentifier();
FutureExecutionResponse futureExecutionResponse = futureExecutionResponses.get(requestIdentifier);
if (futureExecutionResponse == null) {
// Request must have timed out...
logger.log(Level.FINER, "Ignored ExecutionResponse, no pending request found, request must have timed out: {0}", executionResponse);
return;
}
futureExecutionResponse.setResponse(executionResponse);
logger.log(Level.FINER, "Accepted ExecutionResponse, passed to request thread: {0}", executionResponse);
}
/**
* Represents an {@link ExecutionResponse} which will materialize in the future.
*
* Note this would be similar to {@link java.util.concurrent.Future}<ExecutionResponse>, but we don't
* implement {@code Future} because it would require us to implement more methods than we really need here.
*
* The a client thread calling into the {@link #execute} methods will will register this object in a map, then
* send an {@link ExecutionRequest} object to a remote machine.
* The client thread will then block on the {@link #getResponse} method of this object.
*
* When a {@link ExecutionResponse} arrives from the remote machine, the thread processing it will look up this
* object in the map and call {@link #setResponse}. At that point the blocked client thread will receive the
* ExecutionResponse and will continue its work.
*
* Note that only calls to the {@link #execute} methods which specify that responses are required will cause
* the client thread to block.
*/
class FutureExecutionResponse {
private final RequestIdentifier requestIdentifier;
private final BlockingQueue responseQueue = new ArrayBlockingQueue(1);
FutureExecutionResponse(RequestIdentifier requestIdentifier) {
this.requestIdentifier = requestIdentifier;
}
public ExecutionResponse getResponse(long timeout, TimeUnit unit) {
try {
ExecutionResponse executionResponse = responseQueue.poll(timeout, unit);
if (executionResponse == null) {
throw new TimeoutException();
}
return executionResponse;
}
catch (TimeoutException e) {
throw new IllegalStateException("Timed out waiting to receive execution response within timeout of " + timeout + " " + unit.name().toLowerCase(), e);
}
catch (Exception e) {
throw new IllegalStateException("Unexpected exception waiting to receive execution response", e);
}
finally {
futureExecutionResponses.remove(this.requestIdentifier);
}
}
public boolean setResponse(ExecutionResponse executionResponse) {
return responseQueue.add(executionResponse);
}
}
@Override
public SessionClassLoader getSessionClassLoader() {
return sessionClassLoader;
}
@Override
public MobilityController getMobilityController() {
return this.mobilityController;
}
@Override
public void release() {
if (MobilityContextInternal.hasCurrentSession()) {
// This is being called by a thread executing a request from a remote machine.
// Defer releasing the session until all threads processing remote requests in this session have finished.
// Set a flag to signal that last such thread to finish, should release the session...
this.sessionReleaseRequested = true;
}
else {
// This is being called by a thread from the local application.
// Check if remote threads are executing in the session...
if (numRemoteThreadsExecutingInThisSession.get() == 0) {
// No remote threads are executing in this session, release the session...
doRelease();
}
else {
// Some remote threads are executing in the session,
// set the flag to have those threads release the session...
this.sessionReleaseRequested = true;
}
}
}
void doRelease() {
mobilityController.releaseSession(this.sessionId);
}
private Object deserialize(byte[] serializedObject, SerializationFormat serializationFormat) {
try {
switch (serializationFormat) {
case KRYO:
// Note: we only support one serialization format now,
// however the protocol allows for others in future...
return defaultSerializer.deserialize(serializedObject);
default:
throw new IllegalStateException("Unsupported serialization format: " + serializationFormat);
}
}
catch (Exception e) {
throw new IllegalStateException("Exception deserializing object from " + serializedObject.length + " bytes data in " + serializationFormat + " format", e);
}
}
private byte[] serialize(Object object, SerializationFormat serializationFormat) {
try {
switch (serializationFormat) {
case KRYO:
return defaultSerializer.serialize(object);
default:
throw new IllegalStateException("Unsupported serialization format: " + serializationFormat);
}
}
catch (Exception e) {
throw new IllegalStateException("Exception serializing object to " + serializationFormat + " format: " + object, e);
}
}
@Override
public String toString() {
return "MobilitySession{" +
"sessionId=" + sessionId +
'}';
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy