moleculerjs / moleculer

:rocket: Progressive microservices framework for Node.js
https://moleculer.services/
MIT License
6.16k stars 586 forks source link

Inter-namespace communication #425

Closed flaviocordova closed 5 years ago

flaviocordova commented 5 years ago

According to #57, you can define namespaces to segment (or, actually, segregate) services... it's fine in the context of avoiding mixing services in different stages of an application (specially when transporter is network-based, not necessarily an issue when using redis, for exemple) but consider a scenario where you have many different applications (based on Moleculer) that need to share common services (or just share some services to each other).

To let them communicate they must share the same transporter (right?) but it might cause some conflicts if both applications define the same service (I mean, the same service name, not the implementation) since the registry wouldn't be able to differentiate replicas from "domains".

A nice approach would be to be able to call an action from a service in a different namespace. That way, the default behaviour would be keeping services isolated but I could be able to explicitly set the namespace, for example:

broker.call('auth.authenticate', {..}) => Would invoke auth.authenticate in the current namespace broker.call('orders.list@marketplace") => would explicitly call orders.list in the marketplace namespace.

Wallacy commented 5 years ago

"IF" we made the namespace a array, then you can declare two or more namespace for the service: ex: Service 1 - A|B , Service 2 - B|C can interconnect because has 'B' in common.

Then, A|B|C can be made a implicit 'group' of any service (just put A|B on the service group when loaded).

broker.emit , broker.broadcast and broker.call will be implicit called using the group argument for A|B or B|C.

broker.call must be able to call a group of course.

BTW Inter-namespace communication is a nice feature.

flaviocordova commented 5 years ago

I don't know, it seems to me adding multiple namespaces to the same service could make thinks get a little messy. I like the concept of namespaces because it create "clusters" of services and can (probably) optimize messaging since events broadcast can have limited reach inside the cluster. In the other hand, there should be some communication line between then so namespaces could be seen as a "service aggregator" providing services to other clusters/namespaces.

icebob commented 5 years ago

It would be very hard to implement this feature in the Moleculer core. Maybe it would be rewritten the ~20-30% of the whole codes.

However, you can create connector services which starts a new broker which connects to other namespaces. I made a PoC example:

namespace-a.js

"use strict";

const { ServiceBroker } = require("moleculer");

const broker = new ServiceBroker({
    namespace: "projectA",
    nodeID: "node-1",
    transporter: "NATS",
    logFormatter: "short"
});

// Example greeter service in namespace "projectA"
broker.createService({
    name: "greeter",
    actions: {
        hello(ctx) {
            return "Hello from Project A!";
        }
    }
});

/** Inter-namespace connector service */
broker.createService({
    name: "projectB-connector",
    settings: {
        ns: "projectB"
    },
    actions: {
        call: {
            handler(ctx) {
                return this.broker2.call(ctx.params.action, ctx.params.params, { meta: ctx.meta });
            }
        }
    },
    created() {
        this.broker2 = new ServiceBroker({
            namespace: this.settings.ns,
            nodeID: "node-connector",
            transporter: "NATS",
            logFormatter: "short"
        });
    },

    started() {
        return this.broker2.start();
    },

    stopped() {
        return this.broker2.stop();
    }
});

broker.start().then(() => {
    broker.repl();
});

namespace-b.js

"use strict";

const ServiceBroker = require("../src/service-broker");

const broker = new ServiceBroker({
    namespace: "projectB",
    nodeID: "node-1",
    transporter: "NATS",
    logFormatter: "short"
});

// Example greeter service in namespace "projectB"
broker.createService({
    name: "greeter",
    actions: {
        hello(ctx) {
            return "Hello from Project B!";
        }
    }
});

broker.start().then(() => {
    broker.repl();
});

After starting both files, if you run call greeter.hello on the first REPL console, you'll get "Hello from Project A", but run via the connector withcall projectB-connector.call --action greeter.helloyou'll get"Hello from Project B"`.

By the way you can develop further to add namespace array to connector and add ns param to the action and you can reach multiple namespaces with one connector.

icebob commented 5 years ago

Here is an enhanced version to support multiple namespaces:

"use strict";

const { ServiceBroker } = require("moleculer");
const Promise = require("bluebird");

const broker = new ServiceBroker({
    namespace: "projectA",
    nodeID: "node-1",
    transporter: "NATS",
    logFormatter: "short"
});

// Example greeter service in namespace "projectA"
broker.createService({
    name: "greeter",
    actions: {
        hello(ctx) {
            return "Hello from Project A!";
        }
    }
});

/** Inter-namespace connector service */
broker.createService({
    name: "ns-connector",
    settings: {
        namespaces: [
            "projectB",
            "projectC",
            "projectD"
        ],
        brokerOptions: {
            nodeID: "ns-connector",
            transporter: "NATS",
            logFormatter: "short"
        }
    },
    actions: {
        call: {
            params: {
                ns: "string",
                action: "string",
                params: { type: "any", optional: true }
            },
            handler(ctx) {
                const b = this.brokers[ctx.params.ns];
                if (!b)
                    return Promise.reject(new Error("Not defined namespace!"));

                return b.call(ctx.params.action, ctx.params.params, { meta: ctx.meta });
            }
        }
    },
    created() {
        this.brokers = {};
        this.settings.namespaces.forEach(namespace => {
            this.logger.info(`Create broker for '${namespace} namespace...'`);
            this.brokers[namespace] = new ServiceBroker(
                Object.assign({}, this.settings.brokerOptions, { namespace }));
        });
    },

    started() {
        return Promise.all(Object.values(this.brokers).map(b => b.start()));
    },

    stopped() {
        return Promise.all(Object.values(this.brokers).map(b => b.stop()));
    }
});

broker.start().then(() => {
    broker.repl();
});

Call via REPL

$ call ns-connector.call --action greeter.hello --ns projectB
icebob commented 5 years ago

I don't know why it didn't come to my mind, but it can be implemented as a middleware because middleware can wrap the broker.call.

So here is the next level, an inter-namespace middleware:

const InterNamespaceMiddleware = function(opts) {
    if (!Array.isArray(opts))
        throw new Error("Must be an Array");

    let thisBroker;
    const brokers = {};

    return {
        created(broker) {
            thisBroker = broker;
            opts.forEach(nsOpts => {
                if (_.isString(nsOpts)) {
                    nsOpts = {
                        namespace: nsOpts
                    };
                }
                const ns = nsOpts.namespace;

                this.logger.info(`Create internamespace broker for '${ns} namespace...'`);
                const brokerOpts = _.defaultsDeep({}, nsOpts, { nodeID: null, middlewares: null }, broker.options);
                brokers[ns] = new ServiceBroker(brokerOpts);
            });
        },

        started() {
            return Promise.all(Object.values(brokers).map(b => b.start()));
        },

        stopped() {
            return Promise.all(Object.values(brokers).map(b => b.stop()));
        },

        call(next) {
            return function(actionName, params, opts = {}) {
                if (_.isString(actionName) && actionName.includes("@")) {
                    const [action, namespace] = actionName.split("@");

                    if (brokers[namespace]) {
                        return brokers[namespace].call(action, params, opts);
                    } else if (namespace === thisBroker.namespace) {
                        return next(action, params, opts);
                    } else {
                        throw new Error("Unknow namespace: " + namespace);
                    }
                }

                return next(actionName, params, opts);
            };
        },
    };
};

Usage:

// moleculer.config.js
module.exports {
    namespace: "local",
    nodeID: "node-1",
    transporter: "NATS",
    middlewares: [
        InterNamespaceMiddleware(["ns-mars", "ns-venus"])
    ]
}

Calling

// Call service in the local namespace
broker.call("greeter.hello");

// Call service in the local namespace with the namespace
broker.call("greeter.hello@local");

// Call service in the "ns-venus" namespace
broker.call("greeter.hello@ns-venus");

// Call service in the "ns-mars" namespace
broker.call("greeter.hello@ns-mars");
icebob commented 5 years ago

I've added it to a Gist: https://gist.github.com/icebob/c0bce54436379d29c1bee8521ceb5348

veeramarni commented 3 years ago

Is there a way to do broker.waitForServices('greeter@ns-mars')?

icebob commented 3 years ago

No, waitForServices checks only local services.