payload
Version:
Node, React, Headless CMS and Application Framework built on Next.js
275 lines (274 loc) • 9.52 kB
JavaScript
// @ts-strict-ignore
import { Forbidden } from '../../../errors/Forbidden.js';
import isolateObjectProperty from '../../../utilities/isolateObjectProperty.js';
import { jobsCollectionSlug } from '../../config/index.js';
import { updateJob, updateJobs } from '../../utilities/updateJob.js';
import { getUpdateJobFunction } from './runJob/getUpdateJobFunction.js';
import { importHandlerPath } from './runJob/importHandlerPath.js';
import { runJob } from './runJob/index.js';
import { runJSONJob } from './runJSONJob/index.js';
export const runJobs = async (args)=>{
const { id, limit = 10, overrideAccess, processingOrder, queue, req, sequential, where: whereFromProps } = args;
if (!overrideAccess) {
const hasAccess = await req.payload.config.jobs.access.run({
req
});
if (!hasAccess) {
throw new Forbidden(req.t);
}
}
const where = {
and: [
{
completedAt: {
exists: false
}
},
{
hasError: {
not_equals: true
}
},
{
processing: {
equals: false
}
},
{
or: [
{
waitUntil: {
exists: false
}
},
{
waitUntil: {
less_than: new Date().toISOString()
}
}
]
}
]
};
if (queue) {
where.and.push({
queue: {
equals: queue
}
});
}
if (whereFromProps) {
where.and.push(whereFromProps);
}
// Find all jobs and ensure we set job to processing: true as early as possible to reduce the chance of
// the same job being picked up by another worker
const jobsQuery = {
docs: []
};
if (id) {
// Only one job to run
jobsQuery.docs = [
await updateJob({
id,
data: {
processing: true
},
depth: req.payload.config.jobs.depth,
disableTransaction: true,
req,
returning: true
})
];
} else {
let defaultProcessingOrder = req.payload.collections[jobsCollectionSlug].config.defaultSort ?? 'createdAt';
const processingOrderConfig = req.payload.config.jobs?.processingOrder;
if (typeof processingOrderConfig === 'function') {
defaultProcessingOrder = await processingOrderConfig(args);
} else if (typeof processingOrderConfig === 'object' && !Array.isArray(processingOrderConfig)) {
if (queue && processingOrderConfig.queues && processingOrderConfig.queues[queue]) {
defaultProcessingOrder = processingOrderConfig.queues[queue];
} else if (processingOrderConfig.default) {
defaultProcessingOrder = processingOrderConfig.default;
}
} else if (typeof processingOrderConfig === 'string') {
defaultProcessingOrder = processingOrderConfig;
}
const updatedDocs = await updateJobs({
data: {
processing: true
},
depth: req.payload.config.jobs.depth,
disableTransaction: true,
limit,
req,
returning: true,
sort: processingOrder ?? defaultProcessingOrder,
where
});
if (updatedDocs) {
jobsQuery.docs = updatedDocs;
}
}
/**
* Just for logging purposes, we want to know how many jobs are new and how many are existing (= already been tried).
* This is only for logs - in the end we still want to run all jobs, regardless of whether they are new or existing.
*/ const { existingJobs, newJobs } = jobsQuery.docs.reduce((acc, job)=>{
if (job.totalTried > 0) {
acc.existingJobs.push(job);
} else {
acc.newJobs.push(job);
}
return acc;
}, {
existingJobs: [],
newJobs: []
});
if (!jobsQuery.docs.length) {
return {
noJobsRemaining: true,
remainingJobsFromQueried: 0
};
}
if (jobsQuery?.docs?.length) {
req.payload.logger.info({
msg: `Running ${jobsQuery.docs.length} jobs.`,
new: newJobs?.length,
retrying: existingJobs?.length
});
}
const jobsToDelete = req.payload.config.jobs.deleteJobOnComplete ? [] : undefined;
const runSingleJob = async (job)=>{
if (!job.workflowSlug && !job.taskSlug) {
throw new Error('Job must have either a workflowSlug or a taskSlug');
}
const jobReq = isolateObjectProperty(req, 'transactionID');
const workflowConfig = job.workflowSlug ? req.payload.config.jobs?.workflows.find(({ slug })=>slug === job.workflowSlug) : {
slug: 'singleTask',
handler: async ({ job, tasks })=>{
await tasks[job.taskSlug]('1', {
input: job.input
});
}
};
if (!workflowConfig) {
return null // Skip jobs with no workflow configuration
;
}
const updateJob = getUpdateJobFunction(job, jobReq);
// the runner will either be passed to the config
// OR it will be a path, which we will need to import via eval to avoid
// Next.js compiler dynamic import expression errors
let workflowHandler;
if (typeof workflowConfig.handler === 'function' || typeof workflowConfig.handler === 'object' && Array.isArray(workflowConfig.handler)) {
workflowHandler = workflowConfig.handler;
} else {
workflowHandler = await importHandlerPath(workflowConfig.handler);
if (!workflowHandler) {
const jobLabel = job.workflowSlug || `Task: ${job.taskSlug}`;
const errorMessage = `Can't find runner while importing with the path ${workflowConfig.handler} in job type ${jobLabel}.`;
req.payload.logger.error(errorMessage);
await updateJob({
error: {
error: errorMessage
},
hasError: true,
processing: false
});
return;
}
}
if (typeof workflowHandler === 'function') {
const result = await runJob({
job,
req: jobReq,
updateJob,
workflowConfig,
workflowHandler
});
if (result.status !== 'error' && jobsToDelete) {
jobsToDelete.push(job.id);
}
return {
id: job.id,
result
};
} else {
const result = await runJSONJob({
job,
req: jobReq,
updateJob,
workflowConfig,
workflowHandler
});
if (result.status !== 'error' && jobsToDelete) {
jobsToDelete.push(job.id);
}
return {
id: job.id,
result
};
}
};
let resultsArray = [];
if (sequential) {
for (const job of jobsQuery.docs){
const result = await runSingleJob(job);
if (result !== null) {
resultsArray.push(result);
}
}
} else {
const jobPromises = jobsQuery.docs.map(runSingleJob);
resultsArray = await Promise.all(jobPromises);
}
if (jobsToDelete && jobsToDelete.length > 0) {
try {
if (req.payload.config.jobs.runHooks) {
await req.payload.delete({
collection: jobsCollectionSlug,
depth: 0,
disableTransaction: true,
where: {
id: {
in: jobsToDelete
}
}
});
} else {
await req.payload.db.deleteMany({
collection: jobsCollectionSlug,
where: {
id: {
in: jobsToDelete
}
}
});
}
} catch (err) {
req.payload.logger.error({
err,
msg: `failed to delete jobs ${jobsToDelete.join(', ')} on complete`
});
}
}
const resultsObject = resultsArray.reduce((acc, cur)=>{
if (cur !== null) {
// Check if there's a valid result to include
acc[cur.id] = cur.result;
}
return acc;
}, {});
let remainingJobsFromQueried = 0;
for(const jobID in resultsObject){
const jobResult = resultsObject[jobID];
if (jobResult.status === 'error') {
remainingJobsFromQueried++ // Can be retried
;
}
}
return {
jobStatus: resultsObject,
remainingJobsFromQueried
};
};
//# sourceMappingURL=index.js.map