ロック

アトミックロック

アトミックロック(別名mutex)は、共有リソースへのアクセスを同期するために使用されます。つまり、複数のプロセスまたは並行コードが同時にコードの一部を実行するのを防ぎます。

AdonisJSチームは、フレームワークに依存しないパッケージであるVerrouを作成しました。@adonisjs/lockパッケージはこのパッケージに基づいていますので、詳細についてはVerrouのドキュメントも読んでください。

インストール

次のコマンドを使用してパッケージをインストールおよび設定します:

node ace add @adonisjs/lock
  1. 検出されたパッケージマネージャを使用して@adonisjs/lockパッケージをインストールします。

  2. adonisrc.tsファイル内に次のサービスプロバイダを登録します。

    {
    providers: [
    // ...other providers
    () => import('@adonisjs/lock/lock_provider')
    ]
    }
  3. config/lock.tsファイルを作成します。

  4. start/env.tsファイル内で、次の環境変数とそのバリデーションを定義します。

    LOCK_STORE=redis
  5. databaseストアを使用する場合は、locksテーブルのデータベースマイグレーションを作成することもできます(オプション)。

設定

ロックの設定はconfig/lock.tsファイルに保存されます。

import env from '#start/env'
import { defineConfig, stores } from '@adonisjs/lock'
const lockConfig = defineConfig({
default: env.get('LOCK_STORE'),
stores: {
redis: stores.redis({}),
database: stores.database({
tableName: 'locks',
}),
memory: stores.memory()
},
})
export default lockConfig
declare module '@adonisjs/lock/types' {
export interface LockStoresList extends InferLockStores<typeof lockConfig> {}
}

default

ロックを管理するために使用するdefaultストア。ストアは同じ設定ファイル内のstoresオブジェクトで定義されます。

stores

アプリケーション内で使用するストアのコレクション。テスト中に使用できるように常にmemoryストアを設定することをおすすめします。


環境変数

デフォルトのロックストアはLOCK_STORE環境変数を使用して定義されており、したがって、異なるストアを異なる環境で切り替えることができます。たとえば、テスト中にmemoryストアを使用し、開発および本番環境ではredisストアを使用します。

また、環境変数は事前に設定されたストアのいずれかを許可するようにバリデーションする必要があります。バリデーションはstart/env.tsファイル内でEnv.schema.enumルールを使用して定義されます。

{
LOCK_STORE: Env.schema.enum(['redis', 'database', 'memory'] as const),
}

Redisストア

redisストアは@adonisjs/redisパッケージに依存しているため、Redisストアを使用する前にこのパッケージを設定する必要があります。

Redisストアが受け入れるオプションのリストは次のとおりです:

{
redis: stores.redis({
connectionName: 'main',
}),
}
connectionName

connectionNameプロパティは、config/redis.tsファイルで定義された接続を参照します。

データベースストア

databaseストアは@adonisjs/lucidパッケージに依存しているため、データベースストアを使用する前にこのパッケージを設定する必要があります。

データベースストアが受け入れるオプションのリストは次のとおりです:

{
database: stores.database({
connectionName: 'postgres',
tableName: 'my_locks',
}),
}

connectionName

config/database.tsファイルで定義されたデータベース接続への参照。定義されていない場合は、デフォルトのデータベース接続を使用します。

tableName

レート制限を保存するために使用するデータベーステーブル。

メモリストア

memoryストアは、テスト目的だけでなく、現在のプロセスにのみ有効で、複数のプロセス間で共有されないロックが必要な場合に便利なシンプルなインメモリストアです。

メモリストアはasync-mutexパッケージをベースにしています。

{
memory: stores.memory(),
}

リソースのロック

ロックストアを設定したら、アプリケーション内のどこでもリソースを保護するためにロックを使用できます。

以下は、リソースを保護するためにロックを使用する簡単な例です。

import { errors } from '@adonisjs/lock'
import locks from '@adonisjs/lock/services/main'
import { HttpContext } from '@adonisjs/core/http'
export default class OrderController {
async process({ response, request }: HttpContext) {
const orderId = request.input('order_id')
/**
* ロックを即座に取得しようとします(再試行なし)
*/
const lock = locks.createLock(`order.processing.${orderId}`)
const acquired = await lock.acquireImmediately()
if (!acquired) {
return 'オーダーは既に処理中です'
}
/**
* ロックが取得されました。オーダーを処理できます
*/
try {
await processOrder()
return 'オーダーは正常に処理されました'
} finally {
/**
* ロックを解放するために`finally`ブロックを使用することで、
* 処理中に例外がスローされてもロックが解放されることを確認します。
*/
await lock.release()
}
}
}

