suguru.dev

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

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を使う必要はないのかなぁとも思いましたが、あまり詳しくないのでわかりません。もし詳しい方いたらぜひシェアしていただけたらうれしいです。

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

ソース

Sidekiqのベストプラクティス

概要

Sidekiqのベストプラクティスに沿って、メッセージ機能を実装していきます。

0. Sidekiqとは

Sidekiqとはbackgroundでタスクを処理してくれるライブラリです。日付指定でセットして実行したりもできます、便利そうです。

ベストプラクティスに沿ってWorker用のテーブルを用意します。

# messages
class CreateMessages < ActiveRecord::Migration[5.1]
  def change
    create_table :messages do |t|
      t.integer "sender_id"
      t.integer "recipient_id"
      t.string "body"
      t.timestamps
    end
  end
end
# message_tasks
class CreateMessageTasks < ActiveRecord::Migration[5.1]
  def change
    create_table :message_tasks do |t|
      t.integer "sender_id"
      t.integer "recipient_id"
      t.string "body"
      t.timestamps
    end
  end
end

今回はミニマムのデモのため構造は全く同じですが、実際は送信するユーザが複数だったりプッシュ通知をしたりするかもしれません。

1. ジョブのパラメータを小さく・シンプルに

上記MessageTasksのテーブルを用意することで、引数をtask_idのみにすることができます。

class MessageWorker
  include Sidekiq::Worker

  def perform(task_id)
  end
end

また呼び出し側では以下のようになります。

task = MessageTask.create(sender_id: sender_id,
                          recipient_id: recipient_id,
                          body: body)

MessageWorker.perform_async(task.id)

2. 冪等性とトランザクション

perform内でエラーが発生した場合、自動リトライ機能が働きます。デフォルトでは25回のリトライ後(おおよそ21日後)そのジョブは削除されマニュアルで対応しないといけません。 それを防ぐためにもMessageTasksトランザクションを使用することで、より安全にデータを扱うことができます。

class MessageWorker
  include Sidekiq::Worker

  def perform(task_id)
    task = MessageTask.find(task_id)
    if task.nil?
      return
    end

    ActiveRecord::Base.transaction do
      Message.create(sender_id: task.sender_id,
                     recipient_id: task.recipient_id,
                     body: task.body)
      task.destroy
    end
  end
end

3. Concurrencyの設定

Concurrencyを設定することによりsidekiqの処理を並列化することが可能です。デフォルト値は25スレッドで推奨値は50未満。100以上設定した場合、安定性の問題があるとの。 config/database.ymlを変更することで変えることができます。この値は可変ではないので、用途に応じて変更する必要があるようです。

production:
  adapter: mysql2
  database: foo_production
  pool: 25

まとめ

Sidekiqを使うことで同期処理が必要がないタスクは非同期で処理させることができます。同期処理させる必要ないものは非同期にしてレスポンス速度を改善できそうですね。

リンク

ESLintのメソッドチェインのindentの設定について

概要

ESLintのメソッドチェイン(chain)のindentの設定に問題があり3 -> 4のアップグレードができなかったのですが、なんとか苦戦してなんとかアップグレードした話です。 もっといい方法知っている方いたら教えて欲しいです、ホントに。

問題のコード

例としてarrow functionのショートハンドなどは使用していません。実際のコードはもっと大きいものを想定してください。

さて。

元々のESLintの設定はこうでした。

rules:
  indent:
    - error
    - 2

Array編

最初のchainのfunctionに複数行にまたがるArrayを渡す際、以下のコードはLintエラーになります。

// 1 (Lintエラー)
Promise.all([
  1,
  2,
  3
])
.then(value => {
  console.log(value);
});

解決方法としては、

// 2
Promise.all([
  1,
  2,
  3
])
  .then(value => {
    console.log(value);
  });

// 3
Promise
  .all([
    1,
    2,
    3
  ])
  .then(value => {
    console.log(value);
  });

// 4 (Lintエラー)
Promise.all([
    1,
    2,
    3
  ])
  .then(value => {
    console.log(value);
  });

2は気持ち悪すぎるので不採用として(Lintエラーにして欲しいレベル)、4でLintエラーにするのであれば、もはや1を採用して欲しいです。 3はアリと考えていましたが、以下の例でとてもいただけないので不採用としました。

// 5 (気持ち悪い)
_
  .chain([
    1,
    2,
    3
  })
  .map(n => {
    return n * 2;
  })
  .value();

// 6 (短いときはこう書きたい)
Promise.all([1, 2, 3])
  .then(value => {
    console.log(value);
  });

Function編

Functionのほうがもっと厄介です。

// 7 (Lintエラー)
new Promise(resolve => {
  // do something
  setTimeout(resolve);
})
.then(value => {
  console.log(value);
});

// 8 (Lintエラー)
[1, 2, 3].map(n => {
  return n * 2;
})
.filter(n => {
  return n % 2;
});

解決方法はArrayの例と同じく、

