Files
omnivore/packages/content-fetch/src/app.ts

125 lines
3.5 KiB
TypeScript

import { RedisDataSource } from '@omnivore/utils'
import { JobType } from 'bullmq'
import express, { Express } from 'express'
import asyncHandler from 'express-async-handler'
import { createWorker, getQueue, QUEUE } from './worker'
const main = () => {
console.log('Starting worker...')
const app: Express = express()
const port = process.env.PORT || 3002
// create redis source
const redisDataSource = new RedisDataSource({
cache: {
url: process.env.REDIS_URL,
cert: process.env.REDIS_CERT,
},
mq: {
url: process.env.MQ_REDIS_URL,
cert: process.env.MQ_REDIS_CERT,
},
})
const worker = createWorker(redisDataSource)
// respond healthy to auto-scaler.
app.get('/_ah/health', (req, res) => res.sendStatus(200))
app.get(
'/lifecycle/prestop',
asyncHandler(async (_req, res) => {
console.log('Prestop lifecycle hook called.')
await worker.close()
console.log('Worker closed')
res.sendStatus(200)
})
)
app.get(
'/metrics',
asyncHandler(async (_, res) => {
let output = ''
const queue = await getQueue(redisDataSource.queueRedisClient)
const jobsTypes: Array<JobType> = [
'active',
'failed',
'completed',
'prioritized',
]
const counts = await queue.getJobCounts(...jobsTypes)
jobsTypes.forEach((metric) => {
output += `# TYPE omnivore_queue_messages_${metric} gauge\n`
output += `omnivore_queue_messages_${metric}{queue="${QUEUE}"} ${counts[metric]}\n`
})
// Export the age of the oldest prioritized job in the queue
const oldestJobs = await queue.getJobs(['prioritized'], 0, 1, true)
if (oldestJobs.length > 0) {
const currentTime = Date.now()
const ageInSeconds = (currentTime - oldestJobs[0].timestamp) / 1000
output += `# TYPE omnivore_queue_messages_oldest_job_age_seconds gauge\n`
output += `omnivore_queue_messages_oldest_job_age_seconds{queue="${QUEUE}"} ${ageInSeconds}\n`
} else {
output += `# TYPE omnivore_queue_messages_oldest_job_age_seconds gauge\n`
output += `omnivore_queue_messages_oldest_job_age_seconds{queue="${QUEUE}"} ${0}\n`
}
res.status(200).setHeader('Content-Type', 'text/plain').send(output)
})
)
const server = app.listen(port, () => {
console.log('Worker started')
})
const gracefulShutdown = async (signal: string) => {
console.log(`Received ${signal}, closing server...`)
await new Promise<void>((resolve) => {
server.close((err) => {
console.log('Express server closed')
if (err) {
console.log('Error stopping server', { err })
}
resolve()
})
})
await worker.close()
console.log('Worker closed')
await redisDataSource.shutdown()
console.log('Redis connection closed')
process.exit(0)
}
// eslint-disable-next-line @typescript-eslint/no-misused-promises
process.on('SIGINT', () => gracefulShutdown('SIGINT'))
// eslint-disable-next-line @typescript-eslint/no-misused-promises
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'))
process.on('uncaughtException', function (err) {
// Handle the error safely
console.error(err, 'Uncaught exception')
})
process.on('unhandledRejection', (reason, promise) => {
// Handle the error safely
console.error({ promise, reason }, 'Unhandled Rejection at: Promise')
})
}
// only call main if the file was called from the CLI and wasn't required from another module
if (require.main === module) {
main()
}