refactor: drop log/logger boilerplate from lock call sites

The previous shape required every caller to thread `log: $log, logger: $logger`
as named args into each `distributedLock(...)` invocation, plus inject `log`
and `logger` into the surrounding action just to forward them to the lock.
Across 21 call sites this added ~100 LOC of pure plumbing.

The cause: the lock factory was registered on the global container in
`app/init/resources.php`, where per-request resources like `log` aren't
visible. That forced the factory to expose its inner closure with optional
`?Log $log = null, ?Logger $logger = null` params, which every caller had
to satisfy.

Move the lock factory + its `lockErrorReporter`/`lockTargetOf` helpers from
the global container to the per-request container (`resources/request.php`),
and add `'log'` + `'logger'` to the factory's dep list. The factory closure
now runs per-request and closes over the per-request `Log`/`Logger`. Inner
closure returned to callers no longer needs the optional params, and call
sites drop the named args entirely.

Knock-on cleanup:
- Drop `->inject('log')`, `->inject('logger')`, the corresponding action
  params, and `use Utopia\Logger\{Log,Logger}` imports from 19 endpoint
  files where they were only there for the lock
- Drop the same plumbing from `app/controllers/shared/api.php` (3 lock call
  sites)
- Drop just the Logger plumbing from `app/controllers/general.php` (router
  function + 3 callbacks); `Log` is kept because it's used elsewhere in
  that file
- Net 120 LOC removed across 23 files

No behavior change: the lock factories still produce the same closures
(skip-on-contention `distributedLock`, blocking-with-409 `distributedLockOrFail`).
The static lockErrorReporter rate limiter (1 push per 60s per
`(action, target)` bucket) continues to work — it lives on a closure-static
in the helper, which is independent of where the helper is constructed.

