Promise queues and batching concurrent tasks in Deno
25 septembre 2024
0 minutes de lectureWhat is Deno?
Deno is a secure runtime for JavaScript and TypeScript, built on the same V8 JavaScript engine as Node.js, but with significant improvements. It was created by Ryan Dahl, the original creator of Node.js, to address some of the key issues and limitations he identified in Node.js over time. Deno is designed to enhance security, simplify module management, and support TypeScript out of the box.
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/");
You can see how the server responds to requests with the string "Hello World." The import
statement fetches the serve
method from the Deno standard library's HTTP server module directly from a URL. This is one of Deno's unique aspects: it fetches and caches the imported modules from web URLs, eliminating the need for a package manager like npm
.
Handling concurrent tasks in Deno
Handling concurrent tasks is critical to any modern web framework or runtime like Deno. In JavaScript, asynchronous operations are common—whether fetching data from a database, making HTTP requests, or reading files from the disk.
JavaScript, being single-threaded, relies on its event-driven nature to manage these tasks concurrently without blocking the main thread.
js
let promises = [promise1, promise2, promise3];
let results = await Promise.all(promises);
The example above demonstrates how JavaScript handles concurrent tasks using Promise.all()
. When executing multiple promises concurrently, Promise.all()
waits for all of them to complete and then returns their results. This is great for optimizing performance, but it has its drawbacks. If one promise fails, all the others are rejected, too, leading to high memory usage for large numbers of tasks.
In Deno, we can use Promise queues and task batching to manage concurrency more efficiently. This provides a better way to handle large numbers of tasks without risking memory overload or complete failure in case of a single task error.
Let’s dive in and explain how these queuing and task batching techniques can help you write more efficient, robust, and performant code.
Understanding Promise queues and concurrent tasks
Promise queues are an essential part of asynchronous programming in JavaScript. Understanding Promise Queues and concurrency can significantly enhance the efficiency and performance of your applications.
A Sequential Approach to Asynchronous Tasks
A Promise queue is a list of Promise objects waiting to be resolved or rejected. The "Promise" in JavaScript is an object representing the eventual completion or failure of an asynchronous operation.
A Promise queue helps manage multiple promises that must be executed in a specific order. It ensures that promises are resolved sequentially, preventing potential issues from asynchronous code execution.
Here is an example of a simple Promise queue in action:
javascript
let promiseQueue = [Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)];
Promise.all(promiseQueue)
.then(values => {
console.log(values); // logs [1, 2, 3]
});
In the above example, Promise.all
accepts an iterable of promises and returns a new promise that fulfills with an array of the fulfilled values from the passed promises in the same order. If any promise rejects, then the returned promise rejects with the reason of the first promise that rejected.
What is concurrency in JavaScript?
Concurrency, in the context of JavaScript, refers to the ability of the JavaScript engine to execute multiple pieces of code (tasks) without blocking the execution of other tasks. It's worth noting that JavaScript is mostly single-threaded, meaning it can only perform one operation at a time in userspace code. However, with the help of asynchronous programming (like promises), JavaScript can schedule tasks to run concurrently.
Concurrency in JavaScript can lead to significant performance improvements, especially when dealing with I/O operations such as network requests, file system operations, or any operation that might require waiting for some time.
Here's an example of concurrent tasks in 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']
});
In this example, task1
and task2
are executed concurrently. Although task1
takes more time to complete, it doesn't block the execution of task2
.
This code example might look similar to the former one, but in the former JavaScript code examples, we used Promise.resolve(1)
, which immediately resolved with the integer as a return value. This concurrent execution example shows how tasks can be scheduled to execute simultaneously in a background thread.
Real-world scenarios where Promise queues and concurrency are essential
Promise queues and concurrency are everywhere in JavaScript. For instance, in a web application, you might need to fetch data from multiple endpoints concurrently and then process the data sequentially in a specific order, perfectly illustrating the use of Promise queues and concurrency.
Consider an online movie ticket booking system. When a user makes a booking, the system might need to perform several tasks like checking seat availability, reserving the seat, making a payment, and sending a confirmation email. These tasks can be managed using Promise queues to ensure they are carried out in order, while some tasks like sending confirmation emails to multiple users can be performed concurrently for efficiency.
Understanding and applying the concepts of Promise queues and concurrency in JavaScript can significantly boost the performance and efficiency of your applications. As you continue to explore JavaScript, hese concepts will be increasingly useful when dealing with complex, high-performance applications.
Building a Deno application to execute HTTP fetch requests
Let’s explore how to build a basic Deno application capable of sending multiple HTTP fetch requests in bulk. We will achieve this by using the Promise.allSettled()
method. Our application will fetch package data from the npm registry JSON endpoint.
You are encouraged to follow this step-by-step tutorial as is, but if you ever need to see a complete source-code reference to reproduce any of the Deno application details throughout this article you can refer to the following GitHub repository: https://github.com/snyk-snippets/deno-promise-queues-batch-queue
Setting up the Deno environment
The first step in building our application is to set up the Deno environment. Deno is a secure JavaScript and TypeScript runtime based on the V8 JavaScript engine and the Rust programming language. To install Deno, you can use the following command:
bash
curl -fsSL https://deno.land/x/install/install.sh | sh
After the installation is complete, verify your installation by running the following command:
bash
deno --version
This command will print the installed Deno version.
Writing a simple Deno program
Our Deno program will send multiple fetch requests in bulk using the Promise.allSettled()
method.
The Promise.allSettled()
is a method that returns a promise that resolves after all of the given promises have either resolved or rejected, with an array of objects that each describes the outcome of each promise. This method is particularly useful when you do not care about the order in which the promises settle, but rather, you just need to know when they have all settled.
What is the difference between Promise.all()
and Promise.allSettled()
?
Promise.all()
is suitable when you need all promises to succeed and want to fail fast on any rejection, whereas Promise.allSettled()
is useful when you want to wait for all promises to settle regardless of their outcome.
Fetching npm package data from the npm registry JSON endpoint
Our application will fetch package data from the npm registry JSON endpoint. The npm registry is a large database of JavaScript packages, and it provides a public API that allows us to fetch package data in JSON format. This is ideal for our needs, as it allows us to process the fetched data easily.
Code example of the Deno program
Here is an example of how to write a Deno program that sends multiple HTTP fetch requests in bulk using Promise.allSettled()
.
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();
In this example, we create an array of URLs we want to fetch. We then map over this array, sending a fetch request for each URL. We use Promise.allSettled()
to wait for all of the fetch requests to settle, and then we process the results.
A partial response of the above Deno program is:
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: ""
}
Improving the Deno application with the BatchQueue module
The BatchQueue module in Deno provides a mechanism to manage and control the execution of tasks in your Deno application. It is a powerful tool that offers a way to limit the number of concurrently executing tasks, which can be useful when working with resources that have usage restrictions or when you are dealing with tasks that are CPU or memory-intensive.
Specifically in the case of HTTP requests, it is better to play a good citizen and limit the concurrent number of HTTP requests sent to a remote service such as the npm registry, to avoid saturating and overloading it. Limiting concurrent requests will also allow your program to avoid getting flagged as a potential denial of service.
BatchQueue in Deno makes it possible to create a queue of promises and execute them in batches. This technique can significantly improve the performance of your Deno application by preventing the overloading of your own system resources and improving the throughput of the tasks.
Improving the Deno program to use a queue with a small number of concurrency
Let's consider a Deno program that makes multiple HTTP requests concurrently. By default, Deno will attempt to execute all these tasks simultaneously. However, this could lead to a problem if the number of tasks is large enough to consume all available system resources or if the tasks involve interactions with external systems that have usage limits.
By introducing the BatchQueue module, we can control the number of concurrent tasks. Here's a simplified code example of how to use BatchQueue in a Deno program:
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
In this example, the BatchQueue is set with a concurrency
of 5
, which means it will only allow five tasks to be executed concurrently.
Each task is enqueued in the BatchQueue, and the makeHttpRequest
function is invoked when the task is dequeued for execution. The BatchQueue ensures that only five tasks are executed at any given time.
A Promises queue vs Promise.all()
When comparing the improved Deno program using BatchQueue with the initial version, you will observe a significant improvement in the system resource usage and the overall performance of your Deno application.
The BatchQueue mechanism ensures that the Deno program does not exhaust all available system resources, which is common when executing a large number of tasks concurrently.
By using the BatchQueue module in Deno, you can efficiently manage the execution of tasks in your application and improve the performance and responsiveness of your Deno programs.
Adding ProgressBar to the Deno application
Implementing feedback in your Deno application can dramatically improve the user experience. One of the most intuitive ways to provide this feedback to a CLI is by using a progress bar.
One such module is the ProgressBar, which simplifies the process of adding a progress bar to your Deno application.
The ProgressBar module is a lightweight and highly customizable package that provides an easy-to-integrate progress bar for your applications. It offers a simple API that makes it a breeze to implement, and it has customizability options that allow you to tailor its appearance to your application.
To install the ProgressBar module, you need to import it in your application like so:
javascript
import ProgressBar from "https://deno.land/x/progress/mod.ts";
Adding ProgressBar to print the progress of requests completed
After importing the ProgressBar module, you can now use it to provide visual feedback on the progress of requests completed in your Deno application.
You can create a new instance of ProgressBar, specifying the total number of steps that the progress bar should represent. Then, for each completed request, you call the .tick()
method to advance the progress bar.
Here's a basic example of how to use ProgressBar to track and output the progress of completed requests:
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
}
In this example, the progress bar will advance by one step each time a request is completed. The progress bar's appearance can be customized by adjusting the complete
and incomplete
options when creating the ProgressBar instance.
Code example of the Deno program with ProgressBar
Finally, let's look at a more elaborate example of integrating the ProgressBar in a more real-world scenario. Let's assume we have a batch of API requests to send and want to track their progress.
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
In this example, we simulate sending API requests and use ProgressBar to track their progress. Each call to sendRequest()
represents a request being sent and a response being awaited. For each completed request, we advance the progress bar by calling progressBar.tick()
.
The result of running our full Deno program with both the Promises Batch Queue module and the ProgressBar module results in the following command-line output:
bash
@lirantal ➜ /workspaces/project-app (main) $ deno run -A app.ts
================================================== 100.00%
And there you have it! You've now added a ProgressBar to your Deno application to print the progress of requests completed. The ProgressBar module is a great addition to your Deno applications, providing an intuitive way to track the status of long-running tasks. Try it out in your next Deno project!
Remember, a full Deno application for this Promise Batch Queue example is available at: https://github.com/snyk-snippets/deno-promise-queues-batch-queue
Security in Deno application - SSRF vulnerability
Cybersecurity is a fundamental concern in any software development project. In the Deno ecosystem, one potential security risk that developers need to be vigilant about is the Server-Side Request Forgery (SSRF) vulnerability.
This section will explore SSRF vulnerability, how it poses a risk in Deno applications, and ways to mitigate it. We will also provide a practical code example of a secured Deno program.
What is an SSRF vulnerability?
SSRF is a type of vulnerability that allows an attacker to manipulate the server-side application to perform unauthorized network interactions. It is a risk because it enables an attacker to interact with internal systems, which could potentially lead to unauthorized access or data leakage. In essence, SSRF vulnerability can turn the server into a proxy for the attacker's malicious activities.
SSRF vulnerability in Deno applications
Deno is designed to be secure by default, meaning all scripts run in a secure sandbox unless explicitly given permissions by the user. However, when needed permissions are granted, Deno applications are potentially exposed to SSRF vulnerabilities.
For instance, if a Deno application is given the "allow-net" permission, it can make network requests to any host. This could potentially be exploited by an attacker who injects a malicious host into the request, leading to SSRF vulnerability.
Commonly, developers may run Deno applications like the Promise queue program we wrote as follows:
bash
$ deno run --allow-net app.ts
Mitigating SSRF vulnerability in Deno applications
To mitigate potential SSRF vulnerabilities, developers need to adopt secure coding practices, including:
Least Privileges Only grant the minimum permissions required for the Deno application to function effectively. If the application doesn't need to make network requests, don't grant the
--allow-net
permission (or its short syntax-A
command-line flag).Input Validation: Properly validate all user inputs and reject suspicious or malformed ones. Users are generally not allowed to provide absolute URLs. Limit them to relative URLs your application hosts and allows.
Code example of a secured Deno program
Let's look at a basic Deno application with security measures to help mitigate SSRF vulnerability.
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 });
In this code example, we are serving an HTTP server on port 8000. When a request is received, we check the hostname in the request URL. If it is not 'localhost' or '127.0.0.1', we respond with an "Unauthorized host" message, thus preventing any requests to external hosts.
By adopting secure coding practices and being mindful of the permissions we grant our Deno applications, we can effectively mitigate the risk of SSRF vulnerabilities.
Importance of handling concurrent tasks efficiently in Deno
Efficient management of concurrent tasks is critical in developing high-performance applications. In Deno, this becomes even more important as Deno is built to handle modern JavaScript and TypeScript code, which often involves many asynchronous operations.
By leveraging Promise queues and batching, we can effectively manage resources and avoid pitfalls such as memory overflow or CPU throttling. This improves application reliability and responsiveness, enhancing the overall user experience.
Explore Deno and its modules further
Deno offers much more than Promise queues and concurrent task batching. It's a powerful and flexible runtime with a wealth of features and modules that can help build robust, secure, and efficient applications.
For instance, Deno's standard library provides a set of high-quality, reviewed modules that are guaranteed to work with Deno. It includes modules for datetime manipulation, hashing, HTTP servers, testing, and more.
With its emphasis on security, Deno also allows granular control over what an application can access, which is crucial in the era of cybersecurity threats. That said, many vulnerabilities, like the SSRF vulnerability, still require ad hoc intervention and security controls added by developers.
For furtherunderstanding of Deno and how to use it, read the official Deno documentation. Here, you'll find information about various Deno functionalities and modules, which will be instrumental in understanding Promise Queues and batching concurrent tasks.
To ensure your application is secure, it is crucial to understand different application security threats, including Server Side Request Forgery (SSRF) vulnerabilities. Here are some additional resources that can help you understand and mitigate SSRF vulnerabilities:
URL confusion vulnerabilities in the wild: Exploring parser inconsistencies
Secure JavaScript URL validation- A technical analysis of the Capital One cloud misconfiguration breach
Sécurisez votre code à mesure que vous l’écrivez
Snyk analyse votre code pour y détecter d’éventuels problèmes de qualité et de sécurité, et vous fournit des conseils pour les corriger directement dans votre IDE.