Zero to Ratpack - Part 3
It has been a while since I wrote another entry in this series but I was energized at Greach 2016 and I wanted to finish it.
The next step in the reverse proxy is to add additional features. Each new feature is implemented as an additional handler and added to the chain before the handler which will actually call the proxy target and stream the response.
There’s a lot of code in this post. You can see the entire project on github. I’m not including the tests for the new handlers in this post because they are very similar to the functional test in part 2 and you can see them on github.
Refactoring to Modules
The first step is to refactor the existing code into modules. Modules are the top level organizational unit for functionality in Ratpack. In this application there are two modules, an Administration module and a Proxy module.
1 2 3 4 5 6 7 8 9 10
The administration module doesn’t have much to it. The
ConfigHandler is bound to the guice registry and that’s it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
The proxy module is similar to the Admin module, but with new handlers for each feature that is being added to the reverse proxy. The config object also became an internal class of the module which is idiomatic Ratpack. The static methods to retrieve handler instances is also idiomatic Ratpack, when you don’t need (or want) to use guice. You’ll notice it in many of the default Ratpack modules. The ratpack.groovy script needs updated to reflect the new modules.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
Each of the new handlers is added to the chain.
The logging handler. This handler simply logs the request and then executes the
next method to move down the chain.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
The blacklist handler. This handler matches all the request uris against a regular expression and does not forward the request on to the proxy target.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
The configuration is retrieved from the registry and then the request path matches any of the configured blacked out urls. If so, the handler calls
context.render and does not call
next. That means the rest of the handlers in the chain won’t be executed. Generally, handlers which render a response will not call
The last and most interesting new handler is the canary handler. This handler allows a percentage of requests to be proxied to a canary target instead of the normal target.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
The handler builds a
URI instance containing either the canary URI or the normal URI and then calls
next but in this case, we pass the
URI instance inside the registry to the last and final handler. New items can be added to a registry which allows a downstream handler to change it’s behavior.
In order the use the URI added to the registry, the
ProxyHandler had to be changed slightly.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
The only change here is that the handler retrieves an instance of
URI from the registry rather than building one from the configuration instance.
Handler chain execution and ordering
The handler chain is probably the most critical piece of Ratpack to understand. Walking through what happens when a request is processed is a useful mental exercise. The chain is constructed in the
1 2 3 4 5 6 7 8
Consider a request for
/reverseProxyAdmin. The first handler processed is the one created by
get('reverseProxyAdmin', AdminModule.configHandler()). In this case, the request path matches what was passed to the get method, and so the
ConfigHandler.handle() method is executed. This method executes
render handlebarsTemplate('reverseProxyAdmin.html', config: config) and never calls
context.next(). The handler chain stops executing the html rendered in that method is written as the response body text.
Now consider a request for
/randomUri. The handler chain starts to execute and
randomUri does not match
ConfigHandler.handle() is not executed. The next handler in the chain was added by
all ProxyModule.loggingHandler(). So
LoggingHandler.handle() is executed. That method always calls
context.next(). The next handler in the chain is the blacklist handler. That handler was also added for every request (via the
all method) and so that handler executes. The handler chain continues to execute in this manner until a handler does not call
What would happen if the handler chain was defined slightly differently?
1 2 3 4 5 6 7 8 9
In the case of
/reverseProxyAdmin, the first handler processed is the one created by
all ProxyModule.loggingHandler(). The handler logs the request and then calls
context.next(). The next handler is now the
ConfigHandler, and because the path matches
reverseProxyAdmin it is executed. Everything else is the same. Order is important when constructing the handler chain. Changing the handler order will change behavior.
In the next post I’ll talk about creating a service layer to delegate functionality to from handlers, unit testing handlers and persistence using JOOQ.