tcdl / msb-java

MIT License
6 stars 4 forks source link

Improvement of MSB developer API #289

Open vso-tc opened 8 years ago

vso-tc commented 8 years ago

Issues with the current API

Here’s a part of DateExtractor implemented using msb-java (you can find full source here):

public class DateExtractor {

    public static void main(String... args) {

        MsbContext msbContext = new MsbContext.MsbContextBuilder().
                withShutdownHook(true).
                build();
        DefaultChannelMonitorAgent.start(msbContext);

        MessageTemplate options = new MessageTemplate();
        final String namespace = "search:parsers:facets:v1";

        final Pattern YEAR_PATTERN = Pattern.compile("^.*(20(\\d{2})).*$");

        ResponderServer.create(namespace, options, msbContext, (request, responder) -> {

                    RequestQuery query = request.getQueryAs(RequestQuery.class);
                    String queryString = query.getQ();
                    Matcher matcher = YEAR_PATTERN.matcher(queryString);

                    if (matcher.matches()) {
                        // send acknowledge
                        responder.sendAck(500, null);

                        // parse year
                        String str = matcher.group(1);
                        Integer year = Integer.valueOf(matcher.group(2));

                        // populate response body
                        Result result = new Result();
                        result.setStr(str);
                        result.setStartIndex(queryString.indexOf(str));
                        result.setEndIndex(queryString.indexOf(str) + str.length() - 1);
                        result.setInferredDate(new HashMap<>());
                        result.setProbability(0.9f);

                        Result.Date date = new Result.Date();
                        date.setYear(year);
                        result.setDate(date);

                        ResponseBody responseBody = new ResponseBody();
                        responseBody.setResults(Arrays.asList(result));
                        Payload responsePayload = new Payload.PayloadBuilder()
                                .withStatusCode(200)
                                .withBody(responseBody).build();

                        responder.send(responsePayload);
                    }
                })
                .listen();
    }
//…
}

This approach is cloned from msb (NodeJS) and is very flexible. However it leads to the following issues:

With inversion of control and dependency injection we could solve all the problems described in the previous section. The general idea is to free microservice developer from implementing own main method. Instead he/she has to implement some simple interface with business logic and have the lifecycle methods of that interface invoked externally by msb-java "infrastructure" (whatever it is).

The idea is taken from Java Servlet specification because each microservice is in some way resembles good-old javax.servlet.http.HttpServlet.

However different microservices might have different lifecycles so let's discuss various types of them in detail.

Common microservices

Microservice driven by MSB messages

This type of microservice listens to a single request topic, does something and produces acks and responses to the corresponding response topic. DateExtractor considered above is an example of such microservice. It obtains a query string, tries to parse a date from it and sends the result back.

Optionally during request processing such microservice can consult with another microservice by sending request to it. For this reason we need to inject an instance of Requester during initialization. An important point here is that with such approach we have to use single instance of Requester inside the microservice as opposed to creating a new one for each request (as currently implemented).

Here's the proposed interface for such type of microservice:

public interface MsbMicroservlet {
    /**
     * Invoked during initialization of microservice
     * @param msbContext holds initial configuration and core MSB objects
     * @param requester can be used to send requests to (and process responses from) other microservices via bus
     */
    void init(MsbContext msbContext, Requester requester);

    /**
     * Processes incoming request
     * @param request the payload of the incoming request
     * @param responder allows to send responses and acks back
     */
    void process(Payload request, Responder responder);

    /**
     * Shuts down this microservice
     * @param shouldInterrupt whether active tasks should be interrupted
     */
    void shutdown(boolean shouldInterrupt);
}

Of course something has to instantiate the object that implements such interface and subscribe it to the given namespace. That something is going to be instance of library class MsbMicroserviceRunner which has main method implemented. It creates instances of MsbMicroservlet and invokes their lifecycle methods.

So each microservice is launched from command line as a separate OS process like this:

java -cp my_microservice.jar:msb.jar MsbMicroserviceRunner

Also MsbMicroserviceRunner has to know the actual class and namespace name. To achieve this microservice author specifies this information in the config file:

msbConfig {
#...
  microservletClass = io.github.tcdl.examples.DateExtractor
  microservletNamespace = search:parsers:facets:v1
#...
}

Microservice driven by messages coming from external systems

Another common type of microservice doesn't explicitly subscribe to serve requests from bus but rather only puts them there. Examples are:

In this case interface MsbMicroservlet given in the previous section also works and:

An interesting caveat is that we need to reserve a value to specify that MsbMicroservlet shouldn't listen to any topic:

msbConfig {
#...
  microservletNamespace = NONE
#...
}

Less-common microservices

Microservice that listens to multiple topics

Now let's consider a microservice that subscribes to multiple topics (possibly dynamically). An example of such microservice would be logger that dynamically subscribes to every topic that it gets from other microservices.

To streamline development of such microservices we need to introduce further changes in interfaces:

First of all we need to add another parameter to MsbMicroservlet.init:

    /**
     * Invoked during initialization of microservice
     * @param msbContext holds initial configuration and core MSB objects
     * @param requester can be used to send requests to (and process responses from) other microservices via bus
     * @param msbMicroservletManager allows to dynamically instantiate new services in the same JVM
     */
    void init(MsbContext msbContext, Requester requester, MsbMicroservletManager msbMicroservletManager);

And here's MsbMicroservletManager:

public interface MsbMicroservletManager {
    /**
     * Initializes a given microservice by its class name and subscribes it to the given namespace. All initialization lifecycle steps are executed.
     * @param namespace defines a topic to subscribe to
     * @param microserviceClass defines a class name to instantiate microservice from
     * @return initialized microservice instance
     */
    MsbMicroservlet initMicroservice(String namespace, Class<MsbMicroservlet> microserviceClass);

    /**
     * Shuts down the microservice by executing its shutdown lifecycle steps. The microservice is also unsubscribed from its namespace
     * @param namespace defines a topic that the microservice is subscribed to
     */
    void shutdownMicroservice(String namespace);

    /**
     * Shuts down the microservice by executing its shutdown lifecycle steps. The microservice is also unsubscribed from its namespace
     * @param microservice the microservice to shut down
     */
    void shutdownMicroservice(MsbMicroservlet microservice);
}

Another point is that probably those instances of MsbMicroservlet need to share some state so we need to enrich MsbContext as well:

public class MsbContext {
//...
    public Set<String> getAttributeNames() {
        //...
    }

    public Object getAttribute(String name) {
        // ...
    }

    void setAttribute(String name, Object value) {
        // ...
    }

    void removeAttribute(String name) {
        // ...
    }
//...
}

NOTE: The actual logger uses raw broker consumers to react on messages because in that particular case there's no need to send the replies back.

Even more!!!

The current flexible approach is not going away. For ninjas that need to implement other patterns not covered above we still expose ChannelManager, Requester, ResponderServer through MsbContext. That is going to allow doing any low-level stuff one may need.

vso-tc commented 8 years ago

After discussion with Simon and Benny we concluded that: