Skip to content

Commit 722c4c7

Browse files
committed
Add coroutine server + benchmarks
1 parent a5005ac commit 722c4c7

16 files changed

Lines changed: 2165 additions & 318 deletions

HOOKS.md

Lines changed: 130 additions & 175 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,68 @@
1-
# Hook System
1+
# Action System
22

3-
The protocol-proxy provides a flexible hook system that allows applications to inject custom business logic into the routing lifecycle.
3+
The protocol-proxy uses Utopia Platform actions to inject custom business logic into the routing lifecycle.
44

5-
**Key Design**: The proxy doesn't enforce how backends are resolved. Applications provide their own resolution logic via the `resolve` hook.
5+
**Key Design**: The proxy doesn't enforce how backends are resolved. Applications provide their own resolution logic via a `resolve` action.
66

7-
## Available Hooks
7+
## Action Registration
8+
9+
Each adapter initializes a protocol-specific service by default. Use it directly or replace it with your own.
10+
11+
```php
12+
use Utopia\Platform\Action;
13+
use Utopia\Proxy\Adapter\HTTP\Swoole as HTTPAdapter;
14+
use Utopia\Proxy\Service\HTTP as HTTPService;
15+
16+
$adapter = new HTTPAdapter();
17+
$service = $adapter->getService() ?? new HTTPService();
18+
19+
// Required: resolve backend endpoint
20+
$service->addAction('resolve', (new class extends Action {})
21+
->callback(function (string $hostname): string {
22+
return "runtime-{$hostname}.runtimes.svc.cluster.local:8080";
23+
}));
24+
25+
// Optional: beforeRoute actions (TYPE_INIT)
26+
$service->addAction('validateHost', (new class extends Action {})
27+
->setType(Action::TYPE_INIT)
28+
->callback(function (string $hostname) {
29+
if (!preg_match('/^[a-z0-9-]+\.myapp\.com$/', $hostname)) {
30+
throw new \Exception("Invalid hostname: {$hostname}");
31+
}
32+
}));
33+
34+
// Optional: afterRoute actions (TYPE_SHUTDOWN)
35+
$service->addAction('logRoute', (new class extends Action {})
36+
->setType(Action::TYPE_SHUTDOWN)
37+
->callback(function (string $hostname, string $endpoint, $result) {
38+
error_log("Routed {$hostname} -> {$endpoint}");
39+
}));
40+
41+
// Optional: onRoutingError actions (TYPE_ERROR)
42+
$service->addAction('logError', (new class extends Action {})
43+
->setType(Action::TYPE_ERROR)
44+
->callback(function (string $hostname, \Exception $e) {
45+
error_log("Routing error for {$hostname}: {$e->getMessage()}");
46+
}));
47+
48+
$adapter->setService($service);
49+
```
50+
51+
Actions execute in the order they were added to the service.
52+
53+
## Protocol Services
54+
55+
Use the protocol-specific service classes to keep configuration aligned with each adapter:
56+
57+
- `Utopia\Proxy\Service\HTTP`
58+
- `Utopia\Proxy\Service\TCP`
59+
- `Utopia\Proxy\Service\SMTP`
60+
61+
## Action Types and Parameters
862

963
### 1. `resolve` (Required)
1064

11-
Called to **resolve the backend endpoint** for a resource identifier.
65+
Action key: `resolve` (type is `Action::TYPE_DEFAULT` by default)
1266

1367
**Parameters:**
1468
- `string $resourceId` - The identifier to resolve (hostname, domain, etc.)
@@ -24,41 +78,9 @@ Called to **resolve the backend endpoint** for a resource identifier.
2478
- Kubernetes service resolution
2579
- DNS resolution
2680

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-
```
81+
### 2. `beforeRoute` (TYPE_INIT)
5682

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.
83+
Run actions with `Action::TYPE_INIT` **before** routing.
6284

6385
**Parameters:**
6486
- `string $resourceId` - The identifier being routed (hostname, domain, etc.)
@@ -70,24 +92,9 @@ Called **before** any routing logic executes.
7092
- Custom caching lookups
7193
- Request transformation
7294

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`
95+
### 3. `afterRoute` (TYPE_SHUTDOWN)
8996

90-
Called **after** successful routing.
97+
Run actions with `Action::TYPE_SHUTDOWN` **after** successful routing.
9198

