Skip to content

Commit d45893d

Browse files
authored
Merge pull request #11241 from appwrite/fix-leaks
Fix leaks
2 parents 5e495aa + cdd2be1 commit d45893d

4 files changed

Lines changed: 78 additions & 64 deletions

File tree

src/Appwrite/Messaging/Adapter/Realtime.php

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ class Realtime extends MessagingAdapter
3131
* [ROLE_X] ->
3232
* [CHANNEL_NAME_X] ->
3333
* [CONNECTION_ID] ->
34-
* [SUB_ID] -> [query1, query2, ...] // Subscription with queries (AND logic)
34+
* [SUB_ID] -> ['strings' => [...], 'parsed' => [...]]
3535
*
36-
* Each subscription ID maps to an array of query strings.
36+
* Each subscription ID maps to query strings (for metadata) and pre-parsed Query objects (for filtering).
3737
* Within a subscription: AND logic (all queries must match)
3838
* Across subscriptions: OR logic (any subscription matching = send event)
3939
*/
@@ -64,18 +64,27 @@ public function subscribe(string $projectId, mixed $identifier, string $subscrip
6464
$this->subscriptions[$projectId] = [];
6565
}
6666

67-
// Convert Query objects to strings for this subscription
67+
// Convert Query objects to strings and store both for this subscription
6868
$queryStrings = [];
69+
$parsedQueries = [];
6970
if (empty($queryGroup)) {
7071
// No queries means "listen to all events" - use select("*")
71-
$queryStrings[] = Query::select(['*'])->toString();
72+
$selectAll = Query::select(['*']);
73+
$queryStrings[] = $selectAll->toString();
74+
$parsedQueries[] = $selectAll;
7275
} else {
7376
foreach ($queryGroup as $query) {
7477
/** @var Query $query */
7578
$queryStrings[] = $query->toString();
79+
$parsedQueries[] = $query;
7680
}
7781
}
7882

83+
$subscriptionData = [
84+
'strings' => $queryStrings,
85+
'parsed' => $parsedQueries,
86+
];
87+
7988
foreach ($roles as $role) {
8089
if (!isset($this->subscriptions[$projectId][$role])) {
8190
$this->subscriptions[$projectId][$role] = [];
@@ -88,8 +97,7 @@ public function subscribe(string $projectId, mixed $identifier, string $subscrip
8897
if (!isset($this->subscriptions[$projectId][$role][$channel][$identifier])) {
8998
$this->subscriptions[$projectId][$role][$channel][$identifier] = [];
9099
}
91-
// Store subscription under subscription ID
92-
$this->subscriptions[$projectId][$role][$channel][$identifier][$subscriptionId] = $queryStrings;
100+
$this->subscriptions[$projectId][$role][$channel][$identifier][$subscriptionId] = $subscriptionData;
93101
}
94102
}
95103

@@ -131,14 +139,14 @@ public function getSubscriptionMetadata(mixed $connection): array
131139
continue;
132140
}
133141

