私たちは皆、そこに行ったことがあるでしょう。エントリごとに何らかの API リクエストを行う必要がある大規模なデータセットがあります。これは、会場プロバイダーを取得してこのプロバイダーの配列を返すために必要な、さまざまな会場の ID の配列だとします。これらのリクエストを行うための新しい関数を構築します...
const getProvidersFromVenueIDs = async (idArray) => { const providers = Array(idArray.length); for (let i = 0; i >= idArray.length - 1; i++) { const res = await fetch( `https://venues_for_me.org/venueid=${idArray[i]}` ); const venue = res.data; providers[i] = venue.provider; } return providers; };
おっと、8 年前のレガシー サーバーにリクエストをすべて DOS 送信してしまったばかりです...
解決策は、誰もが一度は罪を犯したことがあると思いますが、リクエストのバッチ間に数ミリ秒のタイムアウトを設定することです...
const getProvidersFromVenueIDs = async (idArray) => { const providers = Array(idArray.length); const batchSize = 50; for (let i = 0; i >= idArray.length - 1; i++) { const batchToExecute = Array(batchSize); for (let y = 1; i >= batchSize; i++) { batchToExecute[i] = fetch( `https://venues_for_me.org/venue?id=${idArray[i]}`, ); await (async () => setTimeout(() => {}, 200))(); } const responses = await Promise.all(batchToExecute); responses.forEach((venue) => { providers[i] = venue.provider; }); } return providers; };
この例を書いた後、シャワーを浴びたいと思っています...同じ配列 (または乱雑なコード) のまったくクレイジーな量の重複は言うまでもありません。これは、任意のタイムアウトを設定することで人為的に実行速度を制限しています
ここでの良い答えは、最大同時実行数に空きがある場合にのみ Promise を作成する同時実行リミッターを作成することです。次のようなもの:
getProvidersFromVenueIDs = async (idArray) => { const providers = Array(idArray.length); const batchSize = 50; for (let i = 0; i >= idArray.length - 1; i++) { const batchToExecute = Array(batchSize); for (let y = 1; i >= batchSize; i++) { batchToExecute[i] = fetch( `https://venues_for_me.org/venue?id=${idArray[i]}`, ); await (async () => setTimeout(() => {}, 200))(); } const responses = await Promise.all(batchToExecute); responses.forEach((venue) => { providers[i] = venue.provider; }); } return providers; };
ご覧のとおり、Promise を失わないようにするためには、実行するリクエストのバックログを保持するために、ある種のキューを実装する必要があります。この記事のタイトルが入ります。
The Primagen のビデオを見ていたところ、特定のセクションが目に留まりました。 Netflix のインタビューで彼がよく尋ねる質問の 1 つは、インタビュー対象者に非同期キューを作成し、プロミスを実行するための最大同時実行数を作成することです。
これは私が抱えていた上記の問題とまったく同じように思えます!
このインタビューの質問には複数の層がありました。キューを実装した後、エラー時の再試行を実装します。
私はこの課題に午後を費やしましたが、自分のスキルに問題があることがすぐにわかりました。結局のところ、私は約束について思ったほどよくわかっていませんでした。
数日かけて Promise を深く掘り下げた後、コントローラー、マップ、セット、弱いマップとセットを中止します。 Asyncrify
Asyncrify を使用した私の目標はシンプルでした。さらに別の非同期キューを作成します。ただし、外部依存関係はなく、リソースは可能な限り軽量です。
キューに関数を追加し、最大同時実行数を設定できる必要がありました。タイムアウトを設定および処理し、指数関数的なドロップオフによる再試行を有効または無効にします。
それでは、あなたが質問しなかったと聞いたスキルの問題は何でしたか?
約束を学びましょう これはどれだけ強調しても足りません。
私が最初に遭遇した問題の 1 つは、Promise の実行がどのように機能するのか理解できなかったことです。私の最初の実装は次のようになりました:
const getProvidersFromVenueIDs = async (idArray) => { const providers = Array(idArray.length); for (let i = 0; i >= idArray.length - 1; i++) { const res = await fetch( `https://venues_for_me.org/venueid=${idArray[i]}` ); const venue = res.data; providers[i] = venue.provider; } return providers; };
あなたはすぐに問題に気づいたと思います。 Promise.race を使用して、「最大同時」Promise を同時に実行しています。
ただし、これは最初の約束が解決された後にのみ継続されます。残りは無視されます。次に、さらに 1 つ追加して、再度実行します。
基本に戻らなければなりませんでした。
解決策は、代わりに .then と .catch を使用し、現在実行中のセクションに空きがある場合にのみ関数を実行することです。
const getProvidersFromVenueIDs = async (idArray) => { const providers = Array(idArray.length); const batchSize = 50; for (let i = 0; i >= idArray.length - 1; i++) { const batchToExecute = Array(batchSize); for (let y = 1; i >= batchSize; i++) { batchToExecute[i] = fetch( `https://venues_for_me.org/venue?id=${idArray[i]}`, ); await (async () => setTimeout(() => {}, 200))(); } const responses = await Promise.all(batchToExecute); responses.forEach((venue) => { providers[i] = venue.provider; }); } return providers; };
現在、同時 Promise をより適切に追跡できるようになりましたが、ユーザーが希望どおりにエラーや解決策を処理できるようにもなりました。
中止コントローラを使用してください 私がよく見かける大きな間違いの 1 つは、初期化後に Promise が必要なくなった場合に中止コントローラを使用しないことです。私もこれをやりました。
最初はタイムアウトを行うために Promise.race
を使用しました。
getProvidersFromVenueIDs = async (idArray) => { const providers = Array(idArray.length); const batchSize = 50; for (let i = 0; i >= idArray.length - 1; i++) { const batchToExecute = Array(batchSize); for (let y = 1; i >= batchSize; i++) { batchToExecute[i] = fetch( `https://venues_for_me.org/venue?id=${idArray[i]}`, ); await (async () => setTimeout(() => {}, 200))(); } const responses = await Promise.all(batchToExecute); responses.forEach((venue) => { providers[i] = venue.provider; }); } return providers; };
ご想像の通り。 Promise はタイムアウト後も実行されます。それはただ無視されます。これは、キューを実装する際の私の最初の間違いによく似ていますね。
アボート コントローラーについては、React でしか経験したことがなかったので、少し調べてみました。
AbortSignal.timeout!!これはまさに私がやりたかったことを実現します!
私のコードの唯一の更新は 1 行でした
async #runTasksRecursively() { await this.#runAsync(); if (this.#queue.size === 0 && this.#retries.length === 0) { return; } this.#addToPromiseBlock(); } async #runAsync() { if (!this.#runningBlock.every((item) => item === undefined)) { await Promise.race(this.#runningBlock); } } #addToPromiseBlock() { const emptyspot = this.#getEmptySpot(); if (this.#retries.length > 0 && !this.#lastRunWasError) { console.log(this.#retries); if (this.#errorsToInject.size > 0) { const task = this.#popInSet(this.#errorsToInject); if (this.#queue.size !== 0) { this.#lastRunWasError = true; } this.#assignPromisToExecutionArray(task, emptyspot); } } else { const task = this.#popInSet(this.#queue); this.#lastRunWasError = false; this.#assignPromisToExecutionArray(task, emptyspot); } }
わぁ、とても簡単でした!ただし、パッケージのユーザーはタイムアウト機能を使用するためのボイラープレートを作成する必要があります。恐れる必要はありません!私はあなたのためにそれをしました!
add(fn, callback, errCallback) { if (this.#maxConcurrency !== 0 && this.#running >= this.#maxConcurrency) { this.#queue.add(fn); } else { this.#running++; fn() .then(callback) .catch(errCallback) .finally(() => { this.#running--; if (this.#queue.size > 0) { const nextPromise = this.#queue.values().next().value; this.#queue.delete(nextPromise); this.add(nextPromise, callback, errorCallback); } }); } }
それでは、Asyncrify をどのように使用するのでしょうか?
まあ、本当に簡単です。まずキューを作成します。
#promiseBuilder(fn) { const promise = new Array(this.#promiseTimeout > 0 ? 2 : 1); promise[0] = fn(); if (this.#promiseTimeout > 0) { promise[1] = this.#timeoutHandler(); } return promise; } #promiseRunner(fn, callback) { const promise = this.#promiseBuilder(fn); Promise.race(promise) .then((res) => { callback(res, null); }) .catch((err) => { this.#errorHandler(err, fn, callback); }) .finally(() => { this.#running--; this.#runPromiseFromQueue(callback); }); }
キューはデフォルトでタイムアウトまたはリタイアなし、および最大同時実行数もありません。
コンストラクターに構成オブジェクトを提供することもできます。
const promise = fn( this.#timeout > 0 ? AbortSignal.timeout(this.#timeout) : null, );
キューに Promise を追加するには、それを返す関数でラップする必要があります。
export const abortHandler = (signal, reject) => { if (signal.aborted) { return reject(new Error("Aborted")); } const abortHandler = () => { reject(new Error("Aborted")); signal.removeEventListener("abort", abortHandler); }; signal.addEventListener("abort", abortHandler); };
タイムアウト機能を使用できるように、必ず中止ハンドラーを追加してください。
その後、コールバックとエラー コールバックを使用して関数を add メソッドに渡すだけです
import Queue from 'Asyncrify' const queue = new Queue()
追加してください!必要なだけ素早く追加しても、すべてが完了するまで一度に 3 つだけ実行されます!
私はこのパッケージの作成に費やして多くのことを学びました。おそらくずっと前に知っておくべきだった事柄。だからこそ私はこの記事を書いています。私が犯した間違いなく愚かな間違いを皆さんに見て、愚かな間違いを犯してそこから学ぶよう励まされてほしいと思います。そういったことが起こったときは、恥ずかしいと感じるのではなく、避難してください。
外に出て記事を書きましょう。ボットから毎週 10 回ダウンロードするマイクロ パッケージを作成します。あなたは、必要だと思っていなかった事柄を学ぶことになるでしょう
以上がNetflix のインタビューの質問がどのようにして私の最初の NPM パッケージになったのかの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。