Services (Developer)

Services can be run in a CDAP application to serve data to external clients. Services run in containers and the number of running service instances can be dynamically scaled. Developers can implement custom services to interface with a legacy system and perform additional processing beyond the CDAP processing paradigms. Examples could include running an IP-to-geo lookup and serving user-profiles.

The lifecycle of a custom service can be controlled via the CDAP UI, by using the CDAP Java Client API, or with the CDAP Microservices.

You can add services to your application by calling the addService method in the application's configure method:

1 2 3 4 5 6 7 8 9 10 11 public class AnalyticsApp extends AbstractApplication { @Override public void configure() { setName("AnalyticsApp"); setDescription("Application for generating mobile analytics"); ... addService(new IPGeoLookupService()); addService(new UserLookupService()); ... } }

Services are implemented by extending AbstractService, which consists of HttpServiceHandlers to serve requests:

1 2 3 4 5 6 7 8 9 10 public class IPGeoLookupService extends AbstractService { @Override protected void configure() { setName("IpGeoLookupService"); setDescription("Service to lookup locations of IP addresses."); useDataset("IPGeoTable"); addHandler(new IPGeoLookupHandler()); } }

Service Handlers

ServiceHandlers are used to handle and serve HTTP requests.

You add handlers to your service by calling the addHandler method in the service's configure method, as shown above. Only handler classes that are declared public, with public methods for endpoints, will be exposed by the service.

To use a dataset within a handler, either include the @UseDataSet annotation in the handler, or use the getDataset() method dynamically in the handler to obtain an instance of the dataset (see Service Microservices ). Each request to a method is committed as a single transaction.

1 2 3 4 5 6 7 8 9 10 11 12 public class IPGeoLookupHandler extends AbstractHttpServiceHandler { @UseDataSet("IPGeoTable") Table table; @Path("lookup/{ip}") @GET public void lookup(HttpServiceRequest request, HttpServiceResponder responder, @PathParam("ip") String ip) { // ... responder.sendString(200, location, Charsets.UTF_8); } }

Path and Query Parameters

Handler endpoints can have Path and Query parameters. Path parameters are used to assist with path-mapping of requests, while Query parameters are used to easily parse the query string of a request.

For example, the WordCount application has a Service that exposes an endpoint to retrieve the count of a word and its word associations. In the @Path annotation, {word} is a path parameter that is mapped to a Java String using @PathParam("word") String word. Similarly, the endpoint also allows the query parameter limit with a default value of 10.

1 2 3 4 5 6 7 8 @Path("count/{word}") @GET public void getCount(HttpServiceRequest request, HttpServiceResponder responder, @PathParam("word") String word, @QueryParam("limit") @DefaultValue("10") Integer limit) { // ... }

An example of calling this endpoint with the Microservices is shown in the Service Microservices.

Note: Any reserved or unsafe characters in the path parameters should be encoded using percent-encoding. See the next section, “About Path Parameters”.

Handling a Large Request Body

Sometimes the request body for a PUT or POST request can be huge and it is not feasible to keep all of it in memory. You can have the handler method return an HttpContentConsumer instead of void to process the request body in smaller pieces.

For example, the SportResults application has an UploadService that exposes an endpoint for uploading files to PartitionedFileSets. It returns an HttpContentConsumer so that it receives the request body in a series of small chunks:

1 2 3 4 5 6 @PUT @Path("leagues/{league}/seasons/{season}") public HttpContentConsumer write(HttpServiceRequest request, HttpServiceResponder responder, @PathParam("league") String league, @PathParam("season") int season) { // ... }

About Path Parameters

The value of a path parameter cannot contain any characters that have a special meaning in URI syntax. If a request has a path parameter that contains such a character, it must be URL-encoded using the "%hh" notation, a percent-symbol followed by two hex characters.

In general, any character that is not a letter, a digit, or one of $-_.+!*'() should be encoded.

However, if the special character is a forward-slash (/), then it will appear to the path matcher as a "/", even if it is escaped as "%2f". This occurs because the path is decoded prior to matching.

There are two ways to work around this:

  • Double-escape any forward-slashes (/) as "%252f". This will prevent the decoding before the path is matched. However, the path parameter's value will contain the "%2f" instead of a "/", and the application code must decode the parameter itself to obtain the actual value.

  • Use a query parameter instead. This is a better solution because the "/" is not a reserved character in the query of a URI.

Service Discovery

Services announce the host and port they are running on so that they can be discovered—and accessed—by other programs.

Service are announced using the name passed in the configure method. The application nameservice id, and hostname required for registering the service are automatically obtained.

The service can then be discovered in a MapReduce, Spark, Worker, or another service using the appropriate program context. You may also access a service in a different application by specifying the application name in the getServiceURL call.

For example, in Workers:

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 public class GeoWorker extends AbstractWorker { // URL for IPGeoLookupService private URL serviceURL; // URL for SecurityService in SecurityApplication private URL securityURL; public void run() { // Get URL for service in same application serviceURL = getContext().getServiceURL("IPGeoLookupService"); // Get URL for service in a different application securityURL = getContext().getServiceURL("SecurityApplication", "SecurityService"); // Access the IPGeoLookupService using its URL if (serviceURL != null) { URLConnection connection = new URL(serviceURL, String.format("lookup/%s", ip)).openConnection(); BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); } ... // Access the SecurityService using its URL if (securityURL != null) { ... } } }

Services and Resources

When a service is configured, the resource requirements for the server that runs all handlers of the service can be set, both in terms of the amount of memory (in megabytes) and the number of virtual cores assigned.

If both the memory and the number of cores needs to be set, this can be done using:

1 setResources(new Resources(1024, 2));

The resource requirements can also be altered through runtime arguments, as explained in Configuring Resources.

Service Thread Model

An HTTP server is started for each Service instance, which by default starts 60 threads to handle client requests. Each thread is basically tied to one active client request and each thread would have its own instance of HttpServiceHandlers. This guarantees there will be no concurrent calls to each HttpServiceHandler object instance. Also, by default, when a thread is idled for more than 60 seconds, it will be terminated automatically, with the HttpServiceHandler.destroy method being called to release resources.

Both the number of service threads and the thread keep-alive time can be altered by these runtime arguments:

  • system.service.threads: Number of threads to use in the HTTP server

  • system.service.thread.keepalive.secs: Number of seconds a thread can sit idle before getting terminated