Restlets offer solid support for ephemeral ports. Apache Camel does a solid job of trying to stop you.
Using fixed port numbers in unit tests or standalone integration tests has always felt plain wrong to me. This isn’t just because it compromises the portability of my build across developer workstations and build environments, nor is it simply a result of phantom CI build failures when another branch build, or someone else’s builds are hogging the same port at the same time. I think that it’s mostly because there’s such a simple, easy and bullet-proof tool for the job when you need to grab a free port on demand – ephemeral ports.
It’s a shame then that so many, and often some of the best, open source frameworks out there make using ephemeral ports so difficult. The latest of these to challenge my test writing skills is Apache Camel; specifically its Restlet support.
I’ll forgive this minor black mark against Camel though. Mostly because it’s so damned awesome in so many other ways, and partly because the workaround doesn’t turn out to be all that taxing.
The code for this post is available on GitHub.
Socket Permission Denied
By way of an illustration, here’s a cheap-as-chips builder for a Camel route which uses Restlets to provide an API to retrieve the contents of a file from a configured directory:-
from("restlet:http://localhost:" + restPort + "/files/{fileName}?restletMethods=get") .pollEnrich(simple("file:" + filesystemPath + "/?fileName=${in.headers.fileName}&noop=true&sendEmptyMessageWhenIdle=true&idempotent=false"), -1, null, false) .choice() .when(body().isNull()) .setHeader(Exchange.HTTP_RESPONSE_CODE, constant(HttpStatus.SC_NOT_FOUND));
The Restlet on http://localhost:<port>/files/{fileName} accepts a GET request for a named file. We then use the pollEnrich Content Enricher to consume the content from a file endpoint – with a few options to make sure we don’t delete it or hang waiting for it to appear. If a file is located the content is returned to the caller with an HTTP 200 by default. If no file was found we explicitly set a 404 response code.
With a real port number (e.g. http://localhost:12345) this spins up with no problems, but if we kick it off from an integration test with port 0 (i.e. http://localhost:0) we get a somewhat incongruous error:-
java.net.SocketException: Permission denied at sun.nio.ch.Net.bind0(Native Method) ... at org.restlet.Server.start(Server.java:579) at org.apache.camel.component.restlet.RestletComponent. addServerIfNecessary(RestletComponent.java:388) at org.apache.camel.component.restlet.RestletComponent. connect(RestletComponent.java:174) at org.apache.camel.component.restlet.RestletEndpoint. connect(RestletEndpoint.java:122) at org.apache.camel.component.restlet.RestletConsumer. doStart(RestletConsumer.java:112) ...
Permission denied on an ephemeral port is a new one on me – any user account is normally able to create these sockets. Fortunately there are some clues in the stack trace to guide us on where to look. Sticking a breakpoint in RestletComponent.addServerIfNecessary() reveals that although I’m requesting port 0, my endpoint is trying to use Port 80 – the default HTTP port (and yes, one that requires super-user privileges to grab on my Mac).
That endpoint object is created by RestletComponent.createEndpoint() the code for which quickly reveals the problem:-
URI u = new URI(remaining); … int port = 0; String host = u.getHost(); if (u.getPort() > 0) { port = u.getPort(); } else { port = this.port; } … if (port > 0) { result.setPort(port); }
Although Restlets themselves support ephemeral ports, Camel is treating port 0 as if no port has been specified in the URL, and therefore not assigning that port number to the endpoint. So the endpoint gets its (private static) default when constructed:-
private static final int DEFAULT_PORT = 80; … @UriPath(defaultValue = "80") @Metadata(required = "true") private int port = DEFAULT_PORT;
Socket Permission Granted
Having tried a few different lines of attack and finding that Camel’s defences against requesting a zero port are pretty solid, I finally found the get out of gaol card a little earlier in RestletComponent.createEndpoint():-
RestletEndpoint result = new RestletEndpoint(this, remaining); if (synchronous != null) { result.setSynchronous(synchronous); } result.setDisableStreamCache(isDisableStreamCache()); setEndpointHeaderFilterStrategy(result); setProperties(result, parameters);
The freshly minted RestletEndpoint at the start of this method has the default port of 80, which gets overridden with a non-zero port from the URL later. But before getting to that point, this method calls setProperties to assign any available RestletEndpoint properties from the options in the endpoint URI:-
“…?restletMethods=get”
While Camel’s Restlet documentation doesn’t list a Port property for the endpoint, I can see getters and setters for it in the code. And it turns out that if we specify our port again here as well:-
“…?restletMethods=get&port=" + restPort
We’re able to overwrite the default port of 80 with an explicit 0 and the rest of the component, endpoint and server construction works perfectly.
Socket to me
But how do we find that port in our integration test? The Server class has a handy getEphemeralPort() method (along with a getActualPort() method which will return it as well). Unfortunately the only reference to the server in the RestletComponent is in a private variable:-
private final Map<String, Server> servers = new HashMap<String, Server>();
Getting the RestletComponent instance is straightforward – we can just get it by name from the Camel context – accessed here through Camel’s standalone Main class (on the presumption we are running only one Camel context):-
RestletComponent component = cut.main.getCamelContexts().get(0) .getComponent("restlet", RestletComponent.class);
To get our Server instance we’ll need to use a bit of Reflection jiggery-pokery to make the servers property accessible:-
Field serverMapField = RestletComponent.class.getDeclaredField("servers"); serverMapField.setAccessible(true);
We can now use the get method on this Field object to retrieve the value of servers, and then – on the presumption we’re only sticking one value in the Map – use an iterator to get the first entry and retrieve the actual port
Map<String, Server> servers = (Map<String, Server>)serverMapField.get(component); int portToCall = servers.values().iterator().next().getActualPort();
Which leaves us with a handy, re-usable method for our integration test class to pull the physical port to call:-
@SuppressWarnings("unchecked") private int getActualRestPort() throws Exception { Field serverMapField = RestletComponent.class.getDeclaredField("servers"); serverMapField.setAccessible(true); RestletComponent component = cut.main.getCamelContexts().get(0) .getComponent("restlet", RestletComponent.class); Map<String, Server> servers = (Map<String, Server>)serverMapField.get(component); return servers.values().iterator().next().getActualPort(); }
The full working code for this example can be found on GitHub