9299
**Parameters:**
93100
- `string $resourceId` - The identifier that was routed
@@ -101,28 +108,9 @@ Called **after** successful routing.
101108
- Cache warming
102109
- Audit trails
103110

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-
```
111+
### 4. `onRoutingError` (TYPE_ERROR)
122112

123-
### 3. `onRoutingError`
124-
125-
Called when routing **fails** with an exception.
113+
Run actions with `Action::TYPE_ERROR` when routing fails.
126114

127115
**Parameters:**
128116
- `string $resourceId` - The identifier that failed to route
@@ -135,116 +123,83 @@ Called when routing **fails** with an exception.
135123
- Circuit breaker logic
136124
- Alerting
137125

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-
180126
## Integration with Appwrite Edge
181127

182-
The protocol-proxy can replace the current edge HTTP proxy by using hooks to inject edge-specific logic:
128+
The protocol-proxy can replace the current edge HTTP proxy by using actions to inject edge-specific logic:
183129

184130
```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-
});
131+
use Utopia\Platform\Action;
132+
use Utopia\Proxy\Adapter\HTTP\Swoole as HTTPAdapter;
133+
use Utopia\Proxy\Service\HTTP as HTTPService;
134+
135+
$adapter = new HTTPAdapter();
136+
$service = $adapter->getService() ?? new HTTPService();
137+
138+
// Resolve backend using K8s runtime registry (REQUIRED)
139+
$service->addAction('resolve', (new class extends Action {})
140+
->callback(function (string $hostname) use ($runtimeRegistry): string {
141+
$runtime = $runtimeRegistry->get($hostname);
142+
if (!$runtime) {
143+
throw new \Exception("Runtime not found: {$hostname}");
144+
}
145+
return "{$runtime['projectId']}-{$runtime['deploymentId']}.runtimes.svc.cluster.local:8080";
146+
}));
147+
148+
// Rule resolution and caching
149+
$service->addAction('resolveRule', (new class extends Action {})
150+
->setType(Action::TYPE_INIT)
151+
->callback(function (string $hostname) use ($ruleCache, $sdkForManager) {
152+
$rule = $ruleCache->load($hostname);
153+
if (!$rule) {
154+
$rule = $sdkForManager->getRule($hostname);
155+
$ruleCache->save($hostname, $rule);
156+
}
157+
Context::set('rule', $rule);
158+
}));
159+
160+
// Telemetry and metrics
161+
$service->addAction('telemetry', (new class extends Action {})
162+
->setType(Action::TYPE_SHUTDOWN)
163+
->callback(function (string $hostname, string $endpoint, $result) use ($telemetry) {
164+
$telemetry->record([
165+
'hostname' => $hostname,
166+
'endpoint' => $endpoint,
167+
'cached' => $result->metadata['cached'],
168+
'latency_ms' => $result->metadata['latency_ms'],
169+
]);
170+
}));
171+
172+
// Error logging
173+
$service->addAction('routeError', (new class extends Action {})
174+
->setType(Action::TYPE_ERROR)
175+
->callback(function (string $hostname, \Exception $e) use ($logger) {
176+
$logger->addLog([
177+
'type' => 'error',
178+
'hostname' => $hostname,
179+
'message' => $e->getMessage(),
180+
'trace' => $e->getTraceAsString(),
181+
]);
182+
}));
183+
184+
$adapter->setService($service);
230185
```
231186

232187
## Performance Considerations
233188

234-
- **Hooks are synchronous** - They execute inline during routing
235-
- **Keep hooks fast** - Slow hooks will impact overall proxy performance
189+
- **Actions are synchronous** - They execute inline during routing
190+
- **Keep actions fast** - Slow actions will impact overall proxy performance
236191
- **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
192+
- **Avoid heavy I/O** - Database queries and API calls in actions should be cached or batched
238193

239194
## Best Practices
240195

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
196+
1. **Fail fast** - Throw exceptions early in init actions to avoid unnecessary work
197+
2. **Keep it simple** - Each action should do one thing well
198+
3. **Handle errors** - Wrap action logic in try/catch to prevent cascading failures
199+
4. **Document actions** - Clearly document what each action does and why
200+
5. **Test actions** - Write unit tests for action callbacks
201+
6. **Monitor performance** - Track action execution time to identify bottlenecks
247202

248203
## Example: Complete Edge Integration
249204

250-
See `examples/http-edge-integration.php` for a complete example of how Appwrite Edge can integrate with the protocol-proxy using hooks.
205+
See `examples/http-edge-integration.php` for a complete example of how Appwrite Edge can integrate with the protocol-proxy using actions.

0 commit comments

Comments
 (0)