├── .gitignore ├── exercises ├── phase-5 │ ├── pnpm-workspace.yaml │ ├── web │ │ ├── main │ │ │ ├── watt.json │ │ │ ├── package.json │ │ │ └── index.mjs │ │ └── worker │ │ │ ├── watt.json │ │ │ ├── package.json │ │ │ └── index.mjs │ ├── README.md │ ├── package.json │ └── watt.json ├── phase-6 │ ├── pnpm-workspace.yaml │ ├── README.md │ ├── web │ │ └── composer │ │ │ ├── package.json │ │ │ └── watt.json │ ├── watt.json │ └── package.json ├── phase-7 │ ├── pnpm-workspace.yaml │ ├── README.md │ ├── web │ │ └── composer │ │ │ ├── package.json │ │ │ └── watt.json │ ├── watt.json │ └── package.json ├── phase-2 │ ├── README.md │ ├── package.json │ └── index.mjs ├── phase-4 │ ├── README.md │ ├── worker.mjs │ ├── package.json │ └── index.mjs ├── phase-1 │ ├── package.json │ ├── README.md │ └── index.mjs └── phase-3 │ ├── package.json │ ├── README.md │ └── index.mjs ├── solutions ├── phase-5 │ ├── pnpm-workspace.yaml │ ├── web │ │ ├── main │ │ │ ├── watt.json │ │ │ ├── package.json │ │ │ └── index.mjs │ │ └── worker │ │ │ ├── watt.json │ │ │ ├── package.json │ │ │ └── index.mjs │ ├── package.json │ └── watt.json ├── phase-6 │ ├── pnpm-workspace.yaml │ ├── web │ │ ├── fast │ │ │ ├── watt.json │ │ │ ├── package.json │ │ │ └── index.mjs │ │ ├── slow │ │ │ ├── watt.json │ │ │ ├── package.json │ │ │ └── index.mjs │ │ └── composer │ │ │ ├── package.json │ │ │ └── watt.json │ ├── watt.json │ └── package.json ├── phase-7 │ ├── pnpm-workspace.yaml │ ├── web │ │ ├── fast │ │ │ ├── watt.json │ │ │ ├── package.json │ │ │ └── index.mjs │ │ ├── slow │ │ │ ├── watt.json │ │ │ ├── package.json │ │ │ └── index.mjs │ │ └── composer │ │ │ ├── package.json │ │ │ └── watt.json │ ├── package.json │ └── watt.json ├── phase-1 │ ├── package.json │ └── index.mjs ├── phase-2 │ ├── package.json │ ├── index.mjs │ └── pool.mjs ├── phase-3 │ ├── package.json │ └── index.mjs └── phase-4 │ ├── worker.mjs │ ├── package.json │ └── index.mjs ├── README.md ├── verify.mjs └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /exercises/phase-5/pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | packages: 3 | - 'web/*' 4 | -------------------------------------------------------------------------------- /exercises/phase-6/pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | packages: 3 | - 'web/*' 4 | -------------------------------------------------------------------------------- /exercises/phase-7/pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | packages: 3 | - 'web/*' 4 | -------------------------------------------------------------------------------- /solutions/phase-5/pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | packages: 3 | - 'web/*' 4 | -------------------------------------------------------------------------------- /solutions/phase-6/pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | packages: 3 | - 'web/*' 4 | -------------------------------------------------------------------------------- /solutions/phase-7/pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | packages: 3 | - 'web/*' 4 | -------------------------------------------------------------------------------- /exercises/phase-5/web/main/watt.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schemas.platformatic.dev/@platformatic/node/2.8.0.json" 3 | } -------------------------------------------------------------------------------- /exercises/phase-5/web/worker/watt.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schemas.platformatic.dev/@platformatic/node/2.8.0.json" 3 | } -------------------------------------------------------------------------------- /solutions/phase-5/web/main/watt.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schemas.platformatic.dev/@platformatic/node/2.8.0.json" 3 | } -------------------------------------------------------------------------------- /solutions/phase-5/web/worker/watt.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schemas.platformatic.dev/@platformatic/node/2.8.0.json" 3 | } -------------------------------------------------------------------------------- /solutions/phase-6/web/fast/watt.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schemas.platformatic.dev/@platformatic/node/2.8.0.json" 3 | } -------------------------------------------------------------------------------- /solutions/phase-6/web/slow/watt.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schemas.platformatic.dev/@platformatic/node/2.8.0.json" 3 | } -------------------------------------------------------------------------------- /solutions/phase-7/web/fast/watt.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schemas.platformatic.dev/@platformatic/node/2.8.0.json" 3 | } -------------------------------------------------------------------------------- /solutions/phase-7/web/slow/watt.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schemas.platformatic.dev/@platformatic/node/2.8.0.json" 3 | } -------------------------------------------------------------------------------- /exercises/phase-6/README.md: -------------------------------------------------------------------------------- 1 | # Phase 6 2 | 3 | Pick the server written in Phase 5 and modify it such that the entrypoint is a Platformatic Composer. 4 | -------------------------------------------------------------------------------- /exercises/phase-6/web/composer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "dependencies": { 4 | "@platformatic/composer": "^2.8.0" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /exercises/phase-7/README.md: -------------------------------------------------------------------------------- 1 | # Phase 7 2 | 3 | Pick the server written in Phase 6 and modify it such that it starts 5 workers of the slow service. 4 | -------------------------------------------------------------------------------- /exercises/phase-7/web/composer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "dependencies": { 4 | "@platformatic/composer": "^2.8.0" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /solutions/phase-6/web/composer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "dependencies": { 4 | "@platformatic/composer": "^2.8.0" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /solutions/phase-7/web/composer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "dependencies": { 4 | "@platformatic/composer": "^2.8.0" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /exercises/phase-6/web/composer/watt.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schemas.platformatic.dev/@platformatic/composer/2.8.0.json", 3 | "composer": {} 4 | } 5 | -------------------------------------------------------------------------------- /exercises/phase-7/web/composer/watt.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schemas.platformatic.dev/@platformatic/composer/2.8.0.json", 3 | "composer": {} 4 | } 5 | -------------------------------------------------------------------------------- /solutions/phase-6/web/composer/watt.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schemas.platformatic.dev/@platformatic/composer/2.8.0.json", 3 | "composer": {} 4 | } 5 | -------------------------------------------------------------------------------- /solutions/phase-7/web/composer/watt.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schemas.platformatic.dev/@platformatic/composer/2.8.0.json", 3 | "composer": {} 4 | } 5 | -------------------------------------------------------------------------------- /exercises/phase-2/README.md: -------------------------------------------------------------------------------- 1 | # Phase 2 2 | 3 | Pick the server written in Phase 1 and modify it such that the hash computing happens in a different worker thread. 4 | -------------------------------------------------------------------------------- /exercises/phase-4/README.md: -------------------------------------------------------------------------------- 1 | # Phase 4 2 | 3 | Pick the server written in Phase 2 and modify it such that it uses multiple worker threads using [piscina](https://www.npmjs.com/package/piscina). 4 | -------------------------------------------------------------------------------- /exercises/phase-5/web/main/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "main": "index.mjs", 5 | "dependencies": { 6 | "@platformatic/node": "^2.8.0" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /exercises/phase-5/web/worker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "main": "index.mjs", 5 | "dependencies": { 6 | "@platformatic/node": "^2.8.0" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /solutions/phase-5/web/main/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "main": "index.mjs", 5 | "dependencies": { 6 | "@platformatic/node": "^2.8.0" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /solutions/phase-5/web/worker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "main": "index.mjs", 5 | "dependencies": { 6 | "@platformatic/node": "^2.8.0" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /solutions/phase-6/web/fast/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "main": "index.mjs", 5 | "dependencies": { 6 | "@platformatic/node": "^2.8.0" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /solutions/phase-6/web/slow/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "main": "index.mjs", 5 | "dependencies": { 6 | "@platformatic/node": "^2.8.0" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /solutions/phase-7/web/fast/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "main": "index.mjs", 5 | "dependencies": { 6 | "@platformatic/node": "^2.8.0" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /solutions/phase-7/web/slow/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "main": "index.mjs", 5 | "dependencies": { 6 | "@platformatic/node": "^2.8.0" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /exercises/phase-5/README.md: -------------------------------------------------------------------------------- 1 | # Phase 5 2 | 3 | Pick the server written in Phase 1 and modify it such that the hash computing happens in a different service using [Watt](https://www.npmjs.com/package/wattpm) 4 | -------------------------------------------------------------------------------- /exercises/phase-1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "start": "node index.mjs" 6 | }, 7 | "dependencies": { 8 | "fastify": "^5.0.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /exercises/phase-2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "start": "node index.mjs" 6 | }, 7 | "dependencies": { 8 | "fastify": "^5.0.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /exercises/phase-3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "start": "node index.mjs" 6 | }, 7 | "dependencies": { 8 | "fastify": "^5.0.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /solutions/phase-1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "start": "node index.mjs" 6 | }, 7 | "dependencies": { 8 | "fastify": "^5.0.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /solutions/phase-2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "start": "node index.mjs" 6 | }, 7 | "dependencies": { 8 | "fastify": "^5.0.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /solutions/phase-3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "start": "node index.mjs" 6 | }, 7 | "dependencies": { 8 | "fastify": "^5.0.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /exercises/phase-4/worker.mjs: -------------------------------------------------------------------------------- 1 | import { createHash, randomBytes } from 'node:crypto' 2 | 3 | export default function () { 4 | const bytes = randomBytes(1e9) 5 | return createHash('sha256').update(bytes).digest('hex') 6 | } 7 | -------------------------------------------------------------------------------- /solutions/phase-4/worker.mjs: -------------------------------------------------------------------------------- 1 | import { createHash, randomBytes } from 'node:crypto' 2 | 3 | export default function () { 4 | const bytes = randomBytes(1e9) 5 | return createHash('sha256').update(bytes).digest('hex') 6 | } 7 | -------------------------------------------------------------------------------- /exercises/phase-4/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "start": "node index.mjs" 6 | }, 7 | "dependencies": { 8 | "fastify": "^5.0.0", 9 | "piscina": "^4.7.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /solutions/phase-4/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "start": "node index.mjs" 6 | }, 7 | "dependencies": { 8 | "fastify": "^5.0.0", 9 | "piscina": "^4.7.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /exercises/phase-3/README.md: -------------------------------------------------------------------------------- 1 | # Phase 2 2 | 3 | Pick the server written in Phase 2 and modify it such that: 4 | 5 | - It uses a pool of five workers instead of just one. Workers are used in a round-robin fashion. 6 | - The responses are received by using `Atomics.waitAsync` and `SharedArrayBuffer`. 7 | -------------------------------------------------------------------------------- /exercises/phase-6/watt.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schemas.platformatic.dev/wattpm/2.8.0.json", 3 | "server": { 4 | "hostname": "127.0.0.1", 5 | "port": 3000 6 | }, 7 | "logger": { 8 | "level": "error" 9 | }, 10 | "autoload": { 11 | "path": "web" 12 | }, 13 | "watch": true 14 | } 15 | -------------------------------------------------------------------------------- /exercises/phase-7/watt.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schemas.platformatic.dev/wattpm/2.8.0.json", 3 | "server": { 4 | "hostname": "127.0.0.1", 5 | "port": 3000 6 | }, 7 | "logger": { 8 | "level": "error" 9 | }, 10 | "autoload": { 11 | "path": "web" 12 | }, 13 | "watch": true 14 | } 15 | -------------------------------------------------------------------------------- /solutions/phase-6/watt.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schemas.platformatic.dev/wattpm/2.8.0.json", 3 | "server": { 4 | "hostname": "127.0.0.1", 5 | "port": 3000 6 | }, 7 | "logger": { 8 | "level": "error" 9 | }, 10 | "autoload": { 11 | "path": "web" 12 | }, 13 | "watch": true 14 | } 15 | -------------------------------------------------------------------------------- /exercises/phase-5/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phase-5", 3 | "private": true, 4 | "scripts": { 5 | "dev": "wattpm dev", 6 | "build": "wattpm build", 7 | "start": "wattpm start" 8 | }, 9 | "dependencies": { 10 | "wattpm": "^2.8.0" 11 | }, 12 | "workspaces": [ 13 | "web/*" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /exercises/phase-6/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phase-5", 3 | "private": true, 4 | "scripts": { 5 | "dev": "wattpm dev", 6 | "build": "wattpm build", 7 | "start": "wattpm start" 8 | }, 9 | "dependencies": { 10 | "wattpm": "^2.8.0" 11 | }, 12 | "workspaces": [ 13 | "web/*" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /exercises/phase-7/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phase-5", 3 | "private": true, 4 | "scripts": { 5 | "dev": "wattpm dev", 6 | "build": "wattpm build", 7 | "start": "wattpm start" 8 | }, 9 | "dependencies": { 10 | "wattpm": "^2.8.0" 11 | }, 12 | "workspaces": [ 13 | "web/*" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /solutions/phase-5/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phase-5", 3 | "private": true, 4 | "scripts": { 5 | "dev": "wattpm dev", 6 | "build": "wattpm build", 7 | "start": "wattpm start" 8 | }, 9 | "dependencies": { 10 | "wattpm": "^2.8.0" 11 | }, 12 | "workspaces": [ 13 | "web/*" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /solutions/phase-7/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phase-5", 3 | "private": true, 4 | "scripts": { 5 | "dev": "wattpm dev", 6 | "build": "wattpm build", 7 | "start": "wattpm start" 8 | }, 9 | "dependencies": { 10 | "wattpm": "^2.8.0" 11 | }, 12 | "workspaces": [ 13 | "web/*" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /solutions/phase-7/watt.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schemas.platformatic.dev/wattpm/2.8.0.json", 3 | "server": { 4 | "hostname": "127.0.0.1", 5 | "port": 3000 6 | }, 7 | "logger": { 8 | "level": "error" 9 | }, 10 | "autoload": { 11 | "path": "web" 12 | }, 13 | "watch": true, 14 | "workers": 5 15 | } 16 | -------------------------------------------------------------------------------- /exercises/phase-5/watt.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schemas.platformatic.dev/wattpm/2.8.0.json", 3 | "server": { 4 | "hostname": "127.0.0.1", 5 | "port": 3000 6 | }, 7 | "entrypoint": "main", 8 | "logger": { 9 | "level": "error" 10 | }, 11 | "autoload": { 12 | "path": "web" 13 | }, 14 | "watch": true 15 | } 16 | -------------------------------------------------------------------------------- /solutions/phase-5/watt.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schemas.platformatic.dev/wattpm/2.8.0.json", 3 | "server": { 4 | "hostname": "127.0.0.1", 5 | "port": 3000 6 | }, 7 | "entrypoint": "main", 8 | "logger": { 9 | "level": "error" 10 | }, 11 | "autoload": { 12 | "path": "web" 13 | }, 14 | "watch": true 15 | } 16 | -------------------------------------------------------------------------------- /solutions/phase-6/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phase-5", 3 | "private": true, 4 | "scripts": { 5 | "dev": "wattpm dev", 6 | "build": "wattpm build", 7 | "start": "wattpm start" 8 | }, 9 | "dependencies": { 10 | "fastify": "^5.4.0", 11 | "wattpm": "^2.8.0" 12 | }, 13 | "workspaces": [ 14 | "web/*" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /solutions/phase-6/web/fast/index.mjs: -------------------------------------------------------------------------------- 1 | import fastify from 'fastify' 2 | 3 | export function create() { 4 | const app = fastify({ 5 | loggerInstance: globalThis.platformatic?.logger?.child({}, { level: globalThis.platformatic?.logLevel ?? 'info' }) 6 | }) 7 | 8 | app.get('/', async () => { 9 | return { time: Date.now() } 10 | }) 11 | 12 | return app 13 | } 14 | -------------------------------------------------------------------------------- /solutions/phase-7/web/fast/index.mjs: -------------------------------------------------------------------------------- 1 | import fastify from 'fastify' 2 | 3 | export function create() { 4 | const app = fastify({ 5 | loggerInstance: globalThis.platformatic?.logger?.child({}, { level: globalThis.platformatic?.logLevel ?? 'info' }) 6 | }) 7 | 8 | app.get('/', async () => { 9 | return { time: Date.now() } 10 | }) 11 | 12 | return app 13 | } 14 | -------------------------------------------------------------------------------- /exercises/phase-1/README.md: -------------------------------------------------------------------------------- 1 | # Phase 1 2 | 3 | Write an HTTP server which exposes two routes: 4 | 5 | - `/slow`: Returns an object containing a single `hash` property, which is obtained by computing the SHA256 hash of a buffer 100 MB random bytes. 6 | - `/fast`: Returns an object containing a single `time` property, which the current epoch time (elapsed time since midnight of Jan 1st 1970) in milliseconds. 7 | -------------------------------------------------------------------------------- /solutions/phase-1/index.mjs: -------------------------------------------------------------------------------- 1 | import fastify from 'fastify' 2 | import { createHash, randomBytes } from 'node:crypto' 3 | 4 | const app = fastify({ logger: process.env.VERBOSE === 'true' }) 5 | 6 | app.get('/fast', async () => { 7 | return { time: Date.now() } 8 | }) 9 | 10 | app.get('/slow', async () => { 11 | const bytes = randomBytes(1e9) 12 | const hash = createHash('sha256').update(bytes).digest('hex') 13 | 14 | return { hash } 15 | }) 16 | 17 | app.listen({ port: 3000 }) 18 | -------------------------------------------------------------------------------- /solutions/phase-5/web/main/index.mjs: -------------------------------------------------------------------------------- 1 | import fastify from 'fastify' 2 | 3 | export function create() { 4 | const app = fastify({ 5 | loggerInstance: globalThis.platformatic?.logger?.child({}, { level: globalThis.platformatic?.logLevel ?? 'info' }) 6 | }) 7 | 8 | app.get('/fast', async () => { 9 | return { time: Date.now() } 10 | }) 11 | 12 | app.get('/slow', async () => { 13 | const response = await fetch('http://worker.plt.local/hash') 14 | return response.json() 15 | }) 16 | 17 | return app 18 | } 19 | -------------------------------------------------------------------------------- /solutions/phase-6/web/slow/index.mjs: -------------------------------------------------------------------------------- 1 | import fastify from 'fastify' 2 | import { createHash, randomBytes } from 'node:crypto' 3 | 4 | export function create() { 5 | const app = fastify({ 6 | loggerInstance: globalThis.platformatic?.logger?.child({}, { level: globalThis.platformatic?.logLevel ?? 'info' }) 7 | }) 8 | 9 | app.get('/', async () => { 10 | const bytes = randomBytes(1e9) 11 | const hash = createHash('sha256').update(bytes).digest('hex') 12 | 13 | return { hash } 14 | }) 15 | 16 | return app 17 | } 18 | -------------------------------------------------------------------------------- /solutions/phase-7/web/slow/index.mjs: -------------------------------------------------------------------------------- 1 | import fastify from 'fastify' 2 | import { createHash, randomBytes } from 'node:crypto' 3 | 4 | export function create() { 5 | const app = fastify({ 6 | loggerInstance: globalThis.platformatic?.logger?.child({}, { level: globalThis.platformatic?.logLevel ?? 'info' }) 7 | }) 8 | 9 | app.get('/', async () => { 10 | const bytes = randomBytes(1e9) 11 | const hash = createHash('sha256').update(bytes).digest('hex') 12 | 13 | return { hash } 14 | }) 15 | 16 | return app 17 | } 18 | -------------------------------------------------------------------------------- /exercises/phase-4/index.mjs: -------------------------------------------------------------------------------- 1 | import fastify from 'fastify' 2 | 3 | const app = fastify({ logger: process.env.VERBOSE === 'true' }) 4 | // TODO: Create a new Piscina instance pointing to ./worker.mjs 5 | 6 | app.get('/fast', async () => { 7 | return { time: Date.now() } 8 | }) 9 | 10 | app.get('/slow', async () => { 11 | // TODO: Return the result of the piscina instance task run 12 | }) 13 | 14 | app.listen({ port: 3000 }, () => { 15 | console.log(`The server is listening at http://127.0.0.1:${app.server.address().port} ...`) 16 | }) 17 | -------------------------------------------------------------------------------- /exercises/phase-5/web/worker/index.mjs: -------------------------------------------------------------------------------- 1 | import fastify from 'fastify' 2 | import { createHash, randomBytes } from 'node:crypto' 3 | 4 | export function create() { 5 | const app = fastify({ 6 | loggerInstance: globalThis.platformatic?.logger?.child({}, { level: globalThis.platformatic?.logLevel ?? 'info' }) 7 | }) 8 | 9 | app.get('/hash', async () => { 10 | const bytes = randomBytes(1e9) 11 | const hash = createHash('sha256').update(bytes).digest('hex') 12 | 13 | return { hash } 14 | }) 15 | 16 | return app 17 | } 18 | -------------------------------------------------------------------------------- /solutions/phase-5/web/worker/index.mjs: -------------------------------------------------------------------------------- 1 | import fastify from 'fastify' 2 | import { createHash, randomBytes } from 'node:crypto' 3 | 4 | export function create() { 5 | const app = fastify({ 6 | loggerInstance: globalThis.platformatic?.logger?.child({}, { level: globalThis.platformatic?.logLevel ?? 'info' }) 7 | }) 8 | 9 | app.get('/hash', async () => { 10 | const bytes = randomBytes(1e9) 11 | const hash = createHash('sha256').update(bytes).digest('hex') 12 | 13 | return { hash } 14 | }) 15 | 16 | return app 17 | } 18 | -------------------------------------------------------------------------------- /exercises/phase-5/web/main/index.mjs: -------------------------------------------------------------------------------- 1 | import fastify from 'fastify' 2 | 3 | export function create() { 4 | const app = fastify({ 5 | loggerInstance: globalThis.platformatic?.logger?.child({}, { level: globalThis.platformatic?.logLevel ?? 'info' }) 6 | }) 7 | 8 | app.get('/fast', async () => { 9 | return { time: Date.now() } 10 | }) 11 | 12 | app.get('/slow', async () => { 13 | /* 14 | TODO: Compute the hash in the worker service. 15 | 16 | In order to invoke it use can use fetch pointing to the http://worker.plt.local domain. 17 | */ 18 | }) 19 | 20 | return app 21 | } 22 | -------------------------------------------------------------------------------- /solutions/phase-4/index.mjs: -------------------------------------------------------------------------------- 1 | import fastify from 'fastify' 2 | import { Piscina } from 'piscina' 3 | 4 | const app = fastify({ logger: process.env.VERBOSE === 'true' }) 5 | const piscina = new Piscina({ 6 | filename: new URL('./worker.mjs', import.meta.url).toString(), 7 | maxQueue: 20, 8 | }) 9 | 10 | app.get('/fast', async () => { 11 | return { time: Date.now() } 12 | }) 13 | 14 | app.get('/slow', async () => { 15 | return { hash: await piscina.run({}) } 16 | }) 17 | 18 | app.listen({ port: 3000 }, () => { 19 | console.log(`The server is listening at http://127.0.0.1:${app.server.address().port} ...`) 20 | }) 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The Multithreading Workshop 2 | 3 | Node.js's single-threaded nature is a common misconception. 4 | 5 | While this held true in the past, today, Node.js effectively utilizes an event loop and asynchronous programming techniques to handle concurrent requests efficiently. 6 | 7 | However, it's very easy to do multithreading the wrong way. In this workshop, we'll explore best practices and tools to supercharge Node.js multithreading. 8 | 9 | ## Getting started 10 | 11 | ```sh 12 | git clone https://github.com/platformatic/the-multithreading-workshop.git 13 | cd the-multithreading-workshop 14 | npm install 15 | ``` 16 | -------------------------------------------------------------------------------- /exercises/phase-1/index.mjs: -------------------------------------------------------------------------------- 1 | import fastify from 'fastify' 2 | import { randomBytes } from 'node:crypto' 3 | 4 | const app = fastify({ logger: process.env.VERBOSE === 'true' }) 5 | 6 | app.get('/fast', async () => { 7 | // TODO: Compute the time variable with the current Epoch Time in milliseconds 8 | 9 | return { time } 10 | }) 11 | 12 | app.get('/slow', async () => { 13 | const bytes = randomBytes(1e9) 14 | 15 | // TODO: Compute the hash variable with the SHA256 of bytes. 16 | 17 | return { hash } 18 | }) 19 | 20 | app.listen({ port: 3000 }, () => { 21 | console.log(`The server is listening at http://127.0.0.1:${app.server.address().port} ...`) 22 | }) 23 | -------------------------------------------------------------------------------- /verify.mjs: -------------------------------------------------------------------------------- 1 | import autocannon from 'autocannon' 2 | 3 | let requestIndex = 0 4 | 5 | await fetch('http://127.0.0.1:3000/slow') 6 | await fetch('http://127.0.0.1:3000/fast') 7 | 8 | const result = await autocannon({ 9 | url: `http://127.0.0.1:3000`, 10 | connections: 10, 11 | pipelining: 1, 12 | duration: parseInt(process.env.DURATION || '10'), 13 | requests: [ 14 | { 15 | setupRequest(request) { 16 | // 33% of the requests go to the slow route 17 | request.path = requestIndex++ % 3 === 0 ? '/slow' : '/fast' 18 | 19 | return request 20 | } 21 | } 22 | ] 23 | }) 24 | 25 | console.log(autocannon.printResult(result)) 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "verify": "node ./verify.mjs", 6 | "exercises:phase-1": "node exercises/phase-1/index.mjs", 7 | "exercises:phase-2": "node exercises/phase-2/index.mjs", 8 | "exercises:phase-3": "node exercises/phase-3/index.mjs", 9 | "exercises:phase-4": "node exercises/phase-4/index.mjs", 10 | "exercises:phase-5": "cd exercises/phase-5 && wattpm start", 11 | "exercises:phase-6": "cd exercises/phase-6 && wattpm start", 12 | "solutions:phase-1": "node solutions/phase-1/index.mjs", 13 | "solutions:phase-2": "node solutions/phase-2/index.mjs", 14 | "solutions:phase-3": "node solutions/phase-3/index.mjs", 15 | "solutions:phase-4": "node solutions/phase-4/index.mjs", 16 | "solutions:phase-5": "cd solutions/phase-5 && wattpm start", 17 | "solutions:phase-6": "cd solutions/phase-6 && wattpm start" 18 | }, 19 | "devDependencies": { 20 | "autocannon": "^8.0.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /solutions/phase-2/index.mjs: -------------------------------------------------------------------------------- 1 | import fastify from 'fastify' 2 | import { createHash, randomBytes } from 'node:crypto' 3 | import { isMainThread, parentPort, Worker } from 'node:worker_threads' 4 | 5 | function startWorker() { 6 | parentPort.on('message', message => { 7 | if (message?.type !== 'request') { 8 | return 9 | } 10 | 11 | const bytes = randomBytes(1e9) 12 | parentPort.postMessage({ type: 'response', id: message.id, hash: createHash('sha256').update(bytes).digest('hex') }) 13 | }) 14 | } 15 | 16 | function startServer() { 17 | const app = fastify({ logger: process.env.VERBOSE === 'true' }) 18 | const worker = new Worker(import.meta.filename) 19 | 20 | let requestIndex = 0 21 | const inflights = {} 22 | 23 | worker.on('message', message => { 24 | if (message?.type !== 'response') { 25 | return 26 | } 27 | 28 | inflights[message.id](message.hash) 29 | }) 30 | 31 | app.get('/fast', async () => { 32 | return { time: Date.now() } 33 | }) 34 | 35 | app.get('/slow', async () => { 36 | const id = requestIndex++ 37 | 38 | const promise = new Promise(_resolve => { 39 | inflights[id] = _resolve 40 | }) 41 | 42 | worker.postMessage({ type: 'request', id }) 43 | 44 | return { hash: await promise } 45 | }) 46 | 47 | app.listen({ port: 3000 }, () => { 48 | console.log(`The server is listening at http://127.0.0.1:${app.server.address().port} ...`) 49 | }) 50 | } 51 | 52 | if (isMainThread) { 53 | startServer() 54 | } else { 55 | startWorker() 56 | } 57 | -------------------------------------------------------------------------------- /exercises/phase-2/index.mjs: -------------------------------------------------------------------------------- 1 | import fastify from 'fastify' 2 | import { createHash, randomBytes } from 'node:crypto' 3 | import { isMainThread, parentPort, Worker } from 'node:worker_threads' 4 | 5 | function startWorker() { 6 | parentPort.on('message', message => { 7 | if (message?.type !== 'request') { 8 | return 9 | } 10 | 11 | const bytes = randomBytes(1e9) 12 | const hash = createHash('sha256').update(bytes).digest('hex') 13 | 14 | // TODO: Post a message back to the parent thread. Make sure to use the right type, id and hash. 15 | }) 16 | } 17 | 18 | function startServer() { 19 | const app = fastify({ logger: process.env.VERBOSE === 'true' }) 20 | const worker = new Worker(import.meta.filename) 21 | 22 | let requestIndex = 0 23 | const inflights = {} 24 | 25 | worker.on('message', message => { 26 | if (message?.type !== 'response') { 27 | return 28 | } 29 | 30 | // TODO: Resolve the correct promise with the returned hash value 31 | }) 32 | 33 | app.get('/fast', async () => { 34 | return { time: Date.now() } 35 | }) 36 | 37 | app.get('/slow', async () => { 38 | const id = requestIndex++ 39 | 40 | // TODO: Create a promise and save its resolve method for later in the inflights object. 41 | 42 | worker.postMessage({ type: 'request', id }) 43 | 44 | return { hash: await promise } 45 | }) 46 | 47 | app.listen({ port: 3000 }, () => { 48 | console.log(`The server is listening at http://127.0.0.1:${app.server.address().port} ...`) 49 | }) 50 | } 51 | 52 | if (isMainThread) { 53 | startServer() 54 | } else { 55 | startWorker() 56 | } 57 | -------------------------------------------------------------------------------- /solutions/phase-2/pool.mjs: -------------------------------------------------------------------------------- 1 | import fastify from 'fastify' 2 | import { createHash, randomBytes } from 'node:crypto' 3 | import { isMainThread, parentPort, Worker } from 'node:worker_threads' 4 | 5 | function startWorker() { 6 | parentPort.on('message', message => { 7 | if (message?.type !== 'request') { 8 | return 9 | } 10 | 11 | const bytes = randomBytes(1e9) 12 | parentPort.postMessage({ type: 'response', id: message.id, hash: createHash('sha256').update(bytes).digest('hex') }) 13 | }) 14 | } 15 | 16 | function startServer() { 17 | const app = fastify({ logger: process.env.VERBOSE === 'true' }) 18 | const workers = [] 19 | 20 | const onMessage = message => { 21 | if (message?.type !== 'response') { 22 | return 23 | } 24 | 25 | inflights[message.id](message.hash) 26 | } 27 | 28 | for (let i = 0; i < 4; i++) { 29 | const worker = new Worker(import.meta.filename) 30 | workers.push(worker) 31 | worker.on('message', onMessage) 32 | } 33 | 34 | let requestIndex = 0 35 | const inflights = {} 36 | 37 | app.get('/fast', async () => { 38 | return { time: Date.now() } 39 | }) 40 | 41 | app.get('/slow', async () => { 42 | const id = requestIndex++ 43 | 44 | const promise = new Promise(_resolve => { 45 | inflights[id] = _resolve 46 | }) 47 | 48 | const worker = workers[id % workers.length] 49 | 50 | worker.postMessage({ type: 'request', id }) 51 | 52 | return { hash: await promise } 53 | }) 54 | 55 | app.listen({ port: 3000 }, () => { 56 | console.log(`The server is listening at http://127.0.0.1:${app.server.address().port} ...`) 57 | }) 58 | } 59 | 60 | if (isMainThread) { 61 | startServer() 62 | } else { 63 | startWorker() 64 | } 65 | -------------------------------------------------------------------------------- /solutions/phase-3/index.mjs: -------------------------------------------------------------------------------- 1 | import fastify from 'fastify' 2 | import { createHash, randomBytes } from 'node:crypto' 3 | import { isMainThread, parentPort, Worker } from 'node:worker_threads' 4 | 5 | const SHA256_BYTE_LENGTH = 32 6 | 7 | async function startWorker() { 8 | parentPort.on('message', message => { 9 | if (message?.type !== 'request') { 10 | return 11 | } 12 | 13 | /* 14 | Create a buffer pointing to the shared array buffer. 15 | The second argument, the offset, is set to 4 to skip 16 | the first 4 bytes which are are reserved to notify the main thread. 17 | */ 18 | const buffer = Buffer.from(message.sharedArrayBuffer, 4) 19 | 20 | // Compute the hash 21 | const bytes = randomBytes(1e9) 22 | createHash('sha256').update(bytes).digest().copy(buffer) 23 | 24 | // Notify the parent thread 25 | const int32Array = new Int32Array(message.sharedArrayBuffer) 26 | int32Array[0] = 1 27 | Atomics.notify(int32Array, 0) 28 | }) 29 | } 30 | 31 | function startServer() { 32 | const app = fastify({ logger: process.env.VERBOSE === 'true' }) 33 | 34 | let nextWorker = 0 35 | const workers = [] 36 | for (let i = 0; i < 5; i++) { 37 | workers.push(new Worker(import.meta.filename)) 38 | } 39 | 40 | app.get('/fast', async () => { 41 | return { time: Date.now() } 42 | }) 43 | 44 | app.get('/slow', async () => { 45 | /* 46 | Other than the bytes for the SHA256, we just needed an extra element to allow 47 | to notify the main thread when the operation is completed. 48 | Since Atomics worked with Int32, this means we need 4 bytes. 49 | */ 50 | const sharedArrayBuffer = new SharedArrayBuffer(SHA256_BYTE_LENGTH + 4) 51 | const int32Array = new Int32Array(sharedArrayBuffer) 52 | 53 | const currentWorker = nextWorker++ % workers.length 54 | workers[currentWorker].postMessage({ type: 'request', sharedArrayBuffer }) 55 | await Atomics.waitAsync(int32Array, 0, 0).value 56 | 57 | return { 58 | hash: Buffer.from(sharedArrayBuffer, 4).toString('hex') 59 | } 60 | }) 61 | 62 | app.listen({ port: 3000 }, () => { 63 | console.log(`The server is listening at http://127.0.0.1:${app.server.address().port} ...`) 64 | }) 65 | } 66 | 67 | if (isMainThread) { 68 | await startServer() 69 | } else { 70 | await startWorker() 71 | } 72 | -------------------------------------------------------------------------------- /exercises/phase-3/index.mjs: -------------------------------------------------------------------------------- 1 | import fastify from 'fastify' 2 | import { createHash, randomBytes } from 'node:crypto' 3 | import { isMainThread, parentPort } from 'node:worker_threads' 4 | 5 | const SHA256_BYTE_LENGTH = 32 6 | 7 | async function startWorker() { 8 | parentPort.on('message', message => { 9 | if (message?.type !== 'request') { 10 | return 11 | } 12 | 13 | /* 14 | Create a buffer pointing to the shared array buffer. 15 | The second argument, the offset, is set to 4 to skip 16 | the first 4 bytes which are are reserved to notify the main thread. 17 | */ 18 | const buffer = Buffer.from(message.sharedArrayBuffer, 4) 19 | 20 | // Compute the hash 21 | const bytes = randomBytes(1e9) 22 | createHash('sha256').update(bytes).digest().copy(buffer) 23 | 24 | /* 25 | TODO: Notify the parent thread. 26 | 27 | 1. Create a new array backed by the message.sharedArrayBuffer which is usable by the Atomics API. 28 | 2. Set its first element to be non zero. 29 | 3. Notify the main thread that the array value has changed. 30 | */ 31 | }) 32 | } 33 | 34 | function startServer() { 35 | const app = fastify({ logger: process.env.VERBOSE === 'true' }) 36 | 37 | let nextWorker = 0 38 | const workers = [] 39 | // TODO: Create a pool of 5 workers threads 40 | 41 | app.get('/fast', async () => { 42 | return { time: Date.now() } 43 | }) 44 | 45 | app.get('/slow', async () => { 46 | /* 47 | Other than the bytes for the SHA256, we just needed an extra element to allow 48 | to notify the main thread when the operation is completed. 49 | Since Atomics worked with Int32, this means we need 4 bytes. 50 | */ 51 | const sharedArrayBuffer = new SharedArrayBuffer(SHA256_BYTE_LENGTH + 4) 52 | 53 | // TODO: Select a worker using the Round Robin policy 54 | 55 | workers[currentWorker].postMessage({ type: 'request', sharedArrayBuffer }) 56 | 57 | /* 58 | TODO: Wait for the worker thread to signal completion. 59 | 60 | 1. Create a new array backed by the message.sharedArrayBuffer which is usable by the Atomics API. 61 | 2. Wait for the first element to be non zero. 62 | */ 63 | 64 | // TODO: Create a buffer backed by sharedArrayBuffer to extract the result. Remember to use the right offset. 65 | 66 | return { 67 | hash: buffer.toString('hex') 68 | } 69 | }) 70 | 71 | app.listen({ port: 3000 }, () => { 72 | console.log(`The server is listening at http://127.0.0.1:${app.server.address().port} ...`) 73 | }) 74 | } 75 | 76 | if (isMainThread) { 77 | await startServer() 78 | } else { 79 | await startWorker() 80 | } 81 | --------------------------------------------------------------------------------