Cloudflare に Node.js の AsyncLocalStorage がサポートされたので試してみた

先日、 Cloudflare WorkesでNods.js の一部のAPIが対応されました。 その中で AsyncLocalStorage も対応されていたので Cloudflare Workers で実際に試して見ました
2023.04.02

西田@CX事業本部です

先日、 Cloudflare WorkesでNods.js の一部のAPIが対応 されました。

その中で AsyncLocalStorage も対応されていたので Cloudflare Workers で実際に試して見ました

AsyncLocalStorage について

AsyncLocalStorage は Node.js が提供している非同期処理中に同時にアクセスしてもメモリセーフに使えるデータストアです。同時にストアにアクセスする可能性があっても安全に使えるグローバルなオブジェクトのように使えます。他の言語でいうところの Thread Local な変数のようなものとして扱えます

具体的な例を挙げるとHTTPのリクエスト情報を、AsyncLocalStorage に保持しておけば、それをどこからでもアクセスでき、関数のバケツリレーで引き回す必要がなくなります。また、リクエストごとに独立したストアとして使え、同時にアクセスされても別のストアとして扱うことができます

Cloudflare Workers で実際に試してみる

実際に Cloudflare Workers で試していきます

今から実装するのは、JSONで構造化されたログにHTTPのリクエスト情報を含めれるようにします。その際に Request オブジェクトを引数で渡さなくても良いようにします

使用イメージは以下です

const log = () => {
  // Output to Console.log
}

const funcA = () => {
	log("aa") // { method: "GET", ur: "http:...", "msg": "aa" ... }
}

const funcB = () => {
	log("bb") // { method: "GET", ur: "http:...", "msg": "bb" ... }
}

プロジェクトの作成

Cloudflare workers を作成する環境を準備します。 wrangler を使用します

wranger init . -y

全体のソース

Node.js 対応のAPIを使うには compatibility_flagsに [ “nodejs_compat ] を指定する必要があります。詳しくは こちらのブログ を参考にしてください

wrangler.toml

name = "cloudflare-workers-asynclocalstorage"
main = "src/index.ts"
compatibility_date = "2023-04-01"
compatibility_flags = [ "nodejs_compat" ]

今回の作成した全体のソースです

src/index.ts

import { AsyncLocalStorage } from "node:async_hooks";

type Store = {
  requestId: string;
  request: Request;
};

const storage = new AsyncLocalStorage<Store>();

const doSomethingWithRequestInfo = () => {
  const store = storage.getStore();
  const headers = store?.request.headers;

  // Do something with headers in request info
  console.log(headers);

  log("do something with request info");
};

const log = (msg: string) => {
  const store = storage.getStore();

  const output = {
    time: new Date().toISOString(),
    msg,
    requestId: store?.requestId,
    url: store?.request.url,
    method: store?.request.method,
  };

  console.log(JSON.stringify(output));
};

export default {
  async fetch(
    request: Request,
    env: Env,
    ctx: ExecutionContext
  ): Promise<Response> {
    const requestId = crypto.randomUUID();
    storage.run({ request, requestId }, () => {
      log("request start");

      doSomethingWithRequestInfo();

      log("request end");
    });

    return new Response("Hello World!");
  },
};

ピックアップしてコードの説明をしていきます

AsyncLocalStorage を宣言してる箇所です。 AsyncLocalStorage は node のコアモジュールなので、 node: スキーマをつけてインポートしています。

import { AsyncLocalStorage } from "node:async_hooks";

ストレージに保存するストアの型を宣言し、それを AsyncLocalStorageに渡しストレージのインスタンスを生成しています。このストレージがどこからでもアクセス可能なストレージになります

type Store = {
  requestId: string;
  request: Request;
};

const storage = new AsyncLocalStorage<Store>();

Cloudflare Worker のエントリポイントとなる fetch 関数です。この中で先ほど宣言した AsyncLocalStorage インスタンスの run メソッドをよんでいます。 この run メソッドのコールバックの中なら、リクエスト毎に独立したメモリ領域を持つストレージに、どこからでもアクセスすることができます。また、 run メソッドにリクエストの情報を渡しストレージに格納しています

async fetch(
  request: Request,
  env: Env,
  ctx: ExecutionContext
): Promise<Response> {
  const requestId = crypto.randomUUID();
  storage.run({ request, requestId }, () => {
    log("request start");

    doSomethingWithRequestInfo();

    log("request end");
  });

  return new Response("Hello World!");
},

ログを出力する関数です。この中でAsyncLocalStorageのインスタンスからリクエストの情報を取り出し、引数で渡されたメッセージにマージして、ログに出力しています

const log = (msg: string) => {
  const store = storage.getStore();

  const output = {
    msg,
    requestId: store?.requestId,
    url: store?.request.url,
  };

  console.log(JSON.stringify(output));
};

デプロイ

Cloudflare にデプロイして動作確認していきます

※ npm run deploy で wranger publish が起動し Cloudflare Woreker にデプロイできます

npm run deploy

まとめ

AsyncLocalStorage を使っている Node.js の npm パッケージも多いです。今後 Node 特有の機能が増えていけば、 Cloudflare Workersで動くパッケージも増えていくのではないでしょうか

この記事が誰かの参考になれば幸いです

参考

[アップデート]Cloudflare WorkersでNode.jsのAsyncLocalStorage、EventEmitter、Buffer、assert、およびutilの一部が動作可能になりました! | DevelopersIO