Skip to main content

DenoでのPromiseキューと並行タスクのバッチ処理

著者
snyk code featured

2024年9月25日

0 分で読めます

Denoとは何ですか?

Denoは、Node.jsと同じV8 JavaScriptエンジンを基盤としながら、大幅な改良を加えた、JavaScriptおよびTypeScript向けのセキュアなランタイムです。Node.jsの開発者であるRyan Dahlが、長年の使用を通じて認識したNode.jsの主要な問題や制約に対処するために開発しました。セキュリティの強化、モジュール管理の簡素化、そしてTypeScriptの標準サポートを目的として設計されています。

js
import { serve } from "https://deno.land/std/http/server.ts";

serve((req) => new Response("Hello World\n"), { port: 8000 });
console.log("Server running on http://localhost:8000/");

サーバーが「Hello World」という文字列でリクエストに応答する様子を確認できます。import文は、URLからDeno標準ライブラリのHTTPサーバーモジュールにあるserveメソッドを直接取得します。これはDeno独自の特長の1つです。Web URLからインポートしたモジュールを取得してキャッシュするため、npmのようなパッケージマネージャーは必要ありません。

Denoで並行タスクを処理する

並行タスクの処理は、Denoのような最新のWebフレームワークやランタイムに欠かせません。JavaScriptでは、データベースからのデータ取得、HTTPリクエストの送信、ディスクからのファイル読み込みなど、非同期処理が一般的です。

JavaScriptはシングルスレッドですが、イベント駆動型の特性を活用して、メインスレッドをブロックせずにこれらのタスクを並行して処理します。

js
let promises = [promise1, promise2, promise3];
let results = await Promise.all(promises);

上の例では、JavaScriptがPromise.all()を使って並行タスクを処理する方法を示しています。複数のPromiseを並行して実行すると、Promise.all()はすべての処理が完了するまで待機し、その結果を返します。パフォーマンスの最適化に役立ちますが、欠点もあります。1つのPromiseが失敗すると、ほかのPromiseもすべて拒否されるため、大量のタスクを処理する場合はメモリ使用量が増大します。

Denoでは、Promiseキューとタスクのバッチ処理を使って、並行処理をより効率的に管理できます。これにより、タスクが1つ失敗した場合のメモリ不足や処理全体の失敗を防ぎながら、大量のタスクを処理できます。

キューイングとタスクのバッチ処理によって、より効率的で堅牢かつ高性能なコードを作成する方法を詳しく見ていきましょう。

Promiseキューと並行タスクを理解する


Promiseキューは、JavaScriptの非同期プログラミングに欠かせない要素です。Promiseキューと並行処理を理解することで、アプリケーションの効率とパフォーマンスを大幅に向上させられます。

非同期タスクを順番に処理する

Promiseキューとは、解決または拒否されるのを待っているPromiseオブジェクトのリストです。JavaScriptの「Promise」は、非同期処理の完了または失敗を表すオブジェクトです。

Promiseキューは、決められた順序で実行する必要がある複数のPromiseを管理します。Promiseが順番に解決されるようにすることで、非同期コードの実行に伴う潜在的な問題を防ぎます。

シンプルなPromiseキューの動作例を見てみましょう。

javascript
let promiseQueue = [Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)];

Promise.all(promiseQueue)
  .then(values => {
    console.log(values); // logs [1, 2, 3]
  });

上の例では、Promise.allはPromiseの反復可能オブジェクトを受け取り、渡されたPromiseと同じ順序で、解決された値の配列を返す新しいPromiseを返します。いずれかのPromiseが拒否されると、最初に拒否されたPromiseの理由とともに、返されたPromiseも拒否されます。

JavaScriptにおける並行処理とは?

JavaScriptにおける並行処理とは、ほかのタスクの実行を妨げずに、JavaScriptエンジンが複数のコード(タスク)を実行できることを指します。JavaScriptはほぼシングルスレッドであり、ユーザー空間のコードでは一度に1つの処理しか実行できません。ただし、Promiseなどの非同期プログラミングを利用すれば、JavaScriptはタスクを並行して実行するようスケジュールできます。

