Skip to content

Commit 92a5b9f

Browse files
committed
Add adapter base
1 parent 2b9a550 commit 92a5b9f

30 files changed

Lines changed: 1303 additions & 630 deletions

.dockerignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.git
2+
.gitignore
3+
.idea
4+
vendor
5+
composer.lock
6+
*.md
7+
.dockerignore
8+
Dockerfile
9+
docker-compose.yml

.env.example

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Database Configuration
2+
DB_HOST=mariadb
3+
DB_PORT=3306
4+
DB_USER=appwrite
5+
DB_PASS=password
6+
DB_NAME=appwrite
7+
8+
# Redis Configuration
9+
REDIS_HOST=redis
10+
REDIS_PORT=6379
11+
12+
# Compute API Configuration
13+
COMPUTE_API_URL=http://appwrite-api/v1/compute
14+
COMPUTE_API_KEY=
15+
16+
# MySQL Root Password (for docker-compose)
17+
MYSQL_ROOT_PASSWORD=rootpassword

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,9 @@
77
.DS_Store
88
*.log
99
/coverage/
10+
11+
# Environment files
12+
.env
13+
14+
# Docker volumes
15+
/docker-volumes/

Dockerfile

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
FROM php:8.4-cli-alpine
2+
3+
RUN apk add --no-cache \
4+
autoconf \
5+
g++ \
6+
make \
7+
linux-headers \
8+
libstdc++ \
9+
libzip-dev \
10+
openssl-dev
11+
12+
RUN docker-php-ext-install \
13+
pcntl \
14+
sockets \
15+
zip
16+
17+
RUN pecl install swoole-6.0.1 && \
18+
docker-php-ext-enable swoole
19+
20+
RUN pecl install redis && \
21+
docker-php-ext-enable redis
22+
23+
WORKDIR /app
24+
25+
COPY composer.json composer.lock ./
26+
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
27+
RUN composer install --no-dev --optimize-autoloader
28+
29+
COPY . .
30+
31+
EXPOSE 8080 8081 8025
32+
33+
CMD ["php", "proxies/http.php"]

