Bỏ qua, đến nội dung

@digiforce-nc/lock-manager

Package@digiforce-nc/lock-manager
Depends on(standalone)

1) Tổng quan

@digiforce-nc/lock-manager cung cấp cơ chế distributed locking cho các thao tác đồng thời trong hệ thống Digiforce. Thiết kế theo mô hình pluggable adapter - cho phép chuyển đổi giữa lock in-process (local) và lock phân tán (Redis) mà không thay đổi code ứng dụng.

Khi nào cần lock?

Lock cần thiết khi nhiều process/request cùng truy cập một tài nguyên và cần đảm bảo chỉ một thao tác được thực hiện tại một thời điểm - ví dụ: chạy migration, xử lý webhook, cập nhật counter.


2) LockManager API

acquire(key, ttl?)

Yêu cầu lock cho một key. Trả về ILock object dùng để release khi hoàn tất.

typescript
const lock = await lockManager.acquire('migration:users', 30000);
// key: identifier duy nhất cho tài nguyên cần lock
// ttl: thời gian sống tối đa (ms), tự release nếu quá hạn

try {
  await runMigration();
} finally {
  await lock.release();
}
Tham sốKiểuMô tả
keystringIdentifier duy nhất cho lock
ttlnumber (optional)Time-to-live tính bằng milliseconds. Lock tự giải phóng sau thời gian này

release(key)

Giải phóng lock theo key (alternative cho lock.release()).

registerAdapter(name, adapter)

Đăng ký adapter mới:

typescript
lockManager.registerAdapter('redis', new RedisLockAdapter(redisClient));
lockManager.setDefaultAdapter('redis');

setDefaultAdapter(name)

Chọn adapter mặc định sử dụng khi gọi acquire().


3) ILockAdapter interface

Mọi adapter phải implement interface sau:

typescript
interface ILockAdapter {
  acquire(key: string, ttl?: number): Promise<ILock>;
}

interface ILock {
  release(): Promise<void>;
}

Quy ước adapter

  • acquire() phải block (await) cho đến khi lấy được lock hoặc timeout
  • Nếu không lấy được lock trong thời gian cho phép, throw LockAcquireError
  • release() phải idempotent - gọi nhiều lần không gây lỗi

4) Built-in adapters

Local adapter (mặc định)

Sử dụng thư viện async-mutex để lock trong cùng một process Node.js:

Giới hạn Local adapter

Local adapter chỉ hoạt động trong cùng một process. Khi deploy nhiều instance (cluster mode, multiple containers), lock sẽ không đồng bộ giữa các instance. Dùng Redis adapter cho môi trường production multi-instance.

Redis adapter

Cung cấp qua plugin @digiforce-nc/plugin-lock-adapter-redis:

typescript
// Plugin tự đăng ký adapter khi load
// Cấu hình trong .env:
// LOCK_ADAPTER=redis
// REDIS_URL=redis://localhost:6379

Redis adapter sử dụng thuật toán Redlock để đảm bảo distributed locking an toàn ngay cả khi có failover.


5) Error types

LockAcquireError

Throw khi không thể lấy lock (timeout, adapter lỗi):

typescript
import { LockAcquireError } from '@digiforce-nc/lock-manager';

try {
  const lock = await lockManager.acquire('resource:key', 5000);
} catch (error) {
  if (error instanceof LockAcquireError) {
    console.log('Không thể lấy lock - tài nguyên đang bận');
  }
}

LockAbortError

Throw khi lock bị hủy trước khi hoàn tất (ví dụ: process shutdown):

typescript
import { LockAbortError } from '@digiforce-nc/lock-manager';

try {
  const lock = await lockManager.acquire('long-task', 60000);
  await longRunningTask();
  await lock.release();
} catch (error) {
  if (error instanceof LockAbortError) {
    console.log('Lock bị hủy - process đang shutdown');
  }
}

6) Use cases

Ngăn duplicate operations

typescript
async function processWebhook(webhookId: string, payload: any) {
  const lock = await app.lockManager.acquire(`webhook:${webhookId}`, 10000);

  try {
    const existing = await db.getRepository('webhookLogs').findOne({
      filter: { webhookId },
    });

    if (existing) {
      return; // đã xử lý
    }

    await handleWebhook(payload);
    await db.getRepository('webhookLogs').create({
      values: { webhookId, processedAt: new Date() },
    });
  } finally {
    await lock.release();
  }
}

Distributed mutex cho migration

typescript
async function runMigrations() {
  const lock = await app.lockManager.acquire('db:migration', 120000);

  try {
    await app.db.migrator.up();
  } finally {
    await lock.release();
  }
}

Sequential task processing

typescript
async function processTask(queueName: string) {
  const lock = await app.lockManager.acquire(`queue:${queueName}`);

  try {
    const task = await getNextTask(queueName);
    if (task) {
      await executeTask(task);
      await markComplete(task.id);
    }
  } finally {
    await lock.release();
  }
}

7) Cấu hình

Biến môi trườngMô tảMặc định
LOCK_ADAPTERAdapter sử dụng (local hoặc redis)local
LOCK_DEFAULT_TTLTTL mặc định (ms)30000

TTL quá ngắn

Nếu TTL ngắn hơn thời gian thực thi task, lock sẽ tự giải phóng trong khi task vẫn đang chạy → race condition. Luôn đặt TTL lớn hơn worst-case execution time và sử dụng try/finally để release sớm.