Apache Thrift - Implementing Services



Implementing Services in Apache Thrift

Apache Thrift allows you to define services and data types in an Interface Definition Language (IDL) and generate code for various programming languages. A typical service implementation involves both a server that provides the service and a client that consumes it.

This tutorial will walk you through the process of implementing services using the generated code, focusing on both the server-side and client-side implementation.

Setting Up Your Environment

Before implementing services, ensure you have the following :

  • Apache Thrift Compiler: Installed and configured. You can download it from the Apache Thrift website.
  • Generated Code: Use the Thrift compiler to generate the necessary code for your target programming languages.
  • Programming Environment: Set up your programming environment with the appropriate dependencies (e.g., Thrift libraries for Java, Python, etc.).

Generating Service Code

After defining your service in the Thrift IDL file, the next step is to generate the corresponding code for the server and client in your target programming language.

This code generation process is important as it provides the necessary classes and interfaces to implement the service logic on the server side and interact with the service on the client side.

Understanding the Role of the Thrift Compiler

The Thrift compiler ("thrift" command) is a tool that reads your Thrift IDL file and generates code in the programming language(s) you specify. This generated code includes the following :

  • Data Structures: Classes or types corresponding to the structs, enums, unions, and other data types defined in the IDL file.
  • Service Interfaces: Interfaces or base classes for each service defined in the IDL, which you must implement in your server application.
  • Client Stubs: Client-side classes that provide methods to interact with the server by calling the remote procedures defined in the service.

Example: Thrift IDL File

The following Thrift IDL file defines a "User" struct, a "UserService" service with two methods, and a "UserNotFoundException" exception :

namespace java com.example.thrift
namespace py example.thrift

struct User {
  1: i32 id
  2: string name
  3: bool isActive
}

service UserService {
  User getUserById(1: i32 id) throws (1: UserNotFoundException e)
  void updateUser(1: User user)
}

exception UserNotFoundException {
  1: string message
}

Use the Thrift compiler to generate code :

thrift --gen java example.thrift
thrift --gen py example.thrift

This generates the necessary classes and interfaces in Java and Python that you will use to implement the service.

Implementing the Service in Java

Once you have generated the necessary Java code from your Thrift IDL file, the next step is to implement the service. This involves creating the server-side logic that will process client requests and developing the client-side code to interact with the service.

Server-Side Implementation

In the server-side implementation, you first need to implement the service interface: The Thrift compiler generates a Java interface for each service. Implement this interface to define the behaviour of your service :

public class UserServiceHandler implements UserService.Iface {
   @Override
   public User getUserById(int id) throws UserNotFoundException, TException {
      // Implement the logic to retrieve the user by ID
      if (id == 1) {
         return new User(id, "John Doe", true);
      } else {
         throw new UserNotFoundException("User not found");
      }
   }
   @Override
   public void updateUser(User user) throws TException {
      // Implement the logic to update the user
      System.out.println("Updating user: " + user.name);
   }
}

Then, we need to set up the server: Create a server that listens for client requests and invokes the appropriate methods on the service handler :

public class UserServiceServer {
   public static void main(String[] args) {
      try {
         UserServiceHandler handler = new UserServiceHandler();
         UserService.Processor<UserServiceHandler> processor = new UserService.Processor<>(handler);
         TServerTransport serverTransport = new TServerSocket(9090);
         TServer server = new TSimpleServer(new TServer.Args(serverTransport).processor(processor));

         System.out.println("Starting the server...");
         server.serve();
      } catch (Exception e) {
         e.printStackTrace();
      }
   }
}

Where,

  • Server Transport: Specifies the communication transport (e.g., socket).
  • Processor: Handles incoming requests by delegating them to the service handler.
  • Server: The server listens for requests and passes them to the processor.

Client-Side Implementation

In a client-side implementation, you first need to create a client: The Thrift compiler generates a client class for each service. Use this class to invoke methods on the server :

public class UserServiceClient {
   public static void main(String[] args) {
      try {
         TTransport transport = new TSocket("localhost", 9090);
         transport.open();

         TProtocol protocol = new TBinaryProtocol(transport);
         UserService.Client client = new UserService.Client(protocol);

         User user = client.getUserById(1);
         System.out.println("User retrieved: " + user.name);

         user.isActive = false;
         client.updateUser(user);

         transport.close();
      } catch (Exception e) {
         e.printStackTrace();
      }
   }
}

Where,

  • Transport: Manages the connection to the server.
  • Protocol: Specifies how data is serialized (e.g., binary protocol).
  • Client: Provides methods to invoke the remote service.

Implementing the Service in Python

When implementing a Thrift service in Python, the process involves several steps similar to those in other languages like Java.

You will need to implement the service logic, set up the server to handle client requests, and ensure that the service operates smoothly.

Server-Side Implementation

