キャッシュ
Cacheモジュールは、値をキャッシュすることでアプリケーションの性能を最適化するのを容易にします。
はじめに
多くのアプリケーションでは、重複した作業が行われるシナリオに直面することがあります。例えば、受信リクエストを処理するサービスを開発している場合、重複したリクエストの処理を避けることが重要です。Cacheモジュールを使用することで、冗長な作業を防ぎ、アプリケーションの性能を向上させることができます。
Cacheの主な機能:
-
合成性: Cacheにより、アプリケーションの異なる部分が重複した作業を行いながら、合成プログラミングの原則から恩恵を受けることができます。
-
同期キャッシュと非同期キャッシュの統合: ルックアップ関数を通じてキャッシュを合成的に定義することで、同期キャッシュと非同期キャッシュの統一が図られ、ルックアップ関数は同期または非同期で値を計算できます。
-
深いEffect統合: CacheはEffectライブラリとネイティブに動作するよう設計されており、同時ルックアップ、障害処理、割り込みが可能です。この際、Effectの力を失うことはありません。
-
キャッシングポリシー: キャッシングポリシーは値がキャッシュから削除されるタイミングを決定し、複雑でカスタムなキャッシング戦略への柔軟性を提供します。ポリシーは二つの部分から構成されます:
- 優先度(任意の削除): キャッシュが容量を使い果たす際に、値が削除される可能性のある順序を定義します。
- 追い出し(義務的な削除): 値が削除されなければならない場合を指定します(例: 値が古くなったり、ビジネス要求を満たさなくなった場合)。
-
合成キャッシングポリシー: よりシンプルなポリシーを使用して、複雑なキャッシングポリシーを定義可能にします。
-
キャッシュ/エントリ統計: Cacheは、エントリ、ヒット、ミスなどのメトリクスを追跡し、キャッシュ性能の評価と最適化に役立ちます。
キャッシュの定義方法
キャッシュは、キーに関連付けられた値を計算する方法を記述するルックアップ関数によって定義されます。キャッシュに既に存在しない場合に使用されます。
export type Lookup<Key, Value, Error = never, Environment = never> = ( key: Key) => Effect.Effect<Value, Error, Environment>ルックアップ関数は、Key型のキーを受け取り、Environment型の環境を必要とし、Error型のエラーで失敗するか、Value型の値で成功するEffectを返します。ルックアップ関数はEffectを返すため、同期的および非同期的なワークフローを記述できます。
要するに、Effectで表現できるものであれば、それをキャッシュのルックアップ関数として使用できます。
ルックアップ関数、最大サイズ、および生存時間を指定してキャッシュを構築します。
export declare const make: < Key, Value, Error = never, Environment = never>(options: { readonly capacity: number readonly timeToLive: Duration.DurationInput readonly lookup: Lookup<Key, Value, Error, Environment>}) => Effect.Effect<Cache<Key, Value, Error>, never, Environment>キャッシュが作成されると、最も一般的な使い方はgetオペレーターを使用することです。getオペレーターは、キャッシュに存在する場合は現在の値を返し、存在しない場合は新しい値を計算しキャッシュに入れ、返します。
複数の同時プロセスが同じ値を要求する場合、それは一度だけ計算されます。他のすべてのプロセスは、計算された値が利用可能になると、それを受け取ります。この処理は、効果のファイバーに基づく同時実行モデルを使用して行われ、基礎となるスレッドをブロックしません。
例
この例では、同じキーでtimeConsumingEffectを3回並行して呼び出します。Cacheはこのエフェクトを一度だけ実行するため、並行するルックアップは値が利用可能になるまで待機します。
import { Effect, Cache, Duration } from "effect"
const timeConsumingEffect = (key: string) => Effect.sleep("2 seconds").pipe(Effect.as(key.length))
const program = Effect.gen(function* () { const cache = yield* Cache.make({ capacity: 100, timeToLive: Duration.infinity, lookup: timeConsumingEffect }) const result = yield* cache .get("key1") .pipe( Effect.zip(cache.get("key1"), { concurrent: true }), Effect.zip(cache.get("key1"), { concurrent: true }) ) console.log( `同じキーでの3つのエフェクトを並行して実行した結果: ${result}` )
const hits = yield* cache.cacheStats.pipe(Effect.map((_) => _.hits)) const misses = yield* cache.cacheStats.pipe(Effect.map((_) => _.misses)) console.log(`キャッシュヒット数: ${hits}`) console.log(`キャッシュミス数: ${misses}`)})
Effect.runPromise(program)/*出力:同じキーでの3つのエフェクトを並行して実行した結果: 4,4,4キャッシュヒット数: 2キャッシュミス数: 1*/同時アクセス
キャッシュは同時アクセスに対して安全に設計されており、同時条件下でも効率的です。もし二つの同時プロセスが同じ値を要求し、その値がキャッシュに存在しない場合、その値は一度だけ計算され、利用可能になると両方のプロセスに提供されます。同時プロセスは、オペレーティングシステムのスレッドをブロックすることなく、値が利用可能になるのを待機します。
もしルックアップ関数が失敗したり、割り込まれた場合、そのエラーは値を待機しているすべての同時プロセスに伝播します。失敗した値はキャッシュされ、同じ失敗した値の再計算を防ぎます。割り込まれると、キーはキャッシュから削除され、次回の呼び出しは値の再計算を試みます。
キャパシティ
キャッシュは指定されたキャパシティで作成されます。キャッシュがキャパシティに達すると、最も最近アクセスされなかった値が最初に削除されます。キャッシュのサイズは、操作の間に指定されたキャパシティをわずかに超えることがあります。
有効期限(TTL)
キャッシュには、有効期限(TTL)を指定することもできます。TTLよりも古い値は返されません。エイジは値がキャッシュにロードされた時点から計算されます。
オペレーター
getに加えて、Cacheは他にもいくつかのオペレーターを提供します:
- refresh:
getに似ていますが、値の再計算をトリガーしながらそれを無効にせず、値が再計算されている間に関連するキーへのリクエストを処理できます。 - size: 現在のキャッシュのサイズを返します。同時アクセスの下では、サイズは概算です。
- contains: 指定したキーに関連する値がキャッシュに存在するかを確認します。同時アクセスの下では、結果はチェック時点で有効ですが、その後すぐに変更される可能性があります。
- invalidate: 指定したキーに関連する値を削除します。
- invalidateAll: キャッシュ内のすべての値を削除します。