JavaScriptの並行処理は、ネットワークリクエスト、ファイルシステム操作などのI/O処理や、一定時間の待機が必要な処理で、パフォーマンスを大幅に向上させる可能性があります。

JavaScriptで並行タスクを実行する例を見てみましょう。

javascript
let promise1 = new Promise((resolve, reject) => {
  setTimeout(resolve, 1000, 'task1');
});

let promise2 = new Promise((resolve, reject) => {
  setTimeout(resolve, 500, 'task2');
});

Promise.all([promise1, promise2])
  .then(values => {
    console.log(values); // logs ['task1', 'task2']
  });

この例では、task1とtask2が並行して実行されます。task1の完了には時間がかかりますが、task2の実行を妨げません。

このコード例は先ほどの例と似ているように見えるかもしれませんが、先ほどのJavaScriptのコード例では、整数を戻り値として直ちに解決するPromise.resolve(1)を使用していました。この並行実行の例では、バックグラウンドスレッドでタスクを同時に実行するようスケジュールできることを示しています。

Promiseキューと並行処理が不可欠な実例

Promiseキューと並行処理は、JavaScriptのさまざまな場面で使われています。たとえばWebアプリケーションでは、複数のエンドポイントから並行してデータを取得し、そのデータを決められた順序で処理する必要があるかもしれません。これは、Promiseキューと並行処理の活用例です。

オンライン映画チケット予約システムを考えてみましょう。ユーザーが予約すると、座席の空き状況の確認、座席の確保、支払い、確認メールの送信など、複数のタスクを実行する必要があります。これらのタスクはPromiseキューで管理して順番に実行できます。また、複数のユーザーへの確認メールの送信などは、効率を高めるために並行して実行できます。

JavaScriptのPromiseキューと並行処理の概念を理解して応用することで、アプリケーションのパフォーマンスと効率を大幅に高められます。JavaScriptを学び続ける中で、複雑で高性能なアプリケーションを扱う際に、これらの概念がますます役立つでしょう。

HTTP fetchリクエストを実行するDenoアプリケーションを構築する

複数のHTTP fetchリクエストをまとめて送信できる、基本的なDenoアプリケーションを作ってみましょう。Promise.allSettled()メソッドを使って実装します。このアプリケーションでは、npmレジストリのJSONエンドポイントからパッケージデータを取得します。

このチュートリアルの手順に沿って進めてください。記事内で紹介するDenoアプリケーションの内容を再現するために、ソースコード全体を確認したい場合は、次のGitHubリポジトリを参照してください:https://github.com/snyk-snippets/deno-promise-queues-batch-queue

Deno環境をセットアップする

アプリケーション構築の最初のステップは、Deno環境のセットアップです。Denoは、V8 JavaScriptエンジンとRustプログラミング言語を基盤とする、安全なJavaScriptおよびTypeScriptランタイムです。Denoは次のコマンドでインストールできます。

bash
curl -fsSL https://deno.land/x/install/install.sh | sh

インストールが完了したら、次のコマンドを実行してインストールを確認します。

bash
deno --version

このコマンドを実行すると、インストールされているDenoのバージョンが表示されます。

シンプルなDenoプログラムを書く

このDenoプログラムでは、Promise.allSettled()メソッドを使って、複数のfetchリクエストをまとめて送信します。

Promise.allSettled()は、指定されたすべてのPromiseが解決または拒否されるまで待機し、それぞれの結果を記述したオブジェクトの配列を返すメソッドです。Promiseが確定する順序を気にせず、すべてのPromiseが確定したことだけを知りたい場合に特に便利です。

Promise.all()とPromise.allSettled()の違いは何ですか?


Promise.all()は、すべてのPromiseが成功する必要があり、いずれかが拒否された時点で処理を失敗させたい場合に適しています。一方、Promise.allSettled()は、結果にかかわらずすべてのPromiseが確定するまで待機したい場合に便利です。

