[LockManager] Update token and metadata when an expired lock is re-acquired (#220476)

Related: https://github.com/elastic/kibana/pull/216397

This fixes a bug in the Lock Manager where an expired lock can be
acquired, but the token and metadata is not updated. This means that the
lock cannot be released. Instead it is automatically released when the
TTL expires.
This commit is contained in:
Søren Louv-Jansen 2025-05-08 19:40:20 +02:00 committed by GitHub
parent 9fc42af400
commit 74e876d12d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 38 additions and 12 deletions

View file

@ -87,6 +87,8 @@ export class LockManager {
def instantNow = Instant.ofEpochMilli(now); def instantNow = Instant.ofEpochMilli(now);
ctx._source.createdAt = instantNow.toString(); ctx._source.createdAt = instantNow.toString();
ctx._source.expiresAt = instantNow.plusMillis(params.ttl).toString(); ctx._source.expiresAt = instantNow.plusMillis(params.ttl).toString();
ctx._source.metadata = params.metadata;
ctx._source.token = params.token;
} else { } else {
ctx.op = 'noop'; ctx.op = 'noop';
} }
@ -94,13 +96,11 @@ export class LockManager {
params: { params: {
ttl, ttl,
token: this.token, token: this.token,
metadata,
}, },
}, },
// @ts-expect-error // @ts-expect-error
upsert: { upsert: {},
metadata,
token: this.token,
},
}, },
{ {
retryOnTimeout: true, retryOnTimeout: true,
@ -189,7 +189,7 @@ export class LockManager {
this.logger.debug(`Lock "${this.lockId}" released with token ${this.token}.`); this.logger.debug(`Lock "${this.lockId}" released with token ${this.token}.`);
return true; return true;
case 'noop': case 'noop':
this.logger.debug( this.logger.warn(
`Lock "${this.lockId}" with token = ${this.token} could not be released. Token does not match.` `Lock "${this.lockId}" with token = ${this.token} could not be released. Token does not match.`
); );
return false; return false;

View file

@ -236,15 +236,41 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon
expect(lock?.metadata).to.eql({ attempt: 'one' }); expect(lock?.metadata).to.eql({ attempt: 'one' });
}); });
it('allows re-acquisition after expiration', async () => { describe('when a lock by "manager1" expires, and is attempted re-acquired by "manager2"', () => {
// Acquire with a very short TTL. let expiredLock: LockDocument | undefined;
const acquired = await manager1.acquire({ ttl: 500, metadata: { attempt: 'one' } }); let reacquireResult: boolean;
expect(acquired).to.be(true); beforeEach(async () => {
// Acquire with a very short TTL.
const acquired = await manager1.acquire({ ttl: 500, metadata: { attempt: 'one' } });
expect(acquired).to.be(true);
await sleep(1000); // wait for lock to expire
expiredLock = await getLockById(es, LOCK_ID);
reacquireResult = await manager2.acquire({ metadata: { attempt: 'two' } });
});
await sleep(1000); // wait for lock to expire it('can be re-acquired', async () => {
expect(reacquireResult).to.be(true);
});
const reacquired = await manager2.acquire({ metadata: { attempt: 'two' } }); it('updates the token when re-acquired', async () => {
expect(reacquired).to.be(true); const reacquiredLock = await getLockById(es, LOCK_ID);
expect(expiredLock?.token).not.to.be(reacquiredLock?.token);
});
it('updates the metadata when re-acquired', async () => {
const reacquiredLock = await getLockById(es, LOCK_ID);
expect(reacquiredLock?.metadata).to.eql({ attempt: 'two' });
});
it('cannot be released by "manager1"', async () => {
const res = await manager1.release();
expect(res).to.be(false);
});
it('can be released by "manager2"', async () => {
const res = await manager2.release();
expect(res).to.be(true);
});
}); });
}); });