In the server-side implementation, you first need to implement the service interface: In Python, the Thrift compiler generates a base class for each service. Subclass this base class to implement your service logic :

from example.thrift.UserService import Iface
from example.thrift.ttypes import User, UserNotFoundException

class UserServiceHandler(Iface):
   def getUserById(self, id):
      if id == 1:
         return User(id=1, name="John Doe", isActive=True)
      else:
         raise UserNotFoundException(message="User not found")

   def updateUser(self, user):
      print(f"Updating user: {user.name}")

Then, we need to set up the server: Create a Thrift server to listen for incoming requests and pass them to the service handler :

from thrift.Thrift import TProcessor
from thrift.transport import TSocket, TTransport
from thrift.protocol import TBinaryProtocol
from thrift.server import TSimpleServer
from example.thrift.UserService import Processor

if __name__ == "__main__":
   handler = UserServiceHandler()
   processor = Processor(handler)
   transport = TSocket.TServerSocket(port=9090)
   tfactory = TTransport.TBufferedTransportFactory()
   pfactory = TBinaryProtocol.TBinaryProtocolFactory()

   server = TSimpleServer(processor, transport, tfactory, pfactory)

   print("Starting the server...")
   server.serve()

Where,

  • Processor: Manages the delegation of requests to the handler.
  • Transport and Protocol Factories: Set up the server's communication and data serialization methods.
  • Server: Starts the server to handle client requests.

Client-Side Implementation

In a client-side implementation, you first need to create a client: Use the generated client class to connect to the server and invoke its methods :

from thrift.transport import TSocket, TTransport
from thrift.protocol import TBinaryProtocol
from example.thrift.UserService import Client

if __name__ == "__main__":
   transport = TSocket.TSocket('localhost', 9090)
   transport = TTransport.TBufferedTransport(transport)
   protocol = TBinaryProtocol.TBinaryProtocol(transport)
   client = Client(protocol)

   transport.open()

   try:
      user = client.getUserById(1)
      print(f"User retrieved: {user.name}")

      user.isActive = False
      client.updateUser(user)
   except Exception as e:
      print(f"Error: {e}")

   transport.close()

Where,

  • Transport and Protocol: Manage communication and data formatting.
  • Client: Provides an interface to the remote service, allowing you to invoke methods on the server.

Handling Exceptions

Handling exceptions properly ensures that your service can manage errors smoothly and provide meaningful feedback to clients.

In Apache Thrift, exceptions can be defined in the IDL file and handled in both the service implementation and client code. Handling exceptions involves :

  • Defining Exceptions in the Thrift IDL: Specify exceptions in the Thrift IDL file so that both the server and client understand the types of errors that can occur.
  • Throwing Exceptions in Service Implementation: Implement the logic in the service methods to throw exceptions when necessary.
  • Handling Exceptions on the Server Side: Manage exceptions in the server implementation to ensure the service can recover from errors and provide meaningful responses.
  • Handling Exceptions on the Client Side: Implement error handling in the client code to manage exceptions thrown by the server and respond appropriately.

Define Exceptions in the Thrift IDL

Exceptions are defined in the Thrift IDL file using the exception keyword. You can specify custom exception types that your service methods can throw :

Example: Thrift IDL File with Exceptions

exception InvalidOperationException {
   1: string message
}

service CalculatorService {
   i32 add(1: i32 num1, 2: i32 num2) throws (1: InvalidOperationException e)
   i32 divide(1: i32 num1, 2: i32 num2) throws (1: InvalidOperationException e)
}

Where,

  • Exception Definition: "InvalidOperationException" is a custom exception with a single field "message".
  • Method Signature: The "add" and "divide" methods are specified to throw "InvalidOperationException". The exception is included in the method signature using the "throws" keyword.

Throw Exceptions in Service Implementation

In your service implementation, you need to throw exceptions according to the logic of your methods. This involves using the exceptions defined in the IDL :

from thrift.Thrift import TException

class InvalidOperationException(TException):
   def __init__(self, message):
      self.message = message

class CalculatorServiceHandler:
   def add(self, num1, num2):
      return num1 + num2

   def divide(self, num1, num2):
      if num2 == 0:
         raise InvalidOperationException("Cannot divide by zero")
      return num1 / num2

Where,

  • Custom Exception Class: "InvalidOperationException" inherits from "TException" and includes a "message" attribute.
  • Throwing Exceptions: In the "divide" method, an "InvalidOperationException" is raised if the divisor is zero.

Handle Exceptions on the Server Side

On the server side, you should handle exceptions to ensure that the service can manage errors and provide appropriate responses.

Exception Handling in Python Server Code

from thrift.server import TSimpleServer
from thrift.transport import TSocket, TTransport
from thrift.protocol import TBinaryProtocol
from calculator_service import CalculatorService, CalculatorServiceHandler