これは、アプリケーション内でロックを使用する方法の簡単な例です。

extendメソッドを使用してロックの期間を延長したり、getRemainingTimeメソッドを使用してロックの有効期限までの残り時間を取得したり、ロックを設定するためのオプションなど、他の多くのメソッドも利用できます。

そのため、詳細についてはVerrouのドキュメントを必ず読んでください@adonisjs/lockパッケージはVerrouパッケージに基づいているため、Verrouのドキュメントで読んだ内容は@adonisjs/lockパッケージにも適用されます。

別のストアの使用

config/lock.tsファイル内で複数のストアを定義した場合、useメソッドを使用して特定のロックに異なるストアを使用できます。

import locks from '@adonisjs/lock/services/main'
const lock = locks.use('redis').createLock('order.processing.1')

defaultストアのみを使用する場合は、useメソッドを省略できます。

import locks from '@adonisjs/lock/services/main'
const lock = locks.createLock('order.processing.1')

複数のプロセス間でのロックの管理

場合によっては、ロックを作成および取得するプロセスと、ロックを解放する別のプロセスを持ちたい場合があります。たとえば、Webリクエスト内でロックを取得し、バックグラウンドジョブ内でロックを解放したい場合があります。これは、restoreLockメソッドを使用して実現できます。

メインサーバー
import locks from '@adonisjs/lock/services/main'
export class OrderController {
async process({ response, request }: HttpContext) {
const orderId = request.input('order_id')
const lock = locks.createLock(`order.processing.${orderId}`)
await lock.acquire()
/**
* オーダーを処理するためにバックグラウンドジョブをディスパッチします。
*
* ジョブがオーダーの処理が完了した後、ロックを解放するためにシリアライズされたロックをジョブに渡します。
*/
queue.dispatch('app/jobs/process_order', {
lock: lock.serialize()
})
}
}
バックグラウンドジョブ
import locks from '@adonisjs/lock/services/main'
export class ProcessOrder {
async handle({ lock }) {
/**
* シリアライズされたバージョンからロックを復元しています
*/
const handle = locks.restoreLock(lock)
/**
* オーダーを処理します
*/
await processOrder()
/**
* ロックを解放します
*/
await handle.release()
}
}

テスト

テスト中は、ロックを取得するために実際のネットワークリクエストを行わないために、memoryストアを使用することができます。これは、.env.testingファイル内でLOCK_STORE環境変数をmemoryに設定することで行うことができます。

.env.test
LOCK_STORE=memory

カスタムロックストアの作成

まず、カスタムロックストアの作成については、Verrouのドキュメントを参照してください。AdonisJSでは、ほぼ同じです。

まず、LockStoreインターフェイスを実装するクラスを作成する必要があります。

import type { LockStore } from '@adonisjs/lock/types'
class NoopStore implements LockStore {
/**
* ロックをストアに保存します。
* このメソッドは、指定されたキーが既にロックされている場合はfalseを返す必要があります。
*
* @param key ロックするキー
* @param owner オーナー
* @param ttl ロックの有効期限(ミリ秒)。nullの場合は期限なし
*
* @returns ロックが取得された場合はtrue、それ以外の場合はfalse
*/
async save(key: string, owner: string, ttl: number | null): Promise<boolean> {
return false
}
/**
* オーナーが指定された場合にのみ、ストアからロックを削除します。
* それ以外の場合はE_LOCK_NOT_OWNEDエラーをスローする必要があります。
*
* @param key 削除するキー
* @param owner オーナー
*/
async delete(key: string, owner: string): Promise<void> {
return false
}
/**
* オーナーを確認せずにストアからロックを強制的に削除します。
*/
async forceDelete(key: string): Promise<Void> {
return false
}
/**
* ロックが存在するかどうかをチェックします。存在する場合はtrue、それ以外の場合はfalseを返します。
*/
async exists(key: string): Promise<boolean> {
return false
}
/**
* ロックの有効期限を延長します。ロックが指定されたオーナーによって所有されていない場合はエラーをスローします。
* 期間はミリ秒単位です。
*/
async extend(key: string, owner: string, duration: number): Promise<void> {
return false
}
}

ストアファクトリの定義

ストアを作成したら、@adonisjs/lockがストアのインスタンスを作成するために使用する単純なファクトリ関数を定義する必要があります。

function noopStore(options: MyNoopStoreConfig) {
return { driver: { factory: () => new NoopStore(options) } }
}

カスタムストアの使用

完了したら、noopStore関数を次のように使用できます:

import { defineConfig } from '@adonisjs/lock'
const lockConfig = defineConfig({
default: 'noop',
stores: {
noop: noopStore({}),
},
})