プロセスをクラッシュさせて再起動することは、エラーに対処するための有効な戦略ではありません。バグでさえありません。プロセスが安価で、単一のクライアントにサービスを提供するなど、1 つの独立したことを行う Erlang では問題ありません。これは、プロセスのコストが桁違いに高く、一度に数千のクライアントにサービスを提供するノードには当てはまりません。
サービスによって 1 秒あたり 200 件のリクエストが処理されているとします。それらの 1% がコードのスローイング パスにヒットした場合、1 秒あたり 20 回のプロセス シャットダウンが発生し、およそ 50ms ごとに 1 回になります。コアごとに 1 つのプロセスを持つ 4 つのコアがある場合、それらは 200 ミリ秒で失われます。そのため、プロセスの開始とリクエストの処理準備に 200 ミリ秒以上かかる場合 (モジュールをロードしないノード プロセスの最小コストは約 50 ミリ秒です)、完全なサービス拒否が成功したことになります。言うまでもなく、エラーに遭遇したユーザーはページを繰り返し更新するなどの操作を行う傾向があり、それによって問題が悪化します。
ドメインは、リソースがリークされていないことを保証できないため、問題を解決しません。
問題#5114および#5149で詳細をお読みください。
これについて「スマート」になり、特定のエラー数に基づいて何らかのプロセス リサイクル ポリシーを設定することができますが、どのような戦略にアプローチしても、ノードのスケーラビリティ プロファイルが大幅に変化します。プロセスごとに 1 秒あたり数千ではなく、数十のリクエストについて話しているのです。
ただし、Promise はすべての例外をキャッチし、同期例外がスタックを伝播する方法と非常によく似た方法でそれらを伝播します。さらに、これらの 2 つの機能finally
のtry...finally
おかげで、"context-managers" を構築することでクリーンアップ ロジックをカプセル化できます ( python 、with
C #またはJavaと同様) 。リソースをクリーンアップします。using
try-with-resources
リソースがacquire
とdispose
メソッドを持つオブジェクトとして表され、どちらも promise を返すと仮定しましょう。関数が呼び出されたときに接続は行われず、リソース オブジェクトのみが返されます。このオブジェクトはusing
後で処理されます:
function connect(url) {
return {acquire: cb => pg.connect(url), dispose: conn => conn.dispose()}
}
API は次のように動作する必要があります。
using(connect(process.env.DATABASE_URL), async (conn) => {
await conn.query(...);
do other things
return some result;
});
この API は簡単に実現できます。
function using(resource, fn) {
return Promise.resolve()
.then(() => resource.acquire())
.then(item =>
Promise.resolve(item).then(fn).finally(() =>
// bail if disposing fails, for any reason (sync or async)
Promise.resolve()
.then(() => resource.dispose(item))
.catch(terminate)
)
);
}
fn
リソースは、using の引数内で返されたプロミス チェーンが完了すると、常に破棄されます。その関数 (例: from JSON.parse
) またはその内部.then
クロージャー (2 番目の などJSON.parse
) 内でエラーがスローされた場合、またはチェーン内の promise が拒否された場合 (エラーを伴うコールバック呼び出しと同等) であっても。これが、プロミスがエラーをキャッチして伝播することが非常に重要である理由です。
ただし、リソースの破棄が実際に失敗した場合、それは実際に終了する正当な理由です。この場合、リソースをリークした可能性が非常に高いため、そのプロセスを段階的に縮小することをお勧めします。しかし今では、クラッシュの可能性は、コードのはるかに小さな部分 (リーク可能なリソースを実際に扱う部分) に分離されています!
注: terminate は基本的にアウトオブバンドをスローするため、promise はそれをキャッチできませんprocess.nextTick(() => { throw e });
。どの実装が理にかなっているかは、セットアップに依存する可能性があります。
コールバック ベースのライブラリを使用するのはどうですか? それらは潜在的に安全ではない可能性があります。例を見て、これらのエラーがどこから発生し、どのエラーが問題を引き起こす可能性があるかを確認しましょう。
function unwrapped(arg1, arg2, done) {
var resource = allocateResource();
mayThrowError1();
resource.doesntThrow(arg1, (err, res) => {
mayThrowError2(arg2);
done(err, res);
});
}
mayThrowError2()
unwrapped
は内部コールバック内にあり、別の promise 内で呼び出された場合でも、スローされた場合はプロセスをクラッシュさせます.then
。この種のエラーは通常promisify
のラッパーではキャッチされず、通常どおりプロセス クラッシュを引き起こし続けます。
ただし、mayThrowError1()
内で呼び出されると promise に引っかかり.then
、内部に割り当てられたリソースがリークする可能性があります。
promisify
スローされたエラーが回復不能であり、プロセスがクラッシュすることを確認する偏執的なバージョンを作成できます。
function paranoidPromisify(fn) {
return function(...args) {
return new Promise((resolve, reject) =>
try {
fn(...args, (err, res) => err != null ? reject(err) : resolve(res));
} catch (e) {
process.nextTick(() => { throw e; });
}
}
}
}
別の promise のコールバック内で promisified 関数を使用すると、ラップ.then
されていないスローの場合にプロセス クラッシュが発生し、throw-crash パラダイムにフォールバックします。
プロミス ベースのライブラリをますます使用するようになると、コンテキスト マネージャー パターンを使用してリソースが管理されるため、プロセスをクラッシュさせる必要が少なくなることが一般的に期待されます。
これらのソリューションはどれも防弾ではありません-スローされたエラーでクラッシュすることさえありません. スローしていないにもかかわらず、リソースをリークするコードを誤って作成するのは非常に簡単です。たとえば、次のノード スタイル関数は、スローしなくてもリソースをリークします。
function unwrapped(arg1, arg2, done) {
var resource = allocateResource();
resource.doSomething(arg1, function(err, res) {
if (err) return done(err);
resource.doSomethingElse(res, function(err, res) {
resource.dispose();
done(err, res);
});
});
}
なんで?のコールバックがdoSomething
エラーを受け取ると、コードはリソースの破棄を忘れるためです。
この種の問題は、コンテキスト マネージャーでは発生しません。dispose を呼び出すことを忘れてはなりません: そうする必要はありませusing
ん。
参考文献: promise 、context manager 、および transactionsに切り替える理由