org.cometd.oort.OortService Maven / Gradle / Ivy
/*
* Copyright (c) 2008-2018 the original author or authors.
*
* 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 org.cometd.oort;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicLong;
import org.cometd.bayeux.Promise;
import org.cometd.bayeux.server.BayeuxServer;
import org.cometd.bayeux.server.LocalSession;
import org.cometd.bayeux.server.ServerChannel;
import org.cometd.bayeux.server.ServerMessage;
import org.cometd.bayeux.server.ServerSession;
import org.cometd.server.BayeuxServerImpl;
import org.eclipse.jetty.util.component.AbstractLifeCycle;
import org.eclipse.jetty.util.thread.Scheduler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* An {@link OortService} allows applications to forward actions to Oort
* nodes that own the entity onto which the action should be applied.
* An {@link OortService} builds on the concept introduced by {@link OortObject}
* that the ownership of a particular entity belongs only to one node.
* Any node can read the entity, but only the owner can create/modify/delete it.
* In order to perform actions that modify the entity, a node has to know
* what is the node that owns the entity, and then forward the action to
* the owner node.
* {@link OortService} provides the facilities to forward the action to the
* owner node and return the result of the action, or its failure.
* {@link OortService}s are usually created at application startup, but may
* be created and destroyed on-the-fly.
* In both cases, they must be {@link #start() started} to make them functional
* and {@link #stop() stopped} when they are no longer needed.
* Usage of {@link OortService} follows these steps:
*
* -
* The application running in the requesting node identifies
* the owner node and calls
* {@link #forward(String, Object, Object)}, passing the Oort URL of
* the owner node, the action data, and an opaque context
*
* -
* The application implements {@link #onForward(Request)}, which is
* executed in the owner node. In this method the action
* data is used to perform the action that modifies the entity, and
* the result is returned (or an exception thrown in case of failure).
*
* -
* The application implements {@link #onForwardSucceeded(Object, Object)}
* (which is executed in the requesting node)
* which provides as parameters the result of the action from the
* second step and the opaque context from the first step.
*
* -
* The application implements {@link #onForwardFailed(Object, Object)}
* (which is executed in the requesting node)
* which provides as parameters the failure from the second step and
* the opaque context from the first step.
* The failure object is the message string of a generic exception,
* or the failure returned by {@link Result#failure(Object)}.
*
*
* The steps above do not change if the requesting node and
* the owner node are the same.
*
* @param the result type
* @param the opaque context type
*/
public abstract class OortService extends AbstractLifeCycle implements ServerChannel.MessageListener {
private static final String CONTEXT_FIELD = "oort.service.context";
private static final String DATA_FIELD = "oort.service.data";
private static final String ID_FIELD = "oort.service.id";
private static final String OORT_URL_FIELD = "oort.service.url";
private static final String PARAMETER_FIELD = "oort.service.parameter";
private static final String RESULT_FIELD = "oort.service.result";
private static final String TIMEOUT_FIELD = "oort.service.timeout";
private final AtomicLong contextIds = new AtomicLong();
private final ConcurrentMap> callbacks = new ConcurrentHashMap<>();
private final Oort oort;
private final String name;
private final String forwardChannelName;
private final String broadcastChannelName;
private final String resultChannelName;
private final LocalSession session;
protected final Logger logger;
private volatile long timeout = 5000;
/**
* Creates an {@link OortService} with the given name.
*
* @param oort the Oort where this service lives
* @param name the unique name across the cluster of this service
*/
protected OortService(Oort oort, String name) {
this.oort = oort;
this.name = name;
this.forwardChannelName = "/service/oort/service/" + name;
this.broadcastChannelName = "/oort/service/" + name;
this.resultChannelName = forwardChannelName + "/result";
this.session = oort.getBayeuxServer().newLocalSession(name);
this.logger = LoggerFactory.getLogger(getClass().getName() + "." + Oort.replacePunctuation(oort.getURL(), '_') + name);
}
/**
* @return the Oort of this service
*/
public Oort getOort() {
return oort;
}
/**
* @return the name of this service
*/
public String getName() {
return name;
}
/**
* @return the local session associated with this service
*/
public LocalSession getLocalSession() {
return session;
}
/**
* @return the timeout, in milliseconds, for an action to return a result (by default 5000 ms)
*/
public long getTimeout() {
return timeout;
}
/**
* @param timeout the timeout, in milliseconds, for an action to return a result
*/
public void setTimeout(long timeout) {
this.timeout = timeout;
}
@Override
protected void doStart() throws Exception {
session.handshake();
BayeuxServer bayeuxServer = oort.getBayeuxServer();
bayeuxServer.createChannelIfAbsent(forwardChannelName).getReference().addListener(this);
bayeuxServer.createChannelIfAbsent(broadcastChannelName).getReference().addListener(this);
bayeuxServer.createChannelIfAbsent(resultChannelName).getReference().addListener(this);
oort.observeChannel(broadcastChannelName);
if (logger.isDebugEnabled()) {
logger.debug("Started {}", this);
}
}
@Override
protected void doStop() throws Exception {
oort.deobserveChannel(broadcastChannelName);
BayeuxServer bayeuxServer = oort.getBayeuxServer();
ServerChannel channel = bayeuxServer.getChannel(resultChannelName);
if (channel != null) {
channel.removeListener(this);
}
channel = bayeuxServer.getChannel(broadcastChannelName);
if (channel != null) {
channel.removeListener(this);
}
channel = bayeuxServer.getChannel(forwardChannelName);
if (channel != null) {
channel.removeListener(this);
}
session.disconnect();
if (logger.isDebugEnabled()) {
logger.debug("Stopped {}", this);
}
}
/**
* Subclasses must call this method to forward the action to the owner node.
* If the {@code targetOortURL} is {@code null}, then the action is broadcast to all nodes.
* Nodes that receive an action request that they can't fullfill because they don't own the
* entity the action should be applied to must return {@link Result#ignore(Object)}.
*
* @param targetOortURL the owner node Oort URL, or null to broadcast the action to all nodes
* @param parameter the action parameter that will be passed to {@link #onForward(Request)}
* @param context the opaque context passed to {@link #onForwardSucceeded(Object, Object)}
* @return whether the forward succeeded
*/
protected boolean forward(String targetOortURL, Object parameter, C context) {
Map ctx = new HashMap<>(4);
long contextId = contextIds.incrementAndGet();
ctx.put(ID_FIELD, contextId);
ctx.put(CONTEXT_FIELD, context);
callbacks.put(contextId, ctx);
Map data = new HashMap<>(3);
data.put(ID_FIELD, contextId);
data.put(PARAMETER_FIELD, parameter);
String localOortURL = getOort().getURL();
data.put(OORT_URL_FIELD, localOortURL);
if (targetOortURL == null) {
// Application does not know where the entity is, broadcast
if (logger.isDebugEnabled()) {
logger.debug("Broadcasting action: {}", data);
}
startTimeout(ctx);
oort.getBayeuxServer().getChannel(broadcastChannelName).publish(getLocalSession(), data, Promise.noop());
return true;
} else {
if (localOortURL.equals(targetOortURL)) {
// Local case
if (logger.isDebugEnabled()) {
logger.debug("Forwarding action locally ({}): {}", localOortURL, data);
}
startTimeout(ctx);
onForwardMessage(data, false);
return true;
} else {
// Remote case
OortComet comet = getOort().getComet(targetOortURL);
if (comet != null) {
if (logger.isDebugEnabled()) {
logger.debug("Forwarding action from {} to {}: {}", localOortURL, targetOortURL, data);
}
startTimeout(ctx);
comet.getChannel(forwardChannelName).publish(data);
return true;
} else {
if (logger.isDebugEnabled()) {
logger.debug("Could not forward action from {} to {}: {}", localOortURL, targetOortURL, data);
}
return false;
}
}
}
}
@Override
public boolean onMessage(ServerSession from, ServerChannel channel, ServerMessage.Mutable message) {
if (forwardChannelName.equals(message.getChannel())) {
onForwardMessage(message.getDataAsMap(), false);
} else if (broadcastChannelName.equals(message.getChannel())) {
onForwardMessage(message.getDataAsMap(), true);
} else if (resultChannelName.equals(message.getChannel())) {
onResultMessage(message.getDataAsMap());
}
return true;
}
protected void onForwardMessage(Map data, boolean broadcast) {
if (logger.isDebugEnabled()) {
logger.debug("Received {} action {}", broadcast ? "broadcast" : "forwarded", data);
}
Map resultData = new HashMap<>(3);
resultData.put(ID_FIELD, data.get(ID_FIELD));
resultData.put(OORT_URL_FIELD, getOort().getURL());
String oortURL = (String)data.get(OORT_URL_FIELD);
try {
Result result = onForward(new Request(oort.getURL(), data.get(PARAMETER_FIELD), oortURL));
if (logger.isDebugEnabled()) {
logger.debug("Forwarded action result {}", result);
}
if (result.succeeded()) {
resultData.put(RESULT_FIELD, true);
resultData.put(DATA_FIELD, result.data);
} else if (result.failed()) {
resultData.put(RESULT_FIELD, false);
resultData.put(DATA_FIELD, result.data);
} else {
if (broadcast) {
// Ignore and therefore return
if (logger.isDebugEnabled()) {
logger.debug("Ignoring broadcast action result {}", result);
}
return;
} else {
// Convert ignore into failure
resultData.put(RESULT_FIELD, false);
resultData.put(DATA_FIELD, result.data);
}
}
} catch (Throwable x) {
if (broadcast) {
return;
}
String failure = x.getMessage();
if (failure == null || failure.length() == 0) {
failure = x.getClass().getName();
}
resultData.put(RESULT_FIELD, false);
resultData.put(DATA_FIELD, failure);
}
if (getOort().getURL().equals(oortURL)) {
// Local case
if (logger.isDebugEnabled()) {
logger.debug("Returning forwarded action result {} to local {}", resultData, oortURL);
}
onResultMessage(resultData);
} else {
// Remote case
OortComet comet = getOort().getComet(oortURL);
if (comet != null) {
if (logger.isDebugEnabled()) {
logger.debug("Returning forwarded action result {} to remote {}", resultData, oortURL);
}
comet.getChannel(resultChannelName).publish(resultData);
} else {
// Probably the node disconnected concurrently
if (logger.isDebugEnabled()) {
logger.debug("Could not return forwarded action result {} to remote {}", resultData, oortURL);
}
}
}
}
protected void onResultMessage(Map data) {
long actionId = ((Number)data.get(ID_FIELD)).longValue();
Map ctx = callbacks.remove(actionId);
if (logger.isDebugEnabled()) {
logger.debug("Action result {} with context {}", data, ctx);
}
// Atomically remove the callback, so we guarantee one notification only.
// Multiple notifications may happen when broadcasting the forward request
// and nodes mistakenly return multiple results.
if (ctx != null) {
cancelTimeout(ctx);
@SuppressWarnings("unchecked")
C context = (C)ctx.get(CONTEXT_FIELD);
boolean success = (Boolean)data.get(RESULT_FIELD);
if (success) {
@SuppressWarnings("unchecked")
R result = (R)data.get(DATA_FIELD);
onForwardSucceeded(result, context);
} else {
Object failure = data.get(DATA_FIELD);
onForwardFailed(failure, context);
}
}
}
private void startTimeout(Map ctx) {
long contextId = ((Number)ctx.get(ID_FIELD)).longValue();
TimeoutTask timeoutTask = new TimeoutTask(contextId);
ctx.put(TIMEOUT_FIELD, ((BayeuxServerImpl)oort.getBayeuxServer()).schedule(timeoutTask, getTimeout()));
}
private void cancelTimeout(Map ctx) {
Scheduler.Task timeoutTask = (Scheduler.Task)ctx.get(TIMEOUT_FIELD);
if (timeoutTask != null) {
timeoutTask.cancel();
}
}
/**
* Subclasses must implement this method, that runs on the owner node,
* to implement the action functionality.
* The result to return is {@link Result#success(Object)} or {@link Result#failure(Object)}
* if the implementation of this method was able to find the entity on which the action
* functionality was meant to be applied, or {@link Result#ignore(Object)} if the entity
* was not found.
*
* @param request the request containing the parameter passed from {@link #forward(String, Object, Object)}
* @return the result containing the data that will be passed to {@link #onForwardSucceeded(Object, Object)}
*/
protected abstract Result onForward(Request request);
/**
* Subclasses must implement this method, that runs on the requesting node,
* to complete the functionality after the action has been successfully run on the owner node.
*
* @param result the result of the action
* @param context the opaque context from {@link #forward(String, Object, Object)}
*/
protected abstract void onForwardSucceeded(R result, C context);
/**
* Subclasses must implement this method, that runs on the requesting node,
* to complete the functionality after the action failed on the owner node.
*
* @param failure the failure of the action
* @param context the opaque context from {@link #forward(String, Object, Object)}
*/
protected abstract void onForwardFailed(Object failure, C context);
@Override
public String toString() {
return String.format("%s[%s]@%s", getClass().getSimpleName(), getName(), getOort().getURL());
}
/**
* Encapsulates a forwarded request.
*
* @see #onForward(Request)
* @see Result
*/
public static class Request {
private final String localOortURL;
private final Object data;
private final String oortURL;
private Request(String localOortURL, Object data, String oortURL) {
this.localOortURL = localOortURL;
this.data = data;
this.oortURL = oortURL;
}
/**
* @return the request data
*/
public Object getData() {
return data;
}
/**
* @return the request data as a {@code Map<String, Object>}
*/
@SuppressWarnings("unchecked")
public Map getDataAsMap() {
return (Map)getData();
}
/**
* @return the Oort URL of the requesting node
*/
public String getOortURL() {
return oortURL;
}
/**
* @return whether the request is local to the current Oort node
*/
public boolean isLocal() {
return localOortURL.equals(getOortURL());
}
}
/**
* Encapsulates the result of a forwarded request returned by {@link #onForward(Request)}.
* Applications must use methods {@link #success(Object)}, {@link #failure(Object)} or {@link #ignore(Object)}
* to signal to the implementation the result of the forwarded request.
* {@link OortService} may forward a request action for an entity to the owner node, but before the request
* arrives to the owner node, the entity may be removed from the owner node.
* When the owner node receives the request for the entity, the entity is not available anymore.
* Similarly, a request that is broadcast to all nodes arrives to nodes that do not own the entity.
* The node that receives the request must return a {@link Result} using the following rules:
*
* - If the node owns the entity, use {@link #success(Object)} or {@link #failure(Object)} - never use
* {@link #ignore(Object)}
* - If the node does not own the entity (it never was the owner, or it was the owner but it is not anymore),
* then use {@link #ignore(Object)}
*
*
* @param the result type
* @see Request
*/
public static class Result {
private final Boolean result;
private final Object data;
private Result(Boolean result, Object data) {
this.result = result;
this.data = data;
}
/**
* Returns a successful {@link Result} containing the given result object.
*
* @param result the result object
* @param the type of the result object
* @return a new {@link Result} instance wrapping the result object
*/
public static Result success(S result) {
return new Result<>(true, result);
}
/**
* Returns a failed {@link Result} containing the given failure object.
*
* @param failure the failure object
* @param the type of the result
* @return a new {@link Result} instance wrapping the failure object
*/
public static Result failure(Object failure) {
return new Result<>(false, failure);
}
/**
* Returns an ignored {@link Result} containing the given data object
*
* @param data the data object
* @param the type of the result
* @return a new {@link Result} instance wrapping the data object
*/
public static Result ignore(Object data) {
return new Result<>(null, data);
}
private boolean succeeded() {
return result != null && result;
}
private boolean failed() {
return result != null && !result;
}
@Override
public String toString() {
return String.format("%s[%s] %s",
getClass().getSimpleName(),
result == null ? "ignored" : result ? "success" : "failure",
data);
}
}
/**
* Utility context that stores the {@link ServerSession} and the {@link ServerMessage}.
* CometD services that extend {@link OortService} may register themselves as listeners
* for messages sent by remote clients. In such case, this class will come handy in this way:
*
* @Service
* class MyService extends OortService<Boolean, ServerContext>
* {
* @Listener("/service/some")
* public void processSome(ServerSession remote, ServerMessage message)
* {
* String ownerOortURL = findOwnerOortURL();
* forward(ownerOortURL, "some", new ServerContext(remote, message));
* }
*
* protected Boolean onForward(Object forwardedData)
* {
* return "some".equals(forwardedData);
* }
*
* protected void onForwardSucceeded(Boolean result, ServerContext context)
* {
* context.getServerSession().deliver(getLocalSession(), "/service/some", result, null);
* }
*
* ...
* }
*
*/
public static class ServerContext {
private final ServerSession session;
private final ServerMessage message;
public ServerContext(ServerSession session, ServerMessage message) {
this.session = session;
this.message = message;
}
public ServerSession getServerSession() {
return session;
}
public ServerMessage getServerMessage() {
return message;
}
}
private class TimeoutTask implements Runnable {
private final long contextId;
private TimeoutTask(long contextId) {
this.contextId = contextId;
}
@Override
public void run() {
Map data = new HashMap<>(3);
data.put(ID_FIELD, contextId);
data.put(RESULT_FIELD, false);
data.put(DATA_FIELD, new TimeoutException());
onResultMessage(data);
}
}
}