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 されるまで待機します。
仕組み
- client が shared resource を識別する mutex string を指定して
acquireLock()を呼び出します - Squid が Redis 内で lock を atomic に acquire します
- 別の client がすでに lock を保持している場合、request は lock が release されるか timeout が切れるまで待機します
- lock は保持されている間、background で自動的に renew されます
- 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 update | Database 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 を追加します。
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 して使用する
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 です。
const lock = await squid.acquireLock('my-resource');
別の client がすでに lock を保持している場合、promise は lock が利用可能になるか acquisition timeout が切れるまで待機します。
Lock option
acquireLock() は、lock の動作を構成する optional な 2 番目の parameter を受け取ります。
const lock = await squid.acquireLock('my-resource', {
acquisitionTimeoutMillis: 5000, // Wait up to 5 seconds to acquire
maxHoldTimeMillis: 30000, // Auto-release after 30 seconds
});
| Option | Type | Default | 説明 |
|---|---|---|---|
acquisitionTimeoutMillis | number | 2000 | lock が利用可能になるまで待機する maximum time(millisecond)。この時間内に lock を acquire できない場合、promise は reject されます。 |
maxHoldTimeMillis | number | No limit | 自動 release 前に lock を保持できる maximum time(millisecond)。lock が無期限に保持されるのを防ぐ safety net として便利です。 |
DistributedLock object
acquireLock() が resolve すると、次の interface を持つ DistributedLock object が返されます。
| Property / Method | Type | 説明 |
|---|---|---|
resourceId | string | この lock を識別する mutex string |
lockId | string | この lock instance の unique identifier |
release() | Promise<void> | lock を release します |
isReleased() | boolean | lock が release されている場合に true を返します |
observeRelease() | Observable<void> | lock が release されたときに emit します |
Lock の release
他の client が lock を acquire できるようにするには、release() を呼び出して lock を解放します。
lock.release();
lock は以下の場合にも自動的に release されます。
- client が server から disconnect した場合
maxHoldTimeMillisduration が切れた場合- Squid client instance が destroy された場合
Lock status の確認
lock が release されたかを確認するには、isReleased() を使用します。
console.log(lock.isReleased()); // true or false
Lock release の監視
明示的な release または disconnect によって lock が release されたときに対応するには、observeRelease() を使用します。
// 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 が不要になります。
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 を調整できます。
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 されます。
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 を検出できます。
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 connected | WebSocket connection が確立されていない | lock を acquire する前に Squid client が初期化・接続されていることを確認する |
| Unauthorized | この mutex への lock access を許可する security rule がない | backend に @secureDistributedLock rule を追加する |
ベストプラクティス
常に finally block で release する
error が発生した場合でも lock が release されるよう、lock の使用を try/finally でラップします。または、これを自動的に処理する withLock() を使用します。
const lock = await squid.acquireLock('my-resource');
try {
await performCriticalOperation();
} finally {
lock.release();
}
説明的な Mutex 名を使用する
保護する resource を明確に識別する mutex 名を選びます。これにより debugging が容易になり、accidental collision を防止できます。
// 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 を使用します。
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 の外側で実行します。
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を参照してください。