134-
foreach ($this->subscriptions[$projectId][$role][$channel][$connection] as $subId => $queryStrings) {
142+
foreach ($this->subscriptions[$projectId][$role][$channel][$connection] as $subId => $subscriptionData) {
135143
if (!isset($subscriptions[$subId])) {
136144
$subscriptions[$subId] = [
137145
'channels' => [],
138-
'queries' => $queryStrings
146+
'queries' => $subscriptionData['strings'] ?? []
139147
];
140148
}
141-
if (!in_array($channel, $subscriptions[$subId]['channels'])) {
149+
if (!\in_array($channel, $subscriptions[$subId]['channels'])) {
142150
$subscriptions[$subId]['channels'][] = $channel;
143151
}
144152
}
@@ -282,15 +290,14 @@ public function getSubscribers(array $event): array
282290
$matchedSubscriptions = [];
283291

284292
// Process each subscription (OR logic across subscriptions)
285-
foreach ($subscriptions as $subId => $queryStrings) {
286-
$parsedQueries = [];
287-
foreach ($queryStrings as $queryString) {
288-
$parsed = Query::parseQueries([$queryString]);
289-
$parsedQueries = array_merge($parsedQueries, $parsed);
290-
}
293+
foreach ($subscriptions as $subId => $subscriptionData) {
294+
// Use pre-parsed queries instead of re-parsing on every event
295+
$parsedQueries = $subscriptionData['parsed'] ?? [];
296+
$queryStrings = $subscriptionData['strings'] ?? [];
297+
291298
// Check if this subscription matches (AND logic within subscription)
292299
// Or if empty payload and select all as filter will return empty payload out of it even if it passed
293-
$isEmptyPayloadAndSelectAll = RuntimeQuery::isSelectAll($parsedQueries[0]) && empty($payload);
300+
$isEmptyPayloadAndSelectAll = !empty($parsedQueries) && RuntimeQuery::isSelectAll($parsedQueries[0]) && empty($payload);
294301
if ($isEmptyPayloadAndSelectAll || !empty(RuntimeQuery::filter($parsedQueries, $payload))) {
295302
$matchedSubscriptions[$subId] = $queryStrings;
296303
}
@@ -301,7 +308,7 @@ public function getSubscribers(array $event): array
301308
if (!isset($receivers[$id])) {
302309
$receivers[$id] = [];
303310
}
304-
$receivers[$id] = array_merge($receivers[$id], $matchedSubscriptions);
311+
$receivers[$id] += $matchedSubscriptions;
305312
}
306313
}
307314
break;
@@ -433,7 +440,7 @@ public static function convertQueries(mixed $queries): array
433440
}
434441

435442
if (in_array($method, [Query::TYPE_AND, Query::TYPE_OR], true)) {
436-
$stack = array_merge($stack, $query->getValues());
443+
\array_push($stack, ...$query->getValues());
437444
}
438445
}
439446

src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -293,8 +293,8 @@ protected function processDocument(
293293
array &$collectionsCache,
294294
Authorization $authorization,
295295
?int &$operations = null,
296+
int $depth = 0,
296297
): bool {
297-
298298
if ($operations !== null && $document->isEmpty()) {
299299
return false;
300300
}
@@ -308,6 +308,11 @@ protected function processDocument(
308308
$document->setAttribute('$databaseId', $database->getId());
309309
$document->setAttribute('$' . $this->getCollectionsEventsContext() . 'Id', $collectionId);
310310

311+
// Stop processing relationships if max depth reached
312+
if ($depth >= Database::RELATION_MAX_DEPTH) {
313+
return true;
314+
}
315+
311316
$relationships = $collectionsCache[$collectionId] ??= \array_filter(
312317
$collection->getAttribute('attributes', []),
313318
fn ($attr) => $attr->getAttribute('type') === Database::VAR_RELATIONSHIP
@@ -354,8 +359,9 @@ protected function processDocument(
354359
document: $relation,
355360
dbForProject: $dbForProject,
356361
collectionsCache: $collectionsCache,
362+
authorization: $authorization,
357363
operations: $operations,
358-
authorization: $authorization
364+
depth: $depth + 1
359365
);
360366
}
361367
}

src/Appwrite/Platform/Workers/StatsUsage.php

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -538,13 +538,11 @@ protected function writeToLogsDB(): void
538538
$this->statDocuments
539539
);
540540
Console::success('Usage logs pushed to Logs DB');
541-
542-
/**
543-
* todo: Do we need to unset $this->statDocuments?
544-
*/
545-
546541
} catch (Throwable $th) {
547542
Console::error($th->getMessage());
543+
} finally {
544+
// Clear statDocuments to prevent memory accumulation across batches
545+
$this->statDocuments = [];
548546
}
549547
}
550548
}

src/Appwrite/Platform/Workers/Webhooks.php

