You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by sk...@apache.org on 2021/03/22 21:29:38 UTC

[ignite-3] branch main updated: IGNITE-14202 Netty-based REST API sub-library for Ignite. Fixes #71

This is an automated email from the ASF dual-hosted git repository.

sk0x50 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git


The following commit(s) were added to refs/heads/main by this push:
     new d9e88e9  IGNITE-14202 Netty-based REST API sub-library for Ignite. Fixes #71
d9e88e9 is described below

commit d9e88e9e8972f1a572775223e991262143b17638
Author: Kirill Gusakov <kg...@gmail.com>
AuthorDate: Tue Mar 23 00:29:03 2021 +0300

    IGNITE-14202 Netty-based REST API sub-library for Ignite. Fixes #71
    
    Signed-off-by: Slava Koptilin <sl...@gmail.com>
---
 modules/rest/pom.xml                               |  42 +++++-
 .../java/org/apache/ignite/rest/RestModule.java    | 161 +++++++++++++--------
 .../apache/ignite/rest/netty/RestApiHandler.java   | 112 ++++++++++++++
 .../ignite/rest/netty/RestApiHttpRequest.java      |  60 ++++++++
 .../ignite/rest/netty/RestApiHttpResponse.java     | 134 +++++++++++++++++
 .../ignite/rest/netty/RestApiInitializer.java      |  50 +++++++
 .../java/org/apache/ignite/rest/routes/Route.java  | 152 +++++++++++++++++++
 .../java/org/apache/ignite/rest/routes/Router.java | 126 ++++++++++++++++
 .../org/apache/ignite/rest/routes/RouteTest.java   |  97 +++++++++++++
 modules/runner/pom.xml                             |   5 -
 .../java/org/apache/ignite/app/IgniteRunner.java   |   2 +-
 .../src/main/resources/simplelogger.properties     |   2 +-
 parent/pom.xml                                     |  38 ++++-
 13 files changed, 900 insertions(+), 81 deletions(-)

diff --git a/modules/rest/pom.xml b/modules/rest/pom.xml
index f9fb1c4..8cf8376 100644
--- a/modules/rest/pom.xml
+++ b/modules/rest/pom.xml
@@ -47,11 +47,6 @@
         </dependency>
 
         <dependency>
-            <groupId>io.javalin</groupId>
-            <artifactId>javalin</artifactId>
-        </dependency>
-
-        <dependency>
             <groupId>org.slf4j</groupId>
             <artifactId>slf4j-simple</artifactId>
         </dependency>
@@ -62,6 +57,43 @@
             <version>${project.version}</version>
             <scope>compile</scope>
         </dependency>
+
+        <dependency>
+            <groupId>io.netty</groupId>
+            <artifactId>netty-common</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>io.netty</groupId>
+            <artifactId>netty-buffer</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>io.netty</groupId>
+            <artifactId>netty-codec</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>io.netty</groupId>
+            <artifactId>netty-handler</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>io.netty</groupId>
+            <artifactId>netty-codec-http</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-api</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-engine</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
     <build>
diff --git a/modules/rest/src/main/java/org/apache/ignite/rest/RestModule.java b/modules/rest/src/main/java/org/apache/ignite/rest/RestModule.java
index 5d6a270..6124abd 100644
--- a/modules/rest/src/main/java/org/apache/ignite/rest/RestModule.java
+++ b/modules/rest/src/main/java/org/apache/ignite/rest/RestModule.java
@@ -18,18 +18,30 @@
 package org.apache.ignite.rest;
 
 import com.google.gson.JsonSyntaxException;
-import io.javalin.Javalin;
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelOption;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.nio.NioServerSocketChannel;
+import io.netty.handler.codec.http.HttpHeaderValues;
+import io.netty.handler.logging.LogLevel;
+import io.netty.handler.logging.LoggingHandler;
 import java.io.Reader;
+import java.nio.charset.StandardCharsets;
 import java.util.Collections;
+import java.util.Map;
 import org.apache.ignite.configuration.ConfigurationRegistry;
 import org.apache.ignite.configuration.validation.ConfigurationValidationException;
 import org.apache.ignite.rest.configuration.RestConfiguration;
+import org.apache.ignite.rest.netty.RestApiInitializer;
 import org.apache.ignite.rest.presentation.ConfigurationPresentation;
-import org.apache.ignite.rest.presentation.FormatConverter;
-import org.apache.ignite.rest.presentation.json.JsonConverter;
 import org.apache.ignite.rest.presentation.json.JsonPresentation;