npmレジストリのJSONエンドポイントからパッケージデータを取得する

このアプリケーションでは、npmレジストリのJSONエンドポイントからパッケージデータを取得します。npmレジストリはJavaScriptパッケージの大規模なデータベースで、パッケージデータをJSON形式で取得できるパブリックAPIを提供しています。取得したデータを簡単に処理できるため、今回の用途に最適です。

Denoプログラムのコード例

Promise.allSettled()を使って複数のHTTP fetchリクエストをまとめて送信するDenoプログラムの書き方を紹介します。

javascript
const urls = [
  'https://registry.npmjs.org/package1',
  'https://registry.npmjs.org/package2',
  'https://registry.npmjs.org/package3'
];

async function fetchPackageData() {
  const promises = urls.map(url => fetch(url));

  const results = await Promise.allSettled(promises);

  for (const result of results) {
    if (result.status === 'fulfilled') {
      const data = await result.value.json();
      console.log(data);
    } else {
      console.error(result.reason);
    }
  }
}

fetchPackageData();

この例では、取得したいURLの配列を作成します。次に、この配列をマッピングして、URLごとにfetchリクエストを送信します。Promise.allSettled()を使ってすべてのfetchリクエストが確定するまで待機し、その後結果を処理します。

上記のDenoプログラムの出力例(一部):

json
{
  _id: "package1",
  _rev: "8-591c2789a67390636bd11cdacd69d197",
  name: "package1",
  "dist-tags": { latest: "0.0.0" },
  versions: {
    "0.0.0": {
      name: "package1",
      version: "0.0.0",
      main: "index.js",
      scripts: { test: 'echo "Error: no test specified" && exit 1' },
      author: "",
      license: "ISC",
      _id: "package1@0.0.0",
      dist: {
        shasum: "2c1e73fbc7351131b2f657417e50c2f1330450a8",
        tarball: "https://registry.npmjs.org/package1/-/package1-0.0.0.tgz",
        integrity: "sha512-U4a0VTa026SElK/J6JbW7/pzFZHddrAQ3OEbgCHFwXV/Ytt+Ece37GppLjP+GBRBqge7dZS9qlEOOou8m/1Fiw==",
        signatures: [ [Object] ]
      },
      _from: ".",
      _npmVersion: "1.4.3",
      _npmUser: { name: "zhoutj", email: "zhoutianjia@126.com" },
      maintainers: [ { name: "zhoutj", email: "zhoutianjia@126.com" } ]
    }
  },
  readme: "ERROR: No README data found!",
  maintainers: [ { name: "zhoutj", email: "zhoutianjia@126.com" } ],
  time: {
    modified: "2022-06-23T06:46:19.731Z",
    created: "2014-05-05T02:51:24.933Z",
    "0.0.0": "2014-05-05T02:51:24.933Z",
    "0.0.1": "2014-05-05T02:52:51.624Z"
  },
  license: "ISC",
  readmeFilename: ""
}

BatchQueueモジュールでDenoアプリケーションを改善する

DenoのBatchQueueモジュールは、Denoアプリケーション内のタスク実行を管理・制御する仕組みを提供します。同時に実行するタスク数を制限できる強力なツールです。使用量に制限のあるリソースを扱う場合や、CPUまたはメモリを大量に消費するタスクを処理する場合に役立ちます。

特にHTTPリクエストの場合、npmレジストリなどのリモートサービスに過剰な負荷をかけないよう、適切な利用者として同時に送信するHTTPリクエスト数を制限するのが望ましい方法です。同時リクエスト数を制限すれば、プログラムがサービス妨害の可能性があると判定されるのを防ぐことにもつながります。

DenoのBatchQueueを使うと、Promiseのキューを作成し、バッチ単位で実行できます。この手法は、システムリソースの過負荷を防ぎ、タスクのスループットを高めることで、Denoアプリケーションのパフォーマンスを大幅に向上させます。