Lines changed: 42 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -104,47 +104,50 @@ private function execute(array $events, string $payload, Document $webhook, Docu
104104
$httpPass = $webhook->getAttribute('httpPass');
105105
$ch = \curl_init($webhook->getAttribute('url'));
106106

107-
\curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
108-
\curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
109-
\curl_setopt($ch, CURLOPT_HEADER, 0);
110-
\curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
111-
\curl_setopt($ch, CURLOPT_TIMEOUT, 15);
112-
\curl_setopt($ch, CURLOPT_MAXFILESIZE, self::MAX_FILE_SIZE);
113-
\curl_setopt($ch, CURLOPT_USERAGENT, \sprintf(
114-
APP_USERAGENT,
115-
System::getEnv('_APP_VERSION', 'UNKNOWN'),
116-
System::getEnv('_APP_EMAIL_SECURITY', System::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS', APP_EMAIL_SECURITY))
117-
));
118-
\curl_setopt(
119-
$ch,
120-
CURLOPT_HTTPHEADER,
121-
[
122-
'Content-Type: application/json',
123-
'Content-Length: ' . \strlen($payload),
124-
'X-' . APP_NAME . '-Webhook-Id: ' . $webhook->getId(),
125-
'X-' . APP_NAME . '-Webhook-Events: ' . implode(',', $events),
126-
'X-' . APP_NAME . '-Webhook-Name: ' . $webhook->getAttribute('name', ''),
127-
'X-' . APP_NAME . '-Webhook-User-Id: ' . $user->getId(),
128-
'X-' . APP_NAME . '-Webhook-Project-Id: ' . $project->getId(),
129-
'X-' . APP_NAME . '-Webhook-Signature: ' . $signature,
130-
]
131-
);
132-
curl_setopt($ch, CURLOPT_MAXREDIRS, 5);
133-
134-
if (!$webhook->getAttribute('security', true)) {
135-
\curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
136-
\curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
137-
}
107+
try {
108+
\curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
109+
\curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
110+
\curl_setopt($ch, CURLOPT_HEADER, 0);
111+
\curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
112+
\curl_setopt($ch, CURLOPT_TIMEOUT, 15);
113+
\curl_setopt($ch, CURLOPT_MAXFILESIZE, self::MAX_FILE_SIZE);
114+
\curl_setopt($ch, CURLOPT_USERAGENT, \sprintf(
115+
APP_USERAGENT,
116+
System::getEnv('_APP_VERSION', 'UNKNOWN'),
117+
System::getEnv('_APP_EMAIL_SECURITY', System::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS', APP_EMAIL_SECURITY))
118+
));
119+
\curl_setopt(
120+
$ch,
121+
CURLOPT_HTTPHEADER,
122+
[
123+
'Content-Type: application/json',
124+
'Content-Length: ' . \strlen($payload),
125+
'X-' . APP_NAME . '-Webhook-Id: ' . $webhook->getId(),
126+
'X-' . APP_NAME . '-Webhook-Events: ' . implode(',', $events),
127+
'X-' . APP_NAME . '-Webhook-Name: ' . $webhook->getAttribute('name', ''),
128+
'X-' . APP_NAME . '-Webhook-User-Id: ' . $user->getId(),
129+
'X-' . APP_NAME . '-Webhook-Project-Id: ' . $project->getId(),
130+
'X-' . APP_NAME . '-Webhook-Signature: ' . $signature,
131+
]
132+
);
133+
\curl_setopt($ch, CURLOPT_MAXREDIRS, 5);
134+
135+
if (!$webhook->getAttribute('security', true)) {
136+
\curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
137+
\curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
138+
}
138139

139-
if (!empty($httpUser) && !empty($httpPass)) {
140-
\curl_setopt($ch, CURLOPT_USERPWD, "$httpUser:$httpPass");
141-
\curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
142-
}
140+
if (!empty($httpUser) && !empty($httpPass)) {
141+
\curl_setopt($ch, CURLOPT_USERPWD, "$httpUser:$httpPass");
142+
\curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
143+
}
143144

144-
$responseBody = \curl_exec($ch);
145-
$curlError = \curl_error($ch);
146-
$statusCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
147-
\curl_close($ch);
145+
$responseBody = \curl_exec($ch);
146+
$curlError = \curl_error($ch);
147+
$statusCode = \curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
148+
} finally {
149+
\curl_close($ch);
150+
}
148151

149152
if (!empty($curlError) || $statusCode >= 400) {
150153
$dbForPlatform->increaseDocumentAttribute('webhooks', $webhook->getId(), 'attempts', 1);

0 commit comments

Comments
 (0)