suguru.dev

バンクーバーで働くエンジニアの備忘録

Serverless + AWS Lambda + API Gatewayのヘッダマッピング

概要

AWS Lambda上でリクエストヘッダの取得とレスポンスヘッダの追加をServerless上で設定する方法がわからなくハマったので、それについてまとめました。

リクエストヘッダのマッピング

AWS Lambdaで生のリクエストヘッダを取得するためには、API Gateway上でマッピングテンプレートによりマッピングする必要があります。今回はいくつかのエンドポイントでヘッダ情報をチェックする必要があったため、こちらのドキュメントを参考に以下のように追記しました。

functions:
  hello:
    handler: header.hello
    events:
      - http:
          path: hello
          method: GET
          request: ${self:custom.request}
custom:
  request:
    template:
      application/json:
        method: $context.httpMethod,
        body : $input.json('$'),
        headers:
          #foreach($param in $input.params().header.keySet())
          $param: $util.escapeJavaScript($input.params().header.get($param))
          #if($foreach.hasNext),#end
          #end

これでヘッダ情報が取得できるようになります。

export const hello = async (event, context) => {
  cosnole.log(event.headers);
);

レスポンスヘッダのマッピング

今回、AWS Gatewayのデフォルトはapplication/jsonのようで、これをapplication/json; charset=utf-8に書き換える必要がありました。yaml上で解決したかったのですがうまく解決できず。今回はこちらを参考にコードに追記して解決しました。

export const hello = async (event, context) => ({
  statusCode: 200,
  headers: {
    'Content-Type': 'application/json; charset=utf-8',
  },
  body: JSON.stringify({
    message: 'Go Serverless v1.0! Your function executed successfully!',
    input: event,
  }),
});

共通関数の作成

以下の目的のため、共通の関数を作成しました、ソースコードこちらです。

  • 冗長なコードを減らす
  • リクエストヘッダのチェック
  • レスポンスヘッダに上記の情報を追記する
  • Serverlessの関数はasync関数を取らないといけないが、もっと自由に定義したい
  • 型推論を利用したい

header.ts -> src/funcitons/example.tsに移動し、serverless.yamlの記述を書き換えます。

functions:
  hello:
    handler: src/functions/example.hello
    events:
      - http:
          path: hello
          method: GET

ディレクトリ構造は以下のような感じです。

|--package.json
|--serverless.yml
|--src
|  |--errors.ts
|  |--functions
|  |  |--example.ts
|  |  |--util.ts
|--tsconfig.json
|--yarn.lock

次にaws-lambdaの型定義を追加します。

$ yarn add -D @types/aws-lambda

共通関数は以下のように書きました。

