File size: 3,010 Bytes
4327358
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
import Redis from 'ioredis';
import { Logger } from 'pino';

import { RMutexClient } from './types';

// Lua script to unlock a key if the current value matches the provided lockId
const LUA_UNLOCK_SCRIPT = `if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end`;
// Lua script to extend the TTL of a key if the current value matches the provided lockId
const LUA_EXTEND_SCRIPT = `if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("pexpire", KEYS[1], ARGV[2]) else return 0 end`;

/**
 * Implementation of RMutexClient using Redis
 */
export class RedisMutexClient implements RMutexClient {
  constructor(
    private readonly redis: Redis,
    private readonly logger: Logger,
  ) {}

  /**
   * Attempts to acquire a lock on a key
   * @param key The key to lock
   * @param lockId The unique ID for this lock
   * @param ttl Time to live in milliseconds
   * @returns true if the lock was acquired, false otherwise
   */
  async acquireLock(
    key: string,
    lockId: string,
    ttl: number,
  ): Promise<boolean> {
    this.logger.debug({ key, lockId, ttl }, 'Attempting to acquire lock');
    const result = await this.redis.set(key, lockId, 'PX', ttl, 'NX');

    if (result !== 'OK') {
      this.logger.debug({ key }, 'Failed to acquire lock');
      return false;
    }

    this.logger.debug({ key, lockId }, 'Successfully acquired lock');
    return true;
  }

  /**
   * Releases a lock if the current value matches the lockId
   * @param key The key to unlock
   * @param lockId The unique ID for this lock
   * @returns true if the lock was released, false otherwise
   */
  async releaseLock(key: string, lockId: string): Promise<boolean> {
    this.logger.debug({ key, lockId }, 'Unlocking key');

    // Use Lua script to ensure atomicity and ownership verification
    const result = await this.redis.eval(LUA_UNLOCK_SCRIPT, 1, key, lockId);

    const success = result === 1;
    if (!success) {
      this.logger.debug({ key }, 'Failed to unlock key');
    } else {
      this.logger.debug({ key, lockId, success }, 'Unlock result');
    }

    return success;
  }

  /**
   * Extends the TTL of a lock if the current value matches the lockId
   * @param key The key to extend
   * @param lockId The unique ID for this lock
   * @param ttl New TTL in milliseconds
   * @returns true if the TTL was extended, false otherwise
   */
  async extendLock(key: string, lockId: string, ttl: number): Promise<boolean> {
    this.logger.debug({ key, lockId, ttl }, 'Extending TTL for key');

    // Use Lua script to ensure atomicity and ownership verification
    const result = await this.redis.eval(
      LUA_EXTEND_SCRIPT,
      1,
      key,
      lockId,
      ttl.toString(),
    );

    const success = result === 1;
    if (!success) {
      this.logger.debug({ key }, 'Failed to extend TTL for key');
    } else {
      this.logger.debug({ key, lockId, ttl, success }, 'TTL extension result');
    }

    return success;
  }
}