{"id":154,"date":"2017-11-10T01:16:08","date_gmt":"2017-11-10T01:16:08","guid":{"rendered":"http:\/\/lazarbulic.com\/blog\/?p=154"},"modified":"2019-12-05T12:00:43","modified_gmt":"2019-12-05T12:00:43","slug":"aws-lambda-vertx-framework-url-shortener-backend","status":"publish","type":"post","link":"https:\/\/lazarbulic.com\/blog\/2017\/11\/10\/aws-lambda-vertx-framework-url-shortener-backend\/","title":{"rendered":"AWS Lambda &#038; Vertx Framework URL Shortener Backend"},"content":{"rendered":"<p>7 minutes<\/p><h3>Intro:<\/h3>\n<p>Recently I stumbleupon <a href=\"http:\/\/vertx.io\"><strong>Vertx<\/strong><\/a>. Event-driven,\u00a0asynchronized, lightweight,\u00a0reactive,\u00a0highly performant, polyglot application framework. Ideal for writing micro-services. I played around with it for a while and really enjoyed the concept of serverless applications.<\/p>\n<p>I developed a few apps and cases and started to wonder how to run and deploy them so that I get a 100% reliable service. I suddenly remembered the tech seminar that I attended recently, specifically session about serverless apps with <a href=\"https:\/\/aws.amazon.com\/\"><strong>AWS<\/strong><\/a> (Amazon Web Services) <strong>Lambda<\/strong>.\u00a0Lambda is a\u00a0serverless compute\u00a0service that runs your code in response to events and automatically manages the underlying compute resources for you. Fairly similar concepts Vertx and AWS Lambda, so maybe they complement each other? As it turns out they do, Vertx can get most of your Lambdas&#8230;<\/p>\n<p>Using the <a href=\"https:\/\/serverless.com\"><strong>Serverless Framework<\/strong><\/a> to create, manage and deploy your new Lambdas I was able to get this microservice up and running in no time.<\/p>\n<p><em>Enough with the talk, lets see the implementation.<\/em><\/p>\n<h3>Code:<\/h3>\n<p><em>Handler Class, entry point of AWS Request.<\/em><\/p>\n<p>The first issue that I had was the sync Event Handler that is provided by AWS. So I had to by pass it with Future. In the Handler class I first initiate Vertx instance in a static block and deploy few Verticles that will do the work. This class only receives the event, extracts needed data from the request and passes the data to Vertx EventBus. After the Consumers handle the request Handler class will generate a proper response and finish the AWS request.<\/p>\n<pre class=\"theme:monokai lang:java decode:true\" title=\"Handler.class\">public class Handler implements RequestHandler&lt;Map&lt;String, Object&gt;, ApiGatewayResponse&gt; {\r\n\r\n\tprivate static final Logger logger = Logger.getLogger(Handler.class);\r\n\tprivate static Vertx vertx;\r\n\tstatic {\r\n\t\tSystem.setProperty(\"vertx.disableFileCPResolving\", \"true\");\r\n\t\tvertx = Vertx.vertx();\r\n\t\tfinal JsonObject jdbcClientConfig = new JsonObject()\r\n\t\t\t\t.put(\"url\", System.getenv(\"db_url\"))\r\n\t\t\t\t.put(\"driver_class\", System.getenv(\"db_driver\"))\r\n\t\t\t\t.put(\"user\", System.getenv(\"db_username\"))\r\n\t\t\t\t.put(\"password\", System.getenv(\"db_password\"))\r\n\t\t\t\t.put(\"max_pool_size\", 20);\r\n\t\tfinal JDBCClient client = JDBCClient.createShared(vertx, jdbcClientConfig);\r\n\t\tDeploymentOptions deploymentOptions = new DeploymentOptions().setInstances(Runtime.getRuntime().availableProcessors());\r\n\t\tvertx.deployVerticle(UrlService.class.getName(), deploymentOptions);\r\n\t\tvertx.eventBus().registerDefaultCodec(Message.class, new CustomMessageCodec());\r\n\t}\r\n\r\n\tpublic Handler() {\r\n\t\tBasicConfigurator.configure();\r\n\t}\r\n\r\n\t@Override\r\n\tpublic ApiGatewayResponse handleRequest(Map&lt;String, Object&gt; input, Context context) {\r\n\r\n\t\tString pathParam = System.getenv(\"path_param\") != null ? System.getenv(\"path_param\") : \"shorturl\";\r\n\t\tJsonObject jsonRequest = new JsonObject(input);\r\n\r\n\t\tlogger.debug(\"RAW INPUT: = \" + input);\r\n\t\tlogger.debug(\"INPUT REQUEST = \" + jsonRequest.encode());\r\n\t\tfinal CompletableFuture&lt;Message&gt; future = new CompletableFuture&lt;&gt;();\r\n\r\n\t\tMessage request = new Message(jsonRequest.getString(HTTP_METHOD), jsonRequest.getString(RESOURCE),\r\n\t\t\t\tjsonRequest.getJsonObject(\"pathParameters\") != null ? jsonRequest.getJsonObject(\"pathParameters\").getString(pathParam) : null,\r\n\t\t\t\tjsonRequest.getString(\"body\") != null ? new JsonObject(jsonRequest.getString(\"body\")).mapTo(Url.class) : new Url());\r\n\r\n\t\tvertx.eventBus().send(input.get(HTTP_METHOD).toString() + input.get(RESOURCE), request, rs -&gt; {\r\n\t\t\tif(!rs.succeeded()) {\r\n\t\t\t\tlogger.error(\"Request id = \" + context.getAwsRequestId() + \" failed. Cause = \" + rs.cause().getMessage());\r\n\t\t\t\tfuture.complete(new Message());\r\n\t\t\t\tthrow new RuntimeException(rs.cause());\r\n\t\t\t}\r\n\t\t\tfuture.complete((Message) rs.result().body());\r\n\t\t});\r\n\r\n\t\ttry {\r\n\t\t\tMessage response = future.get(15, TimeUnit.SECONDS);\r\n\r\n\t\t\tswitch (response.getStatusCode()) {\r\n\r\n\t\t\t\tcase 200:\r\n\t\t\t\t\treturn ApiGatewayResponse.builder()\r\n\t\t\t\t\t\t\t.setStatusCode(200)\r\n\t\t\t\t\t\t\t.setObjectBody(response.getUrl())\r\n\t\t\t\t\t\t\t.build();\r\n\t\t\t\tcase 307:\r\n\t\t\t\t\treturn ApiGatewayResponse\r\n\t\t\t\t\t\t\t.builder()\r\n\t\t\t\t\t\t\t.setStatusCode(307)\r\n\t\t\t\t\t\t\t.setHeaders(Collections.singletonMap(\"location\", response.getUrl().getLongUrl()))\r\n\t\t\t\t\t\t\t.build();\r\n\t\t\t\tdefault:\r\n\t\t\t\t\treturn ApiGatewayResponse\r\n\t\t\t\t\t\t\t.builder()\r\n\t\t\t\t\t\t\t.setStatusCode(response.getStatusCode() != 0 ? response.getStatusCode() : 500)\r\n\t\t\t\t\t\t\t.setRawBody(response.getMessage() != null ? response.getMessage() : \"Internal server error\")\r\n\t\t\t\t\t\t\t.build();\r\n\t\t\t}\r\n\r\n\t\t} catch (InterruptedException | ExecutionException | TimeoutException e) {\r\n\t\t\tlogger.error(\"Service timeout. Request id = \" + context.getAwsRequestId() + \" Error = \" + e.getMessage());\r\n\t\t\treturn ApiGatewayResponse.builder()\r\n\t\t\t\t\t.setStatusCode(504)\r\n\t\t\t\t\t.setRawBody(\"Service timeout\")\r\n\t\t\t\t\t.build();\r\n\t\t}\r\n\t}\r\n\r\n\tprivate static final String HTTP_METHOD = \"httpMethod\";\r\n\tprivate static final String RESOURCE = \"resource\";\r\n}<\/pre>\n<p><em>Line 4-18:<\/em>\u00a0This is where Vertx instance is created, Verticles are deployed and Async JDBC client is created. I figured out that it is better to created JDBC client here as in some cases I was timeout when that logic was in the Verticle <strong><em>start\u00a0<\/em><\/strong>method.<\/p>\n<p><em>Line 27-36: <\/em>These are helper lines, parsing and\u00a0formatting\u00a0the data so I can pass it to the Verticles.<\/p>\n<p><em>Line 38-45:<\/em>\u00a0I have decided to map the consumers to the address that is made of request method and url path, example POST\/api. This means each API request is mapped to its own consumer in Verticle class.<\/p>\n<p><em>Line 47-77:\u00a0<\/em>This is nothing but a block of code that handles the response that was passed from Verticles to the Future and generates the final response that will be return to API Gateway.<\/p>\n<p>&nbsp;<\/p>\n<p><em>UrlService, Vertx Verticle.<\/em><\/p>\n<p>Verticle class is pretty forward. Consumers that will process the message, methods for working with JDBC and helper methods for hashing\/dehashing id. The logic behind url shortening is fairly simple here. Each long url is stored in database with a unique id and few additional columns. Row id is hashed and returned as short url. When retrieving long url hash is decoded to row id and long url is retrieved. Later user is redirected to long url. With this implementation, on 6 char short url (characters after the domain) you get 62^6 combinations which is\u00a056 800 235 584 rows for storing your urls. TinyURL is currently at 6 long char urls (characters after domain). You can of course implement methods for reusing and recycling ids.<\/p>\n<pre class=\"theme:monokai lang:java decode:true \">public class UrlService extends AbstractVerticle {\r\n\r\n    private static final Logger logger = Logger.getLogger(UrlService.class);\r\n    private SQLClient client;\r\n    private final UrlValidator urlValidator = new UrlValidator();\r\n\r\n    @Override\r\n    public void start() {\r\n        final EventBus eventBus = vertx.eventBus();\r\n        final JsonObject jdbcClientConfig = new JsonObject()\r\n                .put(\"url\", System.getenv(\"db_url\"))\r\n                .put(\"driver_class\", System.getenv(\"db_driver\"))\r\n                .put(\"user\", System.getenv(\"db_username\"))\r\n                .put(\"password\", System.getenv(\"db_password\"))\r\n                .put(\"max_pool_size\", 20);\r\n        client = JDBCClient.createShared(vertx, jdbcClientConfig);\r\n\r\n        eventBus.consumer(\"POST\/api\", message -&gt; {\r\n            Message replay = (Message) message.body();\r\n            logger.debug(\"URL = \" + replay.getUrl().getLongUrl());\r\n            if(replay.getUrl().getLongUrl() == null || replay.getUrl().getLongUrl().isEmpty() || !urlValidator.isValid(replay.getUrl().getLongUrl())) {\r\n                replay.setStatusCode(400);\r\n                replay.setMessage(\"Longurl was not provided, empty or invalid format, failed to generate shorturl\");\r\n                message.reply(replay);\r\n                return;\r\n            }\r\n            add(replay.getUrl(), done -&gt; {\r\n                if(done.succeeded() &amp;&amp; done.result().getResults().size() == 1) {\r\n                    replay.getUrl().setId(done.result().getResults().get(0).getLong(0));\r\n                    replay.getUrl().setShortUrl(\"http:\/\/\" + System.getenv(\"domain\") + \"\/\" + encode(done.result().getResults().get(0).getLong(0)));\r\n                    replay.setStatusCode(200);\r\n                    message.reply(replay);\r\n                } else {\r\n                    replay.setStatusCode(500);\r\n                    message.reply(replay);\r\n                }\r\n            });\r\n        });\r\n\r\n        eventBus.consumer(\"GET\/{shorturl}\", message -&gt; {\r\n            Message replay = (Message) message.body();\r\n            replay.getUrl().setShortUrl(replay.getPathParams());\r\n            logger.debug(\"PP = \" + replay.getPathParams());\r\n            resolveShortUrl(replay.getUrl(), done -&gt;{\r\n                if(done.succeeded() &amp;&amp; done.result().getResults().size() == 1) {\r\n                    replay.getUrl().setLongUrl((done.result().getResults().get(0).getString(1)));\r\n                    replay.setStatusCode(307);\r\n                    message.reply(replay);\r\n                } else {\r\n                    replay.setStatusCode(404);\r\n                    replay.setMessage(\"No record found for provided longurl\");\r\n                    message.reply(replay);\r\n                }\r\n            });\r\n        });\r\n    }\r\n\r\n    private void add(Url url, Handler&lt;AsyncResult&lt;ResultSet&gt;&gt; done) {\r\n        JsonArray params = new JsonArray().add(url.getLongUrl()).add(Instant.now());\r\n        client.getConnection(conn -&gt; {\r\n            query(conn.result(), INSERT_NEW, params, done);\r\n            conn.result().close(doneConn -&gt; {\r\n                if (doneConn.failed()) {\r\n                    throw new RuntimeException(doneConn.cause());\r\n                }\r\n            });\r\n        });\r\n    }\r\n\r\n    private void resolveShortUrl(Url url, Handler&lt;AsyncResult&lt;ResultSet&gt;&gt; done) {\r\n        JsonArray params = new JsonArray().add(decode(url.getShortUrl()));\r\n        client.getConnection(conn -&gt; {\r\n            query(conn.result(), RETRIVE_BY_SHORT_URL, params, done);\r\n            conn.result().close(doneConn -&gt; {\r\n                if (doneConn.failed()) {\r\n                    throw new RuntimeException(doneConn.cause());\r\n                }\r\n            });\r\n        });\r\n    }\r\n\r\n    private void query(SQLConnection conn, String sql, JsonArray param,  Handler&lt;AsyncResult&lt;ResultSet&gt;&gt; done) {\r\n        conn.queryWithParams(sql, param, res -&gt; {\r\n            if(res.failed()) {\r\n                logger.error(\"CAUSE = \" + res.cause().getCause() + \" MESSAGE = \" + res.cause().getMessage());\r\n                throw new RuntimeException(res.cause());\r\n            } else {\r\n                done.handle(res);\r\n            }\r\n        });\r\n    }\r\n\r\n    private static String encode(long number) {\r\n        long i = number;\r\n        if (i == 0) {\r\n            return Character.toString(CHARSET[0]);\r\n        }\r\n\r\n        StringBuilder stringBuilder = new StringBuilder();\r\n        while (i &gt; 0) {\r\n            long remainder = i % ALPHABET_LENGTH;\r\n            i \/= ALPHABET_LENGTH;\r\n            stringBuilder.append(CHARSET[(int) remainder]);\r\n        }\r\n        return stringBuilder.reverse().toString();\r\n    }\r\n\r\n    private static long decode(String s) {\r\n        long i = 0;\r\n        char[] chars = s.toCharArray();\r\n        for (char c : chars) {\r\n            i = i * ALPHABET_LENGTH + ALPHABET.indexOf(c);\r\n        }\r\n        return i;\r\n    }\r\n\r\n    private static final String SCHEMA = \"public\";\r\n    private static final String TABLE= \"url\";\r\n    private static final String INSERT_NEW = \"INSERT INTO \" + SCHEMA + \".\" + TABLE + \" (longurl, created) VALUES (?,?) RETURNING id\";\r\n    private static final String RETRIVE_BY_SHORT_URL = \"UPDATE \" + SCHEMA + \".\" + TABLE + \" SET count = count + 1 WHERE id = ? RETURNING *\";\r\n\r\n    private static final String ALPHABET = \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\";\r\n    private static final int ALPHABET_LENGTH = ALPHABET.length();\r\n    private static final char[] CHARSET = ALPHABET.toCharArray();\r\n}<\/pre>\n<p>As said, this is all fairly straight forward, if you are already familiar with Vertx. If you are thinking why have I repeated the code for establish a JDBC connection, here is the explanation (<em>line: 10-16<\/em>). I was getting Timeouts when creating JDBC connection in Verticles. To avoid this I also added this code to my Handler class. This way connection is created there and because of the Vertx implementation any later attempt to create it again will result in just getting the instances from the first invocation. This escaped the need to pass the instances directly from the Handler class when creating Verticle instances.<\/p>\n<p>&nbsp;<\/p>\n<p><em>Serverless configuration.<\/em><\/p>\n<p>Lastly I would like to share the serverless.yml, confirmation file that allows seamlessly deploy and management of AWS Lambda. With just a few commands and lines of configuration you are able to configure all necessary steps for deploying your AWS Lambda. Framework takes care of making configuration of Api-Gateway and other AWS hassle that would otherwise needed to be done by hand. In this case Lambda is invoked by HTTP events.<\/p>\n<pre class=\"theme:monokai lang:yaml decode:true \">service: url-shortener-service\r\n\r\nprovider:\r\n  name: aws\r\n  runtime: java8\r\n  region: eu-central-1\r\n\r\n  environment:\r\n    db_url: ${file(.\/serverless.env.yml):dev.db_url}\r\n    db_username: ${file(.\/serverless.env.yml):dev.db_username}\r\n    db_password: ${file(.\/serverless.env.yml):dev.db_password}\r\n    db_driver: ${file(.\/serverless.env.yml):dev.db_driver}\r\n    db_pool_size : ${file(.\/serverless.env.yml):dev.db_pool_size}\r\n    path_param: ${file(.\/serverless.env.yml):dev.path_param}\r\n\r\npackage:\r\n  artifact: target\/url-shortener-service-dev.jar\r\n\r\nfunctions:\r\n  url-shortener-service:\r\n    handler: org.serverless.Handler\r\n    events:\r\n      - http:\r\n          path: \/{shorturl}\r\n          method: get\r\n          cors: true\r\n      - http:\r\n          path: api\r\n          method: post\r\n          cors: true<\/pre>\n<p>&nbsp;<\/p>\n<h3>Performance and Tests:<\/h3>\n<p>Vertx async capabilities eased the stress and memory needs of traditional AWS Lambdas with sync methods. After performing load tests Lambdas that were written using Vertx framework preformed <strong>10% faster<\/strong> and consumed <strong>40% less memory<\/strong>. As I have read somewhere in Vertx documentation, Sync methods will definitely finish the first request faster but in total Async will be faster in the end. This savings in memory and time will definitely reduce the cost of running your Lambdas and the little overhead with additional code is for sure worth it.<\/p>\n<p>&nbsp;<\/p>\n<h3>Conclusion:<\/h3>\n<p>To follow the pace of demanding needs for fast and resilient services we need to move from traditional Monoliths. Embracing the micro service architecture alone will not cut it, not anymore. With the rise and rapid advancement of Cloud solutions it has never been so easy to make a truly Serverless systems build upon network of micro services.<br \/>\nAs you have seen <strong>Vertx<\/strong> with its async API makes the full advantage of <strong>AWS Lambdas<\/strong>, embracing them while also improving the performance and lowering the costs. With the help from <strong>Serverless Framework\u00a0<\/strong>writing, deploying and managing your Lambdas has never been so easy.<\/p>\n<p>If you are interested in the whole project, you can find it on <a href=\"https:\/\/github.com\/pendula95\/vertx-aws-url-shortener-service\/tree\/master\">GitHub<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>7 minutesIntro: Recently I stumbleupon Vertx. Event-driven,\u00a0asynchronized, lightweight,\u00a0reactive,\u00a0highly performant, polyglot application framework. Ideal for writing micro-services. I played around with it for a while and really enjoyed the concept of serverless applications. I developed a few apps and cases and started to wonder how to run and deploy them so that I get a 100% [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[25,12,13,24],"tags":[30,33,34,26,14,29,28,31,32,27],"jetpack_featured_media_url":"","_links":{"self":[{"href":"https:\/\/lazarbulic.com\/blog\/wp-json\/wp\/v2\/posts\/154"}],"collection":[{"href":"https:\/\/lazarbulic.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/lazarbulic.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/lazarbulic.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/lazarbulic.com\/blog\/wp-json\/wp\/v2\/comments?post=154"}],"version-history":[{"count":29,"href":"https:\/\/lazarbulic.com\/blog\/wp-json\/wp\/v2\/posts\/154\/revisions"}],"predecessor-version":[{"id":277,"href":"https:\/\/lazarbulic.com\/blog\/wp-json\/wp\/v2\/posts\/154\/revisions\/277"}],"wp:attachment":[{"href":"https:\/\/lazarbulic.com\/blog\/wp-json\/wp\/v2\/media?parent=154"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/lazarbulic.com\/blog\/wp-json\/wp\/v2\/categories?post=154"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/lazarbulic.com\/blog\/wp-json\/wp\/v2\/tags?post=154"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}