Using worker threads in FeathersJS

Jiri Richter
5 min readFeb 27, 2021

--

Javascript is a single-threaded language and it provides lots of ways to performantly deal with tasks that in other languages would routinely be dispatched off the main thread.

Still, we can sometimes find it useful to be able to lighten up the event loop and move tasks that are not time-critical out of the way.

This is especially true with NodeJS/Express apps where we expect to be able to serve thousands of requests per second. We need the event loop to be as light as possible. Any task that doesn’t have to be finished in one request/response cycle should be moved out of the way and deferred using setImmediate(). But if we are using any blocking techniques in that task (like looping, synchronous writing to file, heavy computation), we could move that task completely out of the main thread and let it be computed by a different runtime — a worker thread. By doing so, we’ll keep our main event loop absolutely clean of any work and ready for maximum request throughput.

I was recently implementing worker threads for my Feathers app, and had some struggles with it — so after I figured it out, I thought I should share my findings.

Why I needed worker threads

In my app I have a Report generating service. This service is being called to create or update a Report in an after hook, when one of the involved entities changes (for example Users).

So — we update a User, and in its `after` hook, Report generating service is being called to update all reports for that User.

Updating the reports can be quite a heavy task, which needs lots of calls to other services, lots of information from the database, then it needs to put all of the information together and again do a lot of database updates.

Even though Feathers is using async calls everywhere and database calls are not heavy by themselves, I think there are a few reasons that make this case a clear candidate for a worker thread:

  • it’s a big lump of self-contained work that is not time-critical (ie. a few seconds or even minutes here and there don’t play any role)
  • I’m using sequelize (through feathers-sequelize) and I found that Sequelize does a lot of looping work when you request nested data — basically it needs to convert duplicates returned by a JOIN in a 1:N or M:N relationship into nested arrays. The bigger the result set, the exponentially more work this is.
  • There can be many instances of this task running concurrently, if lots of Users would be updated at about the same time.
  • I’m requesting deeply nested objects from DB and doing ugly nested loops on them .]

This all is slowing down response time on my app when the Report update is running.

Worker threads to the rescue!

Looking out to implement worker threads, I found a beautiful package called threads.js which simplifies work with the worker_threads NodeJS API. It provides a fresh view on the API, with async/await and Observable support instead of callbacks and message passing, also adds an extremely useful Pool class to let you manage worker thread pools, and is just really nice to work with. (Oh, and threads.js implement worker_threads that come with Node 12, have a polyfill for Node 8+, and also implement web workers so you can use exactly the same API in the browser.)

This is the basic implementation of worker threads with a thread pool in a custom Feathers service (called Jobs):

We need to do three things:

  • Create the worker in a separate file (we expose either an object or a function to be able to run it).
  • In the service, create the thread pool in the setup method, by calling
this.workerPool = Pool<JobWorker & Thread>(() => spawn<JobWorker>(new Worker(“./jobWorker”)), 8)

This will create 8 JobWorkers that will be waiting for tasks.

  • In the service calls, queue the task. In the code above, I’m doing this in the create method.

Now, when we POST on the /jobs service, we’ll see 'Hello from worker thread!' logged in the server logs.

Calling services from the worker thread

Trouble was, I didn’t need just to do some detached work in the worker thread, like hashing passwords or computing differential equations. I needed to call other Feathers services to be able to independently work with the database. But I couldn’t just pass the app object into the worker thread, as there is no shared memory between the main thread and the worker threads (except for ArrayBuffers, which are more data-centric and don’t work well for a very complex 3rd party object).

So how do I get the app object in my worker thread? Turns out, I can simply import it. When the worker is initialized, it will import the app, which will set up a complete feathers app in the worker thread, so I can do everything I could in the main thread — but this app is not exposed on any server port, so it can be only operated from the worker thread interface.

Since heating up a complete Feathers app is a heavier task, it is important to be using the thread pool and initialize it on our main app start — we could also spawn a short-lived worker thread whenever our service is being called, but that would add a significant overhead. It would be heavy even without initializing Feathers inside the worker thread, so I don’t recommend that.

Take a look at the code here — it’s the same code as above, just altered to be able to call Feathers services from inside the worker thread:

There are two things to note:

  • In the service, I’m now initializing the thread pool inside !isWorkerRuntime() check. When we initialize the Feathers app inside each worker thread, it calls setup() method on all services, so if we didn’t do this check, we would be recursively creating infinite thread pools.
  • In the jobWorker, I’m calling app.setup(). This one has bugged me for a while — I have to call app.setup() because this is what Feathers uses to initialize (among other things) Sequelize associations. Without this call, everything was working properly except associations, which led to odd errors when requesting nested data from the database.

Thanks for reading and hope this can help you get running worker threads in your Feathers app in no time!

--

--

Jiri Richter
Jiri Richter

Written by Jiri Richter

Dad of 2 boys, programmer. Experience: web (Angular, Ionic, NodeJS + FeathersJS), mobile (Android), wearable (Garmin, Fitbit, Samsung) and stuff here and there.

No responses yet