Improving Responsiveness in Jakarta REST (JAX-RS) Services through Asynchronous Processing

Photo of Luqman Saeed by Luqman Saeed

Speed and responsiveness are very important in the development and use of modern RESTful APIs in Java applications, as they help ensure efficiency and scalability, especially as businesses move towards cloud-native applications. The primary means of achieving high speed, responsiveness, efficiency and scalability is through asynchronous processing. It allows applications to handle requests efficiently and perform tasks without getting stuck, making things run smoother and faster for the user while supporting scalability.

In this post, we'll look at how asynchronous processing works in Jakarta RESTful Web Services (JAX-RS or Jakarta REST) and how to implement async patterns in JAX-RS to enhance your Java web services, so you can build powerful and scalable RESTful APIs in Java.

The Need for Asynchronicity

In traditional synchronous processing, a thread handling a request is often blocked while waiting for external events or long-running operations to complete. Imagine a thread waiting for a database query to finish or an API call to return. These actions can take a lot of time, and during this wait, the thread is blocked, unable to handle other tasks until the current operation completes. This often leads to inefficient resource usage and degraded performance, especially under heavy load, as multiple threads might be stuck waiting, causing a bottleneck. Asynchronous processing addresses this challenge by allowing threads to delegate expensive tasks to background threads, freeing them to handle other requests while the delegated tasks are being executed. 

Benefits of Asynchronous REST APIs

  • Improved scalability: Handles more requests simultaneously without adding more threads.
  • Better user experience: Reduces waiting times for long-running tasks.
  • Optimized resource usage: Threads aren’t tied up waiting for external services to respond.

Asynchronous Processing in Jakarta REST (JAX-RS)

Jakarta REST (JAX-RS) offers two main ways to handle asynchronous processing on the server-side: AsyncResponse and CompletionStage. These methods help make web applications faster and more responsive by allowing long tasks to be handled without tying up the main server thread.

AsyncResponse

AsyncResponse is like putting a client's request "on hold" while your server performs some lengthy work. This frees up the server resources to handle other requests, improving the server’s responsiveness.

Imagine a customer placing a an order at a restaurant. The chef acknowledges the order, then tells the customer to relax and enjoy a drink while the meal is prepared. AsyncResponse works the same way—it immediately acknowledges the client's request, freeing up server resources, and then sends the complete response once it's ready.

When to Use AsyncResponse - Use Cases

  • Long-Running Tasks: When your application needs to perform time-consuming operations, such as complex computations, database queries, or external API calls.
  • Non-Blocking Operations: AsyncResponse avoids blocking server threads, ensuring your application remains responsive. It is useful when you want to avoid tying up server resources waiting for an operation to complete.

Let's see the use of AsyncResponse with a code example:

@Path("/async")   

public class MyResource {
    private final ExecutorService executor = Executors.newSingleThreadExecutor();
    @GET
    public void longRunningOperation(@Suspended AsyncResponse asyncResponse) {
       executor.submit(() -> {
            // Simulate a long-running operation
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();

           }
            String result = "Hello from the asynchronous world!";
           asyncResponse.resume(result);
       });
   }
}

In this example, the longRunningOperation method injects an AsyncResponse using the @Suspended annotation. It then submits a task to an executor service to simulate a long-running operation. Once the operation is complete, the resume method is called on the AsyncResponse to send the response back to the client.

Callback Features

AsyncResponse also allows you to register callbacks to handle completion and connection termination events. These callbacks provide additional flexibility for managing asynchronous processing:

  • CompletionCallback: Triggered when the response processing is completed.
  • ConnectionCallback: Triggered when the client disconnects before the response is completed.

Here is an example of how to register these callbacks in your code:

