✒️

Fastly Compute 揮発データの読み書き (2) CoreCache API

2023/12/19に公開

この記事は Fastly Compute (旧 Compute@Edge) 一人アドベントカレンダー 17 日目の記事です。

昨日紹介した SimpleCache API よりも高機能な Cache API が CoreCache API です。本稿を書いている 2023 年末時点での Rust/Go SDK では SimpleCache と CoreCache の関係は以下のようになっていて、SimpleCache 自体が CoreCache API を使って実装されています。(※なお JavaScript SDK では SimpleCache も CoreCache も v3.9.0 時点では Hostcall を直接呼び出す実装となっています)

Simple Cache API が高レベルの API で、CoreCache API が低レベルの (より直接的に Hostcall を触るイメージに近い) API という位置付けになります。本稿ではこの CoreCache API についてどのような特徴がある API かを紹介していきます。

CoreCache API の特徴

公式の解説文を紹介する形で特徴から見ていきます。

Offers programmatic access to the cache with full control over all cache metadata and semantics, intended for advanced use cases or building custom higher level abstractions.
(Cache の metadata や semtantics といった情報含めて全てのコントロールが可能で、応用的なユースケースやカスタムロジックを含む形での独自での高レベルのAPIを実現するために利用する)

また、同じページに CoreCache API 経由でキャッシュできるエントリのデータについても解説があるのでこちらも併せて紹介します。

Name 解説
A cache key Cache を識別するための最大 4KiB までの任意のバイト列。重複が許されるため、cache key とキャッシュされるオブジェクトは一対多の関係を取る。ヘッダ情報は複数のオブジェクトの中から一意にオブジェクトを特定するために利用することができる。
General metadata age, いつ expire するか、サロゲートキー等
User-controlled metadata キャッシュされるオブジェクトと共に保存される任意のバイト列で、revalidatingの際に更新することが可能
キャッシュされるオブジェクト Body 経由で読み込んで StreamingBody 経由で書き込まれる任意のバイト列

実装例

よりイメージをつけるために実装例を見ていきます。複雑なユースケースにも使えるのですが、著者自身がそこまでまだ使いこめておらず挙動を精緻に紹介することが難しいことと、記事の文字数の都合により今回はドキュメントで紹介されているベーシックなトランザクションを張った使い方について、紹介されているサンプルコードを 3 分割して順番に処理の流れを追っていきます。

まず初期化処理のコードから。Transaction::lookup で返される TransactionLookupBuilderexecute() メソッドを呼び出すことでトランザクションを開始しています。続いて、"my_key" という Key の Cache Object が存在するかどうかを found() メソッドで確認し、stale 状態になっているかどうかに関わらず暫定的に見つかった記録だけ残しておきます。(このとき use_found_item() は任意のユーザ定義関数であることに注意してください)

const TTL: Duration = Duration::from_secs(3600);
// perform the lookup
let lookup_tx = Transaction::lookup(CacheKey::from_static(b"my_key"))
    .execute()
    .unwrap();
if let Some(found) = lookup_tx.found() {
    // a cached item was found; we use it now even though it might be stale,
    // and we'll revalidate it below
    use_found_item(&found);
}

次に 2 つ目の分割部分です。lookup_tx.must_insert() で改めて利用可能なキャッシュの存在有無を確認した上で、存在しなかった場合(=新規にオブジェクトを保存する場合)のフローが以下です。

// now we need to handle the "must insert" and "must update" cases
if lookup_tx.must_insert() {
    // a cached item was not found, and we've been chosen to insert it
    let contents = build_contents();
    let (mut writer, found) = lookup_tx
        .insert(TTL)
        .surrogate_keys(["my_key"])
        .known_length(contents.len() as u64)
        // stream back the object so we can use it after inserting
        .execute_and_stream_back()
        .unwrap();
    writer.write_all(contents).unwrap();
    writer.finish().unwrap();
    // now we can use the item we just inserted
    use_found_item(&found);

最後の 3 つ目の分割部分では、フレッシュなキャッシュが存在せず、新しいオブジェクトを登録または stale したキャッシュを更新する必要がある場合に実行されるフローが以下です。本フローに進んだ場合、途中で should_replace() というユーザ定義関数が呼ばれていますが、ここでは仮に何かしらのロジックで insert()update() かの呼び出しを分けるといった処理を想定した実装例となっているようです。

} else if lookup_tx.must_insert_or_update() {
    // a cached item was found and used above, and now we need to perform
    // revalidation
    let revalidation_contents = build_contents();
    if let Some(stale_found) = lookup_tx.found() {
        if should_replace(&stale_found, &revalidation_contents) {
            // use `insert` to replace the previous object
            let mut writer = lookup_tx
                .insert(TTL)
                .surrogate_keys(["my_key"])
                .known_length(revalidation_contents.len() as u64)
                .execute()
                .unwrap();
            writer.write_all(revalidation_contents).unwrap();
            writer.finish().unwrap();
        } else {
            // otherwise update the stale object's metadata
            lookup_tx
                .update(TTL)
                .surrogate_keys(["my_key"])
                .execute()
                .unwrap();
        }
    }
}

制限事項 / 注意事項

  • 設計の際、Cache API の読み書きに関する制限値(例:1 つのエントリに格納できるデータサイズの最大値 等)や Purge の回数制限など各種制限値があることに注意してください(制限値では足りない場合、サポートチームへ事前連絡しましょう)
  • 本稿を執筆している 2023 年 12 月現在、CoreCache API はローカルのデバッグ環境($fastly compute serve 、Viceroy)や fiddle ではサポートされておらず、機能しないことに注意してください。(公式doc

利用可能なパージ方法

CoreCache API では SDK が提供する purge メソッドを使ってサロゲートキーによりキー単位での purge ができます。あるいは、キー単位ではなくサービス単位でまるっとキャッシュ全体をパージしたい場合には、SimpleCache の場合と同様に CLI/API/UI の以下のインターフェースを利用することが可能です。

  • CLI
    • $fastly purge --all
  • API
    • Purge all する API call (doc)
  • UI
    • Web コントロールパネル上の以下のボタンからキャッシュ全体クリアを実行可能

まとめ

本稿では CoreCache API の概要を紹介しました。実装例含めて紹介しましたが、実際にコードを動かしたり、実際のユースケースで利用を検討するまではやや分かりづらい内容となったかもしれません。機会があれば、ぜひ動作確認しながら理解を進めてみてください。

明日はこれまでに紹介した Readthrough Cache, SimpleCache, CoreCache の 3 種類について特性や使い所について比較しまとめてみたいと思います。

Discussion