少数のタスクを並行実行するキューを使うようDenoプログラムを改善する

複数のHTTPリクエストを並行して送信するDenoプログラムを考えてみましょう。デフォルトでは、Denoはこれらのタスクをすべて同時に実行しようとします。しかし、タスク数が多く、使用可能なシステムリソースを使い切ってしまう場合や、使用制限のある外部システムとやり取りする場合には、問題が発生する可能性があります。

BatchQueueモジュールを導入すると、並行して実行するタスク数を制御できます。DenoプログラムでBatchQueueを使う簡単なコード例を紹介します。

javascript
import { BatchQueue } from "https://deno.land/x/batch_queue/mod.ts";

// URLs to make HTTP requests to
const urls = [
  'https://registry.npmjs.org/package1',
  'https://registry.npmjs.org/package2',
  'https://registry.npmjs.org/package3'
];

// Initialize the queue with a concurrency of 2
const q = new BatchQueue(2);

// Function to make HTTP request
async function makeHttpRequest(url) {
  try {
    const response = await fetch(url);
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error('Error fetching URL:', url, error);
  }
}

for (const url of urls) {
  q.queue(() => makeHttpRequest(url));
}

await q.run()
await q.allSettled

この例では、BatchQueueのconcurrencyを5に設定しています。つまり、同時に実行できるタスクは5つまでです。

各タスクはBatchQueueに追加され、実行のためにキューから取り出されるとmakeHttpRequest関数が呼び出されます。BatchQueueによって、常に実行されるタスクは5つまでに制限されます。

PromiseキューとPromise.all()の比較

BatchQueueを使った改善後のDenoプログラムを最初のバージョンと比較すると、システムリソースの使用量とDenoアプリケーション全体のパフォーマンスが大幅に改善していることがわかります。

BatchQueueの仕組みによって、Denoプログラムが使用可能なシステムリソースを使い果たすのを防げます。これは、多数のタスクを並行して実行する場合によく起こる問題です。

DenoでBatchQueueモジュールを使うと、アプリケーションのタスク実行を効率的に管理し、Denoプログラムのパフォーマンスと応答性を高められます。

DenoアプリケーションにProgressBarを追加する

Denoアプリケーションにフィードバック機能を実装すると、ユーザーエクスペリエンスを大幅に向上させられます。CLIにフィードバックを提供する直感的な方法の1つが、プログレスバーの使用です。

ProgressBarは、Denoアプリケーションへのプログレスバーの追加を簡単にするモジュールの1つです。

ProgressBarモジュールは軽量でカスタマイズ性が高く、アプリケーションに簡単に組み込めるプログレスバーを提供します。シンプルなAPIで手軽に実装できるほか、アプリケーションに合わせて外観を調整するオプションも用意されています。

ProgressBarモジュールをインストールするには、次のようにアプリケーションにインポートします。

javascript
import ProgressBar from "https://deno.land/x/progress/mod.ts";

ProgressBarを追加して完了したリクエストの進捗状況を表示する

ProgressBarモジュールをインポートしたら、Denoアプリケーションで完了したリクエストの進捗状況を視覚的に表示できます。

プログレスバーが表すステップの合計数を指定して、ProgressBarのインスタンスを作成します。リクエストが完了するたびに.tick()メソッドを呼び出すと、プログレスバーが進みます。

ProgressBarを使って、完了したリクエストの進捗状況を追跡・出力する基本的な例を紹介します。

javascript
import ProgressBar from "https://deno.land/x/progress/mod.ts";

const totalRequests = 100;
const progressBar = new ProgressBar({
  total: totalRequests,
  complete: '=',
  incomplete: ' ',
  display: ':bar :percent',
});

// Simulate a loop that represents a batch of requests
for (let i = 0; i < totalRequests; i++) {
  // Simulate a completed request
  progressBar.render(i);
  await new Promise((resolve) => setTimeout(resolve, 100)); // simulate async delay
}