@GET
public void longRunningOperation(@Suspended AsyncResponse asyncResponse) {
   asyncResponse.register(new CompletionCallback() {
       @Override
       public void onComplete() {
           System.out.println("Response processing completed");
       }
   });
   asyncResponse.register(new ConnectionCallback() {
       @Override
       public void onDisconnect(AsyncResponse disconnected) {
            System.out.println("Client disconnected before response was ready");

        }

    });

   executor.submit(() -> {
       try {
           // Simulate a long-running operation
           Thread.sleep(5000);
       } catch (InterruptedException e) {
           Thread.currentThread().interrupt();
       }
       asyncResponse.resume("Hello from the asynchronous world!");
    });

}

Timeout Settings

To avoid indefinitely waiting for an operation to complete, you can set a timeout for the suspended connection. This ensures that the server will respond to the client even if the task doesn’t finish on time. If the timeout is reached before the response is available, a ServiceUnavailableException is generated and sent to the client, or a registered TimeoutHandler can take action instead.

Here’s how to set up a timeout and register a TimeoutHandler:

@GET

public void longRunningOperation(@Suspended AsyncResponse asyncResponse) {
   asyncResponse.setTimeoutHandler(ar -> {
       ar.resume(Response.status(Response.Status.SERVICE_UNAVAILABLE)
                         .entity("Operation timed out -- please try again")
                         .build());
    });

   asyncResponse.setTimeout(15, TimeUnit.SECONDS);
   executor.submit(() -> {
       try {
           // Simulate a long-running operation
           Thread.sleep(5000);
       } catch (InterruptedException e) {
           Thread.currentThread().interrupt();
       }
       asyncResponse.resume("Hello from the asynchronous world!");
   });
}

In this example, a timeout of 15 seconds is set. If the operation does not complete within this time, the TimeoutHandler sends a "Service Unavailable" response back to the client.

CompletionStage

CompletionStage is useful for managing multiple asynchronous tasks that depend on each other. It is like a recipe for asynchronous operations. It provides a structured way to define and manage a series of tasks that depend on each other, ensuring they execute in the correct order and handling any potential errors that arise. As such, it is ideal for complex workflows.

Imagine preparing a multi-course meal. You might start by chopping vegetables while preheating the oven, then use the chopped vegetables to prepare a sauce while the main course cooks. CompletionStage lets you define these tasks and their dependencies, ensuring each step is completed before moving on to the next.

When to Use CompletionStage - Use Cases

  • Complex Workflows: Suitable for scenarios where multiple asynchronous operations need to be chained together.
  • Reactive Programming: Ideal for applications that follow a more reactive or functional programming style.

Let’s look at an example using CompletionStage

@Path("/async")

public class MyResource {
   private final ExecutorService executor = Executors.newSingleThreadExecutor();
   @GET
public CompletableFuture<String> anotherLongRunningOperation() {
       CompletableFuture<String> future = new CompletableFuture<>();
       executor.submit(() -> {
           // Simulate a long-running operation
           try {
               Thread.sleep(5000); 
           } catch (InterruptedException e) {
               Thread.currentThread().interrupt();
           }
           String result = "Greetings from another asynchronous operation!";
           future.complete(result);
       });
        return future;

    }

}

In this example, the anotherLongRunningOperation method returns a CompletableFuture. It submits a task to the executor service and completes the future when the task is finished. The JAX-RS implementation handles sending the response to the client based on the future's completion.

Conclusions

Asynchronous processing is the main construct for building responsive RESTful web services. The Jakarta REST (JAX-RS) specification provides support for asynchronicity through AsyncResponse and CompletionStage, allowing you to optimize thread usage and enhance the overall user experience. Using asynchronous programming patterns allows you to create web services that efficiently manage long-running operations and effectively scale under load.

f you're looking to create highly responsive APIs that scale efficiently under load, mastering async patterns in Jakarta REST is essential. The Jakarta REST specification has detailed information on the full range of available async features for crafting modern RESTFul web services on the Jakarta EE Platform.

Ready to Build High-Performance REST APIs in Java? Grab the latest released spec, download your trial copy of Payara Server Enterprise and get started creating responsive web APIs that delight your users!

 

Related Posts

Comments