refactor(shared): unified queue handling (#4691)
* feat(shared): unified queue initialization in shared * feat(queues): use the new queue creation everywhere * chore(shared): move to redis module * chore(shared): fix export maps * chore(fileimport): add deps properly * fix(shared): import fix * fix(everything): moear imports * fix(server): cjs imports
This commit is contained in:
@@ -33,6 +33,7 @@
|
||||
"dependencies": {
|
||||
"@speckle/shared": "workspace:^",
|
||||
"bcrypt": "^5.0.0",
|
||||
"bull": "^4.16.5",
|
||||
"crypto": "^1.0.1",
|
||||
"crypto-random-string": "^3.2.0",
|
||||
"dotenv": "^16.4.5",
|
||||
@@ -47,7 +48,8 @@
|
||||
"tarn": "^3.0.2",
|
||||
"undici": "^5.28.4",
|
||||
"valid-filename": "^3.1.0",
|
||||
"web-ifc": "^0.0.36"
|
||||
"web-ifc": "^0.0.36",
|
||||
"znv": "^0.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcrypt": "^5.0.0",
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
PreviewGenerator,
|
||||
PreviewPageResult,
|
||||
TakeScreenshot
|
||||
} from '@speckle/shared/dist/esm/workers/previews/interface.js'
|
||||
} from '@speckle/shared/workers/previews'
|
||||
import {
|
||||
Viewer,
|
||||
DefaultViewerParams,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import Bull from 'bull'
|
||||
import { REDIS_URL } from '../src/config.js'
|
||||
import { initializeQueue } from '@speckle/shared/queue'
|
||||
|
||||
const jobQueue = new Bull('preview-service-jobs', REDIS_URL)
|
||||
const jobQueue = await initializeQueue({
|
||||
queueName: 'preview-service-jobs',
|
||||
redisUrl: REDIS_URL
|
||||
})
|
||||
|
||||
await jobQueue.add({
|
||||
url: 'https://latest.speckle.systems/projects/8b94a55ee5/models/7f98c5b62e',
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { Page, Browser } from 'puppeteer'
|
||||
import type { Logger } from 'pino'
|
||||
|
||||
import type { PreviewGenerator } from '@speckle/shared/dist/esm/workers/previews/interface.js'
|
||||
import type {
|
||||
PreviewGenerator,
|
||||
JobPayload,
|
||||
PreviewResultPayload
|
||||
} from '@speckle/shared/dist/esm/workers/previews/job.js'
|
||||
|
||||
import { AppState } from '@/const.js'
|
||||
} from '@speckle/shared/workers/previews'
|
||||
import { AppState } from '@speckle/shared/workers'
|
||||
import { TIME_MS } from '@speckle/shared'
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -2,11 +2,10 @@ import express from 'express'
|
||||
import puppeteer, { Browser } from 'puppeteer'
|
||||
import { createTerminus } from '@godaddy/terminus'
|
||||
import type { Logger } from 'pino'
|
||||
import { Redis, type RedisOptions } from 'ioredis'
|
||||
import Bull, { type QueueOptions } from 'bull'
|
||||
|
||||
import { jobPayload } from '@speckle/shared/dist/esm/workers/previews/job.js'
|
||||
import type Bull from 'bull'
|
||||
|
||||
import { JobPayload, PreviewResultPayload } from '@speckle/shared/workers/previews'
|
||||
import { AppState } from '@speckle/shared/workers'
|
||||
import {
|
||||
REDIS_URL,
|
||||
HOST,
|
||||
@@ -19,10 +18,10 @@ import {
|
||||
} from '@/config.js'
|
||||
import { logger } from '@/logging.js'
|
||||
import { jobProcessor } from '@/jobProcessor.js'
|
||||
import { AppState } from '@/const.js'
|
||||
import { initMetrics, initPrometheusRegistry } from '@/metrics.js'
|
||||
import { ensureError } from '@speckle/shared'
|
||||
import { isRedisReady } from '@/utils.js'
|
||||
import { initializeQueue } from '@speckle/shared/queue'
|
||||
import { isRedisReady } from '@speckle/shared/redis'
|
||||
|
||||
const app = express()
|
||||
const host = HOST
|
||||
@@ -35,40 +34,7 @@ let appState: AppState = AppState.STARTING
|
||||
// serve the preview-frontend
|
||||
app.use(express.static('public'))
|
||||
await initMetrics({ app, registry: initPrometheusRegistry() })
|
||||
|
||||
let client: Redis
|
||||
let subscriber: Redis
|
||||
|
||||
const opts: QueueOptions = {
|
||||
// redisOpts here will contain at least a property of connectionName which will identify the queue based on its name
|
||||
createClient(type: string, redisOpts: RedisOptions) {
|
||||
switch (type) {
|
||||
case 'client':
|
||||
if (!client) {
|
||||
client = new Redis(REDIS_URL, redisOpts)
|
||||
}
|
||||
return client
|
||||
case 'subscriber':
|
||||
if (!subscriber) {
|
||||
subscriber = new Redis(REDIS_URL, {
|
||||
...redisOpts,
|
||||
maxRetriesPerRequest: null,
|
||||
enableReadyCheck: false
|
||||
})
|
||||
}
|
||||
return subscriber
|
||||
case 'bclient':
|
||||
return new Redis(REDIS_URL, {
|
||||
...redisOpts,
|
||||
maxRetriesPerRequest: null,
|
||||
enableReadyCheck: false
|
||||
})
|
||||
default:
|
||||
throw new Error('Unexpected connection type: ' + type)
|
||||
}
|
||||
}
|
||||
}
|
||||
let jobQueue: Bull.Queue | undefined = undefined
|
||||
let jobQueue: Bull.Queue<JobPayload> | undefined = undefined
|
||||
|
||||
// store this callback, so on shutdown we can error the job
|
||||
let currentJob: { logger: Logger; done: Bull.DoneCallback } | undefined = undefined
|
||||
@@ -116,16 +82,10 @@ const server = app.listen(port, host, async () => {
|
||||
}
|
||||
|
||||
try {
|
||||
const newQueue = new Bull(JobQueueName, opts)
|
||||
|
||||
logger.info('Checking Redis connection is ready...')
|
||||
|
||||
// Bull's Queue.isReady() does not actually check the Redis connection
|
||||
// see https://github.com/OptimalBits/bull/issues/1873#issuecomment-953581143
|
||||
await isRedisReady(newQueue.client)
|
||||
logger.info('Redis is ready')
|
||||
|
||||
jobQueue = await newQueue.isReady()
|
||||
jobQueue = await initializeQueue<JobPayload>({
|
||||
queueName: JobQueueName,
|
||||
redisUrl: REDIS_URL
|
||||
})
|
||||
} catch (e) {
|
||||
const err = ensureError(e, 'Unknown error creating job queue')
|
||||
logger.error({ err }, 'Error creating job queue')
|
||||
@@ -154,21 +114,15 @@ const server = app.listen(port, host, async () => {
|
||||
|
||||
try {
|
||||
currentJob = { done, logger: jobLogger }
|
||||
const parseResult = jobPayload.safeParse(payload.data)
|
||||
if (!parseResult.success) {
|
||||
jobLogger.error(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
{ parseError: parseResult.error, payload: payload.data },
|
||||
'Invalid job payload'
|
||||
)
|
||||
return done(parseResult.error)
|
||||
}
|
||||
const job = parseResult.data
|
||||
const job = payload.data
|
||||
jobLogger = jobLogger.child({
|
||||
jobId: job.jobId,
|
||||
serverUrl: job.url
|
||||
})
|
||||
const resultsQueue = new Bull(job.responseQueue, opts)
|
||||
const resultsQueue = await initializeQueue<PreviewResultPayload>({
|
||||
queueName: job.responseQueue,
|
||||
redisUrl: REDIS_URL
|
||||
})
|
||||
|
||||
browser = await launchBrowser()
|
||||
const result = await jobProcessor({
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
/* Modules */
|
||||
"module": "ES2022" /* Specify what module code is generated. */,
|
||||
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||
"moduleResolution": "node",
|
||||
"moduleResolution": "bundler",
|
||||
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { CheckResponse, RedisCheck } from '@/healthchecks/types'
|
||||
import { isRedisReady } from '@/modules/shared/redis/redis'
|
||||
import { isRedisReady } from '@speckle/shared/dist/commonjs/redis/isRedisReady.js'
|
||||
|
||||
export const isRedisAlive: RedisCheck = async (params): Promise<CheckResponse> => {
|
||||
const { client } = params
|
||||
|
||||
@@ -18,7 +18,7 @@ const command: CommandModule = {
|
||||
})
|
||||
},
|
||||
handler: async (argv) => {
|
||||
initializeQueue()
|
||||
await initializeQueue()
|
||||
const numberOfDays = argv.days as number
|
||||
const end = new Date()
|
||||
const start = new Date(end.getTime())
|
||||
|
||||
@@ -26,11 +26,11 @@ const command: CommandModule<unknown, { testQueueId: string }> = {
|
||||
const testQueueId = argv.testQueueId
|
||||
|
||||
cliLogger.info('Initializing bull queues...')
|
||||
const queues = [buildNotificationsQueue(NOTIFICATIONS_QUEUE)]
|
||||
const queues = [await buildNotificationsQueue(NOTIFICATIONS_QUEUE)]
|
||||
|
||||
if (testQueueId) {
|
||||
cliLogger.info('Also initializing queue %s...', testQueueId)
|
||||
queues.push(buildNotificationsQueue(testQueueId))
|
||||
queues.push(await buildNotificationsQueue(testQueueId))
|
||||
}
|
||||
|
||||
cliLogger.info('Initializing monitor...')
|
||||
|
||||
@@ -24,7 +24,7 @@ const command: CommandModule = {
|
||||
})
|
||||
},
|
||||
handler: async (argv) => {
|
||||
initializeQueue()
|
||||
await initializeQueue()
|
||||
|
||||
// we don't want to submit a real mentions payload, this is for testing only
|
||||
await publishNotification(NotificationType.MentionedInComment, {
|
||||
|
||||
@@ -3,7 +3,6 @@ import { logger } from '@/observability/logging'
|
||||
import { isProdEnv, isTestEnv } from '@/modules/shared/helpers/envHelper'
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
import { Optional, TIME_MS } from '@speckle/shared'
|
||||
import { buildBaseQueueOptions } from '@/modules/shared/helpers/bullHelper'
|
||||
import { UninitializedResourceAccessError } from '@/modules/shared/errors'
|
||||
import {
|
||||
MultiRegionInvalidJobError,
|
||||
@@ -30,6 +29,7 @@ import {
|
||||
} from '@/modules/multiregion/repositories/projectRegion'
|
||||
import { updateProjectRegionKeyFactory } from '@/modules/multiregion/services/projectRegion'
|
||||
import { getGenericRedis } from '@/modules/shared/redis/redis'
|
||||
import { initializeQueue as setupQueue } from '@speckle/shared/dist/commonjs/queue/index.js'
|
||||
import { getEventBus } from '@/modules/shared/services/eventBus'
|
||||
import {
|
||||
copyWorkspaceFactory,
|
||||
@@ -49,6 +49,7 @@ import {
|
||||
countProjectWebhooksFactory
|
||||
} from '@/modules/workspaces/repositories/projectRegions'
|
||||
import { withTransaction } from '@/modules/shared/helpers/dbHelper'
|
||||
import { getRedisUrl } from '@/modules/shared/helpers/envHelper'
|
||||
|
||||
const MULTIREGION_QUEUE_NAME = isTestEnv()
|
||||
? `test:multiregion:${cryptoRandomString({ length: 5 })}`
|
||||
@@ -77,30 +78,7 @@ type MultiregionJob =
|
||||
|
||||
let queue: Optional<Bull.Queue<MultiregionJob>>
|
||||
|
||||
export const buildMultiregionQueue = (queueName: string) =>
|
||||
new Bull(queueName, {
|
||||
...buildBaseQueueOptions(),
|
||||
...(!isTestEnv()
|
||||
? {
|
||||
limiter: {
|
||||
max: 10,
|
||||
duration: TIME_MS.second
|
||||
}
|
||||
}
|
||||
: {}),
|
||||
defaultJobOptions: {
|
||||
attempts: 5,
|
||||
timeout: 15 * TIME_MS.minute,
|
||||
backoff: {
|
||||
type: 'fixed',
|
||||
delay: 5 * TIME_MS.minute
|
||||
},
|
||||
removeOnComplete: isProdEnv(),
|
||||
removeOnFail: false
|
||||
}
|
||||
})
|
||||
|
||||
export const getQueue = (): Bull.Queue => {
|
||||
export const getQueue = (): Bull.Queue<MultiregionJob> => {
|
||||
if (!queue) {
|
||||
throw new UninitializedResourceAccessError(
|
||||
'Attempting to use uninitialized Bull queue'
|
||||
@@ -110,8 +88,31 @@ export const getQueue = (): Bull.Queue => {
|
||||
return queue
|
||||
}
|
||||
|
||||
export const initializeQueue = () => {
|
||||
queue = buildMultiregionQueue(MULTIREGION_QUEUE_NAME)
|
||||
export const initializeQueue = async () => {
|
||||
queue = await setupQueue({
|
||||
queueName: MULTIREGION_QUEUE_NAME,
|
||||
redisUrl: getRedisUrl(),
|
||||
options: {
|
||||
...(!isTestEnv()
|
||||
? {
|
||||
limiter: {
|
||||
max: 10,
|
||||
duration: TIME_MS.second
|
||||
}
|
||||
}
|
||||
: {}),
|
||||
defaultJobOptions: {
|
||||
attempts: 5,
|
||||
timeout: 15 * TIME_MS.minute,
|
||||
backoff: {
|
||||
type: 'fixed',
|
||||
delay: 5 * TIME_MS.minute
|
||||
},
|
||||
removeOnComplete: isProdEnv(),
|
||||
removeOnFail: false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -36,7 +36,7 @@ export async function initializeConsumption(
|
||||
|
||||
registerNotificationHandlers(customHandlers || allHandlers)
|
||||
|
||||
initializeQueue()
|
||||
await initializeQueue()
|
||||
|
||||
if (shouldDisableNotificationsConsumption()) {
|
||||
moduleLogger.info('Skipping notification consumption...')
|
||||
|
||||
@@ -12,9 +12,9 @@ import {
|
||||
NotificationType,
|
||||
NotificationTypeHandlers
|
||||
} from '@/modules/notifications/helpers/types'
|
||||
import { isProdEnv, isTestEnv } from '@/modules/shared/helpers/envHelper'
|
||||
import { getRedisUrl, isProdEnv, isTestEnv } from '@/modules/shared/helpers/envHelper'
|
||||
import Bull from 'bull'
|
||||
import { buildBaseQueueOptions } from '@/modules/shared/helpers/bullHelper'
|
||||
import { initializeQueue as setupQueue } from '@speckle/shared/dist/commonjs/queue/index.js'
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
import { logger, notificationsLogger, Observability } from '@/observability/logging'
|
||||
import { ensureErrorOrWrapAsCause } from '@/modules/shared/errors/ensureError'
|
||||
@@ -47,22 +47,25 @@ if (isTestEnv()) {
|
||||
|
||||
let queue: Optional<Bull.Queue>
|
||||
|
||||
export const buildNotificationsQueue = (queueName: string) =>
|
||||
new Bull(queueName, {
|
||||
...buildBaseQueueOptions(),
|
||||
...(!isTestEnv()
|
||||
? {
|
||||
limiter: {
|
||||
max: 10,
|
||||
duration: TIME_MS.second
|
||||
export const buildNotificationsQueue = async (queueName: string) =>
|
||||
await setupQueue({
|
||||
queueName,
|
||||
redisUrl: getRedisUrl(),
|
||||
options: {
|
||||
...(!isTestEnv()
|
||||
? {
|
||||
limiter: {
|
||||
max: 10,
|
||||
duration: TIME_MS.second
|
||||
}
|
||||
}
|
||||
}
|
||||
: {}),
|
||||
defaultJobOptions: {
|
||||
attempts: 1,
|
||||
timeout: 10 * TIME_MS.second,
|
||||
removeOnComplete: isProdEnv(),
|
||||
removeOnFail: isProdEnv()
|
||||
: {}),
|
||||
defaultJobOptions: {
|
||||
attempts: 1,
|
||||
timeout: 10 * TIME_MS.second,
|
||||
removeOnComplete: isProdEnv(),
|
||||
removeOnFail: isProdEnv()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -82,8 +85,8 @@ export function getQueue(): Bull.Queue {
|
||||
/**
|
||||
* Initialize notifications queue
|
||||
*/
|
||||
export function initializeQueue() {
|
||||
queue = buildNotificationsQueue(NOTIFICATIONS_QUEUE)
|
||||
export async function initializeQueue() {
|
||||
queue = await buildNotificationsQueue(NOTIFICATIONS_QUEUE)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,8 +9,6 @@ import {
|
||||
getRedisUrl,
|
||||
getServerOrigin
|
||||
} from '@/modules/shared/helpers/envHelper'
|
||||
import Bull, { type QueueOptions } from 'bull'
|
||||
import Redis, { type RedisOptions } from 'ioredis'
|
||||
import { createBullBoard } from 'bull-board'
|
||||
import { BullMQAdapter } from 'bull-board/bullMQAdapter'
|
||||
import { authMiddlewareCreator } from '@/modules/shared/middleware'
|
||||
@@ -19,7 +17,11 @@ import { validateServerRoleBuilderFactory } from '@/modules/shared/authz'
|
||||
import { getRolesFactory } from '@/modules/shared/repositories/roles'
|
||||
import { previewRouterFactory } from '@/modules/previews/rest/router'
|
||||
import type { SpeckleModule } from '@/modules/shared/helpers/typeHelper'
|
||||
import { previewResultPayload } from '@speckle/shared/dist/commonjs/workers/previews/job.js'
|
||||
import {
|
||||
JobPayload,
|
||||
PreviewResultPayload,
|
||||
previewResultPayload
|
||||
} from '@speckle/shared/dist/commonjs/workers/previews/job.js'
|
||||
import { getProjectDbClient } from '@/modules/multiregion/utils/dbSelector'
|
||||
import {
|
||||
storePreviewFactory,
|
||||
@@ -31,59 +33,32 @@ import {
|
||||
PreviewJobDurationStep
|
||||
} from '@/modules/previews/observability/metrics'
|
||||
import { addRequestQueueListeners } from '@/modules/previews/queues/previews'
|
||||
import { isRedisReady } from '@/modules/shared/redis/redis'
|
||||
import { initializeQueue } from '@speckle/shared/dist/commonjs/queue/index.js'
|
||||
import type Bull from 'bull'
|
||||
|
||||
const JobQueueName = 'preview-service-jobs'
|
||||
const ResponseQueueNamePrefix = 'preview-service-results'
|
||||
|
||||
const getPreviewQueues = async (params: { responseQueueName: string }) => {
|
||||
const { responseQueueName } = params
|
||||
let client: Redis
|
||||
let subscriber: Redis
|
||||
const redisUrl = getPreviewServiceRedisUrl() ?? getRedisUrl()
|
||||
|
||||
const opts: QueueOptions = {
|
||||
// redisOpts here will contain at least a property of connectionName which will identify the queue based on its name
|
||||
createClient(type: string, redisOpts: RedisOptions) {
|
||||
switch (type) {
|
||||
case 'client':
|
||||
if (!client) {
|
||||
client = new Redis(redisUrl, redisOpts)
|
||||
}
|
||||
return client
|
||||
case 'subscriber':
|
||||
if (!subscriber) {
|
||||
subscriber = new Redis(redisUrl, {
|
||||
...redisOpts,
|
||||
maxRetriesPerRequest: null,
|
||||
enableReadyCheck: false
|
||||
})
|
||||
}
|
||||
return subscriber
|
||||
case 'bclient':
|
||||
return new Redis(redisUrl, {
|
||||
...redisOpts,
|
||||
maxRetriesPerRequest: null,
|
||||
enableReadyCheck: false
|
||||
})
|
||||
default:
|
||||
throw new Error('Unexpected connection type: ' + type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// previews are requested on this queue
|
||||
const previewRequestQueue = new Bull(JobQueueName, opts)
|
||||
await isRedisReady(previewRequestQueue.client)
|
||||
const previewRequestQueue = await initializeQueue<JobPayload>({
|
||||
queueName: JobQueueName,
|
||||
redisUrl
|
||||
})
|
||||
addRequestQueueListeners({
|
||||
logger,
|
||||
previewRequestQueue
|
||||
})
|
||||
|
||||
// rendered previews are sent back on this queue
|
||||
const previewResponseQueue = new Bull(responseQueueName, opts)
|
||||
const previewResponseQueue = await initializeQueue<PreviewResultPayload>({
|
||||
queueName: responseQueueName,
|
||||
redisUrl
|
||||
})
|
||||
|
||||
await isRedisReady(previewResponseQueue.client)
|
||||
return { previewRequestQueue, previewResponseQueue }
|
||||
}
|
||||
|
||||
@@ -103,8 +78,8 @@ export const init: SpeckleModule['init'] = async ({
|
||||
new URL(getServerOrigin()).hostname
|
||||
}`
|
||||
|
||||
let previewRequestQueue: Bull.Queue
|
||||
let previewResponseQueue: Bull.Queue
|
||||
let previewRequestQueue: Bull.Queue<JobPayload>
|
||||
let previewResponseQueue: Bull.Queue<PreviewResultPayload>
|
||||
|
||||
try {
|
||||
;({ previewRequestQueue, previewResponseQueue } = await getPreviewQueues({
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { EventEmitter } from 'stream'
|
||||
import { upsertObjectPreviewFactory } from '@/modules/previews/repository/previews'
|
||||
import { getProjectDbClient } from '@/modules/multiregion/utils/dbSelector'
|
||||
import { PreviewStatus } from '@/modules/previews/domain/consts'
|
||||
import { JobPayload } from '@speckle/shared/dist/commonjs/workers/previews'
|
||||
|
||||
export const requestObjectPreviewFactory =
|
||||
({
|
||||
@@ -12,7 +13,7 @@ export const requestObjectPreviewFactory =
|
||||
queue
|
||||
}: {
|
||||
responseQueue: string
|
||||
queue: Queue
|
||||
queue: Queue<JobPayload>
|
||||
}): RequestObjectPreview =>
|
||||
async ({ jobId, token, url }) => {
|
||||
const payload = { jobId, token, url, responseQueue }
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import Bull from 'bull'
|
||||
import { getRedisUrl } from '@/modules/shared/helpers/envHelper'
|
||||
import { createRedisClient } from '@/modules/shared/redis/redis'
|
||||
|
||||
export function buildBaseQueueOptions(): Bull.QueueOptions {
|
||||
return {
|
||||
createClient: (type) => {
|
||||
const client = createRedisClient(getRedisUrl(), {
|
||||
...(['bclient', 'subscriber'].includes(type)
|
||||
? {
|
||||
enableReadyCheck: false,
|
||||
maxRetriesPerRequest: null
|
||||
}
|
||||
: {})
|
||||
})
|
||||
|
||||
return client
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
MisconfiguredEnvironmentError
|
||||
} from '@/modules/shared/errors'
|
||||
import { getRedisUrl } from '@/modules/shared/helpers/envHelper'
|
||||
import { ensureError } from '@speckle/shared'
|
||||
|
||||
export function createRedisClient(redisUrl: string, redisOptions: RedisOptions): Redis {
|
||||
let redisClient: Redis
|
||||
@@ -35,36 +34,3 @@ export const getGenericRedis = (): Redis => {
|
||||
if (!redisClient) redisClient = createRedisClient(getRedisUrl(), {})
|
||||
return redisClient
|
||||
}
|
||||
|
||||
export const isRedisReady = (client: Redis) => {
|
||||
// MIT Licensed: https://github.com/OptimalBits/bull/blob/develop/LICENSE.md
|
||||
// Reference: https://github.com/OptimalBits/bull/blob/develop/lib/utils.js
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (client.status === 'ready') {
|
||||
resolve()
|
||||
} else {
|
||||
function handleReady() {
|
||||
client.removeListener('end', handleEnd)
|
||||
client.removeListener('error', handleError)
|
||||
resolve()
|
||||
}
|
||||
|
||||
function handleError(e: unknown) {
|
||||
const err = ensureError(e, 'Unknown error in Redis client')
|
||||
client.removeListener('ready', handleReady)
|
||||
client.removeListener('error', handleError)
|
||||
reject(err)
|
||||
}
|
||||
|
||||
function handleEnd() {
|
||||
client.removeListener('ready', handleReady)
|
||||
client.removeListener('error', handleError)
|
||||
reject(new Error('Redis connection ended'))
|
||||
}
|
||||
|
||||
client.once('ready', handleReady)
|
||||
client.on('error', handleError)
|
||||
client.once('end', handleEnd)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^2.0.0-beta.176",
|
||||
"bull": "*",
|
||||
"knex": "*",
|
||||
"mixpanel": "^0.17.0",
|
||||
"pino": "^8.7.0",
|
||||
@@ -64,6 +65,7 @@
|
||||
"@typescript-eslint/parser": "^7.12.0",
|
||||
"@vitest/coverage-v8": "^3.0.9",
|
||||
"@vitest/ui": "^3.0.9",
|
||||
"bull": "^4.16.5",
|
||||
"crypto-random-string": "^5.0.0",
|
||||
"eslint": "^9.4.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
@@ -87,6 +89,11 @@
|
||||
"./environment": "./src/environment/index.ts",
|
||||
"./observability": "./src/observability/index.ts",
|
||||
"./authz": "./src/authz/index.ts",
|
||||
"./queue": "./src/queue/index.ts",
|
||||
"./redis": "./src/redis/index.ts",
|
||||
"./workers": "./src/workers/index.ts",
|
||||
"./workers/previews": "./src/workers/previews/index.ts",
|
||||
"./workers/fileimport": "./src/workers/fileimport/index.ts",
|
||||
"./dist/*": "./dist/*"
|
||||
},
|
||||
"exclude": [
|
||||
@@ -143,6 +150,56 @@
|
||||
"default": "./dist/commonjs/authz/index.js"
|
||||
}
|
||||
},
|
||||
"./queue": {
|
||||
"import": {
|
||||
"types": "./dist/esm/queue/index.d.ts",
|
||||
"default": "./dist/esm/queue/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/commonjs/queue/index.d.ts",
|
||||
"default": "./dist/commonjs/queue/index.js"
|
||||
}
|
||||
},
|
||||
"./redis": {
|
||||
"import": {
|
||||
"types": "./dist/esm/redis/index.d.ts",
|
||||
"default": "./dist/esm/redis/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/commonjs/redis/index.d.ts",
|
||||
"default": "./dist/commonjs/redis/index.js"
|
||||
}
|
||||
},
|
||||
"./workers": {
|
||||
"import": {
|
||||
"types": "./dist/esm/workers/index.d.ts",
|
||||
"default": "./dist/esm/workers/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/commonjs/workers/index.d.ts",
|
||||
"default": "./dist/commonjs/workers/index.js"
|
||||
}
|
||||
},
|
||||
"./workers/previews": {
|
||||
"import": {
|
||||
"types": "./dist/esm/workers/previews/index.d.ts",
|
||||
"default": "./dist/esm/workers/previews/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/commonjs/workers/previews/index.d.ts",
|
||||
"default": "./dist/commonjs/workers/previews/index.js"
|
||||
}
|
||||
},
|
||||
"./workers/fileimport": {
|
||||
"import": {
|
||||
"types": "./dist/esm/workers/fileimport/index.d.ts",
|
||||
"default": "./dist/esm/workers/fileimport/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/commonjs/workers/fileimport/index.d.ts",
|
||||
"default": "./dist/commonjs/workers/fileimport/index.js"
|
||||
}
|
||||
},
|
||||
"./dist/*": "./dist/*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import Bull from 'bull'
|
||||
import { Redis } from 'ioredis'
|
||||
import { isRedisReady } from '../redis/isRedisReady.js'
|
||||
|
||||
// we're caching this here, so that there is one client for the app lifecycle
|
||||
|
||||
type ClientCache = Record<string, { client?: Redis; subscriber?: Redis }>
|
||||
|
||||
const clientCache: ClientCache = {}
|
||||
|
||||
export const initializeQueue = async <T>({
|
||||
queueName,
|
||||
redisUrl,
|
||||
options
|
||||
}: {
|
||||
queueName: string
|
||||
redisUrl: string
|
||||
options?: Partial<Bull.QueueOptions>
|
||||
}): Promise<Bull.Queue<T>> => {
|
||||
if (!(redisUrl in clientCache)) clientCache[redisUrl] = {}
|
||||
const opts: Bull.QueueOptions = {
|
||||
...options,
|
||||
// redisOpts here will contain at least a property of connectionName which will identify the queue based on its name
|
||||
createClient(type, redisOpts) {
|
||||
switch (type) {
|
||||
case 'client':
|
||||
if (redisUrl in clientCache && clientCache[redisUrl].client !== undefined) {
|
||||
return clientCache[redisUrl].client
|
||||
} else {
|
||||
const client = new Redis(redisUrl, redisOpts ?? {})
|
||||
clientCache[redisUrl].client = client
|
||||
return client
|
||||
}
|
||||
case 'subscriber':
|
||||
if (
|
||||
redisUrl in clientCache &&
|
||||
clientCache[redisUrl].subscriber !== undefined
|
||||
) {
|
||||
return clientCache[redisUrl].subscriber
|
||||
} else {
|
||||
const subscriber = new Redis(redisUrl, {
|
||||
...redisOpts,
|
||||
maxRetriesPerRequest: null,
|
||||
enableReadyCheck: false
|
||||
})
|
||||
clientCache[redisUrl].subscriber = subscriber
|
||||
return subscriber
|
||||
}
|
||||
case 'bclient':
|
||||
return new Redis(redisUrl, {
|
||||
...redisOpts,
|
||||
maxRetriesPerRequest: null,
|
||||
enableReadyCheck: false
|
||||
})
|
||||
default:
|
||||
throw new Error(`Unexpected connection type: ${type}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
const newQueue = new Bull<T>(queueName, opts)
|
||||
// bull does not check if redis is ready...
|
||||
//
|
||||
// logger.info('Checking Redis connection is ready...')
|
||||
if (!clientCache[redisUrl].client)
|
||||
throw new Error('Redis client not properly initialized')
|
||||
await isRedisReady(clientCache[redisUrl].client)
|
||||
// await isRedisReady(clientCache[redisUrl].subscriber)
|
||||
return await newQueue.isReady()
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './config.js'
|
||||
@@ -0,0 +1 @@
|
||||
export * from './isRedisReady.js'
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ensureError } from '@speckle/shared'
|
||||
import { Redis, type RedisOptions } from 'ioredis'
|
||||
import { Redis } from 'ioredis'
|
||||
import { ensureError } from '../core/helpers/error.js'
|
||||
|
||||
// MIT Licensed: https://github.com/OptimalBits/bull/blob/develop/LICENSE.md
|
||||
// Reference: https://github.com/OptimalBits/bull/blob/develop/lib/utils.js
|
||||
@@ -0,0 +1 @@
|
||||
export * from './job.js'
|
||||
@@ -0,0 +1 @@
|
||||
export * from './state.js'
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './interface.js'
|
||||
export * from './job.js'
|
||||
@@ -16480,6 +16480,7 @@ __metadata:
|
||||
"@types/node": "npm:^18.19.38"
|
||||
"@vitest/coverage-istanbul": "npm:^1.6.0"
|
||||
bcrypt: "npm:^5.0.0"
|
||||
bull: "npm:^4.16.5"
|
||||
concurrently: "npm:^8.2.2"
|
||||
crypto: "npm:^1.0.1"
|
||||
crypto-random-string: "npm:^3.2.0"
|
||||
@@ -16505,6 +16506,7 @@ __metadata:
|
||||
valid-filename: "npm:^3.1.0"
|
||||
vitest: "npm:^1.6.0"
|
||||
web-ifc: "npm:^0.0.36"
|
||||
znv: "npm:^0.5.0"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
@@ -16985,6 +16987,7 @@ __metadata:
|
||||
"@typescript-eslint/parser": "npm:^7.12.0"
|
||||
"@vitest/coverage-v8": "npm:^3.0.9"
|
||||
"@vitest/ui": "npm:^3.0.9"
|
||||
bull: "npm:^4.16.5"
|
||||
crypto-random-string: "npm:^5.0.0"
|
||||
dayjs: "npm:^1.11.13"
|
||||
eslint: "npm:^9.4.0"
|
||||
@@ -17006,6 +17009,7 @@ __metadata:
|
||||
zod: "npm:^3.22.4"
|
||||
peerDependencies:
|
||||
"@tiptap/core": ^2.0.0-beta.176
|
||||
bull: "*"
|
||||
knex: "*"
|
||||
mixpanel: ^0.17.0
|
||||
pino: ^8.7.0
|
||||
@@ -24810,6 +24814,21 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"bull@npm:^4.16.5":
|
||||
version: 4.16.5
|
||||
resolution: "bull@npm:4.16.5"
|
||||
dependencies:
|
||||
cron-parser: "npm:^4.9.0"
|
||||
get-port: "npm:^5.1.1"
|
||||
ioredis: "npm:^5.3.2"
|
||||
lodash: "npm:^4.17.21"
|
||||
msgpackr: "npm:^1.11.2"
|
||||
semver: "npm:^7.5.2"
|
||||
uuid: "npm:^8.3.0"
|
||||
checksum: 10/3250486db79a99d3cf77f078694b134a4ae66e930e070d05dd099d7f975ab4d70163858641ad1873617ae0b9c0aab0ca5da6cb69e688b9ab91a779997b3b51e3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"bundle-name@npm:^4.1.0":
|
||||
version: 4.1.0
|
||||
resolution: "bundle-name@npm:4.1.0"
|
||||
@@ -26969,6 +26988,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cron-parser@npm:^4.9.0":
|
||||
version: 4.9.0
|
||||
resolution: "cron-parser@npm:4.9.0"
|
||||
dependencies:
|
||||
luxon: "npm:^3.2.1"
|
||||
checksum: 10/ffca5e532a5ee0923412ee6e4c7f9bbceacc6ddf8810c16d3e9fb4fe5ec7e2de1b6896d7956f304bb6bc96b0ce37ad7e3935304179d52951c18d84107184faa7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"croner@npm:^9.0.0":
|
||||
version: 9.0.0
|
||||
resolution: "croner@npm:9.0.0"
|
||||
@@ -38186,6 +38214,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"luxon@npm:^3.2.1":
|
||||
version: 3.6.1
|
||||
resolution: "luxon@npm:3.6.1"
|
||||
checksum: 10/35aad425607708c87af110a52c949190bc35b987770079ec8007ef2365cd29639413db3360d2883777aa01cb3ca5bdb37f42ee3e8e5a0dd277fe22e90cc8a786
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lz-string@npm:^1.4.4":
|
||||
version: 1.4.4
|
||||
resolution: "lz-string@npm:1.4.4"
|
||||
@@ -54323,6 +54358,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"znv@npm:^0.5.0":
|
||||
version: 0.5.0
|
||||
resolution: "znv@npm:0.5.0"
|
||||
peerDependencies:
|
||||
zod: ^3.24.2
|
||||
checksum: 10/4954b21e2856beca67da60d2f647671381284d5434908d69656ccd93c3e9291149f9b6ac1a8015d0b48604c14916241717f85b5a8c3c41d64423c8baebbf9a18
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"zod-express@npm:^0.0.8":
|
||||
version: 0.0.8
|
||||
resolution: "zod-express@npm:0.0.8"
|
||||
|
||||
Reference in New Issue
Block a user