HOOKS.md

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
# Hook System
2+
3+
The protocol-proxy provides a flexible hook system that allows applications to inject custom business logic into the routing lifecycle.
4+
5+
**Key Design**: The proxy doesn't enforce how backends are resolved. Applications provide their own resolution logic via the `resolve` hook.
6+
7+
## Available Hooks
8+
9+
### 1. `resolve` (Required)
10+
11+
Called to **resolve the backend endpoint** for a resource identifier.
12+
13+
**Parameters:**
14+
- `string $resourceId` - The identifier to resolve (hostname, domain, etc.)
15+
16+
**Returns:**
17+
- `string` - Backend endpoint (e.g., `10.0.1.5:8080` or `backend.service:80`)
18+
19+
**Use Cases:**
20+
- Database lookup
21+
- Config file mapping
22+
- Service discovery (Consul, etcd)
23+
- External API calls
24+
- Kubernetes service resolution
25+
- DNS resolution
26+
27+
**Example:**
28+
```php
29+
// Option 1: Static configuration
30+
$adapter->hook('resolve', function (string $hostname) {
31+
$mapping = [
32+
'func-123.app.network' => '10.0.1.5:8080',
33+
'func-456.app.network' => '10.0.1.6:8080',
34+
];
35+
return $mapping[$hostname] ?? throw new \Exception("Not found");
36+
});
37+
38+
// Option 2: Database lookup (like Appwrite Edge)
39+
$adapter->hook('resolve', function (string $hostname) use ($db) {
40+
$doc = $db->findOne('functions', [
41+
Query::equal('hostname', [$hostname])
42+
]);
43+
return $doc->getAttribute('endpoint');
44+
});
45+
46+
// Option 3: Service discovery
47+
$adapter->hook('resolve', function (string $hostname) use ($consul) {
48+
return $consul->resolveService($hostname);
49+
});
50+
51+
// Option 4: Kubernetes service
52+
$adapter->hook('resolve', function (string $hostname) {
53+
return "function-{$hostname}.default.svc.cluster.local:8080";
54+
});
55+
```
56+
57+
**Important:** Only one `resolve` hook can be registered. If you try to register multiple, an exception will be thrown.
58+
59+
### 2. `beforeRoute`
60+
61+
Called **before** any routing logic executes.
62+
63+
**Parameters:**
64+
- `string $resourceId` - The identifier being routed (hostname, domain, etc.)
65+
66+
**Use Cases:**
67+
- Validate request format
68+
- Check authentication/authorization
69+
- Rate limiting
70+
- Custom caching lookups
71+
- Request transformation
72+
73+
**Example:**
74+
```php
75+
$adapter->hook('beforeRoute', function (string $hostname) {
76+
// Validate hostname format
77+
if (!preg_match('/^[a-z0-9-]+\.myapp\.com$/', $hostname)) {
78+
throw new \Exception("Invalid hostname: {$hostname}");
79+
}
80+
81+
// Check rate limits
82+
if (isRateLimited($hostname)) {
83+
throw new \Exception("Rate limit exceeded");
84+
}
85+
});
86+
```
87+
88+
### 2. `afterRoute`
89+
90+
Called **after** successful routing.
91+
92+
**Parameters:**
93+
- `string $resourceId` - The identifier that was routed
94+
- `string $endpoint` - The backend endpoint that was resolved
95+
- `ConnectionResult $result` - The routing result object with metadata
96+
97+
**Use Cases:**
98+
- Logging and telemetry
99+
- Metrics collection
100+
- Response header manipulation
101+
- Cache warming
102+
- Audit trails
103+
104+
**Example:**
105+
```php
106+
$adapter->hook('afterRoute', function (string $hostname, string $endpoint, $result) {
107+
// Log to telemetry
108+
$telemetry->record([
109+
'hostname' => $hostname,
110+
'endpoint' => $endpoint,
111+
'cached' => $result->metadata['cached'],
112+
'latency_ms' => $result->metadata['latency_ms'],
113+
]);
114+
115+
// Update metrics
116+
$metrics->increment('proxy.routes.success');
117+
if ($result->metadata['cached']) {
118+
$metrics->increment('proxy.cache.hits');
119+
}
120+
});
121+
```
122+
123+
### 3. `onRoutingError`
124+
125+
Called when routing **fails** with an exception.
126+
127+
**Parameters:**
128+
- `string $resourceId` - The identifier that failed to route
129+
- `\Exception $e` - The exception that was thrown
130+
131+
**Use Cases:**
132+
- Error logging (Sentry, etc.)
133+
- Custom error responses
134+
- Fallback routing
135+
- Circuit breaker logic
136+
- Alerting
137+
138+
**Example:**
139+
```php
140+
$adapter->hook('onRoutingError', function (string $hostname, \Exception $e) {
141+
// Log to Sentry
142+
Sentry\captureException($e, [
143+
'tags' => ['hostname' => $hostname],
144+
'level' => 'error',
145+
]);
146+
147+
// Try fallback region
148+
if ($e->getMessage() === 'Function not found') {
149+
tryFallbackRegion($hostname);
150+
}
151+
152+
// Update error metrics
153+
$metrics->increment('proxy.routes.errors');
154+
});
155+
```
156+
157+
## Registering Multiple Hooks
158+
159+
You can register multiple callbacks for the same hook:
160+
161+
```php
162+
// Hook 1: Validation
163+
$adapter->hook('beforeRoute', function ($hostname) {
164+
validateHostname($hostname);
165+
});
166+
167+
// Hook 2: Rate limiting
168+
$adapter->hook('beforeRoute', function ($hostname) {
169+
checkRateLimit($hostname);
170+
});
171+
172+
// Hook 3: Authentication
173+
$adapter->hook('beforeRoute', function ($hostname) {
174+
validateJWT();
175+
});
176+
```
177+
178+
All registered hooks will execute in the order they were registered.
179+
180+
## Integration with Appwrite Edge
181+
182+
The protocol-proxy can replace the current edge HTTP proxy by using hooks to inject edge-specific logic:
183+
184+
```php
185+
use Utopia\Proxy\Adapter\HTTP;
186+
187+
$adapter = new HTTP($cache, $dbPool);
188+
189+
// Hook 1: Resolve backend using K8s runtime registry (REQUIRED)
190+
$adapter->hook('resolve', function (string $hostname) use ($runtimeRegistry) {
191+
// Edge resolves hostnames to K8s service endpoints
192+
$runtime = $runtimeRegistry->get($hostname);
193+
if (!$runtime) {
194+
throw new \Exception("Runtime not found: {$hostname}");
195+
}
196+
197+
// Return K8s service endpoint
198+
return "{$runtime['projectId']}-{$runtime['deploymentId']}.runtimes.svc.cluster.local:8080";
199+
});
200+
201+
// Hook 2: Rule resolution and caching
202+
$adapter->hook('beforeRoute', function (string $hostname) use ($ruleCache, $sdkForManager) {
203+
$rule = $ruleCache->load($hostname);
204+
if (!$rule) {
205+
$rule = $sdkForManager->getRule($hostname);
206+
$ruleCache->save($hostname, $rule);
207+
}
208+
Context::set('rule', $rule);
209+
});
210+
211+
// Hook 3: Telemetry and metrics
212+
$adapter->hook('afterRoute', function (string $hostname, string $endpoint, $result) use ($telemetry) {
213+
$telemetry->record([
214+
'hostname' => $hostname,
215+
'endpoint' => $endpoint,
216+
'cached' => $result->metadata['cached'],
217+
'latency_ms' => $result->metadata['latency_ms'],
218+
]);
219+
});
220+
221+
// Hook 4: Error logging
222+
$adapter->hook('onRoutingError', function (string $hostname, \Exception $e) use ($logger) {
223+
$logger->addLog([
224+
'type' => 'error',
225+
'hostname' => $hostname,
226+
'message' => $e->getMessage(),
227+
'trace' => $e->getTraceAsString(),
228+
]);
229+
});
230+
```
231+
232+
## Performance Considerations
233+
234+
- **Hooks are synchronous** - They execute inline during routing
235+
- **Keep hooks fast** - Slow hooks will impact overall proxy performance
236+
- **Use async operations** - For non-critical work (logging, metrics), consider using Swoole coroutines or queues
237+
- **Avoid heavy I/O** - Database queries and API calls in hooks should be cached or batched
238+
239+
## Best Practices
240+
241+
1. **Fail fast** - Throw exceptions early in `beforeRoute` to avoid unnecessary work
242+
2. **Keep it simple** - Each hook should do one thing well
243+
3. **Handle errors** - Wrap hook logic in try/catch to prevent cascading failures
244+
4. **Document hooks** - Clearly document what each hook does and why
245+
5. **Test hooks** - Write unit tests for hook callbacks
246+
6. **Monitor performance** - Track hook execution time to identify bottlenecks
247+
248+
## Example: Complete Edge Integration
249+
250+
See `examples/http-edge-integration.php` for a complete example of how Appwrite Edge can integrate with the protocol-proxy using hooks.

PERFORMANCE.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,17 +148,17 @@ ab -n 100000 -c 1000 http://localhost:8080/
148148
wrk -t12 -c1000 -d30s http://localhost:8080/
149149

150150
# Custom benchmark
151-
php benchmarks/http-benchmark.php
151+
php benchmarks/http.php
152152
```
153153

154154
### TCP Benchmark
155155

156156
```bash
157157
# PostgreSQL connections
158-
php benchmarks/tcp-benchmark.php
158+
php benchmarks/tcp.php
159159

160160
# MySQL connections
161-
php benchmarks/tcp-benchmark.php --port=3306
161+
php benchmarks/tcp.php --port=3306
162162
```
163163

164164
### Load Testing

0 commit comments

Comments
 (0)