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

joynr.dispatching.RequestReplyManager.js Maven / Gradle / Ivy

There is a newer version: 1.4.0
Show newest version
/*jslint es5: true, node: true, node: true */
/*
 * #%L
 * %%
 * Copyright (C) 2011 - 2017 BMW Car IT GmbH
 * %%
 * 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.
 * #L%
 */
var Promise = require("../../global/Promise");
var Reply = require("./types/Reply");
var MessagingQos = require("../messaging/MessagingQos");
var InProcessAddress = require("../messaging/inprocess/InProcessAddress");
var Typing = require("../util/Typing");
var Util = require("../util/UtilInternal");
var JSONSerializer = require("../util/JSONSerializer");
var LongTimer = require("../util/LongTimer");
var MethodInvocationException = require("../exceptions/MethodInvocationException");
var ProviderRuntimeException = require("../exceptions/ProviderRuntimeException");
var Version = require("../../joynr/types/Version");
var LoggerFactory = require("../system/LoggerFactory");
/**
 * The RequestReplyManager is responsible maintaining a list of providers that wish to
 * receive incoming requests, and also a list of requestReplyIds which is used to match
 * an incoming message with an expected reply.
 *
 * @name RequestReplyManager
 * @constructor
 *
 * @param {Dispatcher}
 *            dispatcher
 * @param {TypeRegistry}
 *            typeRegistry - the global type registry that records the type names
 *            together with their constructor.
 */