import { Handler, Context, APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';

import { CustomError } from '../errors';

interface HandlerResult {
  statusCode?: number; // defaultは200
  body?: string | object; // JSON.stringifyするのでなんでもOK
}

type CustomHandler = (
  event: APIGatewayProxyEvent,
  context: Context,
) => void | HandlerResult | Promise<HandlerResult>;

export const createAPIGatewayHandler = (handler: CustomHandler): Handler => async (
  event: APIGatewayProxyEvent,
  context: Context,
): Promise<APIGatewayProxyResult> => {
  try {
    // check headers

    const { statusCode = 200, body = '' } = (await handler(event, context)) || {};
    return createResponse({
      statusCode,
      body: JSON.stringify(body), // JSON.stringify(undefined)にならないようにbodyを初期化
    });
  } catch (error) {
    let { statusCode, message } = error;
    if (!(error instanceof CustomError)) {
      // unknownエラーは500エラー
      statusCode = 500;
      message = 'Internal Server Error';
    }
    return createResponse({
      statusCode,
      body: JSON.stringify({ message }),
    });
  }
};

const createResponse = (response: APIGatewayProxyResult): APIGatewayProxyResult => ({
  ...response,
  headers: {
    'Content-Type': 'application/json; charset=utf-8',
    ...response.headers,
  },
});

example.tsは以下のようになり、event, contextともに型推論が利くので型定義は不要です。

import { createAPIGatewayHandler } from './util';

export const hello = createAPIGatewayHandler(event => ({
  body: {
    message: 'Go Serverless v1.0! Your function executed successfully!',
    input: event,
  },
}));

まとめ

Lambda上でリクエストヘッダ情報のチェック・レスポンスヘッダの書き換えができるようになりました。認証のためのCustom Authorizersという機能もあるのでこちらもまたチェックしてみようと思います。

リンク

Serverless + TypeScript + AWS Lambdaの開発環境構築

概要

Serverless FrameworkとはLambda関数などを開発・デプロイするためのツールです。今回はServerless + TypeScript + AWS Lamdbaのローカル開発環境の導入手順について書きます。

プロジェクトの作成

はじめにServerlessの環境構築をします。今回はAWS LamdbaのNode.jsを利用するため、templateはaws-nodejsで作成します。pathのサービス名は適宜変更してください。

$ npx serverless create --template aws-nodejs --path my-service
 _______                             __
|   _   .-----.----.--.--.-----.----|  .-----.-----.-----.
|   |___|  -__|   _|  |  |  -__|   _|  |  -__|__ --|__ --|
|____   |_____|__|  \___/|_____|__| |__|_____|_____|_____|
|   |   |             The Serverless Application Framework
|       |                           serverless.com, v1.36.0
 -------'

ドキュメントにはnpm install -g serverlessと書かれていますが、npxを使用することをオススメします。

このときのserverlessのバージョンはv1.36.0でした。

次にnpm packageを作成します。Serverlessのライブラリは必須ではないですが、CLIのバージョン固定するためにインストールしておきます。

$ cd my-service

$ yarn init
$ yarn add -D serverless

ローカル開発環境の構築

次にserverless-offlineのライブラリでローカル環境の構築を行います。

$ yarn add -D serverless-offline

serverless.ymlserverless-offlineとテスト用にhelloを以下のように追記しておきます。

plugins:
  - serverless-offline

functions:
  hello:
    handler: handler.hello
    events:
      - http:
          path: hello
          method: GET

起動して以下のログが出れば成功です。

$ yarn sls offline start
Serverless: Offline listening on http://localhost:3000

package.jsonスクリプトを追加しておくと便利ですね。

curlで叩いてみます。

$ curl http://localhost:3000/hello
{"message":"Go Serverless v1.0! Your function executed successfully!",... }

上記のログが出れば設定完了です。

デプロイ

次にAWS Lambdaへデプロイします。serverless.ymlに以下を追記します。region、profile名を適宜変更してください。

provider:
  name: aws
  runtime: nodejs8.10
  region: us-west-2
  profile: my-service

AWSの認証キーは必要に応じて~/.aws/credentialsに追記します。

[my-service]
aws_access_key_id = <your-key-here>
aws_secret_access_key = <your-secret-key-here>

以下のコマンドでデプロイします。

$ yarn sls deploy

AWSコンソール上に表示されていれば完了です。

TypeScriptの設定

次にTypeScriptの設定を行います。設定はとてもシンプルです。

$ yarn add -D typescript serverless-plugin-typescript

offlineと同様に以下の記述を追記します。

plugins:
  - serverless-offline
  - serverless-plugin-typescript

handler.jshandler.tsに変更し、以下のように書き換え、ローカルサーバが正常に立ち上がれば完了です。

export const hello = async (event, context) => ({
  statusCode: 200,
  body: JSON.stringify({
    message: 'Go Serverless v1.0! Your function executed successfully!',
    input: event,
  }),
});

ソースコードこちらに上げてあります。

まとめ

まだ使い始めたばかりですが、Serverless FrameworkでAWS Lambdaの開発・テストがローカル環境で行うことができとても便利です、ぜひ試してみてください。

リンク

MetabaseをElastic Beanstalkにデプロイする

概要

Metabaseを導入したので、それの手順についてまとめます。

Metabaseのデプロイ

こちらのリンクからAWSにデプロイするボタンがあるのですが、うまく動かなかったため今回は手動でセットアップします。

まずはじめにElastic Beanstalkのサーバを構築します。プラットフォームは事前設定済みプラットフォームのDockerを選択します。

f:id:suguru03:20181205161604p:plain

次にアプリケーションコードをアップロードします。ソースコードは既にS3上に上がっているためS3のURLを指定します。

https://s3.amazonaws.com/downloads.metabase.com/v0.31.1/metabase-aws-eb.zip

最新バージョンはこちらで確認できます、適宜URLのバージョン部分を書き換えてください。

f:id:suguru03:20181205162405p:plain

f:id:suguru03:20181205165545p:plain

リージョンのエラーが出る場合はソースコードをローカルにダウンロードし、ローカルファイルからダウンロードしたソースコードを選択しアップロードします。 「更にオプションを追加」をクリックし適宜設定し、環境を構築します。

Auroraの設定

今回は既存のAurora上にMetabase用のDBを作成します。新しくDBを作成する場合はこちらを参考にしてください。

ルートユーザでログインし、Aurora上にMetabase用のデータベースとユーザを作成します。

CREATE DATABASE metabase CHARACTER SET utf8mb4;
CREATE USER 'metabase.admin'@'%' IDENTIFIED BY 'password';
GRANT SELECT ON *.* TO 'metabase.admin'@'%' WITH GRANT OPTION;

必要に応じて、対象となるデータベース上にReadonlyユーザを作成してください。

環境変数の設定

スタートガイドに従ってElastic Beanstalk上に環境変数を設定します。上記で生成したユーザを設定してください。

f:id:suguru03:20190105093442p:plain

設定を更新して以下の画面が表示されれば準備完了です。

f:id:suguru03:20190106072554p:plain

まとめ

Metabaseとても便利そうなのでぜひ使ってみてください。

ソース

VM moduleを利用してLambdaのような機能を提供する

概要

Express + VMモジュールを使用し、Lambdaのような機能を提供するサーバの構築方法、またVMの注意点についてまとめます。

vmとは

VMとは同プロセス上で別のNodeスクリプトの実行をサポートするモジュールです。evalに似ていますが、Contextの値を外から代入できる大きな違いがあります。 また同プロセス上で動くので値の共有が可能です。以下が実行例です。

const vm = require('vm');

const code = `() => num + 1`;
const func = vm.runInNewContext(code, { num: 1 });
func(); // 2

このようにVM上で生成された関数をVM外で使用することができます。

サーバ概要

下図がざっくりとしたサーバ構成図です。

f:id:suguru03:20180713132219p:plain

クライアントがリクエストとともにクライアントバージョンを送信し、そのバージョンとマッチしたVMスクリプトをDBから取得します。また一度取得したスクリプトはパフォーマンス向上のためオンメモリに保持しています。

そのスクリプトVM上で起動し、サーバのAPIをContext経由で渡すことでスクリプト上からサーバへのリクエストを可能にします。 以下が実行例です。

// script1.js
handlers.getData = async () => ({
  a: await apis.getDataA(),
  b: await apis.getDataB(),
  c: await apis.getDataC(),
});
// vm.js
const vm = require('vm');
const fs = require('fs');
const path = require('path');

const script = fs.readFileSync(path.join(__dirname, 'script1.js')); // 実際はDBから取得

const apis = {
  async getDataA() {
    return 'a';
  },
  async getDataB() {
    return 'b';
  },
  async getDataC() {
    return 'c';
  },
};

const handlers = {};
const context = {
  apis,
  handlers,
};

vm.runInNewContext(script, context);

(async () => {
  const res = await handlers.getData();
  console.log(res); // { a: 'a', b: 'b', c: 'c' }
})();
$ node vm.js

このようにVM上で作成されたhandlersをVM外で実行することにより、一つのクラスタで複数のバージョンの異なるクライアントをサポートしています。

使用上の注意

Objectの扱い

JavaScriptでは全てのオブジェクトはObject型を継承します。例えばArrayはObject型を継承しているため、以下のようにinstanceoftrueになることが確認できます。

const array = [];
console.log(array instanceof Array); // true
console.log(array instanceof Object); // true

しかしVM上ではVM外とは異なるObject型が生成され、VM内の全てのObject型はVM外とは異なるObject型を継承します

const vm = require('vm');
const code = `() => []`;
const func = vm.runInNewContext(code, {});
const res = func();
console.log(res instanceof Array); // false
console.log(res instanceof Object); // false

逆も同様な問題があります。

const vm = require('vm');

const code = `() => array instanceof Array`;
const func = vm.runInNewContext(code, { array: [] });
func(); // false

Arrayの場合はArray.isArrayObject.prototype.toStringを使用することで、Arrayであるかどうかの判別が可能です。

console.log(Array.isArray(res)); // true
console.log(Object.prototype.toString.call(res)); // [object Array]

しかしVM上で生成された値をサードパーティのライブラリに代入する際、内部でinstandeof等を使用している可能性があるためこの方法が安全とは言えません。

エラーハンドリング

エラーハンドリングでも同様な問題があります。

const vm = require('vm');

const code = `() => { throw new TypeError() }`;
const func = vm.runInNewContext(code, {});

try {
  func();
} catch (e) {
  if (e instanceof TypeError) {
    console.log('type error!');
  } else if (e instanceof Error) {
    console.log('common error!');
  } else {
    console.log('unkown error!');
  }
}
// unknown error!

つまりVM上で生じたTypeErrorinstnaceofでは判別することができません。

util.inheritsを使った解決方法

util.inheritsとは、Node上でprototype関数を継承するために使われる関数です。 現在はextendsシンタックスを使用してclassを継承することが主流ですが、classの実態はprototype関数であり、util.inheritsを使用して拡張することができます。

基本的な使い方は以下の通りです。

const util = require('util');

// prototype function
function MyError1() {}

new MyError1() instanceof Error; // false

util.inherits(MyError1, Error);

new MyError1() instanceof Error; // true

// class
class MyError2 {}

new MyError2() instanceof Error; // false

util.inherits(MyError2, Error);

new MyError2() instanceof Error; // true

これを踏まえVM上のclassの継承関係を解決していきます。以下のように呼び出し元のclassを渡し、そのclassをVM内で継承することにより解決することができます。

const util = require('util');
const inherit = 'util.inherits(Array, classMap.Array);';
const code = `() => []`;
const func = vm.runInNewContext(`${inherit}${code}`, { util, classMap: { Array } });
const res = func();
console.log(res instanceof Array); // true
console.log(res instanceof Object); // true

他のclassも同様な問題があるため、こちらから適宜追加してください。

まとめ

NodeでLambdaのような機能を提供するサーバの構築方法を紹介しました。 750KBくらいのスクリプトを使用していますが、VMによるオーバーヘッドは5ms程度で無視できるレベルかと思います。

VMは様々な用途に使用でき、Nodeの幅が広がる面白いモジュールです。 VM使ったライブラリも公開してます、ぜひ試してみてください。

リンク

Node.js with TypeScriptのCode Coverageを計測する

概要

Node.js with TypeScriptでcode coverageを計測できるようにしたので、その導入手順について書きます。

NYCのセットアップ

はじめにNYCのセットアップします。 NYCとはIstanbulのコマンドラインツールで、Istanbulとはcode coverageを計測してくれるツールです。

まずはnycts-nodeをinstallします。

$ npm i -D nyc ts-node mocha

次にnycのtaskをpackage.jsonのscriptに追加します。今回はtestファイルをtest directory配下に置いています。

// package.json
{
  ...
  "scripts": {
    "test": "nyc mocha test/*"
  },
  ...
}

次にnycのconfigをpackage.jsonに設定します。必要に応じて設定を変更してください。

// package.json
{
  ...
  "nyc": {
    "include": [
      "lib/**/*.ts"
    ],
    "extension": [
      ".ts"
    ],
    "require": [
      "ts-node/register"
    ],
    "reporter": [
      "text",
      "text-summary",
      "html"
    ],
    "sourceMap": true
  },
  ...
}

opn-cliの設定(オプション)

coverage計測後、ぱっと見れるようにopn-cliの設定も一緒にpackage.jsonに追加します。

$ npm i -D opn-cli
// package.json
{
  ...
  "scripts": {
    "test": "nyc mocha test/*",
    "open:cov": "opn coverage/index.html"
  },
  ...
}

codecovの設定(オプション)

codecovで可視化したいのでcodecovの設定も追加します。codecovはCircleCIから自動的に走らせています。

$ npm i -D codecov
// package.json
{
  ...
  "scripts": {
    "test": "nyc mocha test/*",
    "codecov": "nyc report --reporter=lcovonly && codecov",
    "open:cov": "opn coverage/index.html"
  },
  ...
}

実行

後はnpm testを実行すると結果を見ることができます。nycのHTMLはbranchの詳細まで見れるのでとても便利です。

$ npm test
$ npm run open:cov

まとめ

CodeのCoverageが全てではないですが、テストを書く習慣・目標値の設定としてはとても良い指標だと思います。 これに加え、質の良いテストがかけるように心がけていきたいです。

ちなみにNYCはNew York Cityだそうです。

リンク

https://istanbul.js.org/ https://github.com/istanbuljs/nyc http://azimi.me/2016/09/30/nyc-mocha-typescript.1.html

direnvを使ってgithubのアカウントを複数設定する

はじめに

仕事のGithubアカウントと個人のGithubアカウントが別のため、direnvを使用してディレクトリごとに別のアカウントを使用できるようにしました。

direnvとは

direnvとはディレクトリごとに.envrcを設定することによりディレクトリごとに環境を変更することができます。Githubsshだけでなく色々な用途に使えます。

direnvのインストール

ドキュメントに従って以下を実行します。

$ git clone https://github.com/direnv/direnv
$ cd direnv
$ make install

私の環境では最新版のv2.14.0がうまく動かなかったため、v2.7.0をインストールしました。

$ git clone https://github.com/direnv/direnv
$ cd direnv
$ git checkout v2.7.0
$ make install

.envrcの設定

まずそれぞれのアカウント用に別のssh keyを用意します、ここでは仮に~/.ssh/id_rsa_github1, ~/.ssh/id_rsa_github2とします。 direnvではディレクトリ配下全ての設定を書き換えることができるので、作業ディレクトリをそれぞれ別に~/work, ~/personalとします。

次にそれぞれのディレクトリにssh keyを設定します。 GIT_SSH_COMMANDの環境を設定することで、gitコマンドを入力した際に自動的にこのコマンドが呼ばれるため.envrcに以下のコードを追加します。

# ~/work/.envrc
export GIT_SSH_COMMAND="ssh -i ~/.ssh/id_rsa_github1"
# ~/personal/.envrc
export GIT_SSH_COMMAND="ssh -i ~/.ssh/id_rsa_github2"
direnv: error .envrc is blocked. Run `direnv allow` to approve its content.

のエラーが出た際には

$ direnv allow

を実行すると有効にすることができます。

まとめ

複数のGithubアカウントを扱うことができるようになりましたが、芝が分散してしまうのでとても悲しいです。個人のアカウントが使える時代が来ることを祈っております。

リンク

Async HooksとAsync Resourcesの導入

概要

この記事はNode.js Advent Calendar 2017 15日目の記事です。

カナダから非同期で失礼します。

Async hooksとはNode.jsの非同期イベントをトレースすることができるネイティブライブラリです。まだ試験段階なのでAPIが変わる可能性がありますが、Async Hooksに入門したので、Async HooksとAsync Resouresの使い方、実際の使用例の考察をまとめました。

Node.jsのバージョンはv9.3.0を使用しています。

Async Hooksの基本的な使い方

まずは下記のコードを見てみます。このコードはAsync Resourcesが1秒ごとに生成され、破棄される様子を見ることができます。

const fs = require('fs');
const util = require('util');
const asyncHooks = require('async_hooks');

const hooks = {
  // Resourceが生成されるときに呼ばれる
  init(asyncId, type, triggerAsyncId, resource) {
    const obj = {
      asyncId,
      type,
      triggerAsyncId,
      resource
    };
    fs.writeSync(1, `init\n${util.format(obj)}\n`);
  },
  // Resourceのcallbackが呼ばれる直前に呼ばれる
  before(asyncId) {
    fs.writeSync(1, `before\t${util.format({ asyncId })}\n`);
  },
  // Resourceのcallbackが終了したときに呼ばれる
  after(asyncId) {
    fs.writeSync(1, `after\t${util.format({ asyncId })}\n`);
  },
  // Resourceが破棄されるときに呼ばれる
  destroy(asyncId) {
    fs.writeSync(1, `destroy\t${util.format({ asyncId })}\n`);
  },
  // Promiseのresolveが呼ばれるタイミングで呼ばれる
  promiseResolve(asyncId) {
    fs.writeSync(1, `promiseResolve\t${util.format({ asyncId })}\n`);
  }
};

// Hookの生成
const asyncHook = asyncHooks.createHook(hooks);

// Hookを有効にする
asyncHook.enable();

// 1秒おきにResourceの生成
(function start() {
  // 実行中のasyncId
  const eid = asyncHooks.executionAsyncId();
  // 呼び出し元のasyncId
  const tid = asyncHooks.triggerAsyncId();
  console.log(`start\teid: ${eid} tid: ${tid}`);
  setTimeout(start, 1000);
})();

// 10秒後にhookを無効にする
setTimeout(() => asyncHook.disable(), 10000);

createHookに任意の関数を渡すことで、非同期イベントの検知が簡単にできるようになります。特別難しい機能は無いので、それぞれの用語については下記の表にまとめました。

async_hooks

用語 説明
createHook Function Hookの作成
executionAsyncId Function 実行中のasyncIdの取得
triggerAsyncId Function 呼び出し元のasyncIdの取得

createHook

用語 説明
init Function Resoureが生成される時に呼ばれる
before Function Resourceがcallbackを呼ぶ直前で呼ばれる
after Function Resoureがcallbackを呼び終えた直後に呼ばれる
destroy Function Resoureが破棄される時に呼ばれる
promiseResolve Function PromiseのResourceでresolveが呼ばれる時に呼ばれる
asyncId number Async Resourceに割り当てられたid
triggetAsyncId number 呼び出し元のasyncId
enable Function Hookの有効化
disable Function Hookの無効化

⚠使用上の注意⚠

createHookの関数内でエラーが発生した際はuncaughtExceptionでキャッチすることはできず、スタックトレースを残してプロセスが終了されます。 またconsole.log, process.stdout.writeは非同期オペレーションなためAsync Resourceが生成されます。Hook内の各関数ではfs.writeSyncを使用してください。

スタックトレースを遡る

何か面白いことできないかなぁと探っていたところ、興味深いコードを見つけたので、これをベースにAsync Resouresの誕生から終焉までの軌跡を表示するようにしてみました。 実際のログはこちらです。コードは以下のとおりです。

const delay = util.promisify(setTimeout);
const DELAY = 1000;
const map = new Map();

function init(asyncId, type, triggerAsyncId, resource) {
  const obj = {
    asyncId,
    type,
    triggerAsyncId,
    resource
  };
  Error.captureStackTrace(obj, init);
  map.set(asyncId, [obj.stack, triggerAsyncId]);
  fs.writeSync(1, `init\n${util.format(obj)}\n`);
}

function destroy(asyncId) {
  fs.writeSync(1, `destroy\tasyncId: ${asyncId}\n`);
  showStackTrace(asyncId);
  map.delete(asyncId);
}

function showStackTrace(asyncId) {
  const array = map.get(asyncId);
  if (!array) {
    return;
  }
  const [stack, tid] = array;
  fs.writeSync(1, `stack: \tasyncId: ${asyncId} triggetAsyncId: ${tid}\n${stack.replace(/(.*)\n/, '')}\n`);
  showStackTrace(tid);
}

asyncHooks.createHook({ init, destroy }).enable();

let promise = delay(DELAY);
for (let i = 0; i < 10; i++) {
  promise = promise.then(() => delay(DELAY));
}

スタックトレースを遡ることができるので、デバッグに使えるかもしれません。

メモリリークの検知

以下のコードはAsync Resousesが生成されたものの、リソースが破棄されない例です。mapのサイズが次第に大きくなっていくことが一目でわかるので、メモリリークの検知の早期発見に使えるかもしれません。

const map = new Map();

function init(asyncId) {
  map.set(asyncId, 1);
  fs.writeSync(1, `init\tasyncId: ${asyncId} mapSize: ${map.size}\n`);
}

function destroy(asyncId) {
  map.delete(asyncId);
  fs.writeSync(1, `destroy\tasyncId: ${asyncId} mapSize: ${map.size}\n`);
}

asyncHooks.createHook({ init, destroy }).enable();

const queue = [];
(function start() {
  new Promise(resolve => queue.push(resolve));
  setTimeout(start, 100);
})();

Async Resourcesの基本的な使い方

AsyncResourceクラスを使用することで、任意の非同期イベントを作成することが可能です。emitBeforeemitAfterはセットなので発火する際には必ず両方を呼ぶ必要があります。

const { AsyncResource } = require('async_hooks');

// 自動的にinitが呼ばれる
const asyncResource = new AsyncResource('Async');

// Callbackを呼ぶ直前に呼ぶ
asyncResource.emitBefore();

// Callbackが終了した直後に呼ぶ
asyncResource.emitAfter();

// AsyncResourceが破棄される時に呼ぶ
asyncResource.emitDestroy();

// AsyncResourceのインスタンスに割り当てられたasyncIdを返す
asyncResource.asyncId();

// 呼び出し元のasyncIdを返す
asyncResource.triggerAsyncId();

Async Resourcesの導入

こちらのBluebirdのPRによると、メモリリークの問題も解決し安定してきたようなので、実際にAigleのライブラリに適用してみようと思います。コミットはこちら

まず、Nodeのバージョンがv9.3.0以上推奨とのことでv9.3.0以上のみを有効にしました。

const node = typeof process !== 'undefined' && process.toString() === '[object process]';
const supportAsyncHook = node && (() => {
  const support = '9.3.0';
  const [smajor, sminor] = support.match(/\d+/g);
  const version = process.versions.node;
  const [vmajor, vminor] = version.match(/\d+/g);
  return vmajor > smajor || vmajor === smajor && vminor >= sminor;
})();

次にconstructorAsyncResourceを生成します。

class Aigle {
  constructor(executor) {
    ...
    this._resource = supportAsyncHook && new AsyncResource('PROMISE');
    ...
  }
}

今回はPromiseライブラリなので、Native Promiseと同じtypeを使用しました。 これでAigleインスタンスが生成されるときにinitが呼ばれるようになります。

次に、onFulfilledまたはonRejectedが呼ばれる直前にemitBeforeを、直後にemitAfterを追加します。 簡単な方法としては、

const original = onFulfilled;
onFulfilled = value => {
  this._resource.emitBefore();
  try {
    return original(value);
  } catch (e) {
    throw e;
  } finally {
    this._resource.emitBefore();
  }
};

以上のように追加するのが簡単ですが、パフォーマンスを考慮して別の関数にしました。callbackが呼ばれる直前・直後に追加すれば問題ありません。

AsyncResourceの問題点としてパフォーマンスが著しく低下するため、デフォルトではAsync Resourcesを無効にしています。有効にする方法は、

Aigle.config({ asyncResource: true });

で使用することができます。

パフォーマンスについては後日調査してまた記事を書こうと思います。

まとめ

Async Hooksを使って非同期イベントのトレースを簡単にすることができるようになりました、デバッグにとても便利そうなライブラリです。 またAsync Resourcesを使うことでカスタムな非同期イベントを生成できますが、パフォーマンスに難がありそうです。Immediateイベントは自動的に発行されるので特別Async Resourcesを使う必要はないのかなぁとも思いましたが、あまり詳しくないのでわかりません。もし詳しい方いたらぜひシェアしていただけたらうれしいです。

今後も最新の情報収集とパフォーマンスの調査を行っていきたいと思います。

ソース