Compare commits

...
Author SHA1 Message Date
eldadfux 9d2036024a Refactor migration error handling to provide clearer connection error messages. Updated exception messages for migration sources to guide users on potential credential and network issues. 2026-03-05 08:24:32 +01:00
premtsd-codeandGitHub cf9637288c Merge pull request #11443 from appwrite/bump-migration-1.6.3
chore: bump utopia-php/migration to 1.6.3
2026-03-05 06:59:08 +00:00
Prem Palanisamy d1ffa5daf3 chore: bump utopia-php/migration to 1.6.3 2026-03-04 22:42:55 +00:00
Matej BačoandGitHub 4574385b31 Merge pull request #11447 from appwrite/revert-10862-feat-1.8.x-new-schema
Revert "Features with schema changes"
2026-03-04 14:31:22 +01:00
Matej BačoandGitHub f8b31e7db7 Revert "Features with schema changes" 2026-03-04 14:31:05 +01:00
Matej BačoandGitHub cf16e3e2a3 Merge pull request #10862 from appwrite/feat-1.8.x-new-schema
Features with schema changes
2026-03-04 14:29:48 +01:00
Matej Bačo ff0f132984 Fix build 2026-03-04 14:20:48 +01:00
Matej Bačo 63756ddf1d Merge branch 'main' into feat-1.8.x-new-schema 2026-03-04 14:13:06 +01:00
Matej Bačo 1f5fd919c9 Merge branch '1.8.x' into feat-1.8.x-new-schema 2026-03-04 14:13:01 +01:00
Luke B. SilverandGitHub 718d3377b0 Merge pull request #11445 from appwrite/fix/publisher
fix: stats publisher
2026-03-04 11:43:10 +00:00
loks0n c81029d8aa fix: stats publisher 2026-03-04 11:17:50 +00:00
Shmuel FogelandGitHub 0291f8f943 Merge pull request #11436 from appwrite/executions-collection-index
Executions collection index functionInternalId
2026-03-04 11:14:24 +02:00
fogelito da30142b4d Merge branch '1.8.x' of https://github.com/appwrite/appwrite into executions-collection-index 2026-03-04 10:29:20 +02:00
fogelito f5047afec9 Remove index 2026-03-04 10:28:56 +02:00
Jake BarnbyandGitHub fdd3a58d81 Merge pull request #11440 from appwrite/perf/realtime-tests 2026-03-04 14:19:32 +13:00
loks0nandClaude Sonnet 4.5 cf7710b580 perf: reduce WebSocket timeout in realtime tests from 45s to 2s
Realtime E2E tests were taking 24+ minutes due to intentional timeout waits. Many tests verify event filtering by expecting a TimeoutException, and each was waiting 45 seconds.

Changes:
- Reduce default WebSocket timeout from 45s to 2s in RealtimeBase
- Add optional timeout parameter to getWebsocket() methods
- Use longer timeouts (5-10s) for tests that legitimately wait for slow operations (function executions, test channel events)
- Use named parameters for improved readability

