diff --git a/packages/preview-service/src/jobProcessor.ts b/packages/preview-service/src/jobProcessor.ts index 13938aa15..a05d42b80 100644 --- a/packages/preview-service/src/jobProcessor.ts +++ b/packages/preview-service/src/jobProcessor.ts @@ -33,7 +33,11 @@ export const jobProcessor = async ({ port, timeout }: JobArgs): Promise => { - const start = new Date() + const elapsed = (() => { + const start = new Date().getTime() + return () => (new Date().getTime() - start) / 1000 + })() + logger.info('Picked up job {jobId} for {serverUrl}') const jobMessage = @@ -43,12 +47,10 @@ export const jobProcessor = async ({ page = await browser.newPage() const result = await pageFunction({ page, job, logger, port, timeout }) - const elapsed = (new Date().getTime() - start.getTime()) / 1000 - logger.info({ status: result.status, elapsed }, jobMessage) + logger.info({ status: result.status, elapsed: elapsed() }, jobMessage) return result } catch (err: unknown) { - const elapsed = (new Date().getTime() - start.getTime()) / 1000 - logger.error({ err, elapsed, status: 'error' }, jobMessage) + logger.error({ err, elapsed: elapsed(), status: 'error' }, jobMessage) const reason = err instanceof Error ? err.stack ?? err.toString() @@ -60,7 +62,7 @@ export const jobProcessor = async ({ jobId: job.jobId, status: 'error', result: { - durationSeconds: elapsed + durationSeconds: elapsed() }, reason } diff --git a/packages/server/modules/previews/domain/consts.ts b/packages/server/modules/previews/domain/consts.ts new file mode 100644 index 000000000..8bcaef05c --- /dev/null +++ b/packages/server/modules/previews/domain/consts.ts @@ -0,0 +1,12 @@ +export const PreviewStatus = { + PENDING: 0, + PROCESSING: 1, + DONE: 2, + ERROR: 3 +} as const + +export const PreviewPriority = { + LOW: 0, + MEDIUM: 100, + HIGH: 200 +} as const diff --git a/packages/server/modules/previews/domain/operations.ts b/packages/server/modules/previews/domain/operations.ts index a8df36d60..196548e6b 100644 --- a/packages/server/modules/previews/domain/operations.ts +++ b/packages/server/modules/previews/domain/operations.ts @@ -1,6 +1,6 @@ -import { ObjectPreview } from '@/modules/previews/domain/types' -import { Nullable, Optional } from '@speckle/shared' -import express from 'express' +import type { ObjectPreview } from '@/modules/previews/domain/types' +import type { Nullable, Optional, PartialBy } from '@speckle/shared' +import type { Request, Response } from 'express' export type GetObjectPreviewInfo = (params: { streamId: string @@ -17,7 +17,7 @@ export type ObjectPreviewInput = Pick< > export type StoreObjectPreview = (params: ObjectPreviewInput) => Promise export type UpsertObjectPreview = (params: { - objectPreview: ObjectPreview + objectPreview: PartialBy }) => Promise export type ObjectPreviewRequest = { @@ -54,13 +54,13 @@ export type GetObjectPreviewBufferOrFilepath = (params: { > export type SendObjectPreview = ( - req: express.Request, - res: express.Response, + req: Request, + res: Response, streamId: string, objectId: string, angle?: string ) => Promise export type CheckStreamPermissions = ( - req: express.Request + req: Request ) => Promise<{ hasPermissions: boolean; httpErrorCode: number }> diff --git a/packages/server/modules/previews/index.ts b/packages/server/modules/previews/index.ts index fb29d917c..0a0e4add7 100644 --- a/packages/server/modules/previews/index.ts +++ b/packages/server/modules/previews/index.ts @@ -70,7 +70,10 @@ const getPreviewQueues = (params: { responseQueueName: string }) => { // previews are requested on this queue const previewRequestQueue = new Bull('preview-service-jobs', opts) - addRequestQueueListeners({ logger, previewRequestQueue }) + addRequestQueueListeners({ + logger, + previewRequestQueue + }) // rendered previews are sent back on this queue const previewResponseQueue = new Bull(responseQueueName, opts) diff --git a/packages/server/modules/previews/queues/previews.ts b/packages/server/modules/previews/queues/previews.ts index f688611ed..a75401bcd 100644 --- a/packages/server/modules/previews/queues/previews.ts +++ b/packages/server/modules/previews/queues/previews.ts @@ -2,6 +2,9 @@ import type { RequestObjectPreview } from '@/modules/previews/domain/operations' import type { Logger } from '@/observability/logging' import type { Queue, Job } from 'bull' 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' export const requestObjectPreviewFactory = ({ @@ -30,9 +33,20 @@ export const addRequestQueueListeners = (params: { previewRequestQueue.removeListener('error', requestErrorHandler) previewRequestQueue.on('error', requestErrorHandler) - const requestFailedHandler = (job: Job, err: Error) => { + const requestFailedHandler = async (job: Job, err: Error) => { const jobId = 'jobId' in job.data ? job.data.jobId : undefined logger.error({ err, jobId }, 'Preview job {jobId} failed.') + if (!jobId) return + const [projectId, objectId] = jobId.split('.') + const projectDb = await getProjectDbClient({ projectId }) + upsertObjectPreviewFactory({ db: projectDb })({ + objectPreview: { + streamId: projectId, + objectId, + previewStatus: PreviewStatus.ERROR, + lastUpdate: new Date() + } + }) } previewRequestQueue.removeListener('failed', requestFailedHandler) previewRequestQueue.on('failed', requestFailedHandler) diff --git a/packages/server/modules/previews/repository/previews.ts b/packages/server/modules/previews/repository/previews.ts index d9644e8d1..b7c5e8746 100644 --- a/packages/server/modules/previews/repository/previews.ts +++ b/packages/server/modules/previews/repository/previews.ts @@ -12,6 +12,7 @@ import { } from '@/modules/previews/domain/types' import { Knex } from 'knex' import { SetOptional } from 'type-fest' +import { PreviewStatus } from '@/modules/previews/domain/consts' const ObjectPreview = buildTableHelper('object_preview', [ 'streamId', @@ -53,7 +54,7 @@ export const storeObjectPreviewFactory = streamId, objectId, priority, - previewStatus: 0 + previewStatus: PreviewStatus.PENDING } const sqlQuery = tables.objectPreview(db).insert(insertionObject) diff --git a/packages/server/modules/previews/resultListener.ts b/packages/server/modules/previews/resultListener.ts index 1d3ca199c..aee938df7 100644 --- a/packages/server/modules/previews/resultListener.ts +++ b/packages/server/modules/previews/resultListener.ts @@ -10,6 +10,7 @@ import crypto from 'crypto' import { StorePreview, UpsertObjectPreview } from '@/modules/previews/domain/operations' import { joinImages } from 'join-images' import { GetObjectCommitsWithStreamIds } from '@/modules/core/domain/commits/operations' +import { PreviewPriority, PreviewStatus } from '@/modules/previews/domain/consts' const payloadRegexp = /^([\w\d]+):([\w\d]+):([\w\d]+)$/i @@ -70,8 +71,7 @@ export const consumePreviewResultFactory = }) => { const streamId = projectId const lastUpdate = new Date() - const priority = 0 - const previewStatus = 2 + const priority = PreviewPriority.LOW const log = logger.child({ jobId: previewResult.jobId, status: previewResult.status, @@ -92,7 +92,7 @@ export const consumePreviewResultFactory = lastUpdate, preview: { err: previewResult.reason }, priority, - previewStatus + previewStatus: PreviewStatus.ERROR } }) break @@ -141,7 +141,7 @@ export const consumePreviewResultFactory = lastUpdate, preview, priority, - previewStatus + previewStatus: PreviewStatus.DONE } }) const commits = await getObjectCommitsWithStreamIds([objectId], { diff --git a/packages/server/modules/previews/services/management.ts b/packages/server/modules/previews/services/management.ts index 6a66473bb..cf6b8eb4f 100644 --- a/packages/server/modules/previews/services/management.ts +++ b/packages/server/modules/previews/services/management.ts @@ -13,6 +13,7 @@ import { authorizeResolver, validateScopes } from '@/modules/shared' import { disablePreviews } from '@/modules/shared/helpers/envHelper' import { Roles, Scopes } from '@speckle/shared' import type { Logger } from 'pino' +import { PreviewPriority, PreviewStatus } from '@/modules/previews/domain/consts' const noPreviewImage = require.resolve('#/assets/previews/images/no_preview.png') const previewErrorImage = require.resolve('#/assets/previews/images/preview_error.png') @@ -54,12 +55,16 @@ export const getObjectPreviewBufferOrFilepathFactory = const objPreviewQueued = await deps.createObjectPreview({ streamId, objectId, - priority: 0 + priority: PreviewPriority.LOW }) if (!objPreviewQueued) return { type: 'file', file: noPreviewImage } } - if (!previewInfo || previewInfo.previewStatus !== 2 || !previewInfo.preview) { + if ( + !previewInfo || + previewInfo.previewStatus !== PreviewStatus.DONE || + !previewInfo.preview + ) { return { type: 'file', file: noPreviewImage } } diff --git a/packages/shared/src/core/helpers/utilityTypes.ts b/packages/shared/src/core/helpers/utilityTypes.ts index 92b10f1df..c641bcdb4 100644 --- a/packages/shared/src/core/helpers/utilityTypes.ts +++ b/packages/shared/src/core/helpers/utilityTypes.ts @@ -16,6 +16,8 @@ export type PartialNullable = { [K in keyof T]?: T[K] | null } +export type PartialBy = Omit & Partial> + type NullableKeys = { [K in keyof T]: T[K] extends NonNullable ? never : K }[keyof T]