Node.js v8とasync function

Node.js v8.0.0のリリースが近い。v8は10月にリリースされるLTSの候補版、マイルストーンとして非常に重要なリリースとなる。

LTSラベルは4系列がArgon(アルゴン)、6系列がBoron(ホウ素)だったので、8系列のLTSはCarbon(炭素)あたりが有力なんすかね? むしろ10系列はどうするんだろう。Dで始まる元素なんて知らんぞww

※ v8と書くとJSエンジンのV8と紛らわしいので、以後はv8.xと書きます。

async function (asyna & await)

Node.js v8.xの目玉機能、それは何と言ってもasync function。こいつはv7.6で既に使用可能になっているのだが、本格的に普及するのはLTSとなるv8.x以降だろう。正気ならv7.xとか本番環境で使わないっしょ? 変態でない普通のプログラマが使うようになるのはLTSからですよ。

んで、async functionだが、こいつは非同期処理を同期的なコードのように書けるようにするもの。もう少し具体的にいうと複数のPromiseの呼び出しを簡潔に記述できるようにするものだ。例を見た方が早い。

DBからユーザデータをfindし、ユーザがいれば、さらにそのユーザのオプションを取得してユーザにセットして返す。ユーザがいなければエラーを返すという非同期処理を行う関数 findUserAndOptions を想定する。DBアクセスはPromiseを返すAPIによって行われるものとする。

こいつをPromiseで書くと

'use strict';

const db = require('./stub');

function findUserAndOptions(id) {
    return db.findUser(id).then((user) => {
        if (!user) {
            return Promise.reject(new Error(`ID ${id} not found`));
        }

        return user;
    }).then((user) => {
        return db.findUserOptions(id).then((options) => {
            user.options = options;
            return user;
        });
    });
}


findUserAndOptions(1).then((user) => {
    console.log(JSON.stringify(user))
}).catch((err) => {
    console.error(err.message);
});

単純な処理のくせに複雑で読みにくいコードだ。これをasync functionで書き直すと

'use strict';

const db = require('./stub');

async function findUserAndOptions(id) {
    const user = await db.findUser(id);

    if (!user) {
        throw new Error(`ID ${id} not found`);
    }

    const options = await db.findUserOptions(id);

    user.options = options;

    return user;
}

findUserAndOptions(1).then((user) => {
    console.log(JSON.stringify(user))
}).catch((err) => {
    console.error(err.message);
});

単純明快、非常にわかりやすい、新卒研修のサンプルかと思うほど簡単なコードになった。

Aysnc Function自身は関数内でreturnされた値で解決されるPromiseを返す。なので呼び出し側は単なるPromiseとして扱えばいい。もちろん、Promiseを返すのでAsync Functionを他のAsync Functionの中でawaitと共に利用するのもOKだ。

DBアクセス代わりのstubはこんな感じ。Promiseを返しているところに注目。Async Functionは単純なPromiseによる非同期処理を組み合わせて、複雑な処理を行う場合に威力を発揮する。今後はDBなどの非同期アクセスはcallbackではなく、Promiseで処理するのが主流になるだろう。既にmongoDBのドライバなんかはcallbackを渡さなければPromiseを返すようになっている。

exports.findUser = function(id) {
    return new Promise((resolve, reject) => {
        setImmediate(() => {
            if (id === 1) {
                resolve({
                    name: "なまえ"
                });
            } else {
                resolve(null);
            }
        });
    });
};

exports.findUserOptions = function(id) {
    return new Promise((resolve, reject) => {
        setImmediate(() => {
            if (id === 1) {
                resolve([
                    "オプション1",
                    "オプション2",
                ]);
            } else {
                resolve([]);
            }
        });
    });
};

既存のNodeのAPI、fs.readFileのようなコールバック前提のAPIはどうすんのよ?という話だが、Promiseベースに作り直すのではなく、util.promisify というコールバックベースのAPIをPromiseに変換するユーティリティを導入する予定らしい。

mongoDBのcount正しくない問題

mongoDBでは、シャーディングされたコレクションのcountが正しい値を返さないことがあります。これには2つの原因があります。

  1. チャンク移動中である
  2. 孤児ドキュメントが存在する

1は、チャンク移動中は正しい値が帰らないということです。シャード間でデータが移動している最中なので正しい値が取れないのは直感的に理解できると思います。

2は、孤児ドキュメント(Orphaned Documents)がカウントされてしまう問題です。孤児ドキュメントとはチャンク移動したドキュメントが元のシャードから削除されず残ってしまったものです。シャードキーの範囲外なのでmongosからは到達不能になっていますが、countには計上されてしまいます。

孤児ドキュメントを解消するには、以下のコマンドを各シャードのreplica set primaryのmongod(mongosではありません)で実行します。これは重い処理なのでサービス稼働中に実行してはいけません。

use admin;
db.runCommand({ creanupOrphaned: "<database>.<collection>" });

集計関数を使うと、孤児ドキュメントを無視して正しい件数を得ることができます。ただし、これはかなり時間がかかるので注意してください。

db.collection.aggregate(
   [
      { $group: { _id: null, count: { $sum: 1 } } }
   ]
)

db.collection.count() — MongoDB Manual 3.4

我々の環境では、2系のmongoDBでdumpし、3系の新しいクラスタへrestoreした時に孤児ドキュメントが大量に発生しました。この孤児の発生は、dumpしたデータをrestoreし、データのチャンク移動が完了したのちにremove、再度restoreすることで回避できました。初回restore時には大量のチャンク移動が発生するため、孤児ドキュメントが発生しやすいものと思われます。