この例では、リクエストが完了するたびにプログレスバーが1段階進みます。ProgressBarインスタンスの作成時にcompleteとincompleteのオプションを調整することで、プログレスバーの表示をカスタマイズできます。

ProgressBarを使用したDenoプログラムのコード例

最後に、より実際の状況に近いシナリオでProgressBarを組み込む、少し複雑な例を見てみましょう。APIリクエストをまとめて送信し、その進捗を追跡したいとします。

javascript
import { BatchQueue } from "https://deno.land/x/batch_queue/mod.ts";
import ProgressBar from "https://deno.land/x/progress/mod.ts";

// URLs to make HTTP requests to
const urls = [
  'https://registry.npmjs.org/package1',
  'https://registry.npmjs.org/package2',
  'https://registry.npmjs.org/package3'
];

const totalRequests = urls.length;
let currentRequest = 0;
const progressBar = new ProgressBar({
  total: totalRequests,
  complete: '=',
  incomplete: ' ',
  display: ':bar :percent',
});

// Initialize the queue with a concurrency of 2
const q = new BatchQueue(2);

// Function to make HTTP request
async function makeHttpRequest(url) {
  progressBar.render(++currentRequest);
  const response = await fetch(url);
  const data = await response.json();
  // @TODO do something with the data...
}

for (const url of urls) {
	q.queue(() => makeHttpRequest(url));
}

await q.run()
await q.allSettled

この例では、APIリクエストの送信をシミュレーションし、ProgressBarを使って進捗を追跡します。sendRequest()の呼び出しはそれぞれ、リクエストの送信とレスポンスの待機を表します。リクエストが完了するたびにprogressBar.tick()を呼び出して、プログレスバーを進めます。

Promises Batch QueueモジュールとProgressBarモジュールの両方を使ったDenoプログラム全体を実行すると、コマンドラインには次のように出力されます。

bash
@lirantal ➜ /workspaces/project-app (main) $ deno run -A app.ts 
================================================== 100.00%

これで完了です。DenoアプリケーションにProgressBarを追加し、完了したリクエストの進捗を表示できるようになりました。ProgressBarモジュールを使えば、時間のかかるタスクの状況を直感的に追跡できます。次のDenoプロジェクトでぜひ試してみてください。

Promise Batch Queueの例を含むDenoアプリケーション全体は、こちらからご覧いただけます:https://github.com/snyk-snippets/deno-promise-queues-batch-queue

Denoアプリケーションのセキュリティ:SSRFの脆弱性

サイバーセキュリティは、あらゆるソフトウェア開発プロジェクトにおいて基本となる重要事項です。Denoのエコシステムで開発者が注意すべきセキュリティリスクの一つに、サーバーサイドリクエストフォージェリ(SSRF)の脆弱性があります。

このセクションでは、SSRFの脆弱性とは何か、Denoアプリケーションにどのようなリスクをもたらすのか、そしてそのリスクを軽減する方法について解説します。また、安全なDenoプログラムの実践的なコード例も紹介します。

SSRFの脆弱性とは?

SSRFは、攻撃者がサーバーサイドアプリケーションを操作し、不正なネットワーク通信を行わせる脆弱性です。攻撃者が内部システムと通信できるようになり、不正アクセスやデータ漏えいにつながるおそれがあるため、リスクとなります。つまり、SSRFの脆弱性によって、サーバーが攻撃者の悪意ある活動のためのプロキシとして悪用される可能性があります。

DenoアプリケーションにおけるSSRFの脆弱性

Denoはデフォルトで安全になるよう設計されており、ユーザーが明示的に権限を付与しない限り、すべてのスクリプトは安全なサンドボックス内で実行されます。しかし、必要な権限を付与すると、DenoアプリケーションがSSRFの脆弱性にさらされる可能性があります。

たとえば、Denoアプリケーションに「allow-net」権限を付与すると、あらゆるホストにネットワークリクエストを送信できます。攻撃者がリクエストに悪意あるホストを挿入すると、これを悪用してSSRFの脆弱性を引き起こす可能性があります。

