mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dda7c4c875 | ||
|
|
b71e4d8659 | ||
|
|
e2dc03145f | ||
|
|
f914290198 | ||
|
|
d1b3e2ad81 | ||
|
|
124bc3376f | ||
|
|
7103b61068 | ||
|
|
b048bd3d8e | ||
|
|
00c0f8d7fa | ||
|
|
eca815ca3a | ||
|
|
cf4534a78c | ||
|
|
5a81560605 | ||
|
|
bf77d52695 | ||
|
|
edb533eaea | ||
|
|
af7883f366 | ||
|
|
c7f79fb4c1 | ||
|
|
210821bdf9 | ||
|
|
dacb03053e | ||
|
|
5d6bf5cd30 | ||
|
|
44701257c7 | ||
|
|
b4aaeab81e | ||
|
|
51238e9e93 | ||
|
|
35b67d89cd | ||
|
|
b2db2725ea |
@@ -742,12 +742,23 @@ Http::shutdown()
|
||||
return;
|
||||
}
|
||||
|
||||
for ($i = 0; $i < ($count - $sessionLimit); $i++) {
|
||||
$session = array_shift($sessions);
|
||||
$dbForProject->deleteDocument('sessions', $session->getId());
|
||||
}
|
||||
try {
|
||||
for ($i = 0; $i < ($count - $sessionLimit); $i++) {
|
||||
$session = array_shift($sessions);
|
||||
|
||||
$dbForProject->purgeCachedDocument('users', $userId);
|
||||
if (!$session instanceof Document) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$dbForProject->deleteDocument('sessions', $session->getId());
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
// Session-limit cleanup is best-effort. Concurrent session creation can race with
|
||||
// older-session deletion, but that should not fail the request that just created a
|
||||
// valid session for the user.
|
||||
} finally {
|
||||
$dbForProject->purgeCachedDocument('users', $userId);
|
||||
}
|
||||
});
|
||||
|
||||
Http::shutdown()
|
||||
|
||||
+53
-229
@@ -7,10 +7,8 @@ $registerRequestResources = require __DIR__ . '/init/resources/request.php';
|
||||
|
||||
use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Swoole\Constant;
|
||||
use Swoole\Process;
|
||||
use Swoole\Runtime;
|
||||
use Swoole\Table;
|
||||
use Swoole\Timer;
|
||||
use Utopia\Audit\Adapter\Database as AdapterDatabase;
|
||||
use Utopia\Audit\Adapter\SQL as AuditAdapterSQL;
|
||||
use Utopia\Audit\Audit;
|
||||
@@ -19,14 +17,12 @@ use Utopia\Config\Config;
|
||||
use Utopia\Console;
|
||||
use Utopia\Database\Adapter\Pool as DatabasePool;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Duplicate as DuplicateException;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Http\Adapter\Swoole\Server;
|
||||
use Utopia\Http\Adapter\SwooleCoroutine\Server;
|
||||
use Utopia\Http\Files;
|
||||
use Utopia\Http\Http;
|
||||
use Utopia\Logger\Log;
|
||||
@@ -34,164 +30,70 @@ use Utopia\Logger\Log\User;
|
||||
use Utopia\Span\Span;
|
||||
use Utopia\System\System;
|
||||
|
||||
const DOMAIN_SYNC_TIMER = 30; // 30 seconds
|
||||
use function Swoole\Coroutine\run;
|
||||
|
||||
$files = new Files();
|
||||
$files->load(__DIR__ . '/../public');
|
||||
|
||||
$riskyDomains = new Table(100_000);
|
||||
$riskyDomains->column('value', Table::TYPE_INT, 1);
|
||||
$riskyDomains->create();
|
||||
Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
|
||||
|
||||
$certifiedDomains = new Table(100_000);
|
||||
$certifiedDomains->column('value', Table::TYPE_INT, 1);
|
||||
$certifiedDomains->create();
|
||||
|
||||
global $container;
|
||||
$container->set('riskyDomains', fn () => $riskyDomains);
|
||||
$container->set('certifiedDomains', fn () => $certifiedDomains);
|
||||
$container->set('pools', function ($register) {
|
||||
return $register->get('pools');
|
||||
}, ['register']);
|
||||
|
||||
function parseMemoryLimitToBytes(string|false $memoryLimit): int
|
||||
{
|
||||
if ($memoryLimit === false || $memoryLimit === '' || $memoryLimit === '-1') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$memoryLimit = trim($memoryLimit);
|
||||
$value = (int) $memoryLimit;
|
||||
$unit = strtolower(substr($memoryLimit, -1));
|
||||
|
||||
return match ($unit) {
|
||||
'g' => $value * 1024 * 1024 * 1024,
|
||||
'm' => $value * 1024 * 1024,
|
||||
'k' => $value * 1024,
|
||||
default => $value,
|
||||
};
|
||||
}
|
||||
|
||||
$minimumCoroutineMemoryLimit = System::getEnv('_APP_HTTP_COROUTINE_MEMORY_LIMIT', '1G');
|
||||
$memoryLimitBytes = parseMemoryLimitToBytes(\ini_get('memory_limit'));
|
||||
$minimumCoroutineMemoryLimitBytes = parseMemoryLimitToBytes($minimumCoroutineMemoryLimit);
|
||||
|
||||
if (
|
||||
$minimumCoroutineMemoryLimitBytes > 0
|
||||
&& $memoryLimitBytes > 0
|
||||
&& $memoryLimitBytes < $minimumCoroutineMemoryLimitBytes
|
||||
) {
|
||||
\ini_set('memory_limit', $minimumCoroutineMemoryLimit);
|
||||
$memoryLimitBytes = parseMemoryLimitToBytes(\ini_get('memory_limit'));
|
||||
}
|
||||
|
||||
$payloadSize = 12 * (1024 * 1024); // 12MB - adding slight buffer for headers and other data that might be sent with the payload - update later with valid testing
|
||||
$totalWorkers = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
|
||||
|
||||
$swooleAdapter = new Server(
|
||||
host: "0.0.0.0",
|
||||
port: System::getEnv('PORT', 80),
|
||||
settings: [
|
||||
Constant::OPTION_WORKER_NUM => $totalWorkers,
|
||||
Constant::OPTION_DISPATCH_FUNC => dispatch(...),
|
||||
Constant::OPTION_DISPATCH_MODE => SWOOLE_DISPATCH_UIDMOD,
|
||||
Constant::OPTION_HTTP_COMPRESSION => false,
|
||||
Constant::OPTION_PACKAGE_MAX_LENGTH => $payloadSize,
|
||||
Constant::OPTION_OUTPUT_BUFFER_SIZE => $payloadSize,
|
||||
Constant::OPTION_TASK_WORKER_NUM => 1, // required for the task to fetch domains background
|
||||
'http_compression' => false,
|
||||
'open_http_keepalive' => false,
|
||||
'package_max_length' => $payloadSize,
|
||||
'output_buffer_size' => $payloadSize,
|
||||
],
|
||||
container: $container,
|
||||
);
|
||||
|
||||
$container->set('container', fn () => fn () => $swooleAdapter->getContainer());
|
||||
|
||||
$http = $swooleAdapter->getServer();
|
||||
|
||||
/**
|
||||
* Assigns HTTP requests to worker threads by analyzing its payload/content.
|
||||
*
|
||||
* Routes requests as 'safe' or 'risky' based on specific content patterns (like POST actions or certain domains)
|
||||
* to optimize load distribution between the workers. Utilizes `$safeThreadsPercent` to manage risk by assigning
|
||||
* riskier tasks to a dedicated worker subset. Prefers idle workers, with fallback to random selection if necessary.
|
||||
* doc: https://openswoole.com/docs/modules/swoole-server/configuration#dispatch_func
|
||||
*
|
||||
* @param \Swoole\Http\Server $server Swoole server instance.
|
||||
* @param int $fd client ID
|
||||
* @param int $type the type of data and its current state
|
||||
* @param string|null $data Request content for categorization.
|
||||
* @global int $totalThreads Total number of workers.
|
||||
* @return int Chosen worker ID for the request.
|
||||
*/
|
||||
function dispatch(\Swoole\Http\Server $server, int $fd, int $type, $data = null): int
|
||||
{
|
||||
$resolveWorkerId = function (\Swoole\Http\Server $server, $data = null) {
|
||||
global $totalWorkers, $riskyDomains;
|
||||
|
||||
// If data is not set we can send request to any worker
|
||||
// first we try to pick idle worker, if not we randomly pick a worker
|
||||
if ($data === null) {
|
||||
for ($i = 0; $i < $totalWorkers; $i++) {
|
||||
if ($server->getWorkerStatus($i) === SWOOLE_WORKER_IDLE) {
|
||||
return $i;
|
||||
}
|
||||
}
|
||||
return rand(0, $totalWorkers - 1);
|
||||
}
|
||||
|
||||
$riskyWorkersPercent = intval(System::getEnv('_APP_RISKY_WORKERS_PERCENT', 80)) / 100; // Decimal form 0 to 1
|
||||
|
||||
// Each worker has numeric ID, starting from 0 and incrementing
|
||||
// From 0 to riskyWorkers, we consider safe workers
|
||||
// From riskyWorkers to totalWorkers, we consider risky workers
|
||||
$riskyWorkers = (int)floor($totalWorkers * $riskyWorkersPercent); // Absolute amount of risky workers
|
||||
|
||||
$domain = '';
|
||||
// max up to 3 as first line has request details and second line has host
|
||||
$lines = explode("\n", $data, 3);
|
||||
$request = $lines[0];
|
||||
if (count($lines) > 1) {
|
||||
$domain = trim(explode('Host: ', $lines[1])[1] ?? '');
|
||||
}
|
||||
|
||||
// Sync executions are considered risky
|
||||
$risky = false;
|
||||
if (str_starts_with($request, 'POST') && str_contains($request, '/executions')) {
|
||||
$risky = true;
|
||||
} elseif ($riskyDomains->get(md5($domain), 'value') === 1) {
|
||||
// executions request coming from custom domain
|
||||
$risky = true;
|
||||
} else {
|
||||
foreach (\explode(',', System::getEnv('_APP_DOMAIN_FUNCTIONS')) as $riskyDomain) {
|
||||
if (empty($riskyDomain)) {
|
||||
continue;
|
||||
}
|
||||
if (str_ends_with($domain, $riskyDomain)) {
|
||||
$risky = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($risky) {
|
||||
// If risky request, only consider risky workers
|
||||
for ($j = $riskyWorkers; $j < $totalWorkers; $j++) {
|
||||
/** Reference https://openswoole.com/docs/modules/swoole-server-getWorkerStatus#description */
|
||||
if ($server->getWorkerStatus($j) === SWOOLE_WORKER_IDLE) {
|
||||
// If idle worker found, give to him
|
||||
return $j;
|
||||
}
|
||||
}
|
||||
|
||||
// If no idle workers, give to random risky worker
|
||||
$worker = rand($riskyWorkers, $totalWorkers - 1);
|
||||
Console::warning("swoole_dispatch: Risky branch: did not find a idle worker, picking random worker {$worker}");
|
||||
return $worker;
|
||||
}
|
||||
|
||||
// If safe request, give to any idle worker
|
||||
// Its fine to pick risky worker here, because it's idle. Idle is never actually risky
|
||||
for ($i = 0; $i < $totalWorkers; $i++) {
|
||||
if ($server->getWorkerStatus($i) === SWOOLE_WORKER_IDLE) {
|
||||
return $i;
|
||||
}
|
||||
}
|
||||
|
||||
// If no idle worker found, give to random safe worker
|
||||
// We avoid risky workers here, as it could be in work - not idle. Thats exactly when they are risky.
|
||||
$worker = rand(0, $riskyWorkers - 1);
|
||||
Console::warning("swoole_dispatch: Non-risky branch: did not find a idle worker, picking random worker {$worker}");
|
||||
return $worker;
|
||||
};
|
||||
$workerId = $resolveWorkerId($server, $data);
|
||||
$server->bind($fd, $workerId);
|
||||
return $workerId;
|
||||
}
|
||||
|
||||
$http->on(Constant::EVENT_WORKER_START, function ($server, $workerId) {
|
||||
});
|
||||
|
||||
$http->on(Constant::EVENT_WORKER_STOP, function ($server, $workerId) {
|
||||
Timer::clearAll();
|
||||
Console::success('Worker ' . ++$workerId . ' stopped successfully');
|
||||
});
|
||||
|
||||
$http->on(Constant::EVENT_BEFORE_RELOAD, function ($server) {
|
||||
Console::success('Starting reload...');
|
||||
});
|
||||
|
||||
$http->on(Constant::EVENT_AFTER_RELOAD, function ($server) {
|
||||
Console::success('Reload completed...');
|
||||
});
|
||||
|
||||
$container->set('bus', function ($register) use ($swooleAdapter) {
|
||||
return $register->get('bus')->setResolver(fn (string $name) => $swooleAdapter->getContainer()->get($name));
|
||||
}, ['register']);
|
||||
@@ -290,7 +192,7 @@ function createDatabase(Http $app, string $resourceKey, string $dbName, array $c
|
||||
Span::current()?->finish();
|
||||
}
|
||||
|
||||
$http->on(Constant::EVENT_START, function ($http) use ($payloadSize, $totalWorkers, $swooleAdapter) {
|
||||
$swooleAdapter->onStart(function () use ($payloadSize, $swooleAdapter) {
|
||||
$app = new Http($swooleAdapter, 'UTC');
|
||||
|
||||
/** @var \Utopia\Pools\Group $pools */
|
||||
@@ -496,20 +398,10 @@ $http->on(Constant::EVENT_START, function ($http) use ($payloadSize, $totalWorke
|
||||
});
|
||||
|
||||
Span::init('http.server.start');
|
||||
Span::add('server.workers', $totalWorkers);
|
||||
Span::add('server.adapter', 'swoole-coroutine');
|
||||
Span::add('server.memory_limit', \ini_get('memory_limit'));
|
||||
Span::add('server.payload_size', $payloadSize);
|
||||
Span::add('server.master_pid', $http->master_pid);
|
||||
Span::add('server.manager_pid', $http->manager_pid);
|
||||
Span::current()?->finish();
|
||||
|
||||
// Start the task that starts fetching custom domains
|
||||
$http->task([], 0);
|
||||
|
||||
// listen ctrl + c
|
||||
Process::signal(2, function () use ($http) {
|
||||
Console::log('Stop by Ctrl+C');
|
||||
$http->shutdown();
|
||||
});
|
||||
});
|
||||
|
||||
$swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files, $swooleAdapter, $registerRequestResources) {
|
||||
@@ -647,86 +539,18 @@ $swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files
|
||||
} finally {
|
||||
Span::add('http.response.code', $response->getStatusCode());
|
||||
Span::current()?->finish();
|
||||
|
||||
$request->resetFilters();
|
||||
$request->setRoute(null);
|
||||
$response->resetFilters();
|
||||
|
||||
gc_collect_cycles();
|
||||
if (\function_exists('gc_mem_caches')) {
|
||||
gc_mem_caches();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Fetch domains every `DOMAIN_SYNC_TIMER` seconds and update in the memory
|
||||
$http->on(Constant::EVENT_TASK, function () use ($swooleAdapter) {
|
||||
$lastSyncUpdate = null;
|
||||
|
||||
$app = new Http($swooleAdapter, 'UTC');
|
||||
|
||||
/** @var Utopia\Database\Database $dbForPlatform */
|
||||
$dbForPlatform = $app->getResource('dbForPlatform');
|
||||
|
||||
/** @var \Swoole\Table $riskyDomains */
|
||||
$riskyDomains = $app->getResource('riskyDomains');
|
||||
|
||||
Timer::tick(DOMAIN_SYNC_TIMER * 1000, function () use ($dbForPlatform, $riskyDomains, &$lastSyncUpdate, $app) {
|
||||
try {
|
||||
$time = DateTime::now();
|
||||
$limit = 1000;
|
||||
$sum = $limit;
|
||||
$latestDocument = null;
|
||||
|
||||
while ($sum === $limit) {
|
||||
$queries = [Query::limit($limit)];
|
||||
if ($latestDocument !== null) {
|
||||
$queries[] = Query::cursorAfter($latestDocument);
|
||||
}
|
||||
if ($lastSyncUpdate !== null) {
|
||||
$queries[] = Query::greaterThanEqual('$updatedAt', $lastSyncUpdate);
|
||||
}
|
||||
$results = [];
|
||||
try {
|
||||
$authorization = $app->getResource('authorization');
|
||||
$results = $authorization->skip(fn () => $dbForPlatform->find('rules', $queries));
|
||||
} catch (Throwable $th) {
|
||||
Console::error('rules ' . $th->getMessage());
|
||||
}
|
||||
|
||||
$sum = count($results);
|
||||
foreach ($results as $document) {
|
||||
$domain = $document->getAttribute('domain');
|
||||
|
||||
$denyDomains = [];
|
||||
$denyEnvVars = [
|
||||
System::getEnv('_APP_DOMAIN_FUNCTIONS_FALLBACK', ''),
|
||||
System::getEnv('_APP_DOMAIN_FUNCTIONS', ''),
|
||||
System::getEnv('_APP_DOMAIN_SITES', ''),
|
||||
];
|
||||
foreach ($denyEnvVars as $denyEnvVar) {
|
||||
foreach (\explode(',', $denyEnvVar) as $denyDomain) {
|
||||
if (empty($denyDomain)) {
|
||||
continue;
|
||||
}
|
||||
$denyDomains[] = $denyDomain;
|
||||
}
|
||||
}
|
||||
|
||||
$isDenyDomain = false;
|
||||
foreach ($denyDomains as $denyDomain) {
|
||||
if (str_ends_with($domain, $denyDomain)) {
|
||||
$isDenyDomain = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($isDenyDomain) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$riskyDomains->set(md5($domain), ['value' => 1]);
|
||||
}
|
||||
$latestDocument = !empty(array_key_last($results)) ? $results[array_key_last($results)] : null;
|
||||
}
|
||||
$lastSyncUpdate = $time;
|
||||
if ($sum > 0) {
|
||||
Console::log("Sync domains tick: {$sum} domains were updated");
|
||||
}
|
||||
} catch (Throwable $th) {
|
||||
Console::error($th->getMessage());
|
||||
}
|
||||
});
|
||||
run(static function () use ($swooleAdapter): void {
|
||||
$swooleAdapter->start();
|
||||
});
|
||||
|
||||
$swooleAdapter->start();
|
||||
|
||||
+15
-2
@@ -245,8 +245,17 @@ $register->set('pools', function () {
|
||||
$maxConnections = (int) System::getEnv('_APP_CONNECTIONS_MAX', 151);
|
||||
$instanceConnections = $maxConnections / (int) System::getEnv('_APP_POOL_CLIENTS', 14);
|
||||
|
||||
$scriptPath = \str_replace('\\', '/', (string) ($_SERVER['argv'][0] ?? ''));
|
||||
$isCoroutineHttp = $scriptPath === 'app/http.php' || \str_ends_with($scriptPath, '/app/http.php');
|
||||
|
||||
$workerCount = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
|
||||
$poolSize = max(1, (int)($instanceConnections / $workerCount));
|
||||
$processCount = $isCoroutineHttp ? 1 : $workerCount;
|
||||
$poolSize = max(1, (int)($instanceConnections / $processCount));
|
||||
// The coroutine HTTP server collapses worker fan-out into one process, but the hot
|
||||
// Redis-backed request paths still see roughly the same concurrent pressure as before.
|
||||
// Size these pools to match the previous worker-level concurrency instead of the much
|
||||
// smaller shared DB budget used by other connection types.
|
||||
$redisHotPathPoolSize = $isCoroutineHttp ? max($poolSize, $workerCount) : $poolSize;
|
||||
|
||||
foreach ($connections as $key => $connection) {
|
||||
$type = $connection['type'] ?? '';
|
||||
@@ -333,7 +342,11 @@ $register->set('pools', function () {
|
||||
|
||||
$poolAdapter = System::getEnv('_APP_POOL_ADAPTER', default: 'stack') === 'swoole' ? new SwoolePool() : new StackPool();
|
||||
|
||||
$pool = new Pool($poolAdapter, $name, $poolSize, function () use ($type, $resource, $dsn) {
|
||||
$currentPoolSize = \in_array($type, ['cache', 'publisher', 'consumer', 'pubsub'], true)
|
||||
? $redisHotPathPoolSize
|
||||
: $poolSize;
|
||||
|
||||
$pool = new Pool($poolAdapter, $name, $currentPoolSize, function () use ($type, $resource, $dsn) {
|
||||
// Get Adapter
|
||||
switch ($type) {
|
||||
case 'database':
|
||||
|
||||
@@ -26,9 +26,11 @@ use Appwrite\Network\Platform;
|
||||
use Appwrite\Network\Validator\Origin;
|
||||
use Appwrite\Network\Validator\Redirect;
|
||||
use Appwrite\Usage\Context as UsageContext;
|
||||
use Appwrite\Utopia\Database\Adapter\Pool as DatabasePool;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis;
|
||||
use Utopia\Agents\Adapters\Ollama;
|
||||
use Utopia\Agents\Agent;
|
||||
use Utopia\Audit\Adapter\Database as AdapterDatabase;
|
||||
@@ -41,7 +43,6 @@ use Utopia\Auth\Proofs\Token;
|
||||
use Utopia\Auth\Store;
|
||||
use Utopia\Cache\Cache;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Adapter\Pool as DatabasePool;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime as DatabaseDateTime;
|
||||
use Utopia\Database\Document;
|
||||
@@ -85,6 +86,27 @@ return function (Container $container): void {
|
||||
return new Store();
|
||||
}, []);
|
||||
|
||||
$container->set('redis', function () {
|
||||
$host = System::getEnv('_APP_REDIS_HOST', 'localhost');
|
||||
$port = System::getEnv('_APP_REDIS_PORT', 6379);
|
||||
$pass = System::getEnv('_APP_REDIS_PASS', '');
|
||||
|
||||
$redis = new \Redis();
|
||||
@$redis->connect($host, (int) $port);
|
||||
if ($pass) {
|
||||
$redis->auth($pass);
|
||||
}
|
||||
$redis->setOption(\Redis::OPT_READ_TIMEOUT, -1);
|
||||
|
||||
return $redis;
|
||||
}, []);
|
||||
|
||||
$container->set('timelimit', function (\Redis $redis) {
|
||||
return function (string $key, int $limit, int $time) use ($redis) {
|
||||
return new TimeLimitRedis($key, $limit, $time, $redis);
|
||||
};
|
||||
}, ['redis']);
|
||||
|
||||
$container->set('proofForPassword', function (): Password {
|
||||
$hash = new Argon2();
|
||||
$hash
|
||||
|
||||
@@ -639,6 +639,17 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
$connectionContainer->set('response', fn () => $response);
|
||||
|
||||
$registerRequestResources($connectionContainer);
|
||||
// Realtime keeps a coroutine-local persistent Redis connection for abuse checks.
|
||||
// Overriding the request-scoped HTTP resource avoids opening a fresh TCP socket on
|
||||
// every websocket connection under load.
|
||||
$connectionContainer->set('redis', function () {
|
||||
return getRedis();
|
||||
}, []);
|
||||
$connectionContainer->set('timelimit', function () {
|
||||
return function (string $key, int $limit, int $time) {
|
||||
return new TimeLimitRedis($key, $limit, $time, getRedis());
|
||||
};
|
||||
}, []);
|
||||
|
||||
$project = null;
|
||||
$logUser = null;
|
||||
|
||||
Generated
+6
-6
@@ -4325,16 +4325,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/http",
|
||||
"version": "0.34.16",
|
||||
"version": "0.34.17",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/http.git",
|
||||
"reference": "2b4021ba3f9d476264ce9fd6703d6c79de9add7f"
|
||||
"reference": "d3e4143b8b06d9823d0c29a06dacefa5a1b93677"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/http/zipball/2b4021ba3f9d476264ce9fd6703d6c79de9add7f",
|
||||
"reference": "2b4021ba3f9d476264ce9fd6703d6c79de9add7f",
|
||||
"url": "https://api.github.com/repos/utopia-php/http/zipball/d3e4143b8b06d9823d0c29a06dacefa5a1b93677",
|
||||
"reference": "d3e4143b8b06d9823d0c29a06dacefa5a1b93677",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4373,9 +4373,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/http/issues",
|
||||
"source": "https://github.com/utopia-php/http/tree/0.34.16"
|
||||
"source": "https://github.com/utopia-php/http/tree/0.34.17"
|
||||
},
|
||||
"time": "2026-03-20T10:39:07+00:00"
|
||||
"time": "2026-04-06T04:40:23+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/image",
|
||||
|
||||
@@ -11,8 +11,6 @@ use Appwrite\SDK\MethodType;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\URL\URL as URLParse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use DOMDocument;
|
||||
use DOMElement;
|
||||
use enshrined\svgSanitize\Sanitizer as SvgSanitizer;
|
||||
use Utopia\Domains\Domain;
|
||||
use Utopia\Fetch\Client;
|
||||
@@ -94,19 +92,19 @@ class Get extends Action
|
||||
throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED);
|
||||
}
|
||||
|
||||
$doc = new DOMDocument();
|
||||
$doc->strictErrorChecking = false;
|
||||
@$doc->loadHTML($res->getBody());
|
||||
|
||||
$links = $doc->getElementsByTagName('link') ?? [];
|
||||
$outputHref = '';
|
||||
$outputExt = '';
|
||||
$space = 0;
|
||||
|
||||
foreach ($links as $link) { /* @var $link DOMElement */
|
||||
$href = $link->getAttribute('href');
|
||||
$rel = $link->getAttribute('rel');
|
||||
$sizes = $link->getAttribute('sizes');
|
||||
foreach ($this->findLinkTags($res->getBody()) as $attributes) {
|
||||
$href = $attributes['href'] ?? '';
|
||||
$rel = $attributes['rel'] ?? '';
|
||||
$sizes = $attributes['sizes'] ?? '';
|
||||
|
||||
if (empty($href)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$absolute = URLParse::unparse(\array_merge(\parse_url($url), \parse_url($href)));
|
||||
|
||||
switch (\strtolower($rel)) {
|
||||
@@ -212,4 +210,38 @@ class Get extends Action
|
||||
->file($data);
|
||||
unset($image);
|
||||
}
|
||||
|
||||
/**
|
||||
* Avoid DOMDocument HTML parsing here because libxml state is not reliable under coroutine concurrency.
|
||||
*
|
||||
* @return array<int, array<string, string>>
|
||||
*/
|
||||
private function findLinkTags(string $html): array
|
||||
{
|
||||
if (!\preg_match_all('/<link\b[^>]*>/i', $html, $matches)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$links = [];
|
||||
|
||||
foreach ($matches[0] as $tag) {
|
||||
$attributes = [];
|
||||
|
||||
if (\preg_match_all('/([a-zA-Z:-]+)\s*=\s*([\'"])(.*?)\2/s', $tag, $attributeMatches, \PREG_SET_ORDER)) {
|
||||
foreach ($attributeMatches as $attributeMatch) {
|
||||
$attributes[\strtolower($attributeMatch[1])] = \html_entity_decode($attributeMatch[3], \ENT_QUOTES | \ENT_HTML5);
|
||||
}
|
||||
}
|
||||
|
||||
$rel = \strtolower(\preg_replace('/\s+/', ' ', \trim($attributes['rel'] ?? '')));
|
||||
|
||||
if (!\in_array($rel, ['icon', 'shortcut icon'], true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$links[] = $attributes;
|
||||
}
|
||||
|
||||
return $links;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Database\Adapter;
|
||||
|
||||
use Utopia\Database\Database;
|
||||
|
||||
class Pool extends \Utopia\Database\Adapter\Pool
|
||||
{
|
||||
public function setTimeout(int $milliseconds, string $event = Database::EVENT_ALL): void
|
||||
{
|
||||
$this->timeout = $milliseconds;
|
||||
|
||||
$this->delegate(__FUNCTION__, \func_get_args());
|
||||
}
|
||||
|
||||
public function clearTimeout(string $event = Database::EVENT_ALL): void
|
||||
{
|
||||
$this->timeout = 0;
|
||||
|
||||
$this->delegate(__FUNCTION__, \func_get_args());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user