High-performance, protocol-agnostic proxy built on Swoole for blazing fast connection management across HTTP, TCP, and SMTP protocols.
- Swoole coroutines: Handle 100,000+ concurrent connections per server
- Connection pooling: Reuse connections to backend services
- Zero-copy forwarding: Minimize memory allocations
- Aggressive caching: 1-second TTL with 99%+ cache hit rate
- Async I/O: Non-blocking operations throughout
- Memory efficient: Shared memory tables for state management
- Protocol-agnostic connection management
- Cold-start detection and triggering
- Automatic connection queueing during cold-starts
- Health checking and circuit breakers
- Built-in telemetry and metrics
- SSRF validation for security
- Support for HTTP, TCP (PostgreSQL/MySQL), and SMTP
composer require appwrite/protocol-proxyFor a complete setup with all dependencies:
docker-compose up -dSee DOCKER.md for detailed Docker setup and configuration.
The protocol-proxy uses the Resolver Pattern - a platform-agnostic interface for resolving resource identifiers to backend endpoints.
All servers require a Resolver implementation that maps resource IDs (hostnames, database IDs, domains) to backend endpoints:
<?php
use Utopia\Proxy\Resolver;
use Utopia\Proxy\Resolver\Result;
use Utopia\Proxy\Resolver\Exception;
class MyResolver implements Resolver
{
public function resolve(string $resourceId): Result
{
// Map resource ID to backend endpoint
$backends = [
'api.example.com' => 'localhost:3000',
'app.example.com' => 'localhost:3001',
];
if (!isset($backends[$resourceId])) {
throw new Exception(
"No backend for: {$resourceId}",
Exception::NOT_FOUND
);
}
return new Result(endpoint: $backends[$resourceId]);
}
public function onConnect(string $resourceId, array $metadata = []): void
{
// Called when a connection is established
}
public function onDisconnect(string $resourceId, array $metadata = []): void
{
// Called when a connection is closed
}
public function trackActivity(string $resourceId, array $metadata = []): void
{
// Track activity for cold-start detection
}
public function invalidateCache(string $resourceId): void
{
// Invalidate cached resolution data
}
public function getStats(): array
{
return ['resolver' => 'custom'];
}
}<?php
require 'vendor/autoload.php';
use Utopia\Proxy\Resolver;
use Utopia\Proxy\Resolver\Result;
use Utopia\Proxy\Server\HTTP\Swoole as HTTPServer;
// Create resolver (inline example)
$resolver = new class implements Resolver {
public function resolve(string $resourceId): Result
{
return new Result(endpoint: 'backend:8080');
}
public function onConnect(string $resourceId, array $metadata = []): void {}
public function onDisconnect(string $resourceId, array $metadata = []): void {}
public function trackActivity(string $resourceId, array $metadata = []): void {}
public function invalidateCache(string $resourceId): void {}
public function getStats(): array { return []; }
};
$server = new HTTPServer(
$resolver,
host: '0.0.0.0',
port: 80,
workers: swoole_cpu_num() * 2
);
$server->start();<?php
require 'vendor/autoload.php';
use Utopia\Proxy\Resolver;
use Utopia\Proxy\Resolver\Result;
use Utopia\Proxy\Server\TCP\Swoole as TCPServer;
$resolver = new class implements Resolver {
public function resolve(string $resourceId): Result
{
// resourceId is the database name from connection
return new Result(endpoint: 'postgres:5432');
}
public function onConnect(string $resourceId, array $metadata = []): void {}
public function onDisconnect(string $resourceId, array $metadata = []): void {}
public function trackActivity(string $resourceId, array $metadata = []): void {}
public function invalidateCache(string $resourceId): void {}
public function getStats(): array { return []; }
};
$server = new TCPServer(
$resolver,
host: '0.0.0.0',
ports: [5432, 3306], // PostgreSQL, MySQL
workers: swoole_cpu_num() * 2
);
$server->start();<?php
require 'vendor/autoload.php';
use Utopia\Proxy\Resolver;
use Utopia\Proxy\Resolver\Result;
use Utopia\Proxy\Server\SMTP\Swoole as SMTPServer;
$resolver = new class implements Resolver {
public function resolve(string $resourceId): Result
{
// resourceId is the domain from EHLO/HELO
return new Result(endpoint: 'mailserver:25');
}
public function onConnect(string $resourceId, array $metadata = []): void {}
public function onDisconnect(string $resourceId, array $metadata = []): void {}
public function trackActivity(string $resourceId, array $metadata = []): void {}
public function invalidateCache(string $resourceId): void {}
public function getStats(): array { return []; }
};
$server = new SMTPServer(
$resolver,
host: '0.0.0.0',
port: 25,
workers: swoole_cpu_num() * 2
);
$server->start();<?php
$config = [
// Performance tuning
'max_connections' => 100_000,
'max_coroutine' => 100_000,
'socket_buffer_size' => 8 * 1024 * 1024, // 8MB
'buffer_output_size' => 8 * 1024 * 1024, // 8MB
'log_level' => SWOOLE_LOG_ERROR,
// HTTP-specific
'backend_pool_size' => 2048,
'telemetry_headers' => true,
'fast_path' => true,
'open_http2_protocol' => false,
// Cold-start settings
'cold_start_timeout' => 30_000, // 30 seconds
'health_check_interval' => 100, // 100ms
// Security
'skip_validation' => false, // Enable SSRF protection
];
$server = new HTTPServer($resolver, '0.0.0.0', 80, 16, $config);composer testIntegration tests (Docker Compose):
composer test:integrationCoverage (requires Xdebug or PCOV):
vendor/bin/phpunit --coverage-textThe protocol-proxy uses the Resolver Pattern for platform-agnostic backend resolution, combined with protocol-specific adapters for optimized handling.
┌─────────────────────────────────────────────────────────────────┐
│ Protocol Proxy │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ HTTP │ │ TCP │ │ SMTP │ │
│ │ Server │ │ Server │ │ Server │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
│ └─────────────────┴──────────────────┘ │
│ │ │
│ ┌─────────────┼─────────────┐ │
│ │ │ │ │
│ ┌────▼────┐ ┌────▼────┐ ┌────▼────┐ │
│ │ HTTP │ │ TCP │ │ SMTP │ │
│ │ Adapter │ │ Adapter │ │ Adapter │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │
│ └─────────────┴─────────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ Resolver │ │
│ │ (Interface) │ │
│ └────────┬────────┘ │
│ │ │
│ ┌───────────────┼───────────────┐ │
│ │ │ │ │
│ ┌────▼────┐ ┌────▼────┐ ┌────▼────┐ │
│ │ Routing │ │Lifecycle│ │ Stats │ │
│ │ Cache │ │ Hooks │ │ & Logs │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
The Resolver interface is the core abstraction point:
interface Resolver
{
// Map resource ID to backend endpoint
public function resolve(string $resourceId): Result;
// Lifecycle hooks
public function onConnect(string $resourceId, array $metadata = []): void;
public function onDisconnect(string $resourceId, array $metadata = []): void;
// Activity tracking for cold-start detection
public function trackActivity(string $resourceId, array $metadata = []): void;
// Cache management
public function invalidateCache(string $resourceId): void;
// Statistics
public function getStats(): array;
}The Result class contains the resolved backend endpoint:
new Result(
endpoint: 'host:port', // Required: backend endpoint
metadata: ['key' => 'val'], // Optional: additional data
timeout: 30 // Optional: connection timeout override
);Use Resolver\Exception with appropriate error codes:
throw new Exception('Not found', Exception::NOT_FOUND); // 404
throw new Exception('Unavailable', Exception::UNAVAILABLE); // 503
throw new Exception('Timeout', Exception::TIMEOUT); // 504
throw new Exception('Forbidden', Exception::FORBIDDEN); // 403
throw new Exception('Error', Exception::INTERNAL); // 500- HTTP - Routes requests based on
Hostheader - TCP - Routes connections based on database name from PostgreSQL/MySQL protocol
- SMTP - Routes connections based on domain from EHLO/HELO command
BSD-3-Clause