+import org.apache.ignite.rest.routes.Router;
 import org.slf4j.Logger;
 
+import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
+
 /**
  * Rest module is responsible for starting a REST endpoints for accessing and managing configuration.
  *
@@ -61,8 +73,8 @@ public class RestModule {
     }
 
     /** */
-    public void prepareStart(ConfigurationRegistry sysConfig, Reader moduleConfReader) {
-        sysConf = sysConfig;
+    public void prepareStart(ConfigurationRegistry sysCfg, Reader moduleConfReader) {
+        sysConf = sysCfg;
 
         presentation = new JsonPresentation(Collections.emptyMap());
 
@@ -74,68 +86,77 @@ public class RestModule {
 //        sysConfig.registerConfigurator(restConf);
     }
 
-    /** */
-    public void start() {
-        Javalin app = startRestEndpoint();
-
-        FormatConverter converter = new JsonConverter();
-
-        app.get(CONF_URL, ctx -> {
-            ctx.result(presentation.represent());
-        });
-
-        app.get(CONF_URL + ":" + PATH_PARAM, ctx -> {
-            String configPath = ctx.pathParam(PATH_PARAM);
-
-            try {
-                ctx.result(presentation.representByPath(configPath));
-            }
-            catch (IllegalArgumentException pathE) {
-                ErrorResult eRes = new ErrorResult("CONFIG_PATH_UNRECOGNIZED", pathE.getMessage());
-
-                ctx.status(400).result(converter.convertTo("error", eRes));
-            }
-        });
+    /**
+     *
+     */
+    public void start() throws InterruptedException {
+        var router = new Router();
+        router
+            .get(CONF_URL, (req, resp) -> {
+                resp.json(presentation.represent());
+            })
+            .get(CONF_URL + ":" + PATH_PARAM, (req, resp) -> {
+                String cfgPath = req.queryParams().get(PATH_PARAM);
+                try {
+                    resp.json(presentation.representByPath(cfgPath));
+                }
+                catch (IllegalArgumentException pathE) {
+                    ErrorResult eRes = new ErrorResult("CONFIG_PATH_UNRECOGNIZED", pathE.getMessage());
 
-        app.post(CONF_URL, ctx -> {
-            try {
-                presentation.update(ctx.body());
-            }
-            catch (IllegalArgumentException argE) {
-                ErrorResult eRes = new ErrorResult("CONFIG_PATH_UNRECOGNIZED", argE.getMessage());
+                    resp.status(BAD_REQUEST);
+                    resp.json(Map.of("error", eRes));
+                }
+            })
+            .put(CONF_URL, HttpHeaderValues.APPLICATION_JSON, (req, resp) -> {
+                try {
+                    presentation.update(
+                        req
+                            .request()
+                            .content()
+                            .readCharSequence(req.request().content().readableBytes(), StandardCharsets.UTF_8)
+                            .toString());
+                }
+                catch (IllegalArgumentException argE) {
+                    ErrorResult eRes = new ErrorResult("CONFIG_PATH_UNRECOGNIZED", argE.getMessage());
 
-                ctx.status(400).result(converter.convertTo("error", eRes));
-            }
-            catch (ConfigurationValidationException validationE) {
-                ErrorResult eRes = new ErrorResult("APPLICATION_EXCEPTION", validationE.getMessage());
+                    resp.status(BAD_REQUEST);
+                    resp.json(Map.of("error", eRes));
+                }
+                catch (ConfigurationValidationException validationE) {
+                    ErrorResult eRes = new ErrorResult("APPLICATION_EXCEPTION", validationE.getMessage());
 
-                ctx.status(400).result(converter.convertTo("error", eRes));
-            }
-            catch (JsonSyntaxException e) {
-                String msg = e.getCause() != null ? e.getCause().getMessage() : e.getMessage();
+                    resp.status(BAD_REQUEST);
+                    resp.json(Map.of("error", eRes));
+                    resp.json(eRes);
+                }
+                catch (JsonSyntaxException e) {
+                    String msg = e.getCause() != null ? e.getCause().getMessage() : e.getMessage();
 
-                ErrorResult eRes = new ErrorResult("VALIDATION_EXCEPTION", msg);
+                    ErrorResult eRes = new ErrorResult("VALIDATION_EXCEPTION", msg);
+                    resp.status(BAD_REQUEST);
+                    resp.json(Map.of("error", eRes));
+                }
+                catch (Exception e) {
+                    ErrorResult eRes = new ErrorResult("VALIDATION_EXCEPTION", e.getMessage());
 
-                ctx.status(400).result(converter.convertTo("error", eRes));
-            }
-            catch (Exception e) {
-                ErrorResult eRes = new ErrorResult("VALIDATION_EXCEPTION", e.getMessage());
+                    resp.status(BAD_REQUEST);
+                    resp.json(Map.of("error", eRes));
+                }
+            });
 
-                ctx.status(400).result(converter.convertTo("error", eRes));
-            }
-        });
+        startRestEndpoint(router);
     }
 
     /** */
-    private Javalin startRestEndpoint() {
-        Integer port = sysConf.getConfiguration(RestConfiguration.KEY).port().value();
+    private void startRestEndpoint(Router router) throws InterruptedException {
+        Integer desiredPort = sysConf.getConfiguration(RestConfiguration.KEY).port().value();
         Integer portRange = sysConf.getConfiguration(RestConfiguration.KEY).portRange().value();
 
-        Javalin app = null;
+        int port = 0;
 
         if (portRange == null || portRange == 0) {
             try {
-                app = Javalin.create().start(port != null ? port : DFLT_PORT);
+                port = (desiredPort != null ? desiredPort : DFLT_PORT);
             }
             catch (RuntimeException e) {
                 log.warn("Failed to start REST endpoint: ", e);
@@ -144,23 +165,20 @@ public class RestModule {
             }
         }
         else {
-            int startPort = port;
+            int startPort = desiredPort;
 
             for (int portCandidate = startPort; portCandidate < startPort + portRange; portCandidate++) {
                 try {
-                    app = Javalin.create().start(portCandidate);
+                    port = (portCandidate);
                 }
                 catch (RuntimeException ignored) {
                     // No-op.
                 }
-
-                if (app != null)
-                    break;
             }
 
-            if (app == null) {
+            if (port == 0) {
                 String msg = "Cannot start REST endpoint. " +
-                    "All ports in range [" + startPort + ", " + (startPort + portRange) + ") are in use.";
+                    "All ports in range [" + startPort + ", " + (startPort + portRange) + "] are in use.";
 
                 log.warn(msg);
 
@@ -168,9 +186,28 @@ public class RestModule {
             }
         }
 
-        log.info("REST protocol started successfully on port " + app.port());
+        EventLoopGroup bossGrp = new NioEventLoopGroup(1);
+        EventLoopGroup workerGrp = new NioEventLoopGroup();
+        var hnd = new RestApiInitializer(router);
+        try {
+            ServerBootstrap b = new ServerBootstrap();
+            b.option(ChannelOption.SO_BACKLOG, 1024);
+            b.group(bossGrp, workerGrp)
+                .channel(NioServerSocketChannel.class)
+                .handler(new LoggingHandler(LogLevel.INFO))
+                .childHandler(hnd);
+
+            Channel ch = b.bind(port).sync().channel();
 
-        return app;
+            if (log.isInfoEnabled())
+                log.info("REST protocol started successfully on port " + port);
+
+            ch.closeFuture().sync();
+        }
+        finally {
+            bossGrp.shutdownGracefully();
+            workerGrp.shutdownGracefully();
+        }
     }
 
     /** */
diff --git a/modules/rest/src/main/java/org/apache/ignite/rest/netty/RestApiHandler.java b/modules/rest/src/main/java/org/apache/ignite/rest/netty/RestApiHandler.java
new file mode 100644
index 0000000..66bb0e0
--- /dev/null
+++ b/modules/rest/src/main/java/org/apache/ignite/rest/netty/RestApiHandler.java
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.ignite.rest.netty;
+
+import io.netty.buffer.EmptyByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.buffer.UnpooledByteBufAllocator;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelFutureListener;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.handler.codec.http.DefaultFullHttpResponse;
+import io.netty.handler.codec.http.DefaultHttpResponse;
+import io.netty.handler.codec.http.EmptyHttpHeaders;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.FullHttpResponse;
+import io.netty.handler.codec.http.HttpObject;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.netty.handler.codec.http.HttpUtil;
+import io.netty.handler.codec.http.HttpVersion;
+import org.apache.ignite.rest.routes.Router;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION;
+import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH;
+import static io.netty.handler.codec.http.HttpHeaderValues.CLOSE;
+import static io.netty.handler.codec.http.HttpHeaderValues.KEEP_ALIVE;
+import static io.netty.handler.codec.http.HttpResponseStatus.OK;
+
+/**
+ * Main handler of REST HTTP chain.
+ * It receives http request, process it by {@link Router} and produce http response.
+ */
+public class RestApiHandler extends SimpleChannelInboundHandler<HttpObject> {
+    /** */
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
+    /** Requests' router. */
+    private final Router router;
+
+    /**
+     * @param router Router.
+     */
+    public RestApiHandler(Router router) {
+        this.router = router;
+    }
+
+    /** {@inheritDoc} */
+    @Override public void channelReadComplete(ChannelHandlerContext ctx) {
+        ctx.flush();
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
+        if (msg instanceof FullHttpRequest) {
+            FullHttpRequest req = (FullHttpRequest) msg;
+            FullHttpResponse res;
+
+            var maybeRoute = router.route(req);
+            if (maybeRoute.isPresent()) {
+                var resp = new RestApiHttpResponse(new DefaultHttpResponse(HttpVersion.HTTP_1_1, OK));
+                maybeRoute.get().handle(req, resp);
+                var content = resp.content() != null ?
+                    Unpooled.wrappedBuffer(resp.content()) :
+                    new EmptyByteBuf(UnpooledByteBufAllocator.DEFAULT);
+                res = new DefaultFullHttpResponse(resp.protocolVersion(), resp.status(),
+                     content, resp.headers(), EmptyHttpHeaders.INSTANCE);
+            }
+            else
+                res = new DefaultFullHttpResponse(req.protocolVersion(), HttpResponseStatus.NOT_FOUND);
+
+            res.headers()
+                .setInt(CONTENT_LENGTH, res.content().readableBytes());
+
+            boolean keepAlive = HttpUtil.isKeepAlive(req);
+            if (keepAlive) {
+                if (!req.protocolVersion().isKeepAliveDefault())
+                    res.headers().set(CONNECTION, KEEP_ALIVE);
+            } else
+                res.headers().set(CONNECTION, CLOSE);
+
+            ChannelFuture f = ctx.write(res);
+
+            if (!keepAlive)
+                f.addListener(ChannelFutureListener.CLOSE);
+        }
+
+    }
+
+    /** {@inheritDoc} */
+    @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+        log.error("Failed to process http request:", cause);
+        var res = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.INTERNAL_SERVER_ERROR);
+        ctx.write(res).addListener(ChannelFutureListener.CLOSE);
+    }
+}
diff --git a/modules/rest/src/main/java/org/apache/ignite/rest/netty/RestApiHttpRequest.java b/modules/rest/src/main/java/org/apache/ignite/rest/netty/RestApiHttpRequest.java
new file mode 100644
index 0000000..92c0987
--- /dev/null
+++ b/modules/rest/src/main/java/org/apache/ignite/rest/netty/RestApiHttpRequest.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.ignite.rest.netty;
+
+import io.netty.handler.codec.http.FullHttpRequest;
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * HTTP request wrapper with GET query params if exists.
+ */
+public class RestApiHttpRequest {
+    /** Request. */
+    private final FullHttpRequest req;
+
+    /** Query params. */
+    private final Map<String, String> qryParams;
+
+    /**
+     * @param req Request.
+     * @param qryParams Query params.
+     */
+    public RestApiHttpRequest(FullHttpRequest req, Map<String, String> qryParams) {
+        this.req = req;
+        this.qryParams = Collections.unmodifiableMap(qryParams);
+    }
+
+    /**
+     * Returns complete HTTP request.
+     *
+     * @return Complete HTTP request.
+     */
+    public FullHttpRequest request() {
+        return req;
+    }
+
+    /**
+     * Returns query parameters associated with the request.
+     *
+     * @return Query parameters.
+     */
+    public Map<String, String> queryParams() {
+       return qryParams;
+    }
+}
diff --git a/modules/rest/src/main/java/org/apache/ignite/rest/netty/RestApiHttpResponse.java b/modules/rest/src/main/java/org/apache/ignite/rest/netty/RestApiHttpResponse.java
new file mode 100644
index 0000000..7f25103
--- /dev/null
+++ b/modules/rest/src/main/java/org/apache/ignite/rest/netty/RestApiHttpResponse.java
@@ -0,0 +1,134 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.ignite.rest.netty;
+
+import com.google.gson.Gson;
+import io.netty.handler.codec.http.HttpHeaderNames;
+import io.netty.handler.codec.http.HttpHeaderValues;
+import io.netty.handler.codec.http.HttpHeaders;
+import io.netty.handler.codec.http.HttpResponse;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.netty.handler.codec.http.HttpVersion;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Simple wrapper of HTTP response with some helper methods for filling it with headers and content.
+ */
+public class RestApiHttpResponse {
+    /** Response. */
+    private final HttpResponse res;
+
+    /** Content. */
+    private byte[] content;
+
+    /**
+     * Creates a new HTTP response with the given message body.
+     *
+     * @param res Response.
+     * @param content Content.
+     */
+    public RestApiHttpResponse(HttpResponse res, byte[] content) {
+        this.res = res;
+        this.content = content;
+    }
+
+    /**
+     * Creates a new HTTP response with the given headers and status.
+     *
+     * @param res Response.
+     */
+    public RestApiHttpResponse(HttpResponse res) {
+        this.res = res;
+    }
+
+    /**
+     * Set raw bytes as response body.
+     *
+     * @param content Content data.
+     * @return Updated response.
+     */
+    public RestApiHttpResponse content(byte[] content) {
+        this.content = content;
+        return this;
+    }
+
+    /**
+     * Set JSON representation of input object as response body.
+     *
+     * @param content Content object.
+     * @return Updated response.
+     */
+    public RestApiHttpResponse json(Object content) {
+        // TODO: IGNITE-14344 Gson object should not be created on every response
+        this.content = new Gson().toJson(content).getBytes(StandardCharsets.UTF_8);
+        headers().set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_JSON.toString());
+        return this;
+    }
+
+    /**
+     * @return Content.
+     */
+    public byte[] content() {
+        return content;
+    }
+
+    /**
+     * Returns HTTP status of this response.
+     *
+     * @return HTTP Status.
+     */
+    public HttpResponseStatus status() {
+        return res.status();
+    }
+
+    /**
+     * Sets HTTP status.
+     *
+     * @param status Status.
+     * @return Updated response.
+     */
+    public RestApiHttpResponse status(HttpResponseStatus status) {
+        res.setStatus(status);
+        return this;
+    }
+
+    /**
+     * @return Protocol version
+     */
+    public HttpVersion protocolVersion() {
+        return res.protocolVersion();
+    }
+
+    /**
+     * Sets protocol version.
+     *
+     * @param httpVer HTTP version.
+     * @return Updated response.
+     */
+    public RestApiHttpResponse protocolVersion(HttpVersion httpVer) {
+        res.setProtocolVersion(httpVer);
+        return this;
+    }
+
+    /**
+     * @return Mutable response headers.
+     */
+    public HttpHeaders headers() {
+        return res.headers();
+    }
+}
diff --git a/modules/rest/src/main/java/org/apache/ignite/rest/netty/RestApiInitializer.java b/modules/rest/src/main/java/org/apache/ignite/rest/netty/RestApiInitializer.java
new file mode 100644
index 0000000..8ff84fc
--- /dev/null
+++ b/modules/rest/src/main/java/org/apache/ignite/rest/netty/RestApiInitializer.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.ignite.rest.netty;
+
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelPipeline;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.handler.codec.http.HttpObjectAggregator;
+import io.netty.handler.codec.http.HttpServerCodec;
+import io.netty.handler.codec.http.HttpServerExpectContinueHandler;
+import org.apache.ignite.rest.routes.Router;
+
+/**
+ * Initializes channel with needed handlers for HTTP processing.
+ */
+public class RestApiInitializer extends ChannelInitializer<SocketChannel> {
+    /** Router. */
+    private final Router router;
+
+    /**
+     * @param router Router.
+     */
+    public RestApiInitializer(Router router) {
+        this.router = router;
+    }
+
+    /** {@inheritDoc} */
+    @Override public void initChannel(SocketChannel ch) {
+        ChannelPipeline p = ch.pipeline();
+        p.addLast(new HttpServerCodec());
+        p.addLast(new HttpServerExpectContinueHandler());
+        p.addLast(new HttpObjectAggregator(Integer.MAX_VALUE));
+        p.addLast(new RestApiHandler(router));
+    }
+}
diff --git a/modules/rest/src/main/java/org/apache/ignite/rest/routes/Route.java b/modules/rest/src/main/java/org/apache/ignite/rest/routes/Route.java
new file mode 100644
index 0000000..df4f36d
--- /dev/null
+++ b/modules/rest/src/main/java/org/apache/ignite/rest/routes/Route.java
@@ -0,0 +1,152 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.ignite.rest.routes;
+
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.HttpHeaderNames;
+import io.netty.handler.codec.http.HttpMethod;
+import io.netty.handler.codec.http.HttpRequest;
+import java.util.ArrayDeque;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.BiConsumer;
+import org.apache.ignite.rest.netty.RestApiHttpRequest;
+import org.apache.ignite.rest.netty.RestApiHttpResponse;
+
+/**
+ * URI route with appropriate handler for request.
+ */
+public class Route {
+    /** Route. */
+    private final String route;
+
+    /** Method. */
+    private final HttpMethod method;
+
+    /** Accept type. */
+    private final String acceptType;
+
+    /** Handler. */
+    private final BiConsumer<RestApiHttpRequest, RestApiHttpResponse> hnd;
+
+    /**
+     * @param route Route.
+     * @param method Method.
+     * @param acceptType Accept type.
+     * @param hnd Request handler.
+     */
+    public Route(
+        String route,
+        HttpMethod method,
+        String acceptType,
+        BiConsumer<RestApiHttpRequest, RestApiHttpResponse> hnd
+    ) {
+        this.route = route;
+        this.method = method;
+        this.acceptType = acceptType;
+        this.hnd = hnd;
+    }
+
+    /**
+     * Handles the query by populating the received response object.
+     *
+     * @param req Request.
+     * @param resp Response.
+     */
+    public void handle(FullHttpRequest req, RestApiHttpResponse resp) {
+        hnd.accept(new RestApiHttpRequest(req, paramsDecode(req.uri())), resp);
+    }
+
+    /**
+     * Checks if the current route matches the request.
+     *
+     * @param req Request.
+     * @return true if route matches the request, else otherwise.
+     */
+    public boolean match(HttpRequest req) {
+        return req.method().equals(method) &&
+            matchUri(req.uri()) &&
+            matchContentType(req.headers().get(HttpHeaderNames.CONTENT_TYPE));
+    }
+
+    /**
+     * @param s Content type.
+     * @return true if route matches the request, else otherwise.
+     */
+    private boolean matchContentType(String s) {
+        return (acceptType == null) || (acceptType.equals(s));
+    }
+
+    /**
+     * Checks the current route matches input uri.
+     * REST API like URIs "/user/:user" is also supported.
+     *
+     * @param uri Input URI
+     * @return true if route matches the request, else otherwise.
+     */
+    private boolean matchUri(String uri) {
+        var receivedParts = new ArrayDeque<>(Arrays.asList(uri.split("/")));
+        var realParts = new ArrayDeque<>(Arrays.asList(route.split("/")));
+
+        String part;
+        while ((part = realParts.pollFirst()) != null) {
+            String receivedPart = receivedParts.pollFirst();
+            if (receivedPart == null)
+                return false;
+
+            if (part.startsWith(":"))
+                continue;
+
+            if (!part.equals(receivedPart))
+                return false;
+        }
+
+        return receivedParts.isEmpty();
+    }
+
+    /**
+     * Decodes params from REST like URIs "/user/:user".
+     *
+     * @param uri Input URI.
+     * @return Map of decoded params.
+     */
+    private Map<String, String> paramsDecode(String uri) {
+        var receivedParts = new ArrayDeque<>(Arrays.asList(uri.split("/")));
+        var realParts = new ArrayDeque<>(Arrays.asList(route.split("/")));
+
+         Map<String, String> res = new HashMap<>();
+
+        String part;
+        while ((part = realParts.pollFirst()) != null) {
+            String receivedPart = receivedParts.pollFirst();
+            if (receivedPart == null)
+                throw new IllegalArgumentException("URI is incorrect");
+
+            if (part.startsWith(":")) {
+                res.put(part.substring(1), receivedPart);
+                continue;
+            }
+
+            if (!part.equals(receivedPart))
+                throw new IllegalArgumentException("URI is incorrect");
+        }
+
+        return res;
+    }
+}
diff --git a/modules/rest/src/main/java/org/apache/ignite/rest/routes/Router.java b/modules/rest/src/main/java/org/apache/ignite/rest/routes/Router.java
new file mode 100644
index 0000000..22869b8
--- /dev/null
+++ b/modules/rest/src/main/java/org/apache/ignite/rest/routes/Router.java
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.ignite.rest.routes;
+
+import io.netty.handler.codec.http.HttpMethod;
+import io.netty.handler.codec.http.HttpRequest;
+import io.netty.util.AsciiString;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.BiConsumer;
+import org.apache.ignite.rest.netty.RestApiHttpRequest;
+import org.apache.ignite.rest.netty.RestApiHttpResponse;
+
+/**
+ * Dispatcher of http requests.
+ *
+ * Example:
+ * <pre>
+ * {@code
+ * var router = new Router();
+ * router.get("/user", (req, resp) -> {
+ *     resp.status(HttpResponseStatus.OK);
+ * });
+ * }
+ * </pre>
+ */
+public class Router {
+    /** Routes. */
+    private/*public*/ final List<Route> routes;
+
+    /**
+     * Creates a new router with the given list of {@code routes}.
+     *
+     * @param routes Routes.
+     */
+    public Router(List<Route> routes) {
+        this.routes = routes;
+    }
+
+    /**
+     * Creates a new empty router.
+     */
+    public Router() {
+        routes = new ArrayList<>();
+    }
+
+    /**
+     * GET query helper.
+     *
+     * @param route Route.
+     * @param acceptType Accept type.
+     * @param hnd Actual handler of the request.
+     * @return Router
+     */
+    public Router get(
+        String route,
+        AsciiString acceptType,
+        BiConsumer<RestApiHttpRequest, RestApiHttpResponse> hnd
+    ) {
+        addRoute(new Route(route, HttpMethod.GET, acceptType.toString(), hnd));
+        return this;
+    }
+
+    /**
+     * GET query helper.
+     *
+     * @param route Route.
+     * @param hnd Actual handler of the request.
+     * @return Router
+     */
+    public Router get(String route, BiConsumer<RestApiHttpRequest, RestApiHttpResponse> hnd) {
+        addRoute(new Route(route, HttpMethod.GET, null, hnd));
+        return this;
+    }
+
+    /**
+     * PUT query helper.
+     *
+     * @param route Route.
+     * @param hnd Actual handler of the request.
+     * @return Router
+     */
+    public Router put(
+        String route,
+        AsciiString acceptType,
+        BiConsumer<RestApiHttpRequest, RestApiHttpResponse> hnd
+    ) {
+        addRoute(new Route(route, HttpMethod.PUT, acceptType.toString(), hnd));
+        return this;
+    }
+
+    /**
+     * Adds the route to router chain.
+     *
+     * @param route Route
+     */
+    public void addRoute(Route route) {
+        routes.add(route);
+    }
+
+    /**
+     * Finds the route by request.
+     *
+     * @param req Request.
+     * @return Route if founded.
+     */
+    public Optional<Route> route(HttpRequest req) {
+        return routes.stream().filter(r -> r.match(req)).findFirst();
+    }
+}
diff --git a/modules/rest/src/test/java/org/apache/ignite/rest/routes/RouteTest.java b/modules/rest/src/test/java/org/apache/ignite/rest/routes/RouteTest.java
new file mode 100644
index 0000000..dab6573
--- /dev/null
+++ b/modules/rest/src/test/java/org/apache/ignite/rest/routes/RouteTest.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.ignite.rest.routes;
+
+import io.netty.handler.codec.http.DefaultHttpRequest;
+import io.netty.handler.codec.http.HttpHeaderNames;
+import io.netty.handler.codec.http.HttpHeaderValues;
+import org.junit.jupiter.api.Test;
+
+import static io.netty.handler.codec.http.HttpMethod.GET;
+import static io.netty.handler.codec.http.HttpMethod.PUT;
+import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ *
+ */
+public class RouteTest {
+    /**
+     *
+     */
+    @Test
+    void testMatchByUri() {
+       var route = new Route("/user", GET, null, (request, response) -> {});
+       var req = new DefaultHttpRequest(HTTP_1_1, GET, "/user");
+       assertTrue(route.match(req));
+    }
+
+    /**
+     *
+     */
+    @Test
+    void testNonMatchByUri() {
+        var route = new Route("/user", GET, null, (request, response) -> {});
+        var req = new DefaultHttpRequest(HTTP_1_1, GET, "/user/1");
+        assertFalse(route.match(req));
+    }
+
+    /**
+     *
+     */
+    @Test
+    void testMatchByContentTypeIfAcceptTypeEmpty() {
+        var route = new Route("/user", GET, null, (request, response) -> {});
+        var req = new DefaultHttpRequest(HTTP_1_1, GET, "/user/");
+        req.headers().set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN);
+        assertTrue(route.match(req));
+    }
+
+    /**
+     *
+     */
+    @Test
+    void testMatchByContentTypeIfAcceptTypeNonEmpty() {
+        var route = new Route("/user", PUT, "text/plain", (request, response) -> {});
+        var req = new DefaultHttpRequest(HTTP_1_1, PUT, "/user");
+        req.headers().set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN);
+        assertTrue(route.match(req));
+    }
+
+    /**
+     *
+     */
+    @Test
+    void testNonMatchByContentTypeIfAcceptTypeNonEmpty() {
+        var route = new Route("/user", PUT, "text/plain", (request, response) -> {});
+        var req = new DefaultHttpRequest(HTTP_1_1, GET, "/user/");
+        req.headers().set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_JSON);
+        assertFalse(route.match(req));
+    }
+
+    /**
+     *
+     */
+    @Test
+    void testMatchByUriWithParams() {
+        var route = new Route("/user/:user", GET, null, (request, response) -> {});
+        var req = new DefaultHttpRequest(HTTP_1_1, GET, "/user/John");
+        assertTrue(route.match(req));
+    }
+}
diff --git a/modules/runner/pom.xml b/modules/runner/pom.xml
index af05830..2a2af71 100644
--- a/modules/runner/pom.xml
+++ b/modules/runner/pom.xml
@@ -62,11 +62,6 @@
         </dependency>
 
         <dependency>
-            <groupId>io.javalin</groupId>
-            <artifactId>javalin</artifactId>
-        </dependency>
-
-        <dependency>
             <groupId>org.slf4j</groupId>
             <artifactId>slf4j-simple</artifactId>
         </dependency>
diff --git a/modules/runner/src/main/java/org/apache/ignite/app/IgniteRunner.java b/modules/runner/src/main/java/org/apache/ignite/app/IgniteRunner.java
index 3e5aaf8..007592a 100644
--- a/modules/runner/src/main/java/org/apache/ignite/app/IgniteRunner.java
+++ b/modules/runner/src/main/java/org/apache/ignite/app/IgniteRunner.java
@@ -70,7 +70,7 @@ public class IgniteRunner {
      *
      * @param args Empty or providing path to custom configuration file after marker parameter "--config".
      */
-    public static void main(String[] args) throws IOException {
+    public static void main(String[] args) throws IOException, InterruptedException {
         ackBanner();
 
         ConfigurationModule confModule = new ConfigurationModule();
diff --git a/modules/runner/src/main/resources/simplelogger.properties b/modules/runner/src/main/resources/simplelogger.properties
index 4d52a62..77a2ff2 100644
--- a/modules/runner/src/main/resources/simplelogger.properties
+++ b/modules/runner/src/main/resources/simplelogger.properties
@@ -16,4 +16,4 @@
 #
 
 org.slf4j.simpleLogger.defaultLogLevel=off
-org.slf4j.simpleLogger.log.org.apache.ignite.app.IgniteRunner=info
\ No newline at end of file
+org.slf4j.simpleLogger.log.org.apache.ignite.app.IgniteRunner=info
diff --git a/parent/pom.xml b/parent/pom.xml
index 3eeee99..2616325 100644
--- a/parent/pom.xml
+++ b/parent/pom.xml
@@ -56,7 +56,7 @@
         <gson.version>2.8.6</gson.version>
         <jackson.databind.version>2.11.1</jackson.databind.version>
         <jansi.version>1.18</jansi.version>
-        <javalin.version>3.12.0</javalin.version>
+        <netty.version>4.1.60.Final</netty.version>
         <javapoet.version>1.13.0</javapoet.version>
         <javax.annotation.api.version>1.3.2</javax.annotation.api.version>
         <javax.validation.version>2.0.1.Final</javax.validation.version>
@@ -187,12 +187,6 @@
             </dependency>
 
             <dependency>
-                <groupId>io.javalin</groupId>
-                <artifactId>javalin</artifactId>
-                <version>${javalin.version}</version>
-            </dependency>
-
-            <dependency>
                 <groupId>org.slf4j</groupId>
                 <artifactId>slf4j-simple</artifactId>
                 <version>${slf4j.version}</version>
@@ -288,6 +282,36 @@
                 <artifactId>asm-util</artifactId>
                 <version>${asm.framework.version}</version>
             </dependency>
+
+            <dependency>
+                <groupId>io.netty</groupId>
+                <artifactId>netty-common</artifactId>
+                <version>${netty.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>io.netty</groupId>
+                <artifactId>netty-buffer</artifactId>
+                <version>${netty.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>io.netty</groupId>
+                <artifactId>netty-codec</artifactId>
+                <version>${netty.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>io.netty</groupId>
+                <artifactId>netty-handler</artifactId>
+                <version>${netty.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>io.netty</groupId>
+                <artifactId>netty-codec-http</artifactId>
+                <version>${netty.version}</version>
+            </dependency>
         </dependencies>
     </dependencyManagement>