20

node.js サービスのサードパーティ拡張機能として外部コードを実行しています。API メソッドは promise を返します。解決された promise は、アクションが正常に実行されたことを意味し、失敗した promise は、操作の実行に何らかの問題があったことを意味します。

今ここで私は困っています。

サードパーティのコードは不明であるため、バグ、構文エラー、型の問題、node.js が例外をスローする原因となるさまざまな原因が存在する可能性があります。

ただし、すべてのコードが promise にラップされているため、これらのスローされた例外は実際には失敗した promise として返されます。

関数呼び出しを try/catch ブロック内に配置しようとしましたが、トリガーされませんでした:

// worker process
var mod = require('./3rdparty/module.js');
try {
  mod.run().then(function (data) {
    sendToClient(true, data);
  }, function (err) {
    sendToClient(false, err);
  });
} catch (e) {
  // unrecoverable error inside of module
  // ... send signal to restart this worker process ...
});

上記の疑似コードの例では、エラーがスローされると、キャッチではなく、失敗した promise 関数で発生します。

私が読んだところによると、これは問題ではなく機能であり、約束があります。ただし、例外と予想される拒否を常にまったく同じように扱いたいと思う理由を理解するのに苦労しています。

1 つのケースは、コード内の実際のバグに関するもので、回復不可能な場合があります。もう 1 つのケースは、構成情報、パラメーター、または回復可能なものが欠落している可能性があります。

助けてくれてありがとう!

4

3 に答える 3

15

プロセスをクラッシュさせて再起動することは、エラーに対処するための有効な戦略ではありません。バグでさえありません。プロセスが安価で、単一のクライアントにサービスを提供するなど、1 つの独立したことを行う Erlang では問題ありません。これは、プロセスのコストが桁違いに高く、一度に数千のクライアントにサービスを提供するノードには当てはまりません。

サービスによって 1 秒あたり 200 件のリクエストが処理されているとします。それらの 1% がコードのスローイング パスにヒットした場合、1 秒あたり 20 回のプロセス シャットダウンが発生し、およそ 50ms ごとに 1 回になります。コアごとに 1 つのプロセスを持つ 4 つのコアがある場合、それらは 200 ミリ秒で失われます。そのため、プロセスの開始とリクエストの処理準備に 200 ミリ秒以上かかる場合 (モジュールをロードしないノード プロセスの最小コストは約 50 ミリ秒です)、完全なサービス拒否が成功したことになります。言うまでもなく、エラーに遭遇したユーザーはページを繰り返し更新するなどの操作を行う傾向があり、それによって問題が悪化します。

ドメインは、リソースがリークされていないことを保証できないため、問題を解決しません。

問題#5114および#5149で詳細をお読みください。

これについて「スマート」になり、特定のエラー数に基づいて何らかのプロセス リサイクル ポリシーを設定することができますが、どのような戦略にアプローチしても、ノードのスケーラビリティ プロファイルが大幅に変化します。プロセスごとに 1 秒あたり数千ではなく、数十のリクエストについて話しているのです。

ただし、Promise はすべての例外をキャッチし、同期例外がスタックを伝播する方法と非常によく似た方法でそれらを伝播します。さらに、これらの 2 つの機能finallytry...finallyおかげで、"context-managers" を構築することでクリーンアップ ロジックをカプセル化できます ( python with C #またはJavaと同様) 。リソースをクリーンアップします。usingtry-with-resources

リソースがacquiredisposeメソッドを持つオブジェクトとして表され、どちらも 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に切り替える理由

于 2013-10-23T11:52:26.883 に答える
1

約束の拒否は、単に失敗の抽象化の結果です。ノード スタイルのコールバック (err、res) と例外も同様です。promise は非同期であるため、try-catch を使用して実際に何かをキャッチすることはできません。これは、イベント ループの同じティックでエラーが発生する可能性が高いためです。

簡単な例:

function test(callback){
    throw 'error';
    callback(null);
}

try {
    test(function () {});
} catch (e) {
    console.log('Caught: ' + e);
}

ここでは、関数が同期的であるため (コールバックベースですが)、エラーをキャッチできます。別:

function test(callback){
    process.nextTick(function () {
        throw 'error';
        callback(null); 
    });
}

try {
    test(function () {});
} catch (e) {
    console.log('Caught: ' + e);
}

これで、エラーをキャッチできなくなりました。唯一のオプションは、コールバックで渡すことです。

function test(callback){
    process.nextTick(function () {
        callback('error', null); 
    });
}

test(function (err, res) {
    if (err) return console.log('Caught: ' + err);
});

これで、最初の例と同じように機能します。同じことがプロミスにも当てはまります。try-catch を使用できないため、エラー処理に拒否を使用します。

于 2013-10-22T21:41:20.893 に答える