how to reuse a pool of web servers in node js to do async tasks

Viewed 120

I am running a queue of more than 7000 async tasks in parallel using an async queue with a max concurrency of 18.

In each async task I need to create a webserver, do some work and close the web server. I am using get-port to get an available port but I still get into race conditions where two or more different tasks will try to use the exact same port number.

My idea is to create these webservers once and reuse them accross tasks. This way I won't run into issues where multiple web servers can try to reuse the same port.

How can I make sure I am NOT reusing the same webserver between parallel async tasks?

Here is some code, some irrelevant parts are missing:

const asyncTask = async (task) => {
    const port = await getPort()
    // static files server
    const server = http.createServer((request, response) => {
        // You pass two more arguments for config and middleware
        // More details here: https://github.com/zeit/serve-handler#options
        return serveHandler(request, response, {
            public: 'dist'
        })
    })
    // start listening
    server.listen(port, () => {
        console.log(`worker running for task ${task} at port ${port}`)
    })

    // do the async work

    server.close()

    return 0
}

const q = queue(asyncTask, 18)
q.drain(function() {
    console.log('all items have been processed')
})
q.error(function(err, task) {
    console.error(`task ${JSON.stringify(task)} experienced an error`, err)
})
q.push(tasks)
2 Answers

Reusing your webservers might be a bit tricky because you will have to find a way to change the callback (to handle the async task) everytime you pick up a new task, without recreating a new webserver.

What about just using a fixed queue of ports and unshift() and pop() them after each task?

const ports = [3001, 3002, ..., 3018];

const asyncTask = async (task) => {

    while (ports.length === 0) {
      // Wait for a port to become free
    }

    const port = ports.pop();

    // static files server
    const server = http.createServer((request, response) => {
        // You pass two more arguments for config and middleware
        // More details here: https://github.com/zeit/serve-handler#options
        return serveHandler(request, response, {
            public: 'dist'
        })
    })

    // start listening
    server.listen(port, () => {
        console.log(`worker running for task ${task} at port ${port}`)
    })

    // do the async work

    server.close()

    // Once the work is done, add the port back to the list of ports, 
    // so that another task can pick it up again. This piece of code should be 
    ports.unshift(port);

    return 0
}

I ended up creating a list of webservers and shared them between the async tasks. inside each task I can pop out the webserver from the list, use it and then unshift it back into the shared list of webservers.

I gave up using get-port and instead I am using a similar method based on a github gist

function getAvailablePort(startPort = 10000) {
    let port = startPort

    return () => {
        const server = http.createServer()
        return new Promise((resolve, reject) => server
            .once('error', error => {
                port += 1
                error.code !== 'EADDRINUSE' ? reject(error) : server.listen(port)
            })
            .once('listening', () => server.close(() => {
                port += 1
                resolve(port)
            }))
            .listen(port)
        )
    }
}

My code now looks something like this

(async () => {
    let totalTasks = 0
    let finishedSuccess = 0
    let finishedWithError = 0
    const concurrencyLimit = 18

    try {

        // create a pool of shared servers
        const getNextPort = getAvailablePort()
        const poolOfSharedServers = []
        for(let i=0; i<concurrencyLimit; i++) {
            let port = await getNextPort()

            // create static files server and start listening
            const server = http.createServer((request, response) => {
                // You pass two more arguments for config and middleware
                // More details here: https://github.com/zeit/serve-handler#options
                return serveHandler(request, response, { public: 'dist' })
            }).listen(port)

            poolOfSharedServers.push(server)
        }

        // create async task
        const asyncTask = createAsyncTask(poolOfSharedServers)

        // build an async queue of tasks
        const q = queue(async function(task) {
            try {
                const res = await asyncTask(task)
                if (res.done) {
                    finishedSuccess += 1
                    await logger.saveLog(res.value)
                } else {
                    finishedWithError += 1
                }
            } catch (err) {
                finishedWithError += 1
            }
        }, concurrencyLimit)

        // on all tasks finished
        q.drain(function() {
            console.log(`\nall ${totalTasks} tasks have been processed. ${finishedSuccess} finished successfully and ${finishedWithError} finished with errors\n`)
        })

        // on task error
        q.error(function(err, task) {
            finishedWithError += 1
            console.error(`task ${JSON.stringify(task)} experienced an error`, err)
        })

        // create tasks
        // tasks = ...

        totalTasks = tasks.length

        // push all tasks into the queue
        q.push(tasks)
    } catch (err) {
        console.error(err)
    }
})()

where createAsyncTask looks something like this:

const createAsyncTask = (poolOfSharedServers) => async (task) => {
    let result, server

    try {

        // get server out of shared pool of servers
        server = poolOfSharedServers.pop()

        // get current port
        const port = server.address().port

        // do some async work using task and server
        await new Promise(resolve => setTimeout(resolve, 1000))

        result = {
            done: true,
            value: 'some value'
        }
    } catch (err) {
        result = {
            done: false,
            err
        }
    }

    if (server) {
        // add server back to the pool of shared servers
        poolOfSharedServers.unshift(server)
    }

    return result
}
Related