Verified end-to-end: testConcurrentTogglesAllPersist passes 4/5 (the cold-
start race flake is the same one we've consistently seen and is orthogonal
to lock changes).
This commit is contained in:
Prem Palanisamy
2026-04-29 02:02:28 +01:00
parent b29f9f4a45
commit 380cc3eb27
23 changed files with 229 additions and 349 deletions
+10 -14
View File
@@ -67,7 +67,7 @@ use Utopia\Validator\Text;
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, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Authorization $authorization, ?Key $apiKey, DeleteEvent $queueForDeletes, int $executionsRetentionCount, callable $distributedLock, ?Logger $logger)
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, callable $distributedLock)
{
$host = $request->getHostname();
if (!empty($previewHostname)) {
@@ -143,7 +143,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
$authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), new Document([
'accessedAt' => DateTime::now()
])));
}, log: $log, logger: $logger);
});
}
/**
@@ -853,8 +853,7 @@ Http::init()
->inject('queueForDeletes')
->inject('executionsRetentionCount')
->inject('distributedLock')
->inject('logger')
->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, callable $distributedLock, ?Logger $logger) {
->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, callable $distributedLock) {
/*
* Appwrite Router
*/
@@ -862,7 +861,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, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount, $distributedLock, $logger)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount, $distributedLock)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -1148,15 +1147,14 @@ Http::options()
->inject('queueForDeletes')
->inject('executionsRetentionCount')
->inject('distributedLock')
->inject('logger')
->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, callable $distributedLock, ?Logger $logger) {
->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, callable $distributedLock) {
/*
* 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, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount, $distributedLock, $logger)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount, $distributedLock)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -1552,14 +1550,13 @@ Http::get('/robots.txt')
->inject('queueForDeletes')
->inject('executionsRetentionCount')
->inject('distributedLock')
->inject('logger')
->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, callable $distributedLock, ?Logger $logger) {
->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, callable $distributedLock) {
$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, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount, $distributedLock, $logger)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount, $distributedLock)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -1588,14 +1585,13 @@ Http::get('/humans.txt')
->inject('queueForDeletes')
->inject('executionsRetentionCount')
->inject('distributedLock')
->inject('logger')
->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, callable $distributedLock, ?Logger $logger) {
->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, callable $distributedLock) {
$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, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount, $distributedLock, $logger)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount, $distributedLock)) {
$utopia->getRoute()?->label('router', true);
}
}
+4 -8
View File
@@ -39,8 +39,6 @@ use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Authorization\Input;
use Utopia\Database\Validator\Roles;
use Utopia\Http\Http;
use Utopia\Logger\Log;
use Utopia\Logger\Logger;
use Utopia\Span\Span;
use Utopia\System\System;
use Utopia\Telemetry\Adapter as Telemetry;
@@ -103,9 +101,7 @@ Http::init()
->inject('apiKey')
->inject('authorization')
->inject('distributedLock')
->inject('log')
->inject('logger')
->action(function (Http $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, AuditContext $auditContext, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization, callable $distributedLock, Log $log, ?Logger $logger) {
->action(function (Http $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, AuditContext $auditContext, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization, callable $distributedLock) {
$route = $utopia->getRoute();
if ($route === null) {
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND);
@@ -267,7 +263,7 @@ Http::init()
} elseif (! empty($apiKey->getTeamId())) {
$dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->purgeCachedDocument('teams', $team->getId()));
}
}, log: $log, logger: $logger);
});
}
$userClone = clone $user;
@@ -402,7 +398,7 @@ Http::init()
$authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), new Document([
'accessedAt' => DateTime::now()
])));
}, log: $log, logger: $logger);
});
}
}
@@ -423,7 +419,7 @@ Http::init()
$authorization->skip(fn () => $dbForPlatform->updateDocument('users', $user->getId(), new Document([
'accessedAt' => $user->getAttribute('accessedAt')
])));
}, log: $log, logger: $logger);
});
}
}
}
-196
View File
@@ -8,7 +8,6 @@ use Appwrite\Event\Publisher\Migration as MigrationPublisher;
use Appwrite\Event\Publisher\Screenshot as ScreenshotPublisher;
use Appwrite\Event\Publisher\StatsResources as StatsResourcesPublisher;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Utopia\Database\Documents\User;
use Executor\Executor;
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis;
@@ -23,9 +22,6 @@ use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\DI\Container;
use Utopia\DSN\DSN;
use Utopia\Lock\Distributed as DistributedLock;
use Utopia\Logger\Log;
use Utopia\Logger\Logger;
use Utopia\Pools\Group;
use Utopia\Queue\Broker\Pool as BrokerPool;
use Utopia\Queue\Publisher;
@@ -217,198 +213,6 @@ $container->set('timelimit', function (\Redis $redis) {
};
}, ['redis']);
// Extract the collection segment ("keys" / "projects" / "users") from keys
// of the form "lock:platform:{target}:{id}" so metrics can slice by target.
// Used by both distributedLock and distributedLockOrFail factories below.
$lockTargetOf = function (string $key): string {
$parts = explode(':', $key, 4);
return $parts[2] ?? 'unknown';
};
/**
* Push lock-side errors to the configured logger (Sentry/Raygun/AppSignal/etc.)
* by mutating the caller's per-request `Log` object — same pattern as
* Embeddings/Text/Create.php and the http.php request-end error handler.
*
* Rate limited per pod via a static bucket so a sustained backend outage
* doesn't flood the logger. At most one push per 60s per (action, target)
* combo. Across a fleet of N pods this caps at N events/min, well within
* Sentry's own dedup tolerance.
*
* Only "real" errors are reported here:
* - backend_error: Redis/Dragonfly unreachable
* - release_error: lock release failed (TTL expired or backend dropped)
*
* Skipped on purpose: contention 409s (normal user-facing concurrency),
* skip-on-contention events (idempotent fan-out by design), acquire retry
* conflicts (internal loop), destructor cleanups (have an expected
* baseline rate; aggregate via the `lock.attempts` counter instead).
*/
$lockErrorReporter = function (?Log $log, ?Logger $logger, string $action, string $key, string $target, Throwable $e): void {
static $lastReportAt = [];
Console::warning("Lock {$action} for {$key}: {$e->getMessage()}");
if ($logger === null || $log === null) {
return;
}
$bucket = $action . ':' . $target;
$now = time();
if (($lastReportAt[$bucket] ?? 0) + 60 > $now) {
return;
}
$lastReportAt[$bucket] = $now;
$log->setNamespace('http');
$log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));
$log->setVersion(APP_VERSION_STABLE);
$log->setType(Log::TYPE_WARNING);
$log->setMessage('Distributed lock ' . $action . ': ' . $e->getMessage());
$log->setAction("lock.{$action}");
$log->setEnvironment(System::getEnv('_APP_ENV', 'development') === 'production'
? Log::ENVIRONMENT_PRODUCTION
: Log::ENVIRONMENT_STAGING);
$log->addTag('lock.target', $target);
// Strip the trailing document ID from the key so log aggregators don't
// see unbounded cardinality; the {target} attribute already covers slicing.
$log->addTag('lock.key_pattern', preg_replace('/:[^:]+$/', ':*', $key));
$log->addTag('code', $e->getCode());
$log->addExtra('file', $e->getFile());
$log->addExtra('line', $e->getLine());
$log->addExtra('trace', $e->getTraceAsString());
try {
$logger->addLog($log);
} catch (Throwable) {
// best-effort; don't let logging failures break fail-open
}
};
/**
* Distributed-lock factory: skip-on-contention variant.
*
* For idempotent writes where losing the race is correct (e.g., per-request
* `accessedAt` updates from N pods all writing the same value). On contention,
* the callback is silently skipped — another pod is doing the same work.
*
* Behavior:
* - Non-blocking acquire (one attempt). On conflict, skip and return.
* - Fail-open: if Redis is unreachable, run the callback unlocked + warn.
* - Kill switch: `_APP_LOCKING_ENABLED=disabled` runs the callback unlocked.
*
* Returns void — the caller can't distinguish "acquired and ran" from "skipped".
*
* Metric: `lock.attempts{outcome,target}` where outcome ∈ {acquired, skipped,
* backend_error, release_error}.
*/
$container->set('distributedLock', function (\Redis $redis, Telemetry $telemetry) use ($lockTargetOf, $lockErrorReporter) {
$enabled = System::getEnv('_APP_LOCKING_ENABLED', 'enabled') !== 'disabled';
$attempts = $telemetry->createCounter('lock.attempts', null, 'Distributed lock acquire outcomes');
if (! $enabled) {
return function (string $key, \Closure $fn, float $ttl = 5.0, ?Log $log = null, ?Logger $logger = null): void {
$fn();
};
}
return function (string $key, \Closure $fn, float $ttl = 5.0, ?Log $log = null, ?Logger $logger = null) use ($redis, $attempts, $lockTargetOf, $lockErrorReporter): void {
$target = $lockTargetOf($key);
$lock = new DistributedLock($redis, $key, (int) $ttl);
try {
$acquired = $lock->tryAcquire();
} catch (\RedisException $e) {
$attempts->add(1, ['outcome' => 'backend_error', 'target' => $target]);
$lockErrorReporter($log, $logger, 'backend_error', $key, $target, $e);
$fn();
return;
}
if (! $acquired) {
$attempts->add(1, ['outcome' => 'skipped', 'target' => $target]);
return;
}
$attempts->add(1, ['outcome' => 'acquired', 'target' => $target]);
try {
$fn();
} finally {
try {
$lock->release();
} catch (\Throwable $e) {
$attempts->add(1, ['outcome' => 'release_error', 'target' => $target]);
$lockErrorReporter($log, $logger, 'release_error', $key, $target, $e);
}
}
};
}, ['redis', 'telemetry']);
/**
* Distributed-lock factory: 409-on-contention variant.
*
* For explicit user-write endpoints where read-modify-write on shared mutable
* state must NOT silently drop a request. On contention, throws
* `Exception::GENERAL_RESOURCE_LOCKED` (HTTP 409) so the client retries.
*
* Behavior:
* - Blocking acquire with short timeout (default 3s).
* - On timeout, throws `GENERAL_RESOURCE_LOCKED`.
* - Fail-open: backend unreachable runs the callback unlocked + warning.
* - Kill switch: `_APP_LOCKING_ENABLED=disabled` runs the callback unlocked.
* - Returns the callback's return value so callers can use the result.
*
* Metric: `lock.attempts{outcome,target}` where outcome ∈ {acquired, contended,
* backend_error, release_error}.
*/
$container->set('distributedLockOrFail', function (\Redis $redis, Telemetry $telemetry) use ($lockTargetOf, $lockErrorReporter) {
$enabled = System::getEnv('_APP_LOCKING_ENABLED', 'enabled') !== 'disabled';
$attempts = $telemetry->createCounter('lock.attempts', null, 'Distributed lock acquire outcomes');
if (! $enabled) {
return function (string $key, \Closure $fn, float $ttl = 10.0, float $waitTimeout = 3.0, ?Log $log = null, ?Logger $logger = null): mixed {
return $fn();
};
}
return function (string $key, \Closure $fn, float $ttl = 10.0, float $waitTimeout = 3.0, ?Log $log = null, ?Logger $logger = null) use ($redis, $attempts, $lockTargetOf, $lockErrorReporter): mixed {
$target = $lockTargetOf($key);
$lock = new DistributedLock($redis, $key, (int) $ttl);
try {
$acquired = $lock->acquire($waitTimeout);
} catch (\RedisException $e) {
$attempts->add(1, ['outcome' => 'backend_error', 'target' => $target]);
$lockErrorReporter($log, $logger, 'backend_error', $key, $target, $e);
return $fn();
}
if (! $acquired) {
$attempts->add(1, ['outcome' => 'contended', 'target' => $target]);
// Don't pass a custom message — the catalog message in
// app/config/errors.php is reused so we don't leak the internal
// lock key (which embeds collection name and document id) into a
// user-facing 409 response. The telemetry attribute already
// carries the target collection for operator-side observability.
throw new AppwriteException(AppwriteException::GENERAL_RESOURCE_LOCKED);
}
$attempts->add(1, ['outcome' => 'acquired', 'target' => $target]);
try {
return $fn();
} finally {
try {
$lock->release();
} catch (\Throwable $e) {
$attempts->add(1, ['outcome' => 'release_error', 'target' => $target]);
$lockErrorReporter($log, $logger, 'release_error', $key, $target, $e);
}
}
};
}, ['redis', 'telemetry']);
$container->set('deviceForLocal', function (Telemetry $telemetry) {
return new Device\Telemetry($telemetry, new Local());
}, ['telemetry']);
+194
View File
@@ -37,6 +37,7 @@ use Utopia\Auth\Proofs\Token;
use Utopia\Auth\Store;
use Utopia\Cache\Cache;
use Utopia\Config\Config;
use Utopia\Console;
use Utopia\Database\Adapter\Pool as DatabasePool;
use Utopia\Database\Database;
use Utopia\Database\DateTime as DatabaseDateTime;
@@ -48,7 +49,9 @@ use Utopia\Domains\Domain;
use Utopia\DSN\DSN;
use Utopia\Http\Http;
use Utopia\Locale\Locale;
use Utopia\Lock\Distributed as DistributedLock;
use Utopia\Logger\Log;
use Utopia\Logger\Logger;
use Utopia\Pools\Group;
use Utopia\Queue\Publisher;
use Utopia\Storage\Device;
@@ -73,6 +76,197 @@ return function (Container $container): void {
return $register->get('logger');
}, ['register']);
/**
* Push lock-side errors to the configured logger (Sentry/Raygun/AppSignal/etc.)
* by mutating the per-request `Log` object — same pattern as
* Embeddings/Text/Create.php and the http.php request-end error handler.
*
* Rate limited per pod via a static bucket so a sustained backend outage
* doesn't flood the logger. At most one push per 60s per (action, target)
* combo. Across a fleet of N pods this caps at N events/min, well within
* Sentry's own dedup tolerance.
*
* Only "real" errors are reported here:
* - backend_error: Redis/Dragonfly unreachable
* - release_error: lock release failed (TTL expired or backend dropped)
*
* Skipped on purpose: contention 409s (normal user-facing concurrency),
* skip-on-contention events (idempotent fan-out by design), acquire retry
* conflicts (internal loop), destructor cleanups (have an expected
* baseline rate; aggregate via the `lock.attempts` counter instead).
*/
$lockErrorReporter = function (Log $log, ?Logger $logger, string $action, string $key, string $target, Throwable $e): void {
static $lastReportAt = [];
Console::warning("Lock {$action} for {$key}: {$e->getMessage()}");
if ($logger === null) {
return;
}
$bucket = $action . ':' . $target;
$now = time();
if (($lastReportAt[$bucket] ?? 0) + 60 > $now) {
return;
}
$lastReportAt[$bucket] = $now;
$log->setNamespace('http');
$log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));
$log->setVersion(APP_VERSION_STABLE);
$log->setType(Log::TYPE_WARNING);
$log->setMessage('Distributed lock ' . $action . ': ' . $e->getMessage());
$log->setAction("lock.{$action}");
$log->setEnvironment(System::getEnv('_APP_ENV', 'development') === 'production'
? Log::ENVIRONMENT_PRODUCTION
: Log::ENVIRONMENT_STAGING);
$log->addTag('lock.target', $target);
// Strip the trailing document ID from the key so log aggregators don't
// see unbounded cardinality; the {target} attribute already covers slicing.
$log->addTag('lock.key_pattern', preg_replace('/:[^:]+$/', ':*', $key));
$log->addTag('code', $e->getCode());
$log->addExtra('file', $e->getFile());
$log->addExtra('line', $e->getLine());
$log->addExtra('trace', $e->getTraceAsString());
try {
$logger->addLog($log);
} catch (Throwable) {
// best-effort; don't let logging failures break fail-open
}
};
// Extract the collection segment ("keys" / "projects" / "users") from keys
// of the form "lock:platform:{target}:{id}" so metrics can slice by target.
$lockTargetOf = function (string $key): string {
$parts = explode(':', $key, 4);
return $parts[2] ?? 'unknown';
};
/**
* Distributed-lock factory: skip-on-contention variant.
*
* For idempotent writes where losing the race is correct (e.g., per-request
* `accessedAt` updates from N pods all writing the same value). On contention,
* the callback is silently skipped — another pod is doing the same work.
*
* Behavior:
* - Non-blocking acquire (one attempt). On conflict, skip and return.
* - Fail-open: if Redis is unreachable, run the callback unlocked + warn.
* - Kill switch: `_APP_LOCKING_ENABLED=disabled` runs the callback unlocked.
*
* Returns void — the caller can't distinguish "acquired and ran" from "skipped".
*
* Metric: `lock.attempts{outcome,target}` where outcome ∈ {acquired, skipped,
* backend_error, release_error}.
*/
$container->set('distributedLock', function (\Redis $redis, Telemetry $telemetry, Log $log, ?Logger $logger) use ($lockTargetOf, $lockErrorReporter) {
$enabled = System::getEnv('_APP_LOCKING_ENABLED', 'enabled') !== 'disabled';
$attempts = $telemetry->createCounter('lock.attempts', null, 'Distributed lock acquire outcomes');
if (! $enabled) {
return function (string $key, \Closure $fn, float $ttl = 5.0): void {
$fn();
};
}
return function (string $key, \Closure $fn, float $ttl = 5.0) use ($redis, $attempts, $log, $logger, $lockTargetOf, $lockErrorReporter): void {
$target = $lockTargetOf($key);
$lock = new DistributedLock($redis, $key, (int) $ttl);
try {
$acquired = $lock->tryAcquire();
} catch (\RedisException $e) {
$attempts->add(1, ['outcome' => 'backend_error', 'target' => $target]);
$lockErrorReporter($log, $logger, 'backend_error', $key, $target, $e);
$fn();
return;
}
if (! $acquired) {
$attempts->add(1, ['outcome' => 'skipped', 'target' => $target]);
return;
}
$attempts->add(1, ['outcome' => 'acquired', 'target' => $target]);
try {
$fn();
} finally {
try {
$lock->release();
} catch (\Throwable $e) {
$attempts->add(1, ['outcome' => 'release_error', 'target' => $target]);
$lockErrorReporter($log, $logger, 'release_error', $key, $target, $e);
}
}
};
}, ['redis', 'telemetry', 'log', 'logger']);
/**
* Distributed-lock factory: 409-on-contention variant.
*
* For explicit user-write endpoints where read-modify-write on shared mutable
* state must NOT silently drop a request. On contention, throws
* `Exception::GENERAL_RESOURCE_LOCKED` (HTTP 409) so the client retries.
*
* Behavior:
* - Blocking acquire with short timeout (default 3s).
* - On timeout, throws `GENERAL_RESOURCE_LOCKED`.
* - Fail-open: backend unreachable runs the callback unlocked + warning.
* - Kill switch: `_APP_LOCKING_ENABLED=disabled` runs the callback unlocked.
* - Returns the callback's return value so callers can use the result.
*
* Metric: `lock.attempts{outcome,target}` where outcome ∈ {acquired, contended,
* backend_error, release_error}.
*/
$container->set('distributedLockOrFail', function (\Redis $redis, Telemetry $telemetry, Log $log, ?Logger $logger) use ($lockTargetOf, $lockErrorReporter) {
$enabled = System::getEnv('_APP_LOCKING_ENABLED', 'enabled') !== 'disabled';
$attempts = $telemetry->createCounter('lock.attempts', null, 'Distributed lock acquire outcomes');
if (! $enabled) {
return function (string $key, \Closure $fn, float $ttl = 10.0, float $waitTimeout = 3.0): mixed {
return $fn();
};
}
return function (string $key, \Closure $fn, float $ttl = 10.0, float $waitTimeout = 3.0) use ($redis, $attempts, $log, $logger, $lockTargetOf, $lockErrorReporter): mixed {
$target = $lockTargetOf($key);
$lock = new DistributedLock($redis, $key, (int) $ttl);
try {
$acquired = $lock->acquire($waitTimeout);
} catch (\RedisException $e) {
$attempts->add(1, ['outcome' => 'backend_error', 'target' => $target]);
$lockErrorReporter($log, $logger, 'backend_error', $key, $target, $e);
return $fn();
}
if (! $acquired) {
$attempts->add(1, ['outcome' => 'contended', 'target' => $target]);
// Don't pass a custom message — the catalog message in
// app/config/errors.php is reused so we don't leak the internal
// lock key (which embeds collection name and document id) into a
// user-facing 409 response. The telemetry attribute already
// carries the target collection for operator-side observability.
throw new Exception(Exception::GENERAL_RESOURCE_LOCKED);
}
$attempts->add(1, ['outcome' => 'acquired', 'target' => $target]);
try {
return $fn();
} finally {
try {
$lock->release();
} catch (\Throwable $e) {
$attempts->add(1, ['outcome' => 'release_error', 'target' => $target]);
$lockErrorReporter($log, $logger, 'release_error', $key, $target, $e);
}
}
};
}, ['redis', 'telemetry', 'log', 'logger']);
$container->set('authorization', function () {
return new Authorization();
}, []);
@@ -12,8 +12,6 @@ use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Logger\Log;
use Utopia\Logger\Logger;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
use Utopia\Validator\WhiteList;
@@ -63,8 +61,6 @@ class Update extends Action
->inject('authorization')
->inject('queueForEvents')
->inject('distributedLockOrFail')
->inject('log')
->inject('logger')
->callback($this->action(...));
}
@@ -77,8 +73,6 @@ class Update extends Action
Authorization $authorization,
Event $queueForEvents,
callable $distributedLockOrFail,
Log $log,
?Logger $logger,
): void {
$auth = Config::getParam('auth')[$methodId] ?? [];
$authKey = $auth['key'] ?? '';
@@ -92,7 +86,7 @@ class Update extends Action
return $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), new Document([
'auths' => $auths,
])));
}, log: $log, logger: $logger);
});
$queueForEvents->setParam('methodId', $methodId);
@@ -13,8 +13,6 @@ use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Logger\Log;
use Utopia\Logger\Logger;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Text;
@@ -62,8 +60,6 @@ class Create extends Action
->inject('dbForPlatform')
->inject('authorization')
->inject('distributedLockOrFail')
->inject('log')
->inject('logger')
->callback($this->action(...));
}
@@ -76,8 +72,6 @@ class Create extends Action
Database $dbForPlatform,
Authorization $authorization,
callable $distributedLockOrFail,
Log $log,
?Logger $logger,
) {
// Set to now date
$mockNumber = [
@@ -109,7 +103,7 @@ class Create extends Action
$authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), new Document([
'auths' => $auths,
])));
}, log: $log, logger: $logger);
});
$queueForEvents->setParam('number', $number);
@@ -13,8 +13,6 @@ use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Logger\Log;
use Utopia\Logger\Logger;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
@@ -61,8 +59,6 @@ class Delete extends Action
->inject('dbForPlatform')
->inject('authorization')
->inject('distributedLockOrFail')
->inject('log')
->inject('logger')
->callback($this->action(...));
}
@@ -74,8 +70,6 @@ class Delete extends Action
Database $dbForPlatform,
Authorization $authorization,
callable $distributedLockOrFail,
Log $log,
?Logger $logger,
) {
$distributedLockOrFail("lock:platform:projects:{$project->getId()}", function () use ($project, $number, $dbForPlatform, $authorization) {
$project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $project->getId()));
@@ -101,7 +95,7 @@ class Delete extends Action
$authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), new Document([
'auths' => $auths,
])));
}, log: $log, logger: $logger);
});
$queueForEvents->setParam('number', $number);
@@ -13,8 +13,6 @@ use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Logger\Log;
use Utopia\Logger\Logger;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Text;
@@ -62,8 +60,6 @@ class Update extends Action
->inject('dbForPlatform')
->inject('authorization')
->inject('distributedLockOrFail')
->inject('log')
->inject('logger')
->callback($this->action(...));
}
@@ -76,8 +72,6 @@ class Update extends Action
Database $dbForPlatform,
Authorization $authorization,
callable $distributedLockOrFail,
Log $log,
?Logger $logger,
) {
$mockNumber = $distributedLockOrFail("lock:platform:projects:{$project->getId()}", function () use ($project, $number, $otp, $dbForPlatform, $authorization) {
$project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $project->getId()));
@@ -106,7 +100,7 @@ class Update extends Action
])));
return $mockNumbers[$mockNumberIndex];
}, log: $log, logger: $logger);
});
$queueForEvents->setParam('number', $number);
@@ -11,8 +11,6 @@ use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Logger\Log;
use Utopia\Logger\Logger;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
@@ -63,8 +61,6 @@ class Update extends Action
->inject('authorization')
->inject('queueForEvents')
->inject('distributedLockOrFail')
->inject('log')
->inject('logger')
->callback($this->action(...));
}
@@ -80,8 +76,6 @@ class Update extends Action
Authorization $authorization,
Event $queueForEvents,
callable $distributedLockOrFail,
Log $log,
?Logger $logger,
): void {
$project = $distributedLockOrFail("lock:platform:projects:{$project->getId()}", function () use ($project, $userId, $userEmail, $userPhone, $userName, $userMFA, $dbForPlatform, $authorization) {
$project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $project->getId()));
@@ -107,7 +101,7 @@ class Update extends Action
return $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), new Document([
'auths' => $auths,
])));
}, log: $log, logger: $logger);
});
$queueForEvents
->setParam('projectId', $project->getId())
@@ -11,8 +11,6 @@ use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Logger\Log;
use Utopia\Logger\Logger;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
@@ -59,8 +57,6 @@ class Update extends Action
->inject('authorization')
->inject('queueForEvents')
->inject('distributedLockOrFail')
->inject('log')
->inject('logger')
->callback($this->action(...));
}
@@ -72,8 +68,6 @@ class Update extends Action
Authorization $authorization,
Event $queueForEvents,
callable $distributedLockOrFail,
Log $log,
?Logger $logger,
): void {
$project = $distributedLockOrFail("lock:platform:projects:{$project->getId()}", function () use ($project, $enabled, $dbForPlatform, $authorization) {
$project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $project->getId()));
@@ -84,7 +78,7 @@ class Update extends Action
return $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), new Document([
'auths' => $auths,
])));
}, log: $log, logger: $logger);
});
$queueForEvents
->setParam('projectId', $project->getId())
@@ -11,8 +11,6 @@ use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Logger\Log;
use Utopia\Logger\Logger;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Nullable;
use Utopia\Validator\Range;
@@ -62,8 +60,6 @@ class Update extends Action
->inject('authorization')
->inject('queueForEvents')
->inject('distributedLockOrFail')
->inject('log')
->inject('logger')
->callback($this->action(...));
}
@@ -75,8 +71,6 @@ class Update extends Action
Authorization $authorization,
Event $queueForEvents,
callable $distributedLockOrFail,
Log $log,
?Logger $logger,
): void {
$project = $distributedLockOrFail("lock:platform:projects:{$project->getId()}", function () use ($project, $total, $dbForPlatform, $authorization) {
$project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $project->getId()));
@@ -87,7 +81,7 @@ class Update extends Action
return $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), new Document([
'auths' => $auths,
])));
}, log: $log, logger: $logger);
});
$queueForEvents
->setParam('projectId', $project->getId())
@@ -11,8 +11,6 @@ use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Logger\Log;
use Utopia\Logger\Logger;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
@@ -60,8 +58,6 @@ class Update extends Action
->inject('authorization')
->inject('queueForEvents')
->inject('distributedLockOrFail')
->inject('log')
->inject('logger')
->callback($this->action(...));
}
@@ -73,8 +69,6 @@ class Update extends Action
Authorization $authorization,
Event $queueForEvents,
callable $distributedLockOrFail,
Log $log,
?Logger $logger,
): void {
$project = $distributedLockOrFail("lock:platform:projects:{$project->getId()}", function () use ($project, $enabled, $dbForPlatform, $authorization) {
$project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $project->getId()));
@@ -85,7 +79,7 @@ class Update extends Action
return $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), new Document([
'auths' => $auths,
])));
}, log: $log, logger: $logger);
});
$queueForEvents
->setParam('projectId', $project->getId())
@@ -11,8 +11,6 @@ use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Logger\Log;
use Utopia\Logger\Logger;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
@@ -59,8 +57,6 @@ class Update extends Action
->inject('authorization')
->inject('queueForEvents')
->inject('distributedLockOrFail')
->inject('log')
->inject('logger')
->callback($this->action(...));
}
@@ -72,8 +68,6 @@ class Update extends Action
Authorization $authorization,
Event $queueForEvents,
callable $distributedLockOrFail,
Log $log,
?Logger $logger,
): void {
$project = $distributedLockOrFail("lock:platform:projects:{$project->getId()}", function () use ($project, $enabled, $dbForPlatform, $authorization) {
$project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $project->getId()));
@@ -84,7 +78,7 @@ class Update extends Action
return $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), new Document([
'auths' => $auths,
])));
}, log: $log, logger: $logger);
});
$queueForEvents
->setParam('projectId', $project->getId())
@@ -11,8 +11,6 @@ use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Logger\Log;
use Utopia\Logger\Logger;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Range;
@@ -59,8 +57,6 @@ class Update extends Action
->inject('authorization')
->inject('queueForEvents')
->inject('distributedLockOrFail')
->inject('log')
->inject('logger')
->callback($this->action(...));
}
@@ -72,8 +68,6 @@ class Update extends Action
Authorization $authorization,
Event $queueForEvents,
callable $distributedLockOrFail,
Log $log,
?Logger $logger,
): void {
$project = $distributedLockOrFail("lock:platform:projects:{$project->getId()}", function () use ($project, $duration, $dbForPlatform, $authorization) {
$project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $project->getId()));
@@ -84,7 +78,7 @@ class Update extends Action
return $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), new Document([
'auths' => $auths,
])));
}, log: $log, logger: $logger);
});
$queueForEvents
->setParam('projectId', $project->getId())
@@ -11,8 +11,6 @@ use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Logger\Log;
use Utopia\Logger\Logger;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
@@ -59,8 +57,6 @@ class Update extends Action
->inject('authorization')
->inject('queueForEvents')
->inject('distributedLockOrFail')
->inject('log')
->inject('logger')
->callback($this->action(...));
}
@@ -72,8 +68,6 @@ class Update extends Action
Authorization $authorization,
Event $queueForEvents,
callable $distributedLockOrFail,
Log $log,
?Logger $logger,
): void {
$project = $distributedLockOrFail("lock:platform:projects:{$project->getId()}", function () use ($project, $enabled, $dbForPlatform, $authorization) {
$project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $project->getId()));
@@ -84,7 +78,7 @@ class Update extends Action
return $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), new Document([
'auths' => $auths,
])));
}, log: $log, logger: $logger);
});
$queueForEvents
->setParam('projectId', $project->getId())
@@ -11,8 +11,6 @@ use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Logger\Log;
use Utopia\Logger\Logger;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Nullable;
use Utopia\Validator\Range;
@@ -60,8 +58,6 @@ class Update extends Action
->inject('authorization')
->inject('queueForEvents')
->inject('distributedLockOrFail')
->inject('log')
->inject('logger')
->callback($this->action(...));
}
@@ -73,8 +69,6 @@ class Update extends Action
Authorization $authorization,
Event $queueForEvents,
callable $distributedLockOrFail,
Log $log,
?Logger $logger,
): void {
$project = $distributedLockOrFail("lock:platform:projects:{$project->getId()}", function () use ($project, $total, $dbForPlatform, $authorization) {
$project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $project->getId()));
@@ -85,7 +79,7 @@ class Update extends Action
return $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), new Document([
'auths' => $auths,
])));
}, log: $log, logger: $logger);
});
$queueForEvents
->setParam('projectId', $project->getId())
@@ -11,8 +11,6 @@ use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Logger\Log;
use Utopia\Logger\Logger;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Nullable;
use Utopia\Validator\Range;
@@ -60,8 +58,6 @@ class Update extends Action
->inject('authorization')
->inject('queueForEvents')
->inject('distributedLockOrFail')
->inject('log')
->inject('logger')
->callback($this->action(...));
}
@@ -73,8 +69,6 @@ class Update extends Action
Authorization $authorization,
Event $queueForEvents,
callable $distributedLockOrFail,
Log $log,
?Logger $logger,
): void {
$project = $distributedLockOrFail("lock:platform:projects:{$project->getId()}", function () use ($project, $total, $dbForPlatform, $authorization) {
$project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $project->getId()));
@@ -85,7 +79,7 @@ class Update extends Action
return $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), new Document([
'auths' => $auths,
])));
}, log: $log, logger: $logger);
});
$queueForEvents
->setParam('projectId', $project->getId())
@@ -12,8 +12,6 @@ use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Logger\Log;
use Utopia\Logger\Logger;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
use Utopia\Validator\WhiteList;
@@ -63,8 +61,6 @@ class Update extends Action
->inject('authorization')
->inject('queueForEvents')
->inject('distributedLockOrFail')
->inject('log')
->inject('logger')
->callback($this->action(...));
}
@@ -77,8 +73,6 @@ class Update extends Action
Authorization $authorization,
Event $queueForEvents,
callable $distributedLockOrFail,
Log $log,
?Logger $logger,
): void {
$project = $distributedLockOrFail("lock:platform:projects:{$project->getId()}", function () use ($project, $protocolId, $enabled, $dbForPlatform, $authorization) {
$project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $project->getId()));
@@ -89,7 +83,7 @@ class Update extends Action
return $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), new Document([
'apis' => $protocols,
])));
}, log: $log, logger: $logger);
});
$queueForEvents->setParam('protocolId', $protocolId);
@@ -14,8 +14,6 @@ use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Emails\Validator\Email;
use Utopia\Logger\Log;
use Utopia\Logger\Logger;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
use Utopia\Validator\Hostname;
@@ -75,8 +73,6 @@ class Update extends Action
->inject('project')
->inject('authorization')
->inject('distributedLockOrFail')
->inject('log')
->inject('logger')
->callback($this->action(...));
}
@@ -97,8 +93,6 @@ class Update extends Action
Document $project,
Authorization $authorization,
callable $distributedLockOrFail,
Log $log,
?Logger $logger,
): void {
$inputs = [
'host' => $host,
@@ -191,7 +185,7 @@ class Update extends Action
return $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), new Document([
'smtp' => $smtp,
])));
}, log: $log, logger: $logger);
});
$response->dynamic($project, Response::MODEL_PROJECT);
}
@@ -12,8 +12,6 @@ use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Logger\Log;
use Utopia\Logger\Logger;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
use Utopia\Validator\WhiteList;
@@ -63,8 +61,6 @@ class Update extends Action
->inject('authorization')
->inject('queueForEvents')
->inject('distributedLockOrFail')
->inject('log')
->inject('logger')
->callback($this->action(...));
}
@@ -77,8 +73,6 @@ class Update extends Action
Authorization $authorization,
Event $queueForEvents,
callable $distributedLockOrFail,
Log $log,
?Logger $logger,
): void {
// The services map is a JSON object on the project document. Two
// concurrent service toggles read the same baseline, each set their
@@ -96,7 +90,7 @@ class Update extends Action
return $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), new Document([
'services' => $services,
])));
}, log: $log, logger: $logger);
});
$queueForEvents->setParam('serviceId', $serviceId);
@@ -13,8 +13,6 @@ use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Emails\Validator\Email;
use Utopia\Logger\Log;
use Utopia\Logger\Logger;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
@@ -72,8 +70,6 @@ class Update extends Action
->inject('authorization')
->inject('project')
->inject('distributedLockOrFail')
->inject('log')
->inject('logger')
->callback($this->action(...));
}
@@ -92,8 +88,6 @@ class Update extends Action
Authorization $authorization,
Document $project,
callable $distributedLockOrFail,
Log $log,
?Logger $logger,
) {
$locale = $locale ?: System::getEnv('_APP_LOCALE', 'en');
@@ -147,7 +141,7 @@ class Update extends Action
])));
return $template;
}, log: $log, logger: $logger);
});
$queueForEvents->setParam('templateId', $templateId);
@@ -13,8 +13,6 @@ use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Database\Validator\UID;
use Utopia\Logger\Log;
use Utopia\Logger\Logger;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator;
@@ -58,12 +56,10 @@ class Update extends Action
->inject('response')
->inject('dbForPlatform')
->inject('distributedLockOrFail')
->inject('log')
->inject('logger')
->callback($this->action(...));
}
public function action(string $projectId, string $teamId, Response $response, Database $dbForPlatform, callable $distributedLockOrFail, Log $log, ?Logger $logger)
public function action(string $projectId, string $teamId, Response $response, Database $dbForPlatform, callable $distributedLockOrFail)
{
// Lock around the project doc RMW. Cascade fan-out to installations,
// repositories and vcsComments runs after the lock is released —
@@ -89,7 +85,7 @@ class Update extends Action
]));
return [$project, $permissions];
}, log: $log, logger: $logger);
});
$installations = $dbForPlatform->find('installations', [
Query::equal('projectInternalId', [$project->getSequence()]),
@@ -10,8 +10,6 @@ use Appwrite\Utopia\Database\Validator\Queries\Projects;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Validator\UID;
use Utopia\Logger\Log;
use Utopia\Logger\Logger;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator;
use Utopia\Validator\Text;
@@ -68,12 +66,10 @@ class Update extends Action
->inject('response')
->inject('dbForPlatform')
->inject('distributedLockOrFail')
->inject('log')
->inject('logger')
->callback($this->action(...));
}
public function action(string $projectId, string $name, string $description, string $logo, string $url, string $legalName, string $legalCountry, string $legalState, string $legalCity, string $legalAddress, string $legalTaxId, Response $response, Database $dbForPlatform, callable $distributedLockOrFail, Log $log, ?Logger $logger)
public function action(string $projectId, string $name, string $description, string $logo, string $url, string $legalName, string $legalCountry, string $legalState, string $legalCity, string $legalAddress, string $legalTaxId, Response $response, Database $dbForPlatform, callable $distributedLockOrFail)
{
// Re-fetch and write the full project doc inside the lock. This is the
// worst RMW window in the projects API — the endpoint passes the entire
@@ -98,7 +94,7 @@ class Update extends Action
->setAttribute('legalAddress', $legalAddress)
->setAttribute('legalTaxId', $legalTaxId)
->setAttribute('search', implode(' ', [$projectId, $name])));
}, log: $log, logger: $logger);
});
$response->dynamic($project, Response::MODEL_PROJECT);
}