async.parallel for app.use with second parameter

Viewed 154

I use express v4.17.1 and would like to make my app.use() middlewares to run in parallel.

So I have found some examples in the internet.

Example:

function runInParallel() {
  async.parallel([
    getUserProfile,
    getRecentActivity,
    getSubscriptions,
    getNotifications
  ], function(err, results) {
    //This callback runs when all the functions complete
  });
}

But what I have in my application is:

const app = express();
const APP_FOLDER = "bdt";

app.use(httpContext.middleware);
app.use(metricsMiddleware);
app.use(rtEndMiddleware);
app.use(trackingContextMiddleware);
app.use(healthRoutes());

app.use("/" + APP_FOLDER + "/api/products", productsRoutes);
app.use("/tcalc/" + APP_FOLDER + "/api/products", productsRoutes);

productRoutes is this:

const jsonParser = bodyParser.json({
  limit: "1mb",
});

const accessFilter = accessFilterMiddleware(Registry.list());
const localDevFilter = localDevFilterMiddleware(Registry.list());

const apiRoutes: Router = Router();


apiRoutes.get("/", listProducts);
apiRoutes.get("/healthz", cProductHealth);
apiRoutes.get("/:id", accessFilter, localDevFilter, fetchProductData);
apiRoutes.post(
  "/:id",
  accessFilter,
  localDevFilter,
  jsonParser,
  fetchProductData,
);
apiRoutes.get(
  "/:id/fields/:fieldId/options",
  accessFilter,
  localDevFilter,
  fetchProductOptions,
);
apiRoutes.post(
  "/:id/loadmoreoptions",
  accessFilter,
  localDevFilter,
  jsonParser,
  loadMoreOptions,
);
apiRoutes.post("/:id/ploy", accessFilter, jsonParser, fetchMultipleProductData);
apiRoutes.post(
  "/:id/gxx",
  accessFilter,
  localDevFilter,
  jsonParser,
  fetchGssData,
);
apiRoutes.get("/:id/healthz", collectProductHealth);

I think for the first ones it should be easy:

async.parallel([
  httpContext.middleware,
  metricsMiddleware,
  rtEndMiddleware,
  trackingContextMiddleware,
  healthRoutes()
], function(err, results) {
  //This callback runs when all the functions complete
});

But my question: How can I do it with a second parameter (productRoutes) in this case ?

app.use("/" + APP_FOLDER + "/api/products", productsRoutes);
app.use("/tcalc/" + APP_FOLDER + "/api/products", productsRoutes);

2 Answers

You've been going down a rabbit hole and you should change your approach quite a bit, currently you're completely over complicating a simple problem and the solution you're looking for is not possible in javascript.

If you want to create routes in express then don't use app.use for everything, it should only be used for middleware or to register a router, on which you can define routes.

You should be using:

app.get('/', () => ...

To define your routes. Alternatively you can use a router for that as well by doing:

app.use(router)

...

router.get('/', () => ...

More than that if you want to define async or "parallel" routes in javascript then just define async callbacks like normal, remove most of the stuff you've done.

app.get('/', async () => ...

That is now a route that will execute asynchronously.

You should also be careful not to just mess around with express' middleware because you're going to mess up the existing middleware (like error routes).

What's more is that the library you're referring to is just a helper library with neat functionality, it won't fundamentally change how javascript works. When you call an async function it will be added to the event queue and still be executed synchronously one after the other in serial, true multi threading isn't possible except for service workers and browser API calls executing temporarily in a single thread of their own, before being added to the event queue anyways.

What you're looking for is just a simple: router.get('/', async () => ..., that's the best you can do and it will appear that all your routes are executing in parallel.

After you've declared multiple of those then you can invoke all of them with something like Promise.all. My best guess is that this is what something like parallel is doing as well.

Middleware in Concept

As far as I understand it, middleware is akin to a chain, where each middleware item:

  • performs some initial logic
  • calls the next link in the chain, then
  • performs some final logic

where both of the chunks of logic are optional, but calling the next link is not. For example, with your first set of middleware, through to healthRoutes(), it could be visualised like so:

> httpContext.middleware

  > metricsMiddleware

    > rtEndMiddleware

      > trackingContextMiddleware

        /healthRoutes

      < trackingContextMiddleware

    < rtEndMiddleware

  < metricsMiddleware

< httpContext.middleware

This chain structure is generally used because each middleware may enhance the common state of the request, sometimes conditionally based on the "output" of previously executed middlewares, and for many middlewares the initial logic needs to be performed before the body of the request, and the final logic after.

Middleware - Parallelisable or Not?

Based on the parallelisation in the blog post, the previously separate middlewares (getUser, getSiteList, getCurrentSite and getSubscription) very likely operate independently, which, in my opinion, makes them quite poor candidates for use as middleware. It's no wonder that they observed a significant performance improvement, as these items should never have been run in series in the first place. If we employ the same parallel() function from the post (note, specifically that function, not async.parallel), for the same middlewares as above, the execution looks more like:

> httpContext.middleware

< httpContext.middleware

> metricsMiddleware

< metricsMiddleware

> rtEndMiddleware

< rtEndMiddleware

> trackingContextMiddleware

< trackingContextMiddleware

/healthRoutes

So, parallelising the middlewares changes the order of execution significantly. Only you can determine whether this would be acceptable for your application, but I can imagine that both metricsMiddleware and trackingContextMiddleware may need to perform some logic both before and after the request being executed.

Express Route Syntax

If you decide that you do want to parallelise some or all of the middleware, I'd suggest taking advantage of native Promises directly, not a separate library, and do something like:

const parallel = (...middlewares) => (req, res, next) => {
  Promise.all(middlewares.map(x => new Promise((resolve, reject) => {
    x(req, res, resolve);
  }))).then(() => next());
}

The important parts of this function are:

  • returning a function that takes the req, res, and next arguments, satisfying the requirement of Express middleware, that
    • maps each middleware to a Promise that executes the middleware and is passed the resolve argument of the Promise callback as its next argument
    • gates all these Promises behind Promise.all, and finally
    • calls the externally provided next argument when that gated Promise resolves

Then, if you decided that you wanted to have httpContent.middleware and metricsMiddleware run in parallel, but the others in series, you'd use that like:

app.use(parallel(
  httpContext.middleware,
  metricsMiddleware
));
app.use(rtEndMiddleware);
app.use(trackingContextMiddleware);
app.use(healthRoutes());

the execution of which could be visualised as:

> httpContext.middleware

< httpContext.middleware

> metricsMiddleware

< metricsMiddleware

> rtEndMiddleware

  > trackingContextMiddleware

    /healthRoutes

  < trackingContextMiddleware

< rtEndMiddleware

As for the remaining two app.use() statements, given that each subsequent app.use() will add an item onto the middleware stack, you basically don't have to do anything to parallelise all the middleware up to that point, and if I were going down this path, I would keep the middleware explicitly separate (whether used in parallel or series) from the route implementations themselves, to make it clear where the specific application logic begins.

Related