koajs / discussions

KoaJS Discussions
2 stars 2 forks source link

Respond to Koa request immediately, continue middleware chain #8

Open paulgrieselhuber opened 4 years ago

paulgrieselhuber commented 4 years ago

I am writing the middleware for API endpoints in my app that respond to webhooks from other applications, and am relatively new to Koa, so am not completely familiar with its patterns.

I would like to structure my middleware as follows:

exports.updateReceived = async (ctx, next) => {
  // Respond to server issuing the webhook
  ctx.res.body = "ok";
  ctx.res.statusCode = 200;

  // Grab what we need from the request
  const { headers, state, request } = ctx;
  const { body } = request;

  // Do some async work
  const { example } = await doSomeAsyncWork(ctx);

  // Prepare a database query
  const query = { aValue: anId };

  // Run the DB query
  const result = await Thing.findOne(query);

  // Add data to the request
  state.thing = result;

  // Move on...
  return next();
};

However, this does not appear to be working, as an error in any of my async methods can cause the route to error out.

My goal is for this endpoint to always respond "yep, ok" (immediately), meaning it is simply up to the application to handle any error states.

I have researched this fairly well, and have come across this pattern:

app.use(async ctx => {
  db.fetch() // Assuming a Promise is returned
    .then(() => { ... })
    .catch(err => {
      log(err)
    })

  // status 200 will be returned regardless of if db.fetch() resolves or rejects.
  ctx.status = 200
})

However, this does not meet my needs as the middleware makes no use of next, so it is not really a useful pattern, so far as I can tell.

Could someone tell me what I am overlooking? I wrote this question on StackOverflow several days ago, but getting nowhere.

AleksMeshkov commented 4 years ago

Same thing, I can't manage to respond immediately and continue function execution.

public static async orderFulfilled(ctx: Context, next) {
   // need to immediately respond (in 5 seconds or so) otherwise they assume this request failed
    ctx.status = 200;
    ctx.body = 'ok';
    await next();

   // do the math.......
}

This doesn't work!

paulgrieselhuber commented 4 years ago

Here is the feedback I ended up getting: https://stackoverflow.com/a/60877855/1172519.

By the time I got a response I just ended up refactoring the "middleware" called by the server route as a sync (note: non-async) function (not middleware at all at this point due to the fact that a single sync method has to do all the work).

So I haven't tested the linked answer, but it appears to be a great way to establish general error handling for Koa.

However, I don't understand why this needs to be so hard.

AleksMeshkov commented 4 years ago

Hey @paulisloud! Thanks for the tip! I finally achieved what I wanted without (adding additional) / (restructuring existing) middleware. Here's my solution for this:

// THE ROUTE METHOD, THE INITIAL ENTRYPOINT.

public static async orderFulfilled(ctx: Context, next) {
   // need to immediately respond (in 5 seconds or so) otherwise they assume this request failed
    ctx.status = 200;
    ctx.body = 'ok';
    await next();

   // do the math.......

   // calling timeDemandingTask WITHOUT AWAIT
   timeDemandingTask();
}

// OUR TIME DEMANDING METHOD WE DON'T WANT TO WAIT
private static async timeDemandingTask(ctx: Context) {
  // doing the math.......
  await action_1();
  await action_2();
  await action_3();
  await action_4();
  // two minutes passed!
}

In the end it works as intended.

paulgrieselhuber commented 4 years ago

@AleksMeshkov yep, basically the same approach I ended up with. Cheers.

lagden commented 4 years ago

Hummm... This is not exactly Koa issue (IMO)!!

You can use worker_threads to avoid blocking code and keep chaining flowing.
And your code will be consistent!

// thread-service.js
'use strict'

const {Worker, isMainThread} = require('worker_threads')

function runService(service, workerData) {
  if (isMainThread === false) {
    return
  }
  const worker = new Worker(service, {workerData})
  worker.once('message', console.log)
  worker.once('exit', code => {
    worker.removeAllListeners()
    worker.terminate()
    if (code !== 0) {
      new Error(`Worker stopped with exit code ${code}`)
    }
  })
}

module.exports = runService
// service.js
'use strict'

const {workerData, parentPort} = require('worker_threads')
// const db = require('./my-db')

;(async () => {
  try {
    // do your stuff with data
    // const result = await db.save(workerData)
    console.log('thread --->', workerData)
    const result = Math.random()
    parentPort.postMessage({result})
    process.exit(0)
  } catch (error) {
    process.exit(1)
  }
})()
// app.js
'use strict'

const {join} = require('path')
const Koa = require('koa')
const runService = require('./thread-service.js')

const app = new Koa()

app
  .use(async (ctx, next) => {
    const start = Date.now()
    ctx.state.start = start
    console.log('start --> goto b')
    await next() // goto b
    // a
    const ms = Date.now() - start
    ctx.set('X-Response-Time', ms)
    console.log('a --> end')
  })
  .use(async (ctx, next) => {
    // b
    const {start} = ctx.state
    // start thread and keep running
    runService(join(__dirname, 'service.js'), {start})
    console.log('b --> goto c')
    await next() // goto c
    // b2
    // goto a
    console.log('b2 --> goto a')
  })
  .use(ctx => {
    // c
    ctx.body = {ok: true}
    // goto b2
    console.log('c --> goto b2')
  })
  .on('error', console.error)

app.proxy = true

app.listen(3435, () => {
  console.log('Running: http://[::1]:3435')
})

Output


> node app.js
Running: http://[::1]:3435
start --> goto b
b --> goto c
c --> goto b2
b2 --> goto a
a --> end
thread ---> { start: 1587672397920 }
{ result: 0.5918108149573942 }