UNPKG

trifid-handler-fetch

Version:
206 lines (177 loc) 5.79 kB
// @ts-check import { createHash } from 'node:crypto' import { Worker } from 'node:worker_threads' import { performance } from 'node:perf_hooks' import { v4 as uuidv4 } from 'uuid' import { waitForVariableToBeTrue } from './lib/utils.js' /** @type {import('../core/types/index.js').TrifidPlugin} */ export const factory = async (trifid) => { const { config, logger, trifidEvents } = trifid const { contentType, url, baseIri, graphName, unionDefaultGraph } = config const queryLogLevel = config.queryLogLevel || 'debug' if (!logger[queryLogLevel]) { throw Error(`Invalid queryLogLevel: ${queryLogLevel}`) } /** * Log a query, depending on the `queryLogLevel`. * @param {string} msg Message to log * @returns {void} */ const queryLogger = (msg) => logger[queryLogLevel](msg) const queryTimeout = 30000 const workerUrl = new URL('./lib/worker.js', import.meta.url) const worker = new Worker(workerUrl) let ready = false let stopWait = false trifidEvents.on('close', async () => { logger.debug('Got "close" event from Trifid ; closing worker…') await worker.terminate() logger.debug('Worker terminated') }) worker.on('message', async (message) => { const { type, data } = message if (type === 'log') { logger.debug(data) } if (type === 'ready') { if (!data) { logger.error('There was an error in the worker during initialization.') } ready = data stopWait = true } }) worker.on('error', (error) => { ready = false logger.error(`Error from worker: ${error.message}`) }) worker.on('exit', (code) => { ready = false logger.info(`Worker exited with code ${code}`) }) worker.postMessage({ type: 'config', data: { contentType, url, baseIri, graphName, unionDefaultGraph, }, }) /** * Send the query to the worker and wait for the response. * * @param {string} query The SPARQL query * @returns {Promise<{ response: string, contentType: string }>} The response and its content type */ const handleQuery = async (query) => { return new Promise((resolve, reject) => { if (!ready) { return reject(new Error('Worker is not ready')) } const queryId = uuidv4() const timeoutId = setTimeout(() => { worker.off('message', messageHandler) reject(new Error(`Query timed out after ${queryTimeout / 1000} seconds`)) }, queryTimeout) worker.postMessage({ type: 'query', data: { queryId, query, }, }) const messageHandler = (message) => { const { type, data } = message if (type === 'query' && data.queryId === queryId) { clearTimeout(timeoutId) worker.off('message', messageHandler) if (!data.success) { reject(new Error(data.response)) return } resolve(data) } } worker.on('message', messageHandler) }) } // Wait for the worker to become ready, so we can be sure it can handle queries await waitForVariableToBeTrue( () => stopWait, 30000, 20, 'Worker did not become ready within 30 seconds', ) if (!ready) { await worker.terminate() logger.debug('Worker terminated') throw new Error('Worker initialization error') } return { defaultConfiguration: async () => { return { methods: ['GET', 'POST'], paths: ['/query'], } }, routeHandler: async () => { /** * Query string type. * * @typedef {Object} QueryString * @property {string} [query] The SPARQL query. */ /** * Request body type. * @typedef {Object} RequestBody * @property {string} [query] The SPARQL query. */ /** * Route handler. * @param {import('fastify').FastifyRequest<{ Querystring: QueryString, Body: RequestBody}> & { opentelemetry: () => import('@fastify/otel/types/types.d.ts').FastifyOtelRequestContext}} request Request. * @param {import('fastify').FastifyReply} reply Reply. */ const handler = async (request, reply) => { let query const method = request.method if (method === 'GET') { query = request.query.query } else if (method === 'POST') { query = request.body.query if (!query && request.body) { query = request.body if (typeof query !== 'string') { query = JSON.stringify(query) } } } if (!query) { reply.status(400).send('Missing query parameter') return reply } queryLogger(`Received query via ${method}:\n${query}`) if (request.opentelemetry) { const { span } = request.opentelemetry() span.setAttribute('db.system', 'sparql') span.setAttribute('sparql.query.hash', createHash('sha256').update(query).digest('hex')) span.addEvent('sparql.query', { statement: query }) } try { const start = performance.now() const { response, contentType } = await handleQuery(query) const end = performance.now() const duration = end - start reply.type(contentType) reply.header('Server-Timing', `handler-fetch;dur=${duration};desc="Query execution time"`) logger.debug(`Sending the following ${contentType} response:\n${response}`) reply.status(200).send(response) } catch (error) { logger.error(error) reply.status(500).send(error.message) return reply } return reply } return handler }, } } export default factory