概要

kubernetes上でcronjobでnodejsのスクリプトを実行させていると、どうしてもapiへのリクエストなどで時間がかかってしまうことがあります。いつまでも終了しないcronjobのpodが滞留してくるとクラスタ全体が重くなってmasterに影響がでたりすることもあります。そうならないようスクリプトは適宜タイムアウトするような作りにしておくと良いですが、その際便利だったのがPromise.raceという仕組みでした。

実験

まずは基本のサンプルコードです。

const TIMEOUT = Number(process.env.TIMEOUT) || 5000;

const timeout = async (msec) => {
    return new Promise((_, reject) => setTimeout(() => {
        reject(new Error("timeout"))
    }, msec))
}

function sleep(ms) {
    return new Promise((resolve) => {
        setTimeout(resolve, ms);
    });
}

const exec = async () => {
    try {
        await sleep(1000);
    } catch (error) {
        throw new Error(error);
    }
}

(async () => {
    return Promise.race([exec(), timeout(TIMEOUT)])
        .then(() => {
            console.log('bar');
        })
        .catch(error => {
            console.error(error);
        });
})()

出力結果:

$ time node 01.js
bar

real    0m5.151s
user    0m0.095s
sys     0m0.087s

出力結果にみるとおり、1秒かかる処理を終えたあと、thenの処理が実行されてbarが出力されています。しかしプロセス終了までに5秒かかっています。raceはあくまで、配列内のコールバックのどれかが終了したらthenに進むというものでtimeout関数は裏で実行されつづけています。

そのためタイムアウトを実現する場合には以下のようにexitを追加します。

(async () => {
    return Promise.race([exec(), timeout(TIMEOUT)])
        .then(() => {
						console.log('bar');
						process.exit(0);
        })
        .catch(error => {
						console.error(error);
						process.exit(1);
        });
})()

これでexec()終了後すぐにプロセスが終了します。

bar

real    0m1.172s
user    0m0.072s
sys     0m0.056s

またsleepを長くして処理がスタックした状態を再現すると、以下のようにタイムアウトされます。

Error: timeout
    at Timeout._onTimeout (/home/yoshwata/nodejs-race/01.js:5:16)
    at listOnTimeout (internal/timers.js:554:17)
    at processTimers (internal/timers.js:497:7)

real    0m5.170s
user    0m0.072s
sys     0m0.072s

稀にネットワークの不調などの問題でkubeのmasterがおかしくなっていたのですが原因はこの滞留するjob podたちでした。クリティカルなジョブでなければ通常はこのようにタイムアウトを設けておいて、スタックしたpodは潔く終了させるのがクラスタ運用目線では良さそうです。