if __name__ == "__main__":
   handler = CalculatorServiceHandler()
   processor = CalculatorService.Processor(handler)
   transport = TSocket.TServerSocket(port=9090)
   tfactory = TTransport.TBufferedTransportFactory()
   pfactory = TBinaryProtocol.TBinaryProtocolFactory()

   server = TSimpleServer.TSimpleServer(processor, transport, tfactory, pfactory)

   print("Starting the Calculator service on port 9090...")
   try:
      server.serve()
   except InvalidOperationException as e:
      print(f"Handled exception: {e.message}")
   except Exception as e:
      print(f"Unexpected error: {str(e)}")

Where,

  • Exception Handling Block: The "try" block starts the server and the "except" blocks handle exceptions. "InvalidOperationException" is caught and handled explicitly, while other exceptions are caught by the general "Exception" block.

Handle Exceptions on the Client Side

On the client side, you need to handle exceptions that are thrown by the server. This ensures that the client can manage errors and react appropriately.

Example Python Client Code with Exception Handling

from thrift.transport import TSocket, TTransport
from thrift.protocol import TBinaryProtocol
from calculator_service import CalculatorService, InvalidOperationException

try:
   transport = TSocket.TSocket('localhost', 9090)
   transport = TTransport.TBufferedTransport(transport)
   protocol = TBinaryProtocol.TBinaryProtocol(transport)
   client = CalculatorService.Client(protocol)

   transport.open()
    
   try:
      result = client.divide(10, 0)  # This will raise an exception
   except InvalidOperationException as e:
      print(f"Exception caught from server: {e.message}")
   finally:
      transport.close()

except Exception as e:
   print(f"Client-side error: {str(e)}")

Where,

  • Exception Handling Block: The "try" block surrounds the code that interacts with the server. The "except" block catches "InvalidOperationException" thrown by the server, while the general "Exception" block handles any client-side errors.

Synchronous vs. Asynchronous Processing

In service architecture, the way tasks are handled and processed can significantly impact performance, responsiveness, and user experience.

Synchronous and asynchronous processing are two fundamental approaches that differ in how they handle operations, especially in networked or distributed systems.

Synchronous Processing

Synchronous processing is an approach where tasks are executed in a sequential manner. In this model, each task must be completed before the next task starts. This means that the system waits for the completion of one operation before moving on to the next.

Following are the characteristics of synchronous processing :

  • Blocking Calls: Each operation blocks the execution of subsequent operations until it is completed. For example, if a service method is called, the caller waits until the method returns a result before proceeding.
  • Simple Flow: The execution flow is simple and easy to understand since operations are performed one after another. It is easier to implement and debug because the code executes in a linear sequence.
  • Predictable Performance: Performance is predictable as operations complete in the order they are requested.
  • Resource Utilization: May lead to inefficient resource utilization if an operation is waiting on external resources (e.g., network response), as the system remains idle during this time.

Example

Consider a synchronous Thrift service implementation where a client calls a method, and the server processes the request and returns a result before the client can continue :

# Client-side synchronous call
# Client waits until the server responds with the result
result = client.add(5, 10)  
print(f"Result: {result}")

In this example, the client call to "client.add" blocks until the server responds with the result. The client cannot perform other tasks while waiting.

Asynchronous Processing

Asynchronous processing allows tasks to be executed at the same time without blocking the execution of other tasks. In this model, operations can be initiated and then run independently of the main execution flow.

Following are the characteristics of asynchronous processing :

  • Non-Blocking Calls: Operations are initiated and can run in the background, allowing the main thread or process to continue executing other tasks. For example, a service method call can return immediately while the operation completes in the background.
  • Complex Flow: The execution flow can be more complex because tasks are handled at the same time. This often requires callbacks, promises, or future objects to manage completion.
  • Improved Performance: Asynchronous processing can improve performance by using system resources more efficiently, especially in I/O-bound operations where tasks often wait for external responses.
  • Concurrency: Allows for simultaneous execution of multiple tasks, which is beneficial in high-latency environments or when handling many simultaneous requests.

Example

Consider an asynchronous Thrift service implementation where the client does not block while waiting for the servers response :

import asyncio
from thrift.transport import TSocket, TTransport
from thrift.protocol import TBinaryProtocol
from thrift.server import TAsyncServer

async def call_add(client):
   result = await client.add(5, 10)  # Non-blocking call
   print(f"Result: {result}")

async def main():
   transport = TSocket.TSocket('localhost', 9090)
   transport = TTransport.TBufferedTransport(transport)
   protocol = TBinaryProtocol.TBinaryProtocol(transport)
   client = CalculatorService.Client(protocol)

   await transport.open()
   await call_add(client)
   await transport.close()

asyncio.run(main())

In this example, "call_add" is an asynchronous function that does not block the execution of other tasks. The "await" keyword is used to perform the non-blocking call to "client.add", allowing the program to continue executing other code.

Advertisements