egametang/ET · error · RpcException

location unlock failed key: {key} oldActorId: {oldActorId} n

Error message

location unlock failed key: {key} oldActorId: {oldActorId} newActorId: {newActorId} lockToken: {lockToken} error: {response.Message}

What it means

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).

Source

Thrown at Packages/cn.etetet.actorlocation/Scripts/Hotfix/Server/LocationProxyComponentSystem.cs:273

        }

        public static async ETTask UnLock(this LocationProxyComponent self, int type, long key, ActorId oldActorId,
            ActorId newActorId, long lockToken)
        {
            Log.Info(
                $"location proxy unlock {key}, {newActorId} lockToken: {lockToken} {self.GetSingleton<TimeInfo>().ServerNow()}");

            ObjectUnLockRequest request = ObjectUnLockRequest.Create();
            request.Type = type;
            request.Key = key;
            request.OldActorId = oldActorId;
            request.NewActorId = newActorId;
            request.LockToken = lockToken;

            ObjectUnLockResponse response = (ObjectUnLockResponse)await self.CallPrimaryWithRetry(key, request);
            if (response.Error != ErrorCode.ERR_Success)
            {
                throw new RpcException(response.Error,
                    $"location unlock failed key: {key} oldActorId: {oldActorId} newActorId: {newActorId} lockToken: {lockToken} error: {response.Message}");
            }
        }

        public static async ETTask UnLockWithRetry(this LocationProxyComponent self, int type, long key, ActorId oldActorId,
            ActorId newActorId, long lockToken)
        {
            EntityRef<LocationProxyComponent> selfRef = self;
            int retryCount = 0;
            while (true)
            {
                self = selfRef;
                if (self == null)
                {
                    throw new Exception("location proxy disposed");
                }

                try

View on GitHub (pinned to 5cab01f7a8)

Solutions

  1. 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.
  2. Increase the lock timeout parameter in LockWithToken if the locked operation is long-running.
  3. Ensure the lockToken returned by LockWithToken is correctly stored and propagated to the eventual unlock call without loss or mutation.
  4. Catch RpcException and inspect e.Error to distinguish recoverable (lock already gone) from genuine failures.

Example fix

// before
long token = await proxy.LockWithToken(type, key, actorId);
// ... work ...
await proxy.UnLock(type, key, oldActorId, newActorId, token); // may throw on token mismatch

// after
long token = await proxy.LockWithToken(type, key, actorId);
// ... work ...
await proxy.UnLockWithRetry(type, key, oldActorId, newActorId, token); // compensates automatically
Defensive patterns

Strategy: try-catch

Try / catch

// Option A: use UnLockWithRetry (handles LockNotFound, OwnerMismatch, TokenMismatch)
await proxy.UnLockWithRetry(type, key, oldActorId, newActorId, lockToken);

// Option B: manual catch for specific error codes
try
{
    await proxy.UnLock(type, key, oldActorId, newActorId, lockToken);
}
catch (RpcException e) when (
    e.Error == ErrorCode.ERR_LocationLockTokenMismatch
    || e.Error == ErrorCode.ERR_LocationLockOwnerMismatch
    || e.Error == ErrorCode.ERR_LocationLockNotFound)
{
    // Lock already expired or was released — verify the actor landed correctly
    ActorId current = await proxy.Get(type, key);
    if (current == newActorId)
    {
        Log.Warning($"unlock failed but actor already at target: {key}");
        return; // acceptable
    }
    throw; // genuine failure
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of egametang/ET@5cab01f7a8 (2026-08-13). Data as JSON: /api/errors/2d0a38a9b4d6d43a. Report an issue: GitHub.