Skip to content

Commit a8242e7

Browse files
authored
Merge pull request #25 from KAMALDEEN333/Job-Queue
created the job-queue for large scale
2 parents bab846a + 56b87dd commit a8242e7

7 files changed

Lines changed: 211 additions & 0 deletions

File tree

apps/api/package.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "gasguard-api",
3+
"version": "0.1.0",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"dev": "ts-node src/index.ts",
8+
"typecheck": "tsc --noEmit",
9+
"build": "tsc -p tsconfig.json",
10+
"start": "node dist/index.js"
11+
},
12+
"dependencies": {
13+
"express": "^4.19.2",
14+
"bullmq": "^5.7.11",
15+
"ioredis": "^5.3.2"
16+
},
17+
"devDependencies": {
18+
"ts-node": "^10.9.2",
19+
"typescript": "^5.6.3",
20+
"@types/express": "^4.17.21"
21+
}
22+
}

apps/api/src/index.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { createServer } from './server.js'
2+
import { initQueue } from './queue/index.js'
3+
4+
const port = Number(process.env.PORT || 3000)
5+
const redisUrl = process.env.REDIS_URL || ''
6+
const queueName = process.env.SCAN_QUEUE_NAME || 'gasguard:scan'
7+
8+
const { queue, worker, events } = initQueue({ redisUrl, queueName })
9+
const app = createServer(queue)
10+
11+
app.listen(port, () => {})
12+
13+
process.on('SIGINT', async () => {
14+
await worker?.close()
15+
await events?.close()
16+
process.exit(0)
17+
})

apps/api/src/queue/index.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Queue, Worker, QueueEvents, JobsOptions } from 'bullmq'
2+
import { createClient } from 'ioredis'
3+
import { performScan } from '../scan.js'
4+
import { createInMemoryQueue } from './memory.js'
5+
6+
type InitOptions = { redisUrl: string; queueName: string }
7+
8+
export function initQueue({ redisUrl, queueName }: InitOptions) {
9+
if (!redisUrl) {
10+
const { queue, worker, events } = createInMemoryQueue(queueName)
11+
return { queue, worker, events }
12+
}
13+
const connection = new createClient(redisUrl)
14+
const queue = new Queue(queueName, { connection })
15+
const events = new QueueEvents(queueName, { connection })
16+
const worker = new Worker(
17+
queueName,
18+
async job => {
19+
const payload = job.data.payload
20+
await job.updateProgress(10)
21+
const result = await performScan(payload, p => job.updateProgress(p))
22+
return result
23+
},
24+
{ connection }
25+
)
26+
return { queue, worker, events }
27+
}
28+
29+
export const defaultJobOptions: JobsOptions = { removeOnComplete: true, removeOnFail: true }

apps/api/src/queue/memory.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { Queue } from 'bullmq'
2+
3+
type JobRecord = {
4+
id: string
5+
state: 'waiting' | 'active' | 'completed' | 'failed'
6+
progress: number
7+
returnvalue?: unknown
8+
}
9+
10+
export function createInMemoryQueue(name: string) {
11+
const jobs = new Map<string, JobRecord>()
12+
const q = new Queue(name, { connection: { host: 'localhost', port: 6379 } as any })
13+
14+
const queue = {
15+
add: async (_name: string, data: any) => {
16+
const id = Math.random().toString(36).slice(2)
17+
jobs.set(id, { id, state: 'waiting', progress: 0 })
18+
setTimeout(async () => {
19+
const j = jobs.get(id)
20+
if (!j) return
21+
j.state = 'active'
22+
j.progress = 10
23+
try {
24+
const result = await simulateScan(data.payload, p => {
25+
const jj = jobs.get(id)
26+
if (jj) jj.progress = p
27+
})
28+
j.returnvalue = result
29+
j.state = 'completed'
30+
j.progress = 100
31+
} catch {
32+
j.state = 'failed'
33+
}
34+
}, 0)
35+
return { id }
36+
},
37+
getJob: async (id: string) => {
38+
const j = jobs.get(id)
39+
if (!j) return null
40+
return {
41+
id: j.id,
42+
progress: j.progress,
43+
returnvalue: j.returnvalue,
44+
getState: async () => j.state
45+
} as any
46+
}
47+
} as unknown as Queue
48+
49+
const worker = { close: async () => {} } as any
50+
const events = { close: async () => {} } as any
51+
return { queue, worker, events }
52+
}
53+
54+
async function simulateScan(input: any, onProgress: (p: number) => void) {
55+
onProgress(25)
56+
await sleep(400)
57+
onProgress(50)
58+
await sleep(400)
59+
onProgress(75)
60+
await sleep(400)
61+
onProgress(100)
62+
return { summary: 'ok', issues: [], inputSize: JSON.stringify(input || {}).length }
63+
}
64+
65+
function sleep(ms: number) {
66+
return new Promise(resolve => setTimeout(resolve, ms))
67+
}

apps/api/src/scan.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
type ProgressCb = (p: number) => Promise<void> | void
2+
3+
export async function performScan(input: any, onProgress: ProgressCb) {
4+
await onProgress(25)
5+
await sleep(500)
6+
await onProgress(50)
7+
await sleep(500)
8+
await onProgress(75)
9+
await sleep(500)
10+
const result = { summary: 'ok', issues: [], inputSize: JSON.stringify(input || {}).length }
11+
await onProgress(100)
12+
return result
13+
}
14+
15+
function sleep(ms: number) {
16+
return new Promise(resolve => setTimeout(resolve, ms))
17+
}

apps/api/src/server.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import express, { Request, Response } from 'express'
2+
import { Queue } from 'bullmq'
3+
4+
export function createServer(queue: Queue) {
5+
const app = express()
6+
app.use(express.json({ limit: '2mb' }))
7+
8+
app.post('/scan', async (req: Request, res: Response) => {
9+
const payload = req.body || {}
10+
const isLarge = JSON.stringify(payload).length > 200_000 || payload?.large === true
11+
const job = await queue.add('scan', { payload, isLarge }, { removeOnComplete: true, removeOnFail: true })
12+
res.status(202).json({ jobId: job.id, statusUrl: `/scan/${job.id}/status`, resultUrl: `/scan/${job.id}/result` })
13+
})
14+
15+
app.get('/scan/:id/status', async (req: Request, res: Response) => {
16+
const id = req.params.id
17+
const job = await queue.getJob(id)
18+
if (!job) {
19+
res.status(404).json({ error: 'not_found' })
20+
return
21+
}
22+
const state = await job.getState()
23+
const progress = job.progress || 0
24+
res.json({ state, progress })
25+
})
26+
27+
app.get('/scan/:id/result', async (req: Request, res: Response) => {
28+
const id = req.params.id
29+
const job = await queue.getJob(id)
30+
if (!job) {
31+
res.status(404).json({ error: 'not_found' })
32+
return
33+
}
34+
const state = await job.getState()
35+
if (state !== 'completed') {
36+
res.status(202).json({ state })
37+
return
38+
}
39+
const result = job.returnvalue
40+
res.json({ result })
41+
})
42+
43+
return app
44+
}

apps/api/tsconfig.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2020",
4+
"module": "ES2020",
5+
"moduleResolution": "Node",
6+
"outDir": "dist",
7+
"rootDir": "src",
8+
"strict": true,
9+
"esModuleInterop": true,
10+
"skipLibCheck": true,
11+
"forceConsistentCasingInFileNames": true
12+
},
13+
"include": ["src/**/*"],
14+
"exclude": ["node_modules", "dist"]
15+
}

0 commit comments

Comments
 (0)