メインコンテンツまでスキップ

Distributed Locks

希望する順序で data を transaction できるよう、shared resource への access をリアルタイムで管理します​

Distributed Locks を使用する理由​

2 人の user が、在庫の最後の item で同時に「購入」をクリックしたとします。両者が stock を 1 として読み取り、両者が減算し、item は 1 つしかないにもかかわらず両方の order が通ります。これは race condition です。順番に実行する必要がある 2 つの operation が concurrent に実行されています。

// Without a lock: race condition
const stock = await getStock(itemId); // Both clients read 1
await setStock(itemId, stock - 1); // Both clients write 0 -- two orders, one item
// With a lock: sequential access
const lock = await squid.acquireLock('inventory-item-123');
try {
const stock = await getStock(itemId); // Only one client reads at a time
if (stock > 0) {
await setStock(itemId, stock - 1);
}
} finally {
lock.release();
}

Distributed lock は、一度に 1 つの client のみが shared resource に access できるようにし、concurrent operation による data corruption を防ぎます。

概要​

Squid は、複数の client および application instance 間で access を調整する distributed lock を提供します。 client が lock を acquire すると、同じ lock を要求する他のすべての client は、lock が release されるまで待機します。

仕組み​

  1. client が shared resource を識別する mutex string を指定して acquireLock() を呼び出します
  2. Squid が Redis 内で lock を atomic に acquire します
  3. 別の client がすでに lock を保持している場合、request は lock が release されるか timeout が切れるまで待機します
  4. lock は保持されている間、background で自動的に renew されます
  5. lock は release() の呼び出しによって明示的に、または client の disconnect 時に自動的に release されます

Distributed lock を使用する場合​

ユースケース推奨
shared resource への concurrent modification を防ぐDistributed lock
counter の increment や balance の update を安全に行うDistributed lock
複数の client 間で exclusive access を調整するDistributed lock
atomic な single-document updateDatabase transactions を使用
backend function に rate limit を適用するRate and quota limiting を使用

クイックスタート​

前提条件​

  • @squidcloud/client がインストールされた Squid frontend project
  • lock access を許可する security rule を持つ Squid backend project

ステップ 1: backend で lock access を authorize する​

default では、すべての distributed lock は拒否されます。access を許可する security rule を追加します。

Backend code
import { secureDistributedLock, SquidService } from '@squidcloud/backend';

export class ExampleService extends SquidService {
@secureDistributedLock()
allowLocks(): boolean {
return this.isAuthenticated();
}
}

ステップ 2: backend を deploy する​

ローカル開発では、Squid CLI を使用して backend をローカルで実行します。

squid start

cloud に deploy するには、backend の deployを参照してください。

ステップ 3: client で lock を acquire して使用する​

Client code
const lock = await squid.acquireLock('my-resource');
try {
// Safely read and update a shared resource
} finally {
lock.release();
}

コアコンセプト​

Lock の acquire​

指定した mutex の distributed lock を acquire するには、acquireLock() を使用します。mutex は、保護する shared resource を一意に識別する string です。

Client code
const lock = await squid.acquireLock('my-resource');

別の client がすでに lock を保持している場合、promise は lock が利用可能になるか acquisition timeout が切れるまで待機します。

Lock option​

acquireLock() は、lock の動作を構成する optional な 2 番目の parameter を受け取ります。

Client code
const lock = await squid.acquireLock('my-resource', {
acquisitionTimeoutMillis: 5000, // Wait up to 5 seconds to acquire
maxHoldTimeMillis: 30000, // Auto-release after 30 seconds
});
OptionTypeDefault説明
acquisitionTimeoutMillisnumber2000lock が利用可能になるまで待機する maximum time(millisecond)。この時間内に lock を acquire できない場合、promise は reject されます。
maxHoldTimeMillisnumberNo limit自動 release 前に lock を保持できる maximum time(millisecond)。lock が無期限に保持されるのを防ぐ safety net として便利です。

DistributedLock object​

acquireLock() が resolve すると、次の interface を持つ DistributedLock object が返されます。

Property / MethodType説明
resourceIdstringこの lock を識別する mutex string
lockIdstringこの lock instance の unique identifier
release()Promise<void>lock を release します
isReleased()booleanlock が release されている場合に true を返します
observeRelease()Observable<void>lock が release されたときに emit します

Lock の release​

他の client が lock を acquire できるようにするには、release() を呼び出して lock を解放します。

Client code
lock.release();

lock は以下の場合にも自動的に release されます。

  • client が server から disconnect した場合
  • maxHoldTimeMillis duration が切れた場合
  • Squid client instance が destroy された場合

