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