All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.cometd.oort.OortService Maven / Gradle / Ivy

There is a newer version: 8.0.6
Show newest version
/*
 * 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:

*
    *
  1. * 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 *
  2. *
  3. * 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). *
  4. *
  5. * 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. *
  6. *
  7. * 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)}. *
  8. *
*

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); } } }