Lock status の確認​

lock が release されたかを確認するには、isReleased() を使用します。

Client code
console.log(lock.isReleased()); // true or false

Lock release の監視​

明示的な release または disconnect によって lock が release されたときに対応するには、observeRelease() を使用します。

Client code
// observeRelease() returns an RxJS Observable that emits once when released
lock.observeRelease().subscribe(() => {
console.log('Lock released, resource is now available');
});

Callback による Lock の管理​

withLock() を使用すると、lock の acquire、callback の実行、callback の完了時の lock の自動 release を行えます。これにより、手動の try/finally block が不要になります。

Client code
const result = await squid.withLock('my-resource', async (lock) => {
// The lock is held for the duration of this callback
const data = await readSharedData();
await updateSharedData(data + 1);
return data + 1;
});
// Lock is automatically released here, even if the callback throws

callback は DistributedLock object を parameter として受け取るため、callback 内で status の確認や release event の監視ができます。

withLock() は、optional な 3 番目の argument として acquireLock() と同じ lock option も受け取ります。これにより、callback の lock に対する acquisition timeout と maximum hold time を調整できます。

Client code
const result = await squid.withLock(
'my-resource',
async (lock) => {
return await updateSharedResource();
},
{
acquisitionTimeoutMillis: 5000, // Wait up to 5 seconds to acquire
maxHoldTimeMillis: 30000, // Auto-release after 30 seconds
}
);

Error Handling​

Acquisition timeout​

acquisitionTimeoutMillis(default: 2 秒)以内に lock を acquire できない場合、promise は LOCK_TIMEOUT error で reject されます。

Client code
try {
const lock = await squid.acquireLock('busy-resource', {
acquisitionTimeoutMillis: 3000,
});
// Use the lock...
lock.release();
} catch (error) {
console.error('Could not acquire lock:', error.message);
// Handle timeout: retry later, notify the user, etc.
}

Connection loss​

client が server への connection を失うと、保持しているすべての lock が自動的に release されます。この場合、observeRelease() observable が emit するため、予期しない release を検出できます。

Client code
const lock = await squid.acquireLock('my-resource');

let releasedByUs = false;
lock.observeRelease().subscribe(() => {
if (!releasedByUs) {
console.warn('Lock was released unexpectedly (possible disconnection)');
}
});

// Later, when releasing intentionally:
releasedByUs = true;
lock.release();

一般的な Error​

Error原因解決策
LOCK_TIMEOUT別の client が acquisitionTimeoutMillis より長く lock を保持しているtimeout を増やすか、retry logic を実装する
Client not connectedWebSocket connection が確立されていないlock を acquire する前に Squid client が初期化・接続されていることを確認する
Unauthorizedこの mutex への lock access を許可する security rule がないbackend に @secureDistributedLock rule を追加する

ベストプラクティス​

常に finally block で release する​

error が発生した場合でも lock が release されるよう、lock の使用を try/finally でラップします。または、これを自動的に処理する withLock() を使用します。

Client code
const lock = await squid.acquireLock('my-resource');
try {
await performCriticalOperation();
} finally {
lock.release();
}

説明的な Mutex 名を使用する​

保護する resource を明確に識別する mutex 名を選びます。これにより debugging が容易になり、accidental collision を防止できます。

Client code
// Good: specific and descriptive
await squid.acquireLock('inventory-update-sku-12345');
await squid.acquireLock('user-balance-user-abc');

// Avoid: generic names that may collide
await squid.acquireLock('lock1');
await squid.acquireLock('update');

Safety net として maxHoldTimeMillis を設定する​

bug や unhandled error が発生した場合に lock が無期限に保持されるのを防ぐため、maxHoldTimeMillis を使用します。

Client code
const lock = await squid.acquireLock('critical-resource', {
maxHoldTimeMillis: 10000, // Force release after 10 seconds
});

Lock の保持時間を短く保つ​

必要な最小時間だけ lock を保持します。non-critical work(logging、UI update、notification)は lock された section の外側で実行します。

Client code
const lock = await squid.acquireLock('shared-counter');
let newValue: number;
try {
const current = await readCounter();
newValue = current + 1;
await writeCounter(newValue);
} finally {
lock.release();
}
// Do non-critical work after releasing
console.log('Counter updated to', newValue);

Distributed Locks の保護​

default では、distributed lock は完全に保護されており、backend security rule が許可する場合にのみ acquire できます。どの client がどの lock を acquire できるかを制御するには、@secureDistributedLock decorator を使用します。

mutex ごとの authorization を含む lock security rule の構成に関する詳細は、Securing distributed locksを参照してください。