Interceptors in Jararaca
Interceptors are a powerful mechanism in Jararaca for implementing cross-cutting concerns across different execution contexts. They allow you to wrap the execution of HTTP requests, message handling, scheduled tasks, and WebSocket communications with common behaviors such as transaction management, message publishing, and WebSocket message dispatching.
Overview of the Interceptor System
Interceptors in Jararaca work by wrapping the execution of handlers in different contexts:
- HTTP Controllers: Intercept incoming HTTP requests before they reach controllers
- Message Bus Consumers: Intercept message processing in workers
- Scheduled Tasks: Intercept scheduled task execution
- WebSocket Connections: Manage WebSocket connections and message dispatching
Each interceptor provides a specific capability to the application through context variables, allowing your code to access these capabilities through hook functions like use_session(), use_publisher(), and use_ws_manager().
The Atomic Layer: Transactions, Messages, and WebSockets
The true power of the interceptor system lies in how it creates an atomic layer that ensures consistency across database operations, message publishing, and WebSocket communications. This is particularly important for implementing transactional outbox patterns and ensuring consistency in distributed systems.
sequenceDiagram
participant Client
participant HTTP Context
participant DB Interceptor
participant MessageBus Interceptor
participant WebSocket Interceptor
participant Database
participant MessageBroker
participant WebSocketBackend
Client->>HTTP Context: HTTP Request
Note over HTTP Context,WebSocketBackend: Transaction Boundary Begins
HTTP Context->>DB Interceptor: Begin Transaction
DB Interceptor->>Database: Begin DB Transaction
HTTP Context->>MessageBus Interceptor: Stage Messages
MessageBus Interceptor->>MessageBus Interceptor: Queue Messages
HTTP Context->>WebSocket Interceptor: Stage WebSocket Messages
WebSocket Interceptor->>WebSocket Interceptor: Queue WebSocket Messages
HTTP Context->>DB Interceptor: Commit Transaction
DB Interceptor->>Database: Commit DB Transaction
Database-->>DB Interceptor: Success
DB Interceptor-->>MessageBus Interceptor: DB Commit Success
MessageBus Interceptor->>MessageBroker: Flush Queued Messages
MessageBus Interceptor-->>WebSocket Interceptor: Messages Published
WebSocket Interceptor->>WebSocketBackend: Flush WebSocket Messages
Note over HTTP Context,WebSocketBackend: Transaction Boundary Ends
HTTP Context-->>Client: HTTP Response
Order of Interceptors Matters
The order in which interceptors are configured in your application is critical. The typical order is:
- Configuration Interceptor - Loads and provides application configuration
- Message Bus Publisher Interceptor - Provides message publishing capabilities
- Database Session Interceptor - Provides database transaction capabilities
- WebSocket Interceptor - Provides WebSocket communication capabilities
This order ensures that:
- The database transaction is committed first
- Only after successful database commit, messages are published to the message broker
- Finally, WebSocket messages are dispatched to connected clients
Implementing Transactional Outbox Pattern
Jararaca's interceptor system makes implementing the transactional outbox pattern straightforward. This pattern ensures that database changes and message publishing are atomic:
from jararaca.persistence.session import use_session
from jararaca.messagebus.publisher import use_publisher
async def create_user(user_data: dict):
# Get the current database session from the context
session = use_session()
# Get the message publisher from the context
publisher = use_publisher()
# Create user in database
new_user = User(**user_data)
session.add(new_user)
# Stage a message to be published after successful transaction
await publisher.publish("user_created", UserCreatedEvent(id=new_user.id))
# The actual database commit and message publishing
# happens automatically when the request handling is complete,
# managed by the interceptors
When this code executes: 1. The database session interceptor ensures the user is committed to the database 2. If the database transaction succeeds, the message bus interceptor publishes the staged message 3. If the database transaction fails, no messages are published
Contexts and Interceptors
Jararaca supports different execution contexts, and interceptors behave differently in each:
HTTP Context
In an HTTP context, all interceptors are active: - Database Session Interceptor manages transactions - Message Bus Publisher Interceptor stages and flushes messages - WebSocket Interceptor stages and flushes WebSocket messages
Worker Context
In a message bus worker context: - Database Session Interceptor manages transactions - Message Bus Publisher Interceptor stages and flushes messages - WebSocket Interceptor stages and flushes WebSocket messages
Scheduler Context
In a scheduler context: - Database Session Interceptor manages transactions - Message Bus Publisher Interceptor stages and flushes messages - WebSocket Interceptor stages and flushes WebSocket messages
WebSocket Context
In a WebSocket context:
- Database Session Interceptor manages transactions
- Message Bus Publisher is usually not active (as defined in the intercept method)
- WebSocket communications are managed directly
Code Example: Configuring Interceptors
from jararaca import Microservice
from jararaca.persistence.interceptors import AIOSqlAlchemySessionInterceptor, AIOSQAConfig
from jararaca.messagebus.interceptors import MessageBusPublisherInterceptor
from jararaca.presentation.websocket import WebSocketInterceptor
# Create the microservice
microservice = Microservice(
name="my-service",
interceptors=[
# Configuration interceptor (if any)
# ...
# Message bus interceptor
lambda config: MessageBusPublisherInterceptor(
connection_factory=message_publisher_factory,
connection_name="default",
message_scheduler=message_scheduler,
),
# Database session interceptor
lambda config: AIOSqlAlchemySessionInterceptor(
AIOSQAConfig(
url="postgresql+asyncpg://user:password@localhost/dbname",
connection_name="default",
inject_default=True,
)
),
# WebSocket interceptor
lambda config: WebSocketInterceptor(
backend=redis_websocket_backend,
),
],
# Other microservice configuration...
)
Best Practices
- Order Matters: Always configure interceptors in the order: configuration → message bus → database → WebSocket
- Transaction Boundaries: Be aware that interceptors create implicit transaction boundaries
- Error Handling: Database errors will prevent message publishing and WebSocket dispatching
- Idempotency: Design your message handlers to be idempotent in case of retries
- Context Variables: Use the appropriate context hooks (
use_session(),use_publisher(), etc.) to access the capabilities provided by interceptors
Advanced Usage: Custom Interceptors
You can create custom interceptors by implementing the AppInterceptor interface:
from contextlib import asynccontextmanager
from typing import AsyncGenerator
from jararaca.microservice import AppInterceptor, AppTransactionContext
class CustomInterceptor(AppInterceptor):
@asynccontextmanager
async def intercept(self, app_context: AppTransactionContext) -> AsyncGenerator[None, None]:
# Pre-processing logic
try:
# Let the request handler execute
yield
# Post-processing after successful execution
except Exception as e:
# Handle exceptions
raise e
Conclusion
Jararaca's interceptor system provides a powerful way to implement cross-cutting concerns and ensure consistency across database operations, message publishing, and WebSocket communications. By properly configuring interceptors, you can implement patterns like the transactional outbox pattern with minimal effort, ensuring that your distributed system maintains consistency even in the face of failures.