{"record":{"id":"7403ded808132c8c","repo":"appwrite/appwrite","slug":"general-resource-locked","errorCode":"general_resource_locked","errorMessage":"The requested resource is currently being modified by another request. Please retry after a brief delay.","messagePattern":"The requested resource is currently being modified by another request\\. Please retry after a brief delay\\.","errorType":"exception","errorClass":"Appwrite\\Extend\\Exception","httpStatus":409,"severity":"warning","filePath":"src/Appwrite/Locking/Lock.php","lineNumber":173,"sourceCode":"    ): mixed {\n        try {\n            $acquired = $lock->acquire($waitTimeout);\n        } catch (\\RedisException $e) {\n            $this->attempts->add(1, ['outcome' => self::OUTCOME_BACKEND_ERROR, ...$labels]);\n            $this->reportError(self::OUTCOME_BACKEND_ERROR, $key, $target, $e);\n\n            $callbackException = true;\n            return $fn();\n        }\n\n        if (! $acquired) {\n            if ($skipOnContention) {\n                $this->attempts->add(1, ['outcome' => self::OUTCOME_SKIPPED, ...$labels]);\n\n                return null;\n            }\n            $this->attempts->add(1, ['outcome' => self::OUTCOME_CONTENDED, ...$labels]);\n            throw new Exception(Exception::GENERAL_RESOURCE_LOCKED);\n        }\n\n        $this->attempts->add(1, ['outcome' => self::OUTCOME_ACQUIRED, ...$labels]);\n        $callbackException = true;\n        try {\n            return $fn();\n        } finally {\n            try {\n                $lock->release();\n            } catch (Throwable $e) {\n                $this->attempts->add(1, ['outcome' => self::OUTCOME_RELEASE_ERROR, ...$labels]);\n                $this->reportError(self::OUTCOME_RELEASE_ERROR, $key, $target, $e);\n            }\n        }\n    }\n\n    /**\n     * Rate-limit backend/release reports so outages don't flood Sentry.","sourceCodeStart":155,"sourceCodeEnd":191,"githubUrl":"https://github.com/appwrite/appwrite/blob/cd368e707d4b492bc5e8e9c0f8ecbc5b741c4bf4/src/Appwrite/Locking/Lock.php#L155-L191","documentation":"Thrown by the distributed lock wrapper when a lock could not be acquired within the wait timeout and the caller did not opt into skip-on-contention. After `acquire($waitTimeout)` returns false (and it is not a Redis backend error, which falls through to running unprotected), the lock records a `CONTENDED` outcome and raises `general_resource_locked`, signalling another request is currently modifying the same resource.","triggerScenarios":"Two concurrent requests target the same lock key (e.g. the same document/resource) and the second cannot acquire the lock before its `waitTimeout` elapses, with `skipOnContention` set to false (the default protective behaviour).","commonSituations":"Concurrent writes to the same document from parallel workers/API calls; long-running migrations or bulk updates holding a lock longer than peers wait; fan-out jobs racing on a shared resource; undersized `waitTimeout` for the operation's duration.","solutions":["Retry the request after a short, jittered delay — contention is typically transient.","If the operation is idempotent and tolerates skipping, enable `skipOnContention` so the caller gets `null` instead of an exception under load.","Reduce the lock scope/hold time (smaller transactions) or increase `waitTimeout` to outlast the typical critical section."],"exampleFix":"// before: caller treats lock failure as fatal\n$lock->synchronized('doc:'.$id, fn() => update($id), waitTimeout: 0.5);\n\n// after: retry with jitter, or opt into skip-on-contention\nretry(3, fn() => $lock->synchronized('doc:'.$id, fn() => update($id), waitTimeout: 2.0));\n// or, for idempotent jobs:\n$lock->synchronized('doc:'.$id, fn() => update($id), skipOnContention: true);","handlingStrategy":"retry","validationCode":"// Predict contention by checking whether a peer is likely mid-update;\n// otherwise prefer retry/jitter over a hard failure.\n// (Application-level guard: serialize writes to the same key.)\nif (writeInFlightForResource(id)) {\n  return retryLater(id);\n}","typeGuard":null,"tryCatchPattern":"// Retry the locked operation with jittered backoff; treat RESOURCE_LOCKED as transient.\nfunction withLockRetry(callable $fn, int $tries = 3): mixed {\n  for ($i = 0; $i < $tries; $i++) {\n    try { return $fn(); }\n    catch (Exception $e) {\n      if ($e->getCode() !== Exception::GENERAL_RESOURCE_LOCKED || $i === $tries - 1) throw $e;\n      usleep((100000 + random_int(0, 100000)) * ($i + 1));\n    }\n  }\n}","preventionTips":["Design write paths so concurrent writers rarely target the same lock key (shard or queue).","Prefer `skipOnContention: true` for idempotent jobs where skipping a turn is acceptable.","Size `waitTimeout` to exceed the critical section's worst-case duration."],"tags":["locking","concurrency","contention","retry","redis"],"backgroundTag":null,"analyzedSha":"cd368e707d4b492bc5e8e9c0f8ecbc5b741c4bf4","analyzedAt":"2026-08-12T14:42:48.571Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}