Performance impact:
- Before: 24:07 minutes (1,447 seconds)
- After: 1:09 minutes (69 seconds)
- Speedup: ~21x faster

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-03 22:47:28 +00:00
Luke B. SilverandGitHub ccaaffed48 Merge pull request #11316 from appwrite/refactor-log-emitter
refactor: log emitter
2026-03-03 22:04:44 +00:00
loks0nandClaude Sonnet 4.5 c073743989 fix: lint - order imports in general controller
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-03 20:11:34 +00:00
loks0nandClaude Sonnet 4.5 a0854e0591 refactor: make Bus dispatch synchronous
Remove async coroutine wrapper from event dispatch to simplify execution model and improve trace hierarchy. Listeners now execute synchronously in the caller's context, with dependency resolution inlined.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-03 20:06:06 +00:00
loks0nandClaude Opus 4.6 20f248a6ae refactor: consolidate SiteRequestCompleted into RequestCompleted with optional deployment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 19:36:48 +00:00
loks0nandClaude Opus 4.6 c171e0c3a2 refactor: add bus.event span attribute to listener invocations
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 19:36:48 +00:00
loks0nandClaude Opus 4.6 2081c4c42c refactor: replace bandwidth queueForStatsUsage with Bus events
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 19:36:48 +00:00
loks0nandClaude Opus 4.6 b45ff6b646 refactor: replace queueForExecutions with Bus event bus
Introduce a generic event bus (Utopia\Bus) with typed events, listener
base class, Span instrumentation, and coroutine dispatch. Replace all
direct queueForExecutions and inline execution usage calls with
ExecutionCompleted event and dedicated listeners (Log, Usage).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 19:36:48 +00:00
fogelito 5999db295d Message 2026-03-03 15:44:20 +02:00
fogelito b6b44efdab Revert length 2026-03-03 13:58:00 +02:00
fogelito f4125b8859 Remove length 2026-03-03 13:35:23 +02:00
fogelito 1ccd61ece5 functionInternalId 2026-03-03 12:15:21 +02:00
Eldad A. FuxandGitHub a5fa09b4ce Merge pull request #11185 from Shivangisharma4/doc-fix-typos-contributing
doc: Fix typos in CONTRIBUTING.md
2026-02-27 20:42:57 +01:00
10e2edac90 Update CONTRIBUTING.md
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-24 14:03:11 +05:30
Shivangisharma4 340081a67d doc: Fix typos in CONTRIBUTING.md 2026-01-24 13:58:20 +05:30
Matej Bačo 9c3b78e18f PR review fixes 2026-01-05 13:55:12 +01:00
Matej Bačo 4365e1fee5 FIx default team labels 2026-01-05 11:33:45 +01:00
Matej Bačo d19e7c169e Merge branch '1.8.x' into feat-1.8.x-new-schema 2026-01-05 10:32:18 +01:00
Matej Bačo 226bb6b830 AI core review changes 2026-01-05 10:32:03 +01:00
Matej Bačo da5b68490c Add team labels schema 2026-01-05 10:31:58 +01:00
Matej Bačo 4dc5df3923 Fix test lockfile 2026-01-05 09:27:38 +01:00
Matej Bačo bc7bdf040f Fix vuln in test file 2026-01-02 18:42:33 +01:00
Matej Bačo f8968f69f0 Upgrade test for osv vulns 2026-01-02 18:25:30 +01:00
Matej BačoandGitHub 030e89ac7d Merge branch '1.8.x' into feat-1.8.x-new-schema 2026-01-02 18:08:37 +01:00
Matej BačoandGitHub 37330522a4 Merge pull request #11066 from appwrite/feat-specs-dual-reading
Feat: Add dual read for specs
2026-01-02 18:08:14 +01:00
Matej Bačo 807bb0039a Fix formatting 2026-01-02 16:34:20 +01:00
Matej Bačo c4c260f3cf Add dual read for specs 2026-01-02 16:34:05 +01:00
Matej Bačo 87d6c73b7b Fix failing tests 2026-01-02 12:58:53 +01:00
Matej Bačo a71875eedf Merge branch '1.8.x' into feat-1.8.x-new-schema 2026-01-02 12:47:44 +01:00
Matej Bačo 51e03353dd Merge branch '1.8.x' into feat-1.8.x-new-schema 2025-12-31 15:13:35 +01:00
Matej BačoandGitHub 175b0d92f2 Merge pull request #10959 from appwrite/feat-auto-delete-depoyments
Feat: Auto-delete deployments
2025-12-16 13:30:44 +01:00
Matej Bačo c88a77a31c AI suggestion fixes 2025-12-16 12:53:06 +01:00
Matej Bačo a3a5e05b5c bug fix 2025-12-15 20:13:45 +01:00
Matej Bačo bd2db5e249 AI review fixes 2025-12-15 19:45:57 +01:00
Matej Bačo 6cbc79026f Finish site tests for auto deletion 2025-12-15 18:55:34 +01:00
Matej Bačo 0ec755911e Finish maintenance test 2025-12-15 18:44:27 +01:00
Matej Bačo be626ad0fc Add deployment retention to sites and functions 2025-12-15 17:33:10 +01:00
Matej BačoandGitHub 914723c53a Merge branch '1.8.x' into feat-1.8.x-new-schema 2025-11-24 19:10:44 +01:00
Matej BačoandGitHub db965f8144 Merge pull request #10842 from appwrite/feat-custom-start-commands
Feat: Custom start commands
2025-11-24 16:01:01 +01:00
Matej Bačo d99a52741d Merge branch 'feat-1.8.x-new-schema' into feat-custom-start-commands 2025-11-24 15:41:52 +01:00
Matej BačoandGitHub 96f79fac0e Merge pull request #10849 from appwrite/feat-separate-specifications
Separate specifications
2025-11-24 15:39:02 +01:00
Matej Bačo 856c1d685e Remove functions refference to start command 2025-11-24 15:19:41 +01:00
Matej Bačo 5817aea2ee Remove todo 2025-11-24 14:30:45 +01:00
Matej Bačo 4bc624ea5c Fix response filters order 2025-11-20 18:05:32 +01:00
Matej Bačo 7444f5cf60 leftover fix 2025-11-20 17:50:15 +01:00
Matej Bačo cbb96f8b82 AI review fixes 2025-11-20 17:40:56 +01:00
Matej Bačo 63f6840d1b Finish implementation, update tests 2025-11-20 17:39:19 +01:00
Matej Bačo 62173b8f61 WIP: Separate specifications 2025-11-20 13:56:54 +01:00
Matej Bačo 7f3c0c9c03 AI review fixes 2025-11-19 16:38:47 +01:00
Matej Bačo f907f76eb5 Custom start command implementation 2025-11-19 15:27:54 +01:00
24 changed files with 471 additions and 255 deletions
+4
View File
@@ -318,6 +318,10 @@ $setResource('logError', function (Registry $register) {
$setResource('executor', fn () => new Executor(), []);
$setResource('bus', function (Registry $register) use ($cli) {
return $register->get('bus')->setResolver(fn (string $name) => $cli->getResource($name));
}, ['register']);
$setResource('telemetry', fn () => new NoTelemetry(), []);
$cli
-7
View File
@@ -2190,13 +2190,6 @@ return [
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_function_internal_id'),
'type' => Database::INDEX_KEY,
'attributes' => ['resourceInternalId'],
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_resourceType'),
'type' => Database::INDEX_KEY,
+20 -49
View File
@@ -717,22 +717,14 @@ Http::get('/v1/migrations/appwrite/report')
->inject('project')
->inject('user')
->action(function (array $resources, string $endpoint, string $projectID, string $key, Response $response) {
$appwrite = new Appwrite($projectID, $endpoint, $key);
try {
$appwrite = new Appwrite($projectID, $endpoint, $key);
$report = $appwrite->report($resources);
} catch (\Throwable $e) {
switch ($e->getCode()) {
case 401:
throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, 'Source Error: ' . $e->getMessage());
case 429:
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Source Error: Rate Limit Exceeded, Is your Cloud Provider blocking Appwrite\'s IP?');
case 500:
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
}
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
throw new Exception(
Exception::MIGRATION_PROVIDER_ERROR,
'Unable to connect to the migration source. Please verify your credentials and ensure the source is reachable from this server. Check for network restrictions such as firewalls, IP allowlists, or outbound connectivity limits.'
);
}
$response
@@ -771,21 +763,14 @@ Http::get('/v1/migrations/firebase/report')
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Invalid Service Account JSON');
}
$firebase = new Firebase($serviceAccount);
try {
$firebase = new Firebase($serviceAccount);
$report = $firebase->report($resources);
} catch (\Throwable $e) {
switch ($e->getCode()) {
case 401:
throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, 'Source Error: ' . $e->getMessage());
case 429:
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Source Error: Rate Limit Exceeded, Is your Cloud Provider blocking Appwrite\'s IP?');
case 500:
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
}
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
throw new Exception(
Exception::MIGRATION_PROVIDER_ERROR,
'Unable to connect to the migration source. Please verify your credentials and ensure the source is reachable from this server. Check for network restrictions such as firewalls, IP allowlists, or outbound connectivity limits.'
);
}
$response
@@ -820,21 +805,14 @@ Http::get('/v1/migrations/supabase/report')
->inject('response')
->inject('dbForProject')
->action(function (array $resources, string $endpoint, string $apiKey, string $databaseHost, string $username, string $password, int $port, Response $response) {
$supabase = new Supabase($endpoint, $apiKey, $databaseHost, 'postgres', $username, $password, $port);
try {
$supabase = new Supabase($endpoint, $apiKey, $databaseHost, 'postgres', $username, $password, $port);
$report = $supabase->report($resources);
} catch (\Throwable $e) {
switch ($e->getCode()) {
case 401:
throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, 'Source Error: ' . $e->getMessage());
case 429:
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Source Error: Rate Limit Exceeded, Is your Cloud Provider blocking Appwrite\'s IP?');
case 500:
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
}
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
throw new Exception(
Exception::MIGRATION_PROVIDER_ERROR,
'Unable to connect to the migration source. Please verify your credentials and ensure the source is reachable from this server. Check for network restrictions such as firewalls, IP allowlists, or outbound connectivity limits.'
);
}
$response
@@ -869,21 +847,14 @@ Http::get('/v1/migrations/nhost/report')
->param('port', 5432, new Integer(true), 'Source\'s Database Port.', true)
->inject('response')
->action(function (array $resources, string $subdomain, string $region, string $adminSecret, string $database, string $username, string $password, int $port, Response $response) {
$nhost = new NHost($subdomain, $region, $adminSecret, $database, $username, $password, $port);
try {
$nhost = new NHost($subdomain, $region, $adminSecret, $database, $username, $password, $port);
$report = $nhost->report($resources);
} catch (\Throwable $e) {
switch ($e->getCode()) {
case 401:
throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, 'Source Error: ' . $e->getMessage());
case 429:
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Source Error: Rate Limit Exceeded, Is your Cloud Provider blocking Appwrite\'s IP?');
case 500:
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
}
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
throw new Exception(
Exception::MIGRATION_PROVIDER_ERROR,
'Unable to connect to the migration source. Please verify your credentials and ensure the source is reachable from this server. Check for network restrictions such as firewalls, IP allowlists, or outbound connectivity limits.'
);
}
$response
+40 -109
View File
@@ -5,11 +5,11 @@ require_once __DIR__ . '/../init.php';
use Ahc\Jwt\JWT;
use Ahc\Jwt\JWTException;
use Appwrite\Auth\Key;
use Appwrite\Bus\Events\ExecutionCompleted;
use Appwrite\Bus\Events\RequestCompleted;
use Appwrite\Event\Certificate;
use Appwrite\Event\Delete as DeleteEvent;
use Appwrite\Event\Event;
use Appwrite\Event\Execution;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Network\Cors;
use Appwrite\Platform\Appwrite;
@@ -35,6 +35,7 @@ use Executor\Executor;
use MaxMind\Db\Reader;
use Swoole\Http\Request as SwooleRequest;
use Swoole\Table;
use Utopia\Bus\Bus;
use Utopia\Config\Config;
use Utopia\Console;
use Utopia\Database\Database;
@@ -62,7 +63,7 @@ Config::setParam('domainVerification', false);
Config::setParam('cookieDomain', 'localhost');
Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE);
function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, StatsUsage $queueForStatsUsage, Execution $queueForExecutions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Authorization $authorization, ?Key $apiKey, DeleteEvent $queueForDeletes, int $executionsRetentionCount)
function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Authorization $authorization, ?Key $apiKey, DeleteEvent $queueForDeletes, int $executionsRetentionCount)
{
$host = $request->getHostname() ?? '';
if (!empty($previewHostname)) {
@@ -706,10 +707,12 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
}
} finally {
if ($type === 'function' || $type === 'site') {
$queueForExecutions
->setExecution($execution)
->setProject($project)
->trigger();
$bus->dispatch(new ExecutionCompleted(
execution: $execution->getArrayCopy(),
project: $project->getArrayCopy(),
spec: $spec,
resource: $resource->getArrayCopy(),
));
}
}
@@ -754,70 +757,12 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
->setStatusCode($execution['responseStatusCode'] ?? 200)
->send($body);
$fileSize = 0;
$file = $request->getFiles('file');
if (!empty($file)) {
$fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size'];
}
if (!empty($apiKey) && !empty($apiKey->getDisabledMetrics())) {
foreach ($apiKey->getDisabledMetrics() as $key) {
$queueForStatsUsage->disableMetric($key);
}
}
$metricTypeExecutions = str_replace(['{resourceType}'], [$deployment->getAttribute('resourceType')], METRIC_RESOURCE_TYPE_EXECUTIONS);
$metricTypeIdExecutions = str_replace(['{resourceType}', '{resourceInternalId}'], [$deployment->getAttribute('resourceType'), $resource->getSequence()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS);
$metricTypeExecutionsCompute = str_replace(['{resourceType}'], [$deployment->getAttribute('resourceType')], METRIC_RESOURCE_TYPE_EXECUTIONS_COMPUTE);
$metricTypeIdExecutionsCompute = str_replace(['{resourceType}', '{resourceInternalId}'], [$deployment->getAttribute('resourceType'), $resource->getSequence()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_COMPUTE);
$metricTypeExecutionsMbSeconds = str_replace(['{resourceType}'], [$deployment->getAttribute('resourceType')], METRIC_RESOURCE_TYPE_EXECUTIONS_MB_SECONDS);
$metricTypeIdExecutionsMBSeconds = str_replace(['{resourceType}', '{resourceInternalId}'], [$deployment->getAttribute('resourceType'), $resource->getSequence()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_MB_SECONDS);
if ($deployment->getAttribute('resourceType') === 'sites') {
$queueForStatsUsage
->disableMetric(METRIC_NETWORK_REQUESTS)
->disableMetric(METRIC_NETWORK_INBOUND)
->disableMetric(METRIC_NETWORK_OUTBOUND);
if ($resource->getAttribute('adapter') !== 'ssr') {
$queueForStatsUsage
->disableMetric(METRIC_EXECUTIONS)
->disableMetric(METRIC_EXECUTIONS_COMPUTE)
->disableMetric(METRIC_EXECUTIONS_MB_SECONDS)
->disableMetric($metricTypeExecutions)
->disableMetric($metricTypeIdExecutions)
->disableMetric($metricTypeExecutionsCompute)
->disableMetric($metricTypeIdExecutionsCompute)
->disableMetric($metricTypeExecutionsMbSeconds)
->disableMetric($metricTypeIdExecutionsMBSeconds);
}
$queueForStatsUsage
->addMetric(METRIC_SITES_REQUESTS, 1)
->addMetric(METRIC_SITES_INBOUND, $request->getSize() + $fileSize)
->addMetric(METRIC_SITES_OUTBOUND, $response->getSize())
->addMetric(str_replace('{siteInternalId}', $resource->getSequence(), METRIC_SITES_ID_REQUESTS), 1)
->addMetric(str_replace('{siteInternalId}', $resource->getSequence(), METRIC_SITES_ID_INBOUND), $request->getSize() + $fileSize)
->addMetric(str_replace('{siteInternalId}', $resource->getSequence(), METRIC_SITES_ID_OUTBOUND), $response->getSize())
;
}
$compute = (int)($execution->getAttribute('duration') * 1000);
$mbSeconds = (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT));
$queueForStatsUsage
->addMetric(METRIC_NETWORK_REQUESTS, 1)
->addMetric(METRIC_NETWORK_INBOUND, $request->getSize() + $fileSize)
->addMetric(METRIC_NETWORK_OUTBOUND, $response->getSize())
->addMetric(METRIC_EXECUTIONS, 1)
->addMetric($metricTypeExecutions, 1)
->addMetric($metricTypeIdExecutions, 1)
->addMetric(METRIC_EXECUTIONS_COMPUTE, $compute) // per project
->addMetric($metricTypeExecutionsCompute, $compute) // per function
->addMetric($metricTypeIdExecutionsCompute, $compute) // per function
->addMetric(METRIC_EXECUTIONS_MB_SECONDS, $mbSeconds)
->addMetric($metricTypeExecutionsMbSeconds, $mbSeconds)
->addMetric($metricTypeIdExecutionsMBSeconds, $mbSeconds)
->setProject($project)
->trigger();
$bus->dispatch(new RequestCompleted(
project: $project->getArrayCopy(),
request: $request,
response: $response,
deployment: $deployment->getArrayCopy(),
));
/* cleanup */
if ($executionsRetentionCount > 0 && ENABLE_EXECUTIONS_LIMIT_ON_ROUTE) {
@@ -881,9 +826,8 @@ Http::init()
->inject('locale')
->inject('localeCodes')
->inject('geodb')
->inject('queueForStatsUsage')
->inject('queueForEvents')
->inject('queueForExecutions')
->inject('bus')
->inject('executor')
->inject('platform')
->inject('isResourceBlocked')
@@ -894,7 +838,7 @@ Http::init()
->inject('authorization')
->inject('queueForDeletes')
->inject('executionsRetentionCount')
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, Reader $geodb, StatsUsage $queueForStatsUsage, Event $queueForEvents, Execution $queueForExecutions, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, Reader $geodb, Event $queueForEvents, Bus $bus, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
/*
* Appwrite Router
*/
@@ -902,7 +846,7 @@ Http::init()
$platformHostnames = $platform['hostnames'] ?? [];
// Only run Router when external domain
if (!\in_array($hostname, $platformHostnames) || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForExecutions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -1178,8 +1122,7 @@ Http::options()
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('queueForEvents')
->inject('queueForStatsUsage')
->inject('queueForExecutions')
->inject('bus')
->inject('executor')
->inject('geodb')
->inject('isResourceBlocked')
@@ -1192,14 +1135,14 @@ Http::options()
->inject('authorization')
->inject('queueForDeletes')
->inject('executionsRetentionCount')
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Execution $queueForExecutions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
/*
* Appwrite Router
*/
$platformHostnames = $platform['hostnames'] ?? [];
// Only run Router when external domain
if (!in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForExecutions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -1215,12 +1158,11 @@ Http::options()
/** OPTIONS requests in utopia do not execute shutdown handlers, as a result we need to track the OPTIONS requests explicitly
* @see https://github.com/utopia-php/http/blob/0.33.16/src/App.php#L825-L855
*/
$queueForStatsUsage
->addMetric(METRIC_NETWORK_REQUESTS, 1)
->addMetric(METRIC_NETWORK_INBOUND, $request->getSize())
->addMetric(METRIC_NETWORK_OUTBOUND, $response->getSize())
->setProject($project)
->trigger();
$bus->dispatch(new RequestCompleted(
project: $project->getArrayCopy(),
request: $request,
response: $response,
));
});
Http::error()
@@ -1231,10 +1173,10 @@ Http::error()
->inject('project')
->inject('logger')
->inject('log')
->inject('queueForStatsUsage')
->inject('bus')
->inject('devKey')
->inject('authorization')
->action(function (Throwable $error, Http $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, StatsUsage $queueForStatsUsage, Document $devKey, Authorization $authorization) {
->action(function (Throwable $error, Http $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, Bus $bus, Document $devKey, Authorization $authorization) {
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
$route = $utopia->getRoute();
$class = \get_class($error);
@@ -1307,21 +1249,12 @@ Http::error()
*/
if (!$publish && $project->getId() !== 'console') {
if (!DBUser::isPrivileged($authorization->getRoles())) {
$fileSize = 0;
$file = $request->getFiles('file');
if (!empty($file)) {
$fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size'];
}
$queueForStatsUsage
->addMetric(METRIC_NETWORK_REQUESTS, 1)
->addMetric(METRIC_NETWORK_INBOUND, $request->getSize() + $fileSize)
->addMetric(METRIC_NETWORK_OUTBOUND, $response->getSize());
$bus->dispatch(new RequestCompleted(
project: $project->getArrayCopy(),
request: $request,
response: $response,
));
}
$queueForStatsUsage
->setProject($project)
->trigger();
}
if ($logger && $publish) {
@@ -1568,8 +1501,7 @@ Http::get('/robots.txt')
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('queueForEvents')
->inject('queueForStatsUsage')
->inject('queueForExecutions')
->inject('bus')
->inject('executor')
->inject('geodb')
->inject('isResourceBlocked')
@@ -1579,13 +1511,13 @@ Http::get('/robots.txt')
->inject('authorization')
->inject('queueForDeletes')
->inject('executionsRetentionCount')
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Execution $queueForExecutions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
$platformHostnames = $platform['hostnames'] ?? [];
if (in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
$template = new View(__DIR__ . '/../views/general/robots.phtml');
$response->text($template->render(false));
} else {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForExecutions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -1603,8 +1535,7 @@ Http::get('/humans.txt')
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('queueForEvents')
->inject('queueForStatsUsage')
->inject('queueForExecutions')
->inject('bus')
->inject('executor')
->inject('geodb')
->inject('isResourceBlocked')
@@ -1614,13 +1545,13 @@ Http::get('/humans.txt')
->inject('authorization')
->inject('queueForDeletes')
->inject('executionsRetentionCount')
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Execution $queueForExecutions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
$platformHostnames = $platform['hostnames'] ?? [];
if (in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
$template = new View(__DIR__ . '/../views/general/humans.phtml');
$response->text($template->render(false));
} else {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForExecutions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
}
}
+9 -11
View File
@@ -2,6 +2,7 @@
use Appwrite\Auth\Key;
use Appwrite\Auth\MFA\Type\TOTP;
use Appwrite\Bus\Events\RequestCompleted;
use Appwrite\Event\Audit;
use Appwrite\Event\Build;
use Appwrite\Event\Database as EventDatabase;
@@ -21,6 +22,7 @@ use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Utopia\Abuse\Abuse;
use Utopia\Bus\Bus;
use Utopia\Cache\Adapter\Filesystem;
use Utopia\Cache\Cache;
use Utopia\Config\Config;
@@ -746,7 +748,8 @@ Http::shutdown()
->inject('authorization')
->inject('timelimit')
->inject('eventProcessor')
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, StatsUsage $queueForStatsUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor) use ($parseLabel) {
->inject('bus')
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, StatsUsage $queueForStatsUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus) use ($parseLabel) {
$responsePayload = $response->getPayload();
@@ -958,16 +961,11 @@ Http::shutdown()
if ($project->getId() !== 'console') {
if (!User::isPrivileged($authorization->getRoles())) {
$fileSize = 0;
$file = $request->getFiles('file');
if (!empty($file)) {
$fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size'];
}
$queueForStatsUsage
->addMetric(METRIC_NETWORK_REQUESTS, 1)
->addMetric(METRIC_NETWORK_INBOUND, $request->getSize() + $fileSize)
->addMetric(METRIC_NETWORK_OUTBOUND, $response->getSize());
$bus->dispatch(new RequestCompleted(
project: $project->getArrayCopy(),
request: $request,
response: $response,
));
}
$queueForStatsUsage
+4
View File
@@ -188,6 +188,10 @@ $http->on(Constant::EVENT_AFTER_RELOAD, function ($server) {
Console::success('Reload completed...');
});
Http::setResource('bus', function ($register, $utopia) {
return $register->get('bus')->setResolver(fn (string $name) => $utopia->getResource($name));
}, ['register', 'utopia']);
include __DIR__ . '/controllers/general.php';
function createDatabase(Http $app, string $resourceKey, string $dbName, array $collections, mixed $pools, ?callable $extraSetup = null): void
+8
View File
@@ -449,3 +449,11 @@ $register->set('promiseAdapter', function () {
$register->set('hooks', function () {
return new Hooks();
});
$listeners = require __DIR__ . '/../listeners.php';
$register->set('bus', function () use ($listeners) {
$bus = new \Utopia\Bus\Bus();
foreach ($listeners as $listener) {
$bus->subscribe($listener);
}
return $bus;
});
-4
View File
@@ -10,7 +10,6 @@ use Appwrite\Event\Certificate;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Execution;
use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
@@ -160,9 +159,6 @@ Http::setResource('queueForAudits', function (Publisher $publisher) {
Http::setResource('queueForFunctions', function (Publisher $publisher) {
return new Func($publisher);
}, ['publisher']);
Http::setResource('queueForExecutions', function (Publisher $publisher) {
return new Execution($publisher);
}, ['publisher']);
Http::setResource('eventProcessor', function () {
return new EventProcessor();
}, []);
+9
View File
@@ -0,0 +1,9 @@
<?php
use Appwrite\Bus\Listeners\Log;
use Appwrite\Bus\Listeners\Usage;
return [
new Log(),
new Usage(),
];
+4 -4
View File
@@ -9,7 +9,6 @@ use Appwrite\Event\Certificate;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Execution;
use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
@@ -355,9 +354,6 @@ Server::setResource('queueForFunctions', function (Publisher $publisher) {
return new Func($publisher);
}, ['publisher']);
Server::setResource('queueForExecutions', function (Publisher $publisher) {
return new Execution($publisher);
}, ['publisher']);
Server::setResource('queueForRealtime', function () {
return new Realtime();
@@ -542,6 +538,10 @@ try {
$worker = $platform->getWorker();
Server::setResource('bus', function ($register) use ($worker) {
return $register->get('bus')->setResolver(fn (string $name) => $worker->getResource($name));
}, ['register']);
$worker
->error()
->inject('error')
+2 -1
View File
@@ -19,7 +19,8 @@
"autoload": {
"psr-4": {
"Appwrite\\": "src/Appwrite",
"Executor\\": "src/Executor"
"Executor\\": "src/Executor",
"Utopia\\Bus\\": "src/Utopia/Bus"
}
},
"autoload-dev": {
Generated
+6 -6
View File
@@ -4517,16 +4517,16 @@
},
{
"name": "utopia-php/migration",
"version": "1.6.2",
"version": "1.6.3",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/migration.git",
"reference": "037bf4b3813d44f1b0990bc124e35b501ed27fca"
"reference": "c2d016944cb029fa5ff822ceee704785a06ef289"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/migration/zipball/037bf4b3813d44f1b0990bc124e35b501ed27fca",
"reference": "037bf4b3813d44f1b0990bc124e35b501ed27fca",
"url": "https://api.github.com/repos/utopia-php/migration/zipball/c2d016944cb029fa5ff822ceee704785a06ef289",
"reference": "c2d016944cb029fa5ff822ceee704785a06ef289",
"shasum": ""
},
"require": {
@@ -4566,9 +4566,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/migration/issues",
"source": "https://github.com/utopia-php/migration/tree/1.6.2"
"source": "https://github.com/utopia-php/migration/tree/1.6.3"
},
"time": "2026-02-25T12:00:11+00:00"
"time": "2026-03-04T07:08:22+00:00"
},
{
"name": "utopia-php/mongo",
+4
View File
@@ -1,7 +1,11 @@
parameters:
level: 8
paths:
- src/Utopia/Bus
- src/Appwrite/Bus
- src/Appwrite/Transformation
bootstrapFiles:
- app/init/constants.php
scanDirectories:
- vendor/swoole/ide-helper
excludePaths:
@@ -0,0 +1,22 @@
<?php
namespace Appwrite\Bus\Events;
use Utopia\Bus\Event;
class ExecutionCompleted implements Event
{
/**
* @param array<string, mixed> $execution
* @param array<string, mixed> $project
* @param array<string, mixed> $spec
* @param array<string, mixed> $resource
*/
public function __construct(
public readonly array $execution,
public readonly array $project,
public readonly array $spec = [],
public readonly array $resource = [],
) {
}
}
@@ -0,0 +1,22 @@
<?php
namespace Appwrite\Bus\Events;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Utopia\Bus\Event;
class RequestCompleted implements Event
{
/**
* @param array<string, mixed> $project
* @param array<string, mixed> $deployment
*/
public function __construct(
public readonly array $project,
public readonly Request $request,
public readonly Response $response,
public readonly array $deployment = [],
) {
}
}
+39
View File
@@ -0,0 +1,39 @@
<?php
namespace Appwrite\Bus\Listeners;
use Appwrite\Bus\Events\ExecutionCompleted;
use Appwrite\Event\Execution;
use Utopia\Bus\Listener;
use Utopia\Database\Document;
use Utopia\Queue\Publisher;
class Log extends Listener
{
public static function getName(): string
{
return 'log';
}
public static function getEvents(): array
{
return [ExecutionCompleted::class];
}
public function __construct()
{
$this
->desc('Persists execution logs to database via queue')
->inject('publisher')
->callback($this->handle(...));
}
public function handle(ExecutionCompleted $event, Publisher $publisher): void
{
$queueForExecutions = new Execution($publisher);
$queueForExecutions
->setExecution(new Document($event->execution))
->setProject(new Document($event->project))
->trigger();
}
}
+114
View File
@@ -0,0 +1,114 @@
<?php
namespace Appwrite\Bus\Listeners;
use Appwrite\Bus\Events\ExecutionCompleted;
use Appwrite\Bus\Events\RequestCompleted;
use Appwrite\Event\StatsUsage;
use Utopia\Bus\Event;
use Utopia\Bus\Listener;
use Utopia\Database\Document;
use Utopia\Queue\Publisher;
class Usage extends Listener
{
public static function getName(): string
{
return 'usage';
}
public static function getEvents(): array
{
return [
ExecutionCompleted::class,
RequestCompleted::class,
];
}
public function __construct()
{
$this
->desc('Records usage metrics')
->inject('publisherStatsUsage')
->callback($this->handle(...));
}
public function handle(Event $event, Publisher $publisher): void
{
match (true) {
$event instanceof ExecutionCompleted => $this->handleExecutionCompleted($event, $publisher),
$event instanceof RequestCompleted => $this->handleRequestCompleted($event, $publisher),
default => null,
};
}
private function handleExecutionCompleted(ExecutionCompleted $event, Publisher $publisher): void
{
$execution = new Document($event->execution);
$resource = new Document($event->resource);
// Non-SSR sites don't record execution metrics
if ($execution->getAttribute('resourceType') === 'sites' && $resource->getAttribute('adapter') !== 'ssr') {
return;
}
$project = new Document($event->project);
$spec = $event->spec;
$resourceType = $execution->getAttribute('resourceType', '');
$resourceInternalId = $execution->getAttribute('resourceInternalId', '');
$duration = $execution->getAttribute('duration', 0);
$compute = (int)($duration * 1000);
$mbSeconds = (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $duration * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT));
$queueForStatsUsage = new StatsUsage($publisher);
$queueForStatsUsage
->setProject($project)
->addMetric(METRIC_EXECUTIONS, 1)
->addMetric(str_replace(['{resourceType}'], [$resourceType], METRIC_RESOURCE_TYPE_EXECUTIONS), 1)
->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [$resourceType, $resourceInternalId], METRIC_RESOURCE_TYPE_ID_EXECUTIONS), 1)
->addMetric(METRIC_EXECUTIONS_COMPUTE, $compute)
->addMetric(str_replace(['{resourceType}'], [$resourceType], METRIC_RESOURCE_TYPE_EXECUTIONS_COMPUTE), $compute)
->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [$resourceType, $resourceInternalId], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_COMPUTE), $compute)
->addMetric(METRIC_EXECUTIONS_MB_SECONDS, $mbSeconds)
->addMetric(str_replace(['{resourceType}'], [$resourceType], METRIC_RESOURCE_TYPE_EXECUTIONS_MB_SECONDS), $mbSeconds)
->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [$resourceType, $resourceInternalId], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_MB_SECONDS), $mbSeconds)
->trigger();
}
private function handleRequestCompleted(RequestCompleted $event, Publisher $publisher): void
{
$fileSize = 0;
$file = $event->request->getFiles('file');
if (!empty($file)) {
$fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size'];
}
$project = new Document($event->project);
$deployment = new Document($event->deployment);
$queueForStatsUsage = new StatsUsage($publisher);
$inbound = $event->request->getSize() + $fileSize;
$outbound = $event->response->getSize();
$queueForStatsUsage->setProject($project);
if ($deployment->getAttribute('resourceType') === 'sites') {
$siteInternalId = $deployment->getAttribute('resourceInternalId', '');
$queueForStatsUsage
->addMetric(METRIC_SITES_REQUESTS, 1)
->addMetric(METRIC_SITES_INBOUND, $inbound)
->addMetric(METRIC_SITES_OUTBOUND, $outbound)
->addMetric(str_replace('{siteInternalId}', $siteInternalId, METRIC_SITES_ID_REQUESTS), 1)
->addMetric(str_replace('{siteInternalId}', $siteInternalId, METRIC_SITES_ID_INBOUND), $inbound)
->addMetric(str_replace('{siteInternalId}', $siteInternalId, METRIC_SITES_ID_OUTBOUND), $outbound);
} else {
$queueForStatsUsage
->addMetric(METRIC_NETWORK_REQUESTS, 1)
->addMetric(METRIC_NETWORK_INBOUND, $inbound)
->addMetric(METRIC_NETWORK_OUTBOUND, $outbound);
}
$queueForStatsUsage->trigger();
}
}
+22 -43
View File
@@ -3,15 +3,15 @@
namespace Appwrite\Platform\Workers;
use Ahc\Jwt\JWT;
use Appwrite\Bus\Events\ExecutionCompleted;
use Appwrite\Event\Event;
use Appwrite\Event\Execution as ExecutionEvent;
use Appwrite\Event\Func;
use Appwrite\Event\Realtime;
use Appwrite\Event\StatsUsage;
use Appwrite\Event\Webhook;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Utopia\Response\Model\Execution;
use Executor\Executor;
use Utopia\Bus\Bus;
use Utopia\Config\Config;
use Utopia\Console;
use Utopia\Database\Database;
@@ -47,8 +47,7 @@ class Functions extends Action
->inject('queueForFunctions')
->inject('queueForRealtime')
->inject('queueForEvents')
->inject('queueForStatsUsage')
->inject('queueForExecutions')
->inject('bus')
->inject('log')
->inject('executor')
->inject('isResourceBlocked')
@@ -63,8 +62,7 @@ class Functions extends Action
Func $queueForFunctions,
Realtime $queueForRealtime,
Event $queueForEvents,
StatsUsage $queueForStatsUsage,
ExecutionEvent $queueForExecutions,
Bus $bus,
Log $log,
Executor $executor,
callable $isResourceBlocked
@@ -158,9 +156,8 @@ class Functions extends Action
queueForWebhooks: $queueForWebhooks,
queueForFunctions: $queueForFunctions,
queueForRealtime: $queueForRealtime,
queueForStatsUsage: $queueForStatsUsage,
queueForEvents: $queueForEvents,
queueForExecutions: $queueForExecutions,
bus: $bus,
project: $project,
function: $function,
executor: $executor,
@@ -203,9 +200,8 @@ class Functions extends Action
queueForWebhooks: $queueForWebhooks,
queueForFunctions: $queueForFunctions,
queueForRealtime: $queueForRealtime,
queueForStatsUsage: $queueForStatsUsage,
queueForEvents: $queueForEvents,
queueForExecutions: $queueForExecutions,
bus: $bus,
project: $project,
function: $function,
executor: $executor,
@@ -230,9 +226,8 @@ class Functions extends Action
queueForWebhooks: $queueForWebhooks,
queueForFunctions: $queueForFunctions,
queueForRealtime: $queueForRealtime,
queueForStatsUsage: $queueForStatsUsage,
queueForEvents: $queueForEvents,
queueForExecutions: $queueForExecutions,
bus: $bus,
project: $project,
function: $function,
executor: $executor,
@@ -266,7 +261,7 @@ class Functions extends Action
private function fail(
string $message,
Document $project,
ExecutionEvent $queueForExecutions,
Bus $bus,
Document $function,
string $trigger,
string $path,
@@ -309,10 +304,10 @@ class Functions extends Action
'duration' => 0.0,
]);
$queueForExecutions
->setExecution($execution)
->setProject($project)
->trigger();
$bus->dispatch(new ExecutionCompleted(
execution: $execution->getArrayCopy(),
project: $project->getArrayCopy(),
));
}
/**
@@ -320,7 +315,6 @@ class Functions extends Action
* @param Database $dbForProject
* @param Func $queueForFunctions
* @param Realtime $queueForRealtime
* @param StatsUsage $queueForStatsUsage
* @param Event $queueForEvents
* @param Document $project
* @param Document $function
@@ -343,9 +337,8 @@ class Functions extends Action
Webhook $queueForWebhooks,
Func $queueForFunctions,
Realtime $queueForRealtime,
StatsUsage $queueForStatsUsage,
Event $queueForEvents,
ExecutionEvent $queueForExecutions,
Bus $bus,
Document $project,
Document $function,
Executor $executor,
@@ -373,19 +366,19 @@ class Functions extends Action
if ($deployment->getAttribute('resourceId') !== $functionId) {
$errorMessage = 'The execution could not be completed because a corresponding deployment was not found. A function deployment needs to be created before it can be executed. Please create a deployment for your function and try again.';
$this->fail($errorMessage, $project, $queueForExecutions, $function, $trigger, $path, $method, $user, $jwt, $event);
$this->fail($errorMessage, $project, $bus, $function, $trigger, $path, $method, $user, $jwt, $event);
return;
}
if ($deployment->isEmpty()) {
$errorMessage = 'The execution could not be completed because a corresponding deployment was not found. A function deployment needs to be created before it can be executed. Please create a deployment for your function and try again.';
$this->fail($errorMessage, $project, $queueForExecutions, $function, $trigger, $path, $method, $user, $jwt, $event);
$this->fail($errorMessage, $project, $bus, $function, $trigger, $path, $method, $user, $jwt, $event);
return;
}
if ($deployment->getAttribute('status') !== 'ready') {
$errorMessage = 'The execution could not be completed because the build is not ready. Please wait for the build to complete and try again.';
$this->fail($errorMessage, $project, $queueForExecutions, $function, $trigger, $path, $method, $user, $jwt, $event);
$this->fail($errorMessage, $project, $bus, $function, $trigger, $path, $method, $user, $jwt, $event);
return;
}
@@ -592,26 +585,12 @@ class Functions extends Action
$error = $th->getMessage();
$errorCode = $th->getCode();
} finally {
/** Persist final execution status */
$queueForExecutions
->setExecution($execution)
->setProject($project)
->trigger();
/** Trigger usage queue */
$queueForStatsUsage
->setProject($project)
->addMetric(METRIC_EXECUTIONS, 1)
->addMetric(str_replace(['{resourceType}'], [RESOURCE_TYPE_FUNCTIONS], METRIC_RESOURCE_TYPE_EXECUTIONS), 1)
->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [RESOURCE_TYPE_FUNCTIONS, $function->getSequence()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS), 1)
->addMetric(METRIC_EXECUTIONS_COMPUTE, (int)($execution->getAttribute('duration') * 1000))// per project
->addMetric(str_replace(['{resourceType}'], [RESOURCE_TYPE_FUNCTIONS], METRIC_RESOURCE_TYPE_EXECUTIONS_COMPUTE), (int)($execution->getAttribute('duration') * 1000))
->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [RESOURCE_TYPE_FUNCTIONS, $function->getSequence()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_COMPUTE), (int)($execution->getAttribute('duration') * 1000))
->addMetric(METRIC_EXECUTIONS_MB_SECONDS, (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT)))
->addMetric(str_replace(['{resourceType}'], [RESOURCE_TYPE_FUNCTIONS], METRIC_RESOURCE_TYPE_EXECUTIONS_MB_SECONDS), (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT)))
->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [RESOURCE_TYPE_FUNCTIONS, $function->getSequence()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_MB_SECONDS), (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT)))
->trigger()
;
/** Persist final execution status and record usage */
$bus->dispatch(new ExecutionCompleted(
execution: $execution->getArrayCopy(),
project: $project->getArrayCopy(),
spec: $spec,
));
}
$executionModel = new Execution();
+51
View File
@@ -0,0 +1,51 @@
<?php
namespace Utopia\Bus;
use Utopia\Span\Span;
class Bus
{
/** @var array<class-string<Event>, Listener[]> */
private array $listeners = [];
/** @var ?\Closure(string): mixed */
private ?\Closure $resolver = null;
public function setResolver(callable $resolver): self
{
$this->resolver = $resolver(...);
return $this;
}
public function subscribe(Listener $listener): self
{
foreach ($listener::getEvents() as $event) {
$this->listeners[$event][] = $listener;
}
return $this;
}
public function dispatch(Event $event): void
{
if ($this->resolver === null) {
throw new \LogicException('Bus resolver must be set via setResolver() before dispatching events');
}
$resolver = $this->resolver;
$listeners = $this->listeners[$event::class] ?? [];
foreach ($listeners as $listener) {
$deps = array_map($resolver, $listener->getInjections());
Span::init('listener.' . $listener::getName());
Span::add('bus.event', $event::class);
try {
($listener->getCallback())($event, ...$deps);
} catch (\Throwable $e) {
Span::error($e);
} finally {
Span::current()?->finish();
}
}
}
}
+7
View File
@@ -0,0 +1,7 @@
<?php
namespace Utopia\Bus;
interface Event
{
}
+51
View File
@@ -0,0 +1,51 @@
<?php
namespace Utopia\Bus;
abstract class Listener
{
protected ?string $desc = null;
/** @var array<string> */
protected array $injections = [];
protected ?\Closure $callback = null;
abstract public static function getName(): string;
/**
* @return array<class-string<Event>>
*/
abstract public static function getEvents(): array;
protected function desc(string $desc): self
{
$this->desc = $desc;
return $this;
}
protected function inject(string $injection): self
{
$this->injections[] = $injection;
return $this;
}
protected function callback(callable $callback): self
{
$this->callback = $callback(...);
return $this;
}
/** @return array<string> */
public function getInjections(): array
{
return $this->injections;
}
public function getCallback(): callable
{
if ($this->callback === null) {
throw new \LogicException(static::class . ' must set a callback via $this->callback()');
}
return $this->callback;
}
}
+6 -4
View File
@@ -11,7 +11,8 @@ trait RealtimeBase
array $channels = [],
array $headers = [],
?string $projectId = null,
?array $queries = null
?array $queries = null,
int $timeout = 2
): WebSocketClient {
if (is_null($projectId)) {
$projectId = $this->getProject()['$id'];
@@ -63,7 +64,7 @@ trait RealtimeBase
"ws://appwrite.test/v1/realtime?" . $queryString,
[
"headers" => $headers,
"timeout" => 45,
"timeout" => $timeout,
]
);
}
@@ -74,9 +75,10 @@ trait RealtimeBase
*
* @param array $queryParams Custom query parameters (e.g., ['channels' => ['project'], 'project' => [...]])
* @param array $headers HTTP headers
* @param int $timeout Timeout in seconds (default: 2)
* @return WebSocketClient
*/
private function getWebsocketWithCustomQuery(array $queryParams, array $headers = []): WebSocketClient
private function getWebsocketWithCustomQuery(array $queryParams, array $headers = [], int $timeout = 2): WebSocketClient
{
$queryString = http_build_query($queryParams);
@@ -84,7 +86,7 @@ trait RealtimeBase
"ws://appwrite.test/v1/realtime?" . $queryString,
[
"headers" => $headers,
"timeout" => 45,
"timeout" => $timeout,
]
);
}
@@ -2535,29 +2535,35 @@ class RealtimeCustomClientQueryTest extends Scope
$projectId = 'console';
// Subscribe without queries - should receive all events
$clientNoQuery = $this->getWebsocket(['tests'], [
'origin' => 'http://localhost',
], $projectId);
$clientNoQuery = $this->getWebsocket(
channels: ['tests'],
headers: ['origin' => 'http://localhost'],
projectId: $projectId,
timeout: 5
);
$response = json_decode($clientNoQuery->receive(), true);
$this->assertEquals('connected', $response['type']);
// Subscribe with matching query - should receive events
$clientWithMatchingQuery = $this->getWebsocket(['tests'], [
'origin' => 'http://localhost',
], $projectId, [
Query::equal('response', ['WS:/v1/realtime:passed'])->toString(),
]);
$clientWithMatchingQuery = $this->getWebsocket(
channels: ['tests'],
headers: ['origin' => 'http://localhost'],
projectId: $projectId,
queries: [Query::equal('response', ['WS:/v1/realtime:passed'])->toString()],
timeout: 5
);
$response = json_decode($clientWithMatchingQuery->receive(), true);
$this->assertEquals('connected', $response['type']);
// Subscribe with non-matching query - should NOT receive events
$clientWithNonMatchingQuery = $this->getWebsocket(['tests'], [
'origin' => 'http://localhost',
], $projectId, [
Query::equal('response', ['failed'])->toString(),
]);
$clientWithNonMatchingQuery = $this->getWebsocket(
channels: ['tests'],
headers: ['origin' => 'http://localhost'],
projectId: $projectId,
queries: [Query::equal('response', ['failed'])->toString()]
);
$response = json_decode($clientWithNonMatchingQuery->receive(), true);
$this->assertEquals('connected', $response['type']);
@@ -2225,10 +2225,14 @@ class RealtimeCustomClientTest extends Scope
$session = $user['session'] ?? '';
$projectId = $this->getProject()['$id'];
$client = $this->getWebsocket(['executions'], [
'origin' => 'http://localhost',
'cookie' => 'a_session_' . $projectId . '=' . $session
]);
$client = $this->getWebsocket(
channels: ['executions'],
headers: [
'origin' => 'http://localhost',
'cookie' => 'a_session_' . $projectId . '=' . $session
],
timeout: 10
);
$response = json_decode($client->receive(), true);