There may be cases when you need to do CPU-intensive tasks on the backend like big JSON parsing, video encoding, compression, etc. If you have at least 2 cores in your processor's configuration you can run javascript in parallel for these tasks and not block the main thread of the NestJS app that handles client requests.
An excellent way of doing this is by using node.js worker threads.
You shouldn't use worker threads for I/O operations as node.js already gracefully handles this for you.
If you're eager to dive into the complete example right away, you can check out the GitHub repo for the full example.
In order to illustrate this, we will use a very simple service that calculates the fibonacci sum, which is a CPU-intensive task, a great candidate for node.js worker threads!
import { Injectable } from '@nestjs/common';
@Injectable()
export class FibonacciService {
fibonacci(n) {
if (n <= 1) {
return 1;
}
return this.fibonacci(n - 1) + this.fibonacci(n - 2);
}
}
Now, let's dive into the code snippet that launches the worker thread:
import { Injectable, Logger } from '@nestjs/common';
import { Worker, isMainThread } from 'worker_threads';
import workerThreadFilePath from './worker-threads/config';
@Injectable()
export class AppService {
private readonly logger = new Logger(AppService.name);
checkMainThread() {
this.logger.debug(
'Are we on the main thread here?',
isMainThread ? 'Yes.' : 'No.',
);
}
// do not run this from the worker thread or you will spawn an infinite number of threads in cascade
runWorker(fibonacci: number): string {
this.checkMainThread();
const thisService = this;
const worker = new Worker(workerThreadFilePath, {
workerData: fibonacci,
});
worker.on('message', (fibonacciSum) => {
thisService.logger.verbose('Calculated sum', fibonacciSum);
});
worker.on('error', (e) => console.log('on error', e));
worker.on('exit', (code) => console.log('on exit', code));
return 'Processing the fibonacci sum... Check NestJS app console for the result.';
}
}
As we can see from the snippet above, we defined a new worker, gave it a path to a file to be executed by the worker, and gave it as param the workerData. Worker data is data sent from the main thread to the other thread.
We are using the constant workerThreadFilePath
from associated config file on the same level in the directory tree so we can safely use the path for the worker thread regardless of where the app is deployed.
// it will import the compiled js file from dist directory
const workerThreadFilePath = __dirname + '/findFibonacciSum.js';
export default workerThreadFilePath;
Take a note that we are using the js extension here because the transpiled output of typescript, as we know, it's javascript.
Let's explore the file which will be executed in the worker thread:
import { NestFactory } from '@nestjs/core';
import { workerData, parentPort } from 'worker_threads';
import { AppModule } from '../app.module';
import { FibonacciService } from '../fibonacci/fibonacci.service';
import { AppService } from '../app.service';
async function run() {
const app = await NestFactory.createApplicationContext(AppModule);
const appService = app.get(AppService);
const fibonacciService = app.get(FibonacciService);
const fibonacciNumber: number = workerData; // this is data received from main thread
// here we apply business logic inside the worker thread
appService.checkMainThread();
const fibonacciSum = fibonacciService.fibonacci(fibonacciNumber);
parentPort.postMessage(fibonacciSum);
}
run();
In order to have access to dependency injection in our NestJs app in the thread, we leverage Nest standalone application which is a wrapper around the Nest IoC container, which holds all instantiated classes.
Now, we can execute any method from any service.
Caveats:
- Be cautious when using dependency injection from NestJS in a worker thread, as it comes with a cost. The startup process of the NestJS app must be awaited before accessing the dependency injection. Therefore, utilize this method only if absolutely necessary, or if the startup bottleneck is negligible in terms of overall performance.
- If your app setup is starting processes that you don't want to run on the separate thread you will need to configure your app module into a dynamic module to accept a parameter to not initiate the setup. Example:
const app = await NestFactory.createApplicationContext(
AppModule.register({ attachRabbitMqConsumers: false }),
);
Click here for the github repo.
If you'd like me to cover more interesting topics about the node.js ecosystem, feel free to leave your suggestions in the comments section. Don't forget to subscribe to my newsletter on rabbitbyte.club for updates!
Top comments (0)