{"record":{"id":"2d0a38a9b4d6d43a","repo":"egametang/ET","slug":"location-unlock-failed-key-key-oldactorid-old","errorCode":null,"errorMessage":"location unlock failed key: {key} oldActorId: {oldActorId} newActorId: {newActorId} lockToken: {lockToken} error: {response.Message}","messagePattern":"location unlock failed key: (.+?) oldActorId: (.+?) newActorId: (.+?) lockToken: (.+?) error: (.+?)","errorType":"exception","errorClass":"RpcException","httpStatus":null,"severity":"error","filePath":"Packages/cn.etetet.actorlocation/Scripts/Hotfix/Server/LocationProxyComponentSystem.cs","lineNumber":273,"sourceCode":"        }\n\n        public static async ETTask UnLock(this LocationProxyComponent self, int type, long key, ActorId oldActorId,\n            ActorId newActorId, long lockToken)\n        {\n            Log.Info(\n                $\"location proxy unlock {key}, {newActorId} lockToken: {lockToken} {self.GetSingleton<TimeInfo>().ServerNow()}\");\n\n            ObjectUnLockRequest request = ObjectUnLockRequest.Create();\n            request.Type = type;\n            request.Key = key;\n            request.OldActorId = oldActorId;\n            request.NewActorId = newActorId;\n            request.LockToken = lockToken;\n\n            ObjectUnLockResponse response = (ObjectUnLockResponse)await self.CallPrimaryWithRetry(key, request);\n            if (response.Error != ErrorCode.ERR_Success)\n            {\n                throw new RpcException(response.Error,\n                    $\"location unlock failed key: {key} oldActorId: {oldActorId} newActorId: {newActorId} lockToken: {lockToken} error: {response.Message}\");\n            }\n        }\n\n        public static async ETTask UnLockWithRetry(this LocationProxyComponent self, int type, long key, ActorId oldActorId,\n            ActorId newActorId, long lockToken)\n        {\n            EntityRef<LocationProxyComponent> selfRef = self;\n            int retryCount = 0;\n            while (true)\n            {\n                self = selfRef;\n                if (self == null)\n                {\n                    throw new Exception(\"location proxy disposed\");\n                }\n\n                try","sourceCodeStart":255,"sourceCodeEnd":291,"githubUrl":"https://github.com/egametang/ET/blob/5cab01f7a8bee5f49f4781eebe9e2b1c6d7ebe0f/Packages/cn.etetet.actorlocation/Scripts/Hotfix/Server/LocationProxyComponentSystem.cs#L255-L291","documentation":"The valid token-based UnLock overload sent an ObjectUnLockRequest to the primary location server via CallPrimaryWithRetry (which already retries on primary failover up to locationRequestRetryTimes). The server returned a non-success error, so an RpcException is thrown carrying the server's error code and message. Common server-side codes include ERR_LocationLockTokenMismatch (wrong token), ERR_LocationLockOwnerMismatch (oldActorId doesn't match the lock owner), and ERR_LocationLockNotFound (lock expired or was never acquired).","triggerScenarios":"UnLock(type, key, oldActorId, newActorId, lockToken) where the lockToken doesn't match the server's stored token, oldActorId differs from the lock owner, or the lock already expired (default 60s timeout) and was garbage-collected by the server.","commonSituations":"The lock's 60s timeout elapsed before unlock was called. Concurrent unlock attempts where one succeeds and invalidates the token. Actor migration where oldActorId changed between lock and unlock. Process crash after lock but before unlock, leaving a stale lock that expires.","solutions":["Use UnLockWithRetry instead of UnLock — it catches LockNotFound, OwnerMismatch, and TokenMismatch, then compensates by re-querying location to verify the actor already landed at newActorId.","Increase the lock timeout parameter in LockWithToken if the locked operation is long-running.","Ensure the lockToken returned by LockWithToken is correctly stored and propagated to the eventual unlock call without loss or mutation.","Catch RpcException and inspect e.Error to distinguish recoverable (lock already gone) from genuine failures."],"exampleFix":"// before\nlong token = await proxy.LockWithToken(type, key, actorId);\n// ... work ...\nawait proxy.UnLock(type, key, oldActorId, newActorId, token); // may throw on token mismatch\n\n// after\nlong token = await proxy.LockWithToken(type, key, actorId);\n// ... work ...\nawait proxy.UnLockWithRetry(type, key, oldActorId, newActorId, token); // compensates automatically","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"// Option A: use UnLockWithRetry (handles LockNotFound, OwnerMismatch, TokenMismatch)\nawait proxy.UnLockWithRetry(type, key, oldActorId, newActorId, lockToken);\n\n// Option B: manual catch for specific error codes\ntry\n{\n    await proxy.UnLock(type, key, oldActorId, newActorId, lockToken);\n}\ncatch (RpcException e) when (\n    e.Error == ErrorCode.ERR_LocationLockTokenMismatch\n    || e.Error == ErrorCode.ERR_LocationLockOwnerMismatch\n    || e.Error == ErrorCode.ERR_LocationLockNotFound)\n{\n    // Lock already expired or was released — verify the actor landed correctly\n    ActorId current = await proxy.Get(type, key);\n    if (current == newActorId)\n    {\n        Log.Warning($\"unlock failed but actor already at target: {key}\");\n        return; // acceptable\n    }\n    throw; // genuine failure\n}","preventionTips":["Prefer UnLockWithRetry over UnLock — it compensates for token/owner/not-found automatically.","Increase lock timeout in LockWithToken for long operations.","Propagate the lockToken faithfully from lock to unlock without mutation.","Log the lockToken at lock time to aid debugging if unlock fails."],"tags":["location-lock","rpc","server-error","unlock"],"backgroundTag":null,"analyzedSha":"5cab01f7a8bee5f49f4781eebe9e2b1c6d7ebe0f","analyzedAt":"2026-08-13T21:10:40.377Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}