Rethinking JavaScript Concurrency & Parallelism Primitives

Karim Alibhai - karim@alibhai.co

npx karimsa

libuv thread #1

libuv thread #2

libuv thread #3

libuv thread #N

await fs.readFile('blah.txt')

Node.js + libuv

export async function sendMessage({ conversationID, messageID, message }) {
  const users = await UserConversations.find({ conversationID })
  const timeStart = Date.now()

  for (const user of users) {
    await sendPushNotification(user, { message })
    await sendEmailNotification(user, { message })
    await sendWebsocketUpdates(user, { type: 'new_message', message })
  }

  await markMessageAsDelivered({ messageID, conversationID })

  console.log(`Sent message in ${Date.now() - timeStart}ms`)
}

Using async/await - Attempt #1

  • Code is pretty clean
  • Not very concurrent
  • Not very fault tolerant
export async function sendMessage({ conversationID, messageID, message }) {
  const users = await UserConversations.find({ conversationID })
  const timeStart = Date.now()

  await Promise.all(users, async user => {
    await sendPushNotification(user, { message })
    await sendEmailNotification(user, { message })
    await sendWebsocketUpdates(user, { type: 'new_message', message })
  })

  await markMessageAsDelivered({ messageID, conversationID })

  console.log(`Sent message in ${Date.now() - timeStart}ms`)
}

Using async/await - Attempt #2

  • Code is still clean
  • Concurrency is unbound
  • Not very fault tolerant
export async function sendMessageToSingleUser({ userID, messageID }) {
  if (!await pushNotifSent({ userID, messageID }) {
    await sendPushNotification(user, { message })
    await markPushNotificationSent({ userID, messageID })
  }
  if (!await emailSent({ userID, messageID }) {
    await sendEmailNotification(user, { message })
    await markEmailNotificationSent({ userID, messageID })
  }
  if (!await websocketUpdateSent({ userID, messageID }) {
    await sendWebsocketUpdates(user, { type: 'new_message', message })
    await markWebsocketSent({ userID, messageID })
  }
}

export async function sendMessage({ conversationID, messageID }) {
  const users = await UserConversations.find({ conversationID })
  await saveTimeStarted(Date.now())
  const jobIDs = await Promise.all(users, user => Queue.enqueue('send-message-to-user', {
    userID: user._id,
    message: messageID,
  }))
  await Queue.enqueue('mark-message-as-delivered', {
    messageID,
  }, {
    dependencies: jobIDs,
  })
}

export async function markMessageAsDelivered({ messageID }) {
  await markMessageAsDelivered({ messageID, conversationID })
  const timeStart = await getSavedTime()
  await deleteSavedTime()
  console.log(`Sent message in ${Date.now() - timeStart}ms`)
}
  • Task implementations in a queue system require specific segmentation
  • Distributed memory requires synchronization & cleanup

What are the common patterns?

babel-plugin-macros

Thanks, Kent C. Dodds!

import { useState, forEach, waitFor, checkpoint } from '@karimsa/xs/core.macro'

export const sendMessage = routine(async ({ conversationID, messageID, message }) => {
  const users = await UserConversations.find({ conversationID })
  const timeStart = useState('timeStart', Date.now())

  await forEach(users, async user => {
    await waitFor(sendPushNotification(user, { message }))
    checkpoint()

    await waitFor(sendEmailNotification(user, { message }))
    checkpoint()

    await waitFor(sendWebsocketUpdates(user, { type: 'new_message', message }))
    checkpoint()
  })

  await markMessageAsDelivered({ messageID, conversationID })

  console.log(`Sent message in ${Date.now() - await timeStart.get()}ms`)
})
  • Code is almost the same as the original one
  • Scales horizontally & vertically without effort
  • Memory is synchronized
  • Fault tolerance is builtin - also customizable

`forEach()`

export const doStuff = routine(async N => {
  const nums = useState([... new Array(N)])

  await forEach(nums, i => {
    console.log(i)
  })

  console.log('all done')
})
export const doStuff = routine(async N => {
  const nums = [/* state magic */]
  for (;; ++ctx.state) {
    switch (ctx.state) {
      case 0:
        return this.produce(nums)
      case 1:
        console.log(ctx.yieldValue)
        return this.halt()
      case 3:
        console.log('all done')
        return this.done()
    }
  }
})

`useState()`

export const doStuff = routine(async N => {
  const nums = [... new Array(N)]
  const sum = useState('sum', 0)

  await forEach(nums, i => {
    return sum.incrby(i)
  })

  console.log(`The sum is: ${await sum.get()}`)
})
export const doStuff = routine(async N => {
  const nums = [... new Array(N)]
  const sum = await this.redis.set(`${this.execID}:sum`, 0, 'NX')

  // forEach magics
  await this.redis.incrby(`${this.execID}:sum`, i)

  console.log(`The sum is: ${Number(await this.redis.get(`${this.execID}:sum`))}`)
}

So what?

  • Metaprogramming is cool

  • Concurrency in JS has a long way to go

  • Queues are not just for lunatics

Thanks!

Come talk to me after, I don't bite! (usually)

Karim Alibhai - karim@alibhai.co

https://alibhai.co

Rethinking JavaScript Concurrency & Parallelism Primitives

By Karim Alibhai

Rethinking JavaScript Concurrency & Parallelism Primitives

Asynchronous programming has come a long way in JavaScript - from the likes of callback hell all the way to the wonderful world of async/await. But creating highly parallel Node.js applications still requires a lot of extra coding effort & comes with a large maintainence cost. In this talk, I go over the different approaches to parallelizing JS backend applications & a new approach using babel.

  • 897