// 9
new Promise(resolve => {
  // do something
  setTimeout(resolve);
})
  .then(value => {
    console.log(value);
  });


// 10
[1, 2, 3].map(n => {
  return n * 2;
})
  .filter(n => {
    return n % 2;
  });

一つの案としては変数を定義することですが、Functionの例で毎度変数を作らないといけなくなるため不採用にしました。

解決方法

IndentにMemberExpressionというオプションがあり、chain時のindentを定義できます。 上記の例を実現したいとき、chainのindentが0だったり2だったりするため、offにすることで回避することができました。

rules:
  indent:
    - error
    - 2
    - MemberExpression: off

まとめ

ESLintのバージョンをアップデートすることで、多くの機能が追加されているのでもっと厳しくチェックしていただけるようになりました。助かります、ありがとうございます。

がしかし、果たしてこれは良いコードなのか…という疑問も残ります。

例えば、

// 11
const obj = {};
const array = _.chain([
  1,
  2,
  3,
]).map(n => {
  return n * 2;
})
.filter(n => {
  return n % 2;
});
obj.a = 1;

この例の場合、arrayのchainがどこで終わりかわかりづらいため、その後に続く行が読みづらい可能性が高いです。

// 12
const obj = {};
const array = _
  .chain([
    1, 
    2, 
    3
  ])
  .map(n => {
    return n * 2;
  })
  .filter(n => {
    return n % 2;
  });
obj.a = 1;

そういう意味ではこう書いたほうが読みやすい可能性も…

リーダブルコードって難しいですね。

リンク

Perforce + Vim

概要

PerforceをVimで仲良くやっていくためのセットアップを考え中なのですが、特に良いライブラリもなく、はたまた需要もそこまで大きくなく… いったん作業ができるようvim-perforceとvimdiffのセットアップをしました。

vim-perforce

ドキュメントによるとP4info, P4edit, P4revertP4movetocl だけサポートしてるそうです。

f:id:suguru03:20171107092146p:plain

f:id:suguru03:20171107092629p:plain

f:id:suguru03:20171107092523p:plain

Vim上で手動でチェックアウトする必要があります。WebStormを使っていると自動チェックアウトができるそうです。 p4 deleteが一番の曲者なのでサポートして欲しいところではあります。

vimdiff

vimdiffのセットアップは一行足すだけでp4 diffvimに置き換わります。

$ export P4DIFF=vimdiff

f:id:suguru03:20171107102935p:plain

これはとても見やすく便利です。

まとめ

vim-perforceを改良したいところではありますが、いったんこの2つさえあれば作業は捗るかと思います。

もう少し効率よくするために、ローカル環境ではgitを用意して、gitの情報を元にPerforceのコマンドを自動化しています(謎)。 Perforceはあまりブランチを作成したり複数のことを同時にしたりすることが難しいので、gitでできることはgitに移行したいところです。

リンク

Perforceのセットアップ for Mac

はじめに

PerforceSubversionのようなバージョン管理システムでbinaryの管理に向いているそうです。今回はp4vというツールとp4というコマンドラインツールを導入したのでそのセットアップ方法について書きます。

p4vのセットアップ

こちらのリンクからmac用のリンクをダウンロードします。

設定はこちらのリンク に沿ってやるだけですが、管理者からServerのアドレスとuser名をいただき入力するだけです。

https://www.perforce.com/perforce/doc.current/manuals/p4v-gs/images/p4v-gs001.png

以上で設定は完了です。

p4vの使い方

ログイン後、始めに差分を取得するためルートディレクトリで右クリックを押しGet Latest Revisionを取得します。 全てのファイルはRead Onlyになっており編集権限がありません。もし編集したい場合はファイルを右クリックでCheck outする必要があります。チェックアウトをすると編集が可能になり、その情報は他のユーザにシェアがされます。編集後、右クリックを押しsubmitすることで他のユーザに変更がシェアされます。 もし差分を見たい場合は、右クリックでDiff Against Have Revisionを押すとp4vdiffが起動し差分を見ることが可能です。

p4のセットアップ

p4vだとマウス操作が多いため、やはりCLIでどうにかしたいところです。そのためp4をインストールします。リンクはこちら

次にパスなどの情報をセットします。 私は.zshrcGithub上で管理しているため、secretな情報を隠すために別ファイルを用意しました。

# ~/.secret.sh
export P4PATH=<server-url>
export P4USER=<user-name>
export P4CLIENT=<workspace-name>
p4 login
# ~/.zshrc
if [ -f $HOME/.secret.sh ]; then
  source $HOME/.secret.sh
fi

上記の.secret.sh.zshrcで読み込み、terminal起動時にp4にログインをしています。これで準備は完了です。

編集したいときは、p4 editを使用することでチェックアウトが可能になります。また他にもいくつかコマンドがあるので試してください。

まとめ

私はgitばかり触っているゆとりですが、少しずつ慣れていこうかなと思っています。 次回はp4 + vimあたりについて書こうと思います。

リンク