一般に、開発者は作成したPromiseキュープログラムなどのDenoアプリケーションを、次のように実行することがあります。


bash
$ deno run --allow-net app.ts

DenoアプリケーションにおけるSSRFの脆弱性の軽減

SSRFの潜在的な脆弱性を軽減するには、開発者は次のようなセキュアコーディングの実践が必要です。

  1. 最小権限:Denoアプリケーションが適切に機能するために必要な最小限の権限のみを付与します。アプリケーションがネットワークリクエストを必要としない場合は、--allow-net権限(または短縮形の-Aコマンドラインフラグ)を付与しないでください。

  2. 入力の検証:すべてのユーザー入力を適切に検証し、不審な入力や形式が正しくない入力は拒否します。一般に、ユーザーが絶対URLを指定できないようにします。アプリケーションがホストし、許可する相対URLのみに制限してください。

安全なDenoプログラムのコード例

SSRFの脆弱性を軽減するためのセキュリティ対策を取り入れた、基本的なDenoアプリケーションを見てみましょう。

javascript
import { serve } from "https://deno.land/std/http/server.ts";

console.log("Server running on http://localhost:8000/");

serve((req) => {
  const url = new URL(req.url, `http://${req.headers.get('host')}`);
  const host = url.hostname;

  if (host !== 'localhost' && host !== '127.0.0.1') {
    return new Response("Unauthorized host\n");
  } else {
    return new Response("Hello World\n");
  }
}, { port: 8000 });

このコード例では、ポート8000でHTTPサーバーを起動します。リクエストを受信すると、リクエストURLのホスト名を確認します。「localhost」または「127.0.0.1」以外の場合は「Unauthorized host」というメッセージを返し、外部ホストへのリクエストを防ぎます。

セキュアコーディングを実践し、Denoアプリケーションに付与する権限に注意することで、SSRFの脆弱性のリスクを効果的に軽減できます。

Denoで並行タスクを効率的に処理する重要性

高性能なアプリケーションを開発するには、並行タスクを効率的に管理することが重要です。Denoは、多くの非同期処理を伴うことが多い最新のJavaScriptやTypeScriptのコードを扱うように設計されているため、特に重要になります。

Promiseキューとバッチ処理を活用すれば、リソースを効率的に管理し、メモリ不足やCPUスロットリングといった問題を回避できます。これによりアプリケーションの信頼性と応答性が向上し、ユーザーエクスペリエンス全体が改善されます。

Denoとそのモジュールについてさらに詳しく知る

Denoでできることは、Promiseキューや並行タスクのバッチ処理だけではありません。堅牢で安全かつ効率的なアプリケーションの構築に役立つ、豊富な機能とモジュールを備えた強力で柔軟なランタイムです。

たとえば、Denoの標準ライブラリには、Denoとの互換性が保証された、高品質でレビュー済みのモジュールが揃っています。日時の操作、ハッシュ化、HTTPサーバー、テストなどに使えるモジュールが含まれています。

セキュリティを重視するDenoでは、アプリケーションがアクセスできる対象を細かく制御できます。これはサイバーセキュリティの脅威が高まる時代に欠かせません。ただし、SSRFの脆弱性をはじめとする多くの脆弱性には、依然として開発者による個別の対応とセキュリティ制御の追加が必要です。

Denoについて、またその使い方についてさらに理解を深めるには、Denoの公式ドキュメントをご覧ください。Promiseキューや並行タスクのバッチ処理を理解するうえで役立つ、Denoのさまざまな機能やモジュールに関する情報が掲載されています。

アプリケーションの安全性を確保するには、サーバーサイドリクエストフォージェリ(SSRF)の脆弱性を含む、さまざまなアプリケーションセキュリティの脅威を理解することが重要です。SSRFの脆弱性を理解し、軽減するために役立つリソースを以下に紹介します。

開発しながらコードを安全に

Snykはコードの品質とセキュリティ上の問題をスキャンし、IDE上で修正方法を提案します。