A high-performance, WebSocket-based pub-sub system with REST API built using FastAPI. Arya provides real-time message broadcasting with thread-safe in-memory storage, backpressure handling, and comprehensive error management.
- WebSocket-based pub-sub with real-time message delivery
- REST API for topic management and system monitoring
- Thread-safe in-memory storage with asyncio locks
- Message history replay (configurable per subscription)
- Backpressure handling with bounded message queues
- Graceful error handling with proper error codes
- Health monitoring and system statistics
- Docker support for easy deployment
- Comprehensive test suite with 90%+ coverage
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ WebSocket │ │ PubSubManager │ │ REST API │
│ Handler │◄──►│ (Thread-Safe) │◄──►│ Endpoints │
│ │ │ │ │ │
│ • Connection │ │ • Topic Storage │ │ • Topic CRUD │
│ • Message Route │ │ • Subscriptions │ │ • Health Check │
│ • Broadcasting │ │ • Message Queue │ │ • Statistics │
└─────────────────┘ └──────────────────┘ └─────────────────┘
- PubSubManager: Thread-safe singleton managing topics, subscriptions, and message distribution
- WebSocketManager: Handles individual WebSocket connections with heartbeat and error handling
- Topic: Manages message history with configurable retention (default: 100 messages)
- Message Queue: Bounded queues (100 messages) for backpressure handling
arya/
├── src/arya/ # Main application code
│ ├── api/ # REST API endpoints
│ ├── core/ # Core business logic
│ │ ├── models.py # Pydantic data models
│ │ └── pubsub.py # PubSub manager
│ ├── websocket/ # WebSocket handlers
│ ├── config/ # Configuration management
│ ├── exceptions/ # Custom exceptions
│ └── utils/ # Utility functions
├── tests/ # Test suite
├── simple_test.py # Contract compliance test
├── test_client.py # Interactive demo client
├── docker-compose.yml # Docker setup
└── requirements.txt # Dependencies
# Build and run with Docker docker build -t arya-pubsub . docker run -p 8000:8000 arya-pubsub # Or use docker-compose docker-compose up --build
# Install dependencies pip install -r requirements.txt # Run the server PYTHONPATH=src python src/main.py # Or with uvicorn directly (development with auto-reload) PYTHONPATH=src uvicorn arya.api.main:create_app --factory --host 0.0.0.0 --port 8000 --reload
The server will start on http://localhost:8000
curl -X POST "http://localhost:8000/topics" \ -H "Content-Type: application/json" \ -d '{"name": "orders"}' # Response: {"status": "created", "topic": "orders"}
curl -X DELETE "http://localhost:8000/topics/orders" # Response: {"status": "deleted", "topic": "orders"}
curl "http://localhost:8000/topics" # Response: { # "topics": [ # {"name": "orders", "subscribers": 3, "messages": 42} # ] # }
curl "http://localhost:8000/health" # Response: { # "uptime_sec": 3600, # "topics": 5, # "subscribers": 15 # }
curl "http://localhost:8000/stats" # Response: { # "topics": { # "orders": {"messages": 42, "subscribers": 3}, # "notifications": {"messages": 15, "subscribers": 7} # } # }
Connect to WebSocket endpoint: ws://localhost:8000/ws
All WebSocket messages use JSON format with the following structure:
{
"type": "message_type",
"request_id": "optional_correlation_id",
// ... additional fields based on message type
}{
"type": "subscribe",
"topic": "orders",
"client_id": "unique-client-123",
"last_n": 5,
"request_id": "sub-1"
}Response:
{
"type": "ack",
"message": "Subscribed to topic 'orders'",
"request_id": "sub-1",
"timestamp": "2024年01月15日T10:30:00Z"
}{
"type": "unsubscribe",
"topic": "orders",
"client_id": "unique-client-123",
"request_id": "unsub-1"
}{
"type": "publish",
"topic": "orders",
"message": {
"order_id": 12345,
"customer_id": "cust-789",
"amount": 99.99,
"items": ["item1", "item2"]
},
"request_id": "pub-1"
}{
"type": "event",
"topic": "orders",
"message": {
"id": "msg-uuid-here",
"payload": {
"order_id": 12345,
"customer_id": "cust-789",
"amount": 99.99
},
"timestamp": "2024年01月15日T10:30:00Z"
}
}// Send ping {"type": "ping", "request_id": "ping-1"} // Receive pong {"type": "pong", "request_id": "ping-1", "timestamp": "2024年01月15日T10:30:00Z"}
{
"type": "error",
"error_code": "TOPIC_NOT_FOUND",
"error_message": "Topic 'nonexistent' not found",
"request_id": "sub-1",
"timestamp": "2024年01月15日T10:30:00Z"
}Error Codes:
BAD_REQUEST: Invalid request format or parametersTOPIC_NOT_FOUND: Requested topic doesn't existSLOW_CONSUMER: Client is too slow, connection terminatedINTERNAL_ERROR: Server-side error
# Create and activate virtual environment (if not already done) python3 -m venv venv source venv/bin/activate # Install dependencies pip install -r requirements.txt # Run all tests PYTHONPATH=src python -m pytest tests/ -v # Run with coverage (if pytest-cov is installed) PYTHONPATH=src python -m pytest tests/ --cov=src --cov-report=html # Run specific test categories PYTHONPATH=src python -m pytest tests/test_models.py tests/test_pubsub.py -v # Unit tests PYTHONPATH=src python -m pytest tests/test_api.py tests/test_websocket.py -v # Integration tests
- Unit Tests:
test_models.py,test_pubsub.py - Integration Tests:
test_api.py,test_websocket.py - Coverage Target: 90%+
# Start the service first (in one terminal): source venv/bin/activate PYTHONPATH=src python src/main.py # Then run the test (in another terminal): source venv/bin/activate python simple_test.py
# Start the service first (in one terminal): source venv/bin/activate PYTHONPATH=src python src/main.py # Then run the test client (in another terminal): source venv/bin/activate python test_client.py
LOG_LEVEL: Logging level (DEBUG, INFO, WARNING, ERROR)MAX_MESSAGE_HISTORY: Maximum messages per topic (default: 100)MAX_QUEUE_SIZE: Maximum message queue size per connection (default: 100)HEARTBEAT_INTERVAL: Heartbeat interval in seconds (default: 30)
- Message History: Adjust per-topic message retention
- Queue Size: Tune backpressure thresholds
- Connection Limits: Configure via uvicorn workers
- Memory Usage: Monitor with
/statsendpoint
# Use multi-stage build for smaller image FROM python:3.11-slim as builder WORKDIR /app COPY requirements.txt . RUN pip install --user -r requirements.txt FROM python:3.11-slim WORKDIR /app COPY --from=builder /root/.local /root/.local COPY . . RUN useradd --create-home --shell /bin/bash arya USER arya EXPOSE 8000 CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
apiVersion: apps/v1 kind: Deployment metadata: name: arya-pubsub spec: replicas: 3 selector: matchLabels: app: arya-pubsub template: metadata: labels: app: arya-pubsub spec: containers: - name: arya-pubsub image: arya-pubsub:latest ports: - containerPort: 8000 env: - name: LOG_LEVEL value: "INFO" resources: requests: memory: "256Mi" cpu: "250m" limits: memory: "512Mi" cpu: "500m"
For horizontal scaling, use a load balancer with session affinity or implement client-side connection management for WebSocket persistence.
# Basic health check curl http://localhost:8000/health # Detailed statistics curl http://localhost:8000/stats
- Connection Count: Active WebSocket connections
- Topic Count: Number of active topics
- Message Throughput: Messages per second
- Memory Usage: Topic message history size
- Error Rates: Failed connections and operations
The system provides structured logging with different levels:
# Critical operations logger.info("Client subscribed to topic") # Debug information logger.debug("Message added to topic history") # Errors and warnings logger.error("Failed to broadcast message")
- Input Validation: Pydantic models validate all inputs
- Error Handling: Sanitized error messages
- Resource Limits: Bounded queues and message history
- Non-root User: Docker runs as non-privileged user
- Authentication: Add JWT or API key authentication
- Rate Limiting: Implement per-client rate limits
- TLS/SSL: Use HTTPS/WSS in production
- Network Security: Deploy behind reverse proxy
- Monitoring: Add security event logging
- In-Memory Only: No persistence across restarts
- Single Instance: No clustering support
- No Authentication: Open access (add auth layer)
- Message Size: No explicit size limits (JSON parsing limits apply)
- Backpressure Policy: Disconnect slow consumers (prevents memory exhaustion)
- Message History: 100 messages per topic (configurable)
- Thread Safety: AsyncIO locks (not thread-safe across OS threads)
- Error Handling: Fail fast with clear error messages
arya/
├── main.py # FastAPI application
├── models.py # Pydantic data models
├── pubsub.py # Core pub-sub manager
├── websocket_handler.py # WebSocket connection management
├── requirements.txt # Python dependencies
├── Dockerfile # Container configuration
├── docker-compose.yml # Local development setup
├── pytest.ini # Test configuration
├── tests/ # Test suite
│ ├── conftest.py # Test fixtures
│ ├── test_models.py # Model validation tests
│ ├── test_pubsub.py # Core logic tests
│ ├── test_api.py # REST API tests
│ └── test_websocket.py # WebSocket tests
└── README.md # This file
- New Message Types: Add to
models.pyand handle inwebsocket_handler.py - New Endpoints: Add to
main.pywith proper error handling - Storage Backends: Extend
PubSubManagerwith pluggable storage - Authentication: Add middleware to FastAPI application
- Fork the repository
- Create feature branch
- Add tests for new functionality
- Ensure all tests pass
- Submit pull request
import asyncio import websockets import json async def client_example(): uri = "ws://localhost:8000/ws" async with websockets.connect(uri) as websocket: # Subscribe to topic subscribe_msg = { "type": "subscribe", "topic": "orders", "client_id": "python-client-1", "last_n": 5 } await websocket.send(json.dumps(subscribe_msg)) # Receive acknowledgment response = await websocket.recv() print(f"Subscribe response: {response}") # Publish a message publish_msg = { "type": "publish", "topic": "orders", "message": {"order_id": 123, "amount": 99.99} } await websocket.send(json.dumps(publish_msg)) # Listen for events while True: message = await websocket.recv() data = json.loads(message) if data["type"] == "event": print(f"Received: {data['message']['payload']}") # Run the client asyncio.run(client_example())
const ws = new WebSocket('ws://localhost:8000/ws'); ws.onopen = function() { // Subscribe to topic const subscribeMsg = { type: 'subscribe', topic: 'orders', client_id: 'js-client-1', last_n: 10 }; ws.send(JSON.stringify(subscribeMsg)); }; ws.onmessage = function(event) { const data = JSON.parse(event.data); if (data.type === 'event') { console.log('Received message:', data.message.payload); } else if (data.type === 'ack') { console.log('Operation acknowledged:', data.message); } else if (data.type === 'error') { console.error('Error:', data.error_message); } }; // Publish message function publishMessage(orderData) { const publishMsg = { type: 'publish', topic: 'orders', message: orderData }; ws.send(JSON.stringify(publishMsg)); } // Example usage publishMessage({order_id: 456, customer: 'john@example.com', amount: 149.99});
This project is licensed under the MIT License - see the LICENSE file for details.
For questions, issues, or contributions:
- Create an issue on GitHub
- Check existing documentation
- Review test cases for usage examples
Built with ❤️ using FastAPI, WebSockets, and Python asyncio