function RequestReplyManager(dispatcher, typeRegistry) {
    var log = LoggerFactory.getLogger("joynr.dispatching.RequestReplyManager");

    var providers = {};
    var replyCallers = {};
    var started = true;

    var CLEANUP_CYCLE_INTERVAL = 1000;

    var cleanupInterval = setInterval(function() {
        var currentTime = Date.now();
        var id;
        for (id in replyCallers) {
            if (replyCallers.hasOwnProperty(id)) {
                var caller = replyCallers[id];
                if (caller.expiresAt <= currentTime) {
                    caller.reject(new Error('Request with id "' + id + '" failed: ttl expired'));
                    delete replyCallers[id];
                }
            }
        }
    }, CLEANUP_CYCLE_INTERVAL);

    function checkIfReady() {
        if (!started) {
            throw new Error("RequestReplyManager is already shut down");
        }
    }

    /**
     * @name RequestReplyManager#sendRequest
     * @function
     *
     * @param {Object}
     *            settings
     * @param {String}
     *            settings.from participantId of the sender
     * @param {DiscoveryEntryWithMetaInfo}
     *            settings.toDiscoveryEntry DiscoveryEntry of the receiver
     * @param {MessagingQos}
     *            settings.messagingQos quality-of-service parameters such as time-to-live
     * @param {Request}
     *            settings.request the Request to send
     * @returns {Promise} the Promise for the Request
     */
    this.sendRequest = function sendRequest(settings) {
        checkIfReady();

        var deferred = Util.createDeferred();
        this.addReplyCaller(
            settings.request.requestReplyId,
            {
                resolve: deferred.resolve,
                reject: deferred.reject
            },
            settings.messagingQos.ttl
        );
        // resolve will be called upon successful response

        function requestErrorCatcher(error) {
            delete replyCallers[settings.request.requestReplyId];
            deferred.reject(error);
        }

        dispatcher.sendRequest(settings).catch(requestErrorCatcher);

        return deferred.promise;
    };

    /**
     * @name RequestReplyManager#sendOneWayRequest
     * @function
     *
     * @param {Object}
     *            settings
     * @param {String}
     *            settings.from participantId of the sender
     * @param {DiscoveryEntryWithMetaInfo}
     *            settings.toDiscoveryEntry DiscoveryEntry of the receiver
     * @param {MessagingQos}
     *            settings.messagingQos quality-of-service parameters such as time-to-live
     * @param {OneWayRequest}
     *            settings.request the Request to send
     * @returns {Promise} the Promise for the Request
     */
    this.sendOneWayRequest = function sendOneWayRequest(settings) {
        checkIfReady();
        return dispatcher.sendOneWayRequest(settings);
    };

    /**
     * The function addRequestCaller is called when a provider wishes to receive
     * incoming requests for the given participantId
     *
     * @name RequestReplyManager#addRequestCaller
     * @function
     *
     * @param {String}
     *            participantId of the provider receiving the incoming requests
     * @param {Provider}
     *            provider
     */
    this.addRequestCaller = function addRequestCaller(participantId, provider) {
        checkIfReady();
        providers[participantId] = provider;
    };

    /**
     * The function addReplyCaller is called when a proxy get/set or rpc is called and
     * is waiting for a reply The reply caller is automatically
     * removed when the ttl expires.
     *
     * @name RequestReplyManager#addReplyCaller
     * @function
     *
     * @param {String}
     *            requestReplyId
     * @param {ReplyCaller}
     *            replyCaller
     * @param {Number}
     *            ttl_ms relative number of milliseconds to wait for the reply.
     *            The replycaller will be removed in ttl_ms and and Error will be passed
     *            to the replyCaller
     */
    this.addReplyCaller = function addReplyCaller(requestReplyId, replyCaller, ttl_ms) {
        checkIfReady();
        replyCaller.expiresAt = Date.now() + ttl_ms;
        replyCallers[requestReplyId] = replyCaller;
    };

    /**
     * The function removeRequestCaller is called when a provider no longer wishes to
     * receive incoming requests
     *
     * @name RequestReplyManager#removeRequestCaller
     * @function
     *
     * @param {String}
     *            participantId
     */
    this.removeRequestCaller = function removeRequestCaller(participantId) {
        checkIfReady();
        try {
            delete providers[participantId];
        } catch (error) {
            log.error("error removing provider with participantId: " + participantId + " error: " + error);
        }
    };

    /**
     * @name RequestReplyManager#handleRequest
     * @function
     *
     * @param {Request}
     *            request
     */
    this.handleRequest = function handleRequest(providerParticipantId, request) {
        var exception;

        function createReplyFromError(exception) {
            return new Reply({
                error: exception,
                requestReplyId: request.requestReplyId
            });
        }

        function createReplyFromSuccess(response) {
            return new Reply({
                response: response,
                requestReplyId: request.requestReplyId
            });
        }

        try {
            checkIfReady();
        } catch (error) {
            exception = new MethodInvocationException({
                detailMessage:
                    "error handling request: " +
                    JSONSerializer.stringify(request) +
                    " for providerParticipantId " +
                    providerParticipantId +
                    ". Joynr runtime already shut down."
            });
            return Promise.resolve(createReplyFromError(exception));
        }
        var provider = providers[providerParticipantId];
        if (!provider) {
            // TODO error handling request
            // TODO what if no provider is found in the mean time?
            // Do we need to add a task to handleRequest later?
            exception = new MethodInvocationException({
                detailMessage:
                    "error handling request: " +
                    JSONSerializer.stringify(request) +
                    " for providerParticipantId " +
                    providerParticipantId
            });
            return Promise.resolve(createReplyFromError(exception));
        }

        // if there's an operation available to call
        var result;
        if (provider[request.methodName] && provider[request.methodName].callOperation) {
            // may throw an immediate exception when callOperation checks the
            // arguments, in this case exception must be caught.
            // If final customer provided method implementation gets called,
            // that one may return either promise (preferred) or direct result
            // and may possibly also throw exception in the latter case.
            try {
                result = provider[request.methodName].callOperation(request.params, request.paramDatatypes);
            } catch (internalException) {
                exception = internalException;
            }
            // otherwise, check whether request is an attribute get, set or an operation
        } else {
            var match = request.methodName.match(/([gs]et)?(\w+)/);
            var getSet = match[1];
            if (getSet) {
                var attributeName = match[2];
                var attributeObject = provider[attributeName] || provider[Util.firstLower(attributeName)];
                // if the attribute exists in the provider
                if (attributeObject && !attributeObject.callOperation) {
                    try {
                        if (getSet === "get") {
                            result = attributeObject.get();
                        } else if (getSet === "set") {
                            result = attributeObject.set(request.params[0]);
                        }
                    } catch (internalGetterSetterException) {
                        if (internalGetterSetterException instanceof ProviderRuntimeException) {
                            exception = internalGetterSetterException;
                        } else {
                            exception = new ProviderRuntimeException({
                                detailMessage:
                                    "getter/setter method of attribute " + attributeName + " reported an error"
                            });
                        }
                    }
                } else {
                    // if neither an operation nor an attribute exists in the
                    // provider => deliver MethodInvocationException
                    exception = new MethodInvocationException({
                        detailMessage:
                            'Could not find an operation "' +
                            request.methodName +
                            '" or an attribute "' +
                            attributeName +
                            '" in the provider',
                        providerVersion: new Version({
                            majorVersion: provider.constructor.MAJOR_VERSION,
                            minorVersion: provider.constructor.MINOR_VERSION
                        })
                    });
                }
            } else {
                // if no operation was found and methodName didn't start with "get"
                // or "set" => deliver MethodInvocationException
                exception = new MethodInvocationException({
                    detailMessage: 'Could not find an operation "' + request.methodName + '" in the provider',
                    providerVersion: new Version({
                        majorVersion: provider.constructor.MAJOR_VERSION,
                        minorVersion: provider.constructor.MINOR_VERSION
                    })
                });
            }
        }

        /* Asynchronously pass the result back to the dispatcher
         *
         * Call operations can be a sync or async method. In the sync case,
         * the return value of the call operation
         * is simply the result of the call. In the async case, the provider has
         * the possibility to return a promise
         * object. In this case, we wait until the promise object is resolved
         * and call then the callbackDispatcher
         */

        if (!exception && Util.isPromise(result)) {
            return result.then(createReplyFromSuccess).catch(createReplyFromError);
        }
        if (exception) {
            return Promise.resolve(createReplyFromError(exception));
        }
        return Promise.resolve(createReplyFromSuccess(result));
    };

    /**
     * @name RequestReplyManager#handleOneWayRequest
     * @function
     *
     * @param {String}
     *            providerParticipantId
     * @param {OneWayRequest}
     *            request
     */
    this.handleOneWayRequest = function handleOneWayRequest(providerParticipantId, request) {
        checkIfReady();
        var provider = providers[providerParticipantId];
        if (!provider) {
            throw new MethodInvocationException({
                detailMessage:
                    "error handling one-way request: " +
                    JSONSerializer.stringify(request) +
                    " for providerParticipantId " +
                    providerParticipantId
            });
        }

        // if there's an operation available to call
        if (provider[request.methodName] && provider[request.methodName].callOperation) {
            // If final customer provided method implementation gets called,
            // that one may return either promise (preferred) or direct result
            // and may possibly also throw exception in the latter case.
            provider[request.methodName].callOperation(request.params, request.paramDatatypes);
        } else {
            throw new MethodInvocationException({
                detailMessage: 'Could not find an operation "' + request.methodName + '" in the provider',
                providerVersion: new Version({
                    majorVersion: provider.constructor.MAJOR_VERSION,
                    minorVersion: provider.constructor.MINOR_VERSION
                })
            });
        }
    };

    /**
     * @name RequestReplyManager#handleReply
     * @function
     *
     * @param {Reply}
     *            reply
     */
    this.handleReply = function handleReply(reply) {
        var replyCaller = replyCallers[reply.requestReplyId];

        if (replyCaller === undefined) {
            log.error(
                "error handling reply resolve, because replyCaller could not be found: " +
                    JSONSerializer.stringify(reply, undefined, 4)
            );
            return;
        }

        try {
            if (reply.error) {
                if (reply.error instanceof Error) {
                    replyCaller.reject(reply.error);
                } else {
                    replyCaller.reject(Typing.augmentTypes(reply.error, typeRegistry));
                }
            } else {
                replyCaller.resolve(reply.response);
            }

            delete replyCallers[reply.requestReplyId];
        } catch (e) {
            log.error(
                "exception thrown during handling reply " +
                    JSONSerializer.stringify(reply, undefined, 4) +
                    ":\n" +
                    e.stack
            );
        }
    };

    /**
     * Shutdown the request reply manager
     *
     * @function
     * @name RequestReplyManager#shutdown
     */
    this.shutdown = function shutdown() {
        clearInterval(cleanupInterval);

        var requestReplyId;
        for (requestReplyId in replyCallers) {
            if (replyCallers.hasOwnProperty(requestReplyId)) {
                var replyCaller = replyCallers[requestReplyId];
                if (replyCaller) {
                    replyCaller.reject(new Error("RequestReplyManager is already shut down"));
                }
            }
        }
        replyCallers = {};
        started = false;
    };
}

module.exports = RequestReplyManager;




© 2015 - 2025 Weber Informatics LLC | Privacy Policy