Skip to content

Kontraktor 4 Async REST

moru0011 edited this page Jan 3, 2018 · 4 revisions

Actor-based REST services with kontraktor

Why async

The main issue with synchronous/multithreaded request processing is the limit in the number of threads. Usually a rest service needs to talk to other services via network (e.g. database or other services (microservice or SOA)), the overall system throughput is determined by the maximum number of concurrent requests (in-flight), not by raw processing power. Therefore async processing (does not require a thread-per-request) excels on throughput, even if raw single client performance tests suggest a different result (see nodejs vs java servlet based apps).

Kontraktor

Kontraktor provides a simple convention based mapping of REST requests to actor methods to @reduce @annotation @mess.

A REST Actor can be published on a base path which then is automatically removed from REST requests.

MyRESTActor act = AsActor(MyRESTActor.class);
    Http4K.Build("localhost", 8080)
        .restAPI("/api", act )
        .build();

Mapping rules

Method Name

The method name is derived from the http request method ("GET","PUT","POST", ..) in combination with the first path element given.

E.g. an API published under "/api" the request

curl -i -X GET http://localhost:8080/api/books/234234

will result in the method name getBooks(..).

Default handlers

if no matching method has been found, an attempt is made to look for a plain 'get' / 'post' .. method named like the http method of the incoming request.

E.g.:

    public IPromise get(String[] path, @RequestPath String rpath ) {
        Log.Info(this,"GET "+rpath);
        return resolve(404);
    }

    public IPromise post(String[] path, @RequestPath String rpath, byte[] data ) {
        Log.Info(this,"POST "+rpath);
        return resolve(404);
    }

Path parameters

Once the method name is determined, kontraktor tries to map arguments of the matching method to remaining path of a REST request (query params and some types are handled different see below).

E.g. public IPromise getBooks( int id ) will try to parse an int (234234) from the request path http://localhost:8080/api/books/234234.

The following types can be parsed: int, long, double, String

Special types

A http-request also has a header, optionally a request body, the url might contain 'query parameters'.

Request header

The request header is injected into parameters of type HeaderMap (if present).

Query params

All query parameters are injected into parameters of type Map<String,Deque<String>> (if present). Additionally its possible to parse single query params using the @FromQuery annotation.

request path (>=version 4.20.1)

Gets injected into arguments of type String[] or annotated with "@RequestPath"

E.g.:

//curl -i -X PUT --data "{ \"name\": \"satoshi\", \"nkey\": 13345 }" 'http://localhost:8080/api/user/nakamoto/?x=simple&something=pokpokpok&x=13&age=111'
public IPromise putUser(String name, @FromQuery("age") int age, JsonObject body, Map<String,Deque<String>> queryparms)

Request Body

The request body is injected into parameters of type byte[] (utf-8) or JsonObject (parsed json).

Return values

Responses are returned async (IPromise). Check docs on how kontraktor's Actors/Promises work.

  • prom.resolve("A String response") results in a 200 response with given String
  • prom.resolve( someSerializablePojo ) results in a json-serialized (fst style) kson response
  • prom.resolve( 400 ) results in an empty response with given http status code
  • prom.resolve( new Pair(403), "not this time" ) results in a response with given status code and given String content

How to deal with blocking calls

An Actor is single threaded, therefore you should not do any blocking operations or heavy computing on the actor thread.

To execute blocking calls (e.g. database query) use a kontraktor's threadpool which enables thread safe delivery of a result object:

public class RESTActor<T extends RESTActor> extends Actor<T> {
    
    public IPromise getBooks( int id ) {
        Promise res = promise();
        
        execInThreadPool( () -> {
            // simulate blocking operation (e.g. database query)
            return new Book().title("Title "+id).id(""+id).author("kontraktor");
        })
        .then(res);
        
        return res;
    }

}

Full Example

see examples/REST for a full source example project.

public class RESTActor<T extends RESTActor> extends Actor<T> {

    // curl -i -X GET http://localhost:8080/api/books/234234
    public IPromise getBooks( int id ) {
        Promise res = promise();
        // simulate blocking operation (e.g. database query)
        execInThreadPool( () -> new Book().title("Title "+id).id(""+id).author("kontraktor") ).
            then(res);
        return res;
    }

    //curl -i -X POST --data "param1=value1&param2=value2" http://localhost:8080/api/stuff
    public IPromise postStuff( byte[] body ) {
        Log.Info(this,"posted:"+new String(body));
        return resolve(200);
    }

    //curl -i -X POST --data "{ \"key\": \"value\", \"nkey\": 13 }" http://localhost:8080/api/stuff1
    public IPromise postStuff1(JsonObject body, HeaderMap headerValues ) {
        headerValues.forEach( hv -> {
            Log.Info(this,""+hv.getHeaderName());
            hv.forEach( s -> {
                Log.Info(this,"    "+s);
            });
        });
        Log.Info(this,""+body);
        return resolve(new Pair(202,body.toString()));
    }

    //curl -i -X PUT --data "{ \"name\": \"satoshi\", \"nkey\": 13345 }" 'http://localhost:8080/api/user/nakamoto/?x=simple&something=pokpokpok&x=13&age=111'
    @ContentType("application/json")
    public IPromise putUser(String name, @FromQuery("age") int age, JsonObject body, Map<String,Deque<String>> queryparms) {
        Log.Info(this,"name:"+name+" age:"+age);
        queryparms.forEach( (k,v) -> {
            Log.Info(this,""+k+"=>");
            v.forEach( s -> {
                Log.Info(this,"    "+s);
            });
        });
        return resolve(body);
    }

    public static void main(String[] args) {
        RESTActor act = AsActor(RESTActor.class);
        Http4K.Build("localhost", 8080)
            .restAPI("/api", act )
            .build();
    }

}