-
Notifications
You must be signed in to change notification settings - Fork 38
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'staging' of https://github.com/3drepo/3drepo.io into IS…
…SUE_5173
- Loading branch information
Showing
10 changed files
with
379 additions
and
31 deletions.
There are no files selected for viewing
100 changes: 100 additions & 0 deletions
100
backend/src/scripts/utility/modelProcessing/detectZombieProcessing.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
/** | ||
* Copyright (C) 2024 3D Repo Ltd | ||
* | ||
* This program is free software: you can redistribute it and/or modify | ||
* it under the terms of the GNU Affero General Public License as | ||
* published by the Free Software Foundation, either version 3 of the | ||
* License, or (at your option) any later version. | ||
* | ||
* This program is distributed in the hope that it will be useful, | ||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
* GNU Affero General Public License for more details. | ||
* | ||
* You should have received a copy of the GNU Affero General Public License | ||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
*/ | ||
|
||
/** | ||
* This script is used to check the processing status of models/drawings. | ||
* Processing status should be 'ok' or 'failed'. | ||
* The utility script `resetProcessingFlag` can be used to reset zombie statuses for models. | ||
*/ | ||
|
||
const { v5Path } = require('../../../interop'); | ||
|
||
const { logger } = require(`${v5Path}/utils/logger`); | ||
const { getTeamspaceList } = require('../../utils'); | ||
|
||
const { find } = require(`${v5Path}/handler/db`); | ||
const { SETTINGS_COL, processStatuses } = require(`${v5Path}/models/modelSettings.constants`); | ||
const { DRAWINGS_HISTORY_COL } = require(`${v5Path}/models/revisions.constants`); | ||
const { sendSystemEmail } = require(`${v5Path}/services/mailer`); | ||
const { templates: emailTemplates } = require(`${v5Path}/services/mailer/mailer.constants`); | ||
const { UUIDToString } = require(`${v5Path}/utils/helper/uuids`); | ||
const Path = require('path'); | ||
|
||
let TIME_LIMIT = 24 * 60 * 60 * 1000; // hours * 1 hour in ms | ||
|
||
const processTeamspace = async (teamspace) => { | ||
const expiredTimestamp = new Date(new Date() - TIME_LIMIT); | ||
const zombieQuery = { | ||
status: { $exists: true, $not: { $regex: `(${processStatuses.OK})|(${processStatuses.FAILED})` } }, | ||
timestamp: { $lt: expiredTimestamp }, | ||
}; | ||
|
||
logger.logInfo(`\t-${teamspace}`); | ||
|
||
const zombieModels = await find(teamspace, SETTINGS_COL, zombieQuery, { status: 1, timestamp: 1 }); | ||
const zombieDrawings = await find(teamspace, DRAWINGS_HISTORY_COL, zombieQuery, { status: 1, timestamp: 1 }); | ||
|
||
return [ | ||
...zombieModels.map(({ _id, status, timestamp }) => `${teamspace}, model, ${_id}, ${status}, ${timestamp}`), | ||
...zombieDrawings.map(({ _id, status, timestamp }) => `${teamspace}, drawing, ${UUIDToString(_id)}, ${status}, ${timestamp}`), | ||
]; | ||
}; | ||
|
||
const run = async (teamspace, limit, notify) => { | ||
logger.logInfo(`Check processing flag(s) in ${teamspace ?? 'all teamspaces'}`); | ||
|
||
if (limit) { | ||
TIME_LIMIT = limit * 60 * 60 * 1000; | ||
} | ||
|
||
const teamspaces = teamspace ? [teamspace] : await getTeamspaceList(); | ||
const results = (await Promise.all(teamspaces.map((ts) => processTeamspace(ts)))).flat(); | ||
|
||
if (notify && results.length > 0) { | ||
logger.logInfo(`Zombie processing statuses found: ${results.length}`); | ||
const data = { | ||
script: Path.basename(__filename, Path.extname(__filename)), | ||
title: 'Zombie processing statuses found', | ||
message: `${results.length} zombie processing statuses found`, | ||
logExcerpt: JSON.stringify(results), | ||
}; | ||
await sendSystemEmail(emailTemplates.ZOMBIE_PROCESSING_STATUSES.name, data); | ||
} | ||
}; | ||
|
||
const genYargs = /* istanbul ignore next */(yargs) => { | ||
const commandName = Path.basename(__filename, Path.extname(__filename)); | ||
const argsSpec = (subYargs) => subYargs.option('teamspace', { | ||
describe: 'Target a specific teamspace (if unspecified, all teamspaces will be targetted)', | ||
type: 'string', | ||
}).option('limit', { | ||
describe: 'Time limit (hours, default: 24) where models/drawings may still be processing', | ||
type: 'number', | ||
}).option('notify', { | ||
describe: 'Send e-mail notification if results are found (default: false)', | ||
type: 'boolean', | ||
}); | ||
return yargs.command(commandName, | ||
'Checks the processing status of models/drawings.', | ||
argsSpec, | ||
(argv) => run(argv.teamspace, argv.limit, argv.notify)); | ||
}; | ||
|
||
module.exports = { | ||
run, | ||
genYargs, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
6 changes: 6 additions & 0 deletions
6
backend/src/v5/services/mailer/templates/html/zombieProcessingStatuses.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
<p><%= message %> on <%= domain %><p> | ||
<p><b>Log:</b></p> | ||
<p> | ||
<%-logExcerpt-%> | ||
</p> | ||
|
41 changes: 41 additions & 0 deletions
41
backend/src/v5/services/mailer/templates/zombieProcessingStatuses.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
/** | ||
* Copyright (C) 2024 3D Repo Ltd | ||
* | ||
* This program is free software: you can redistribute it and/or modify | ||
* it under the terms of the GNU Affero General Public License as | ||
* published by the Free Software Foundation, either version 3 of the | ||
* License, or (at your option) any later version. | ||
* | ||
* This program is distributed in the hope that it will be useful, | ||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
* GNU Affero General Public License for more details. | ||
* | ||
* You should have received a copy of the GNU Affero General Public License | ||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
*/ | ||
|
||
const Yup = require('yup'); | ||
const config = require('../../../utils/config'); | ||
const { generateTemplateFn } = require('./common'); | ||
|
||
const TEMPLATE_PATH = `${__dirname}/html/zombieProcessingStatuses.html`; | ||
|
||
const dataSchema = Yup.object({ | ||
script: Yup.string().default(''), | ||
title: Yup.string().default('Zombie Processing Statuses'), | ||
domain: Yup.string().default(() => config.getBaseURL()), | ||
logExcerpt: Yup.string().default('No logs found.').transform( | ||
(val) => val.replaceAll('<', '<').replaceAll('>', '>').replace(/(\r\n|\n|\r)/gm, '<br>')), | ||
|
||
}).required(true); | ||
|
||
const ZombieProcessingStatusesTemplate = {}; | ||
ZombieProcessingStatusesTemplate.subject = (data) => { | ||
const { domain, title, script } = dataSchema.cast(data); | ||
return `[${domain}][${script}] ${title}`; | ||
}; | ||
|
||
ZombieProcessingStatusesTemplate.html = generateTemplateFn(dataSchema, TEMPLATE_PATH); | ||
|
||
module.exports = ZombieProcessingStatusesTemplate; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
131 changes: 131 additions & 0 deletions
131
backend/tests/v5/scripts/modelProcessing/detectZombieProcessing.test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
/** | ||
* Copyright (C) 2024 3D Repo Ltd | ||
* | ||
* This program is free software: you can redistribute it and/or modify | ||
* it under the terms of the GNU Affero General Public License as | ||
* published by the Free Software Foundation, either version 3 of the | ||
* License, or (at your option) any later version. | ||
* | ||
* This program is distributed in the hope that it will be useful, | ||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
* GNU Affero General Public License for more details. | ||
* | ||
* You should have received a copy of the GNU Affero General Public License | ||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
*/ | ||
|
||
const { | ||
determineTestGroup, | ||
db: { reset: resetDB, createModel, createRevision }, | ||
generateRandomString, | ||
generateRandomModel, | ||
generateRevisionEntry, | ||
} = require('../../helper/services'); | ||
const { times } = require('lodash'); | ||
const { utilScripts, src } = require('../../helper/path'); | ||
|
||
const { modelTypes, processStatuses } = require(`${src}/models/modelSettings.constants`); | ||
const { deleteIfUndefined } = require(`${src}/utils/helper/objects`); | ||
const { disconnect } = require(`${src}/handler/db`); | ||
const { templates: emailTemplates } = require(`${src}/services/mailer/mailer.constants`); | ||
|
||
jest.mock('../../../../src/v5/services/mailer'); | ||
const Mailer = require(`${src}/services/mailer`); | ||
|
||
const DetectZombieProcessing = require(`${utilScripts}/modelProcessing/detectZombieProcessing`); | ||
const Path = require('path'); | ||
|
||
const modelStates = Object.values(processStatuses); | ||
|
||
const recentDate = new Date((new Date()) - 36 * 60 * 60 * 1000); | ||
|
||
const setupData = () => { | ||
const modelProms = times(2, async () => { | ||
const teamspace = generateRandomString(); | ||
const models = await Promise.all(times(modelStates.length, async (n) => { | ||
const { _id, name, properties } = generateRandomModel({ | ||
properties: deleteIfUndefined({ status: modelStates[n], timestamp: recentDate }), | ||
}); | ||
await createModel(teamspace, _id, name, properties); | ||
return { model: _id, status: modelStates[n] }; | ||
})); | ||
const drawings = await Promise.all(times(modelStates.length, async (n) => { | ||
const project = generateRandomString(); | ||
const revision = generateRevisionEntry(false, false, modelTypes.DRAWING, recentDate, modelStates[n]); | ||
await createRevision(teamspace, project, revision._id, revision, modelTypes.DRAWING); | ||
return { drawing: revision._id, status: modelStates[n] }; | ||
})); | ||
|
||
return { teamspace, models, drawings }; | ||
}); | ||
return Promise.all(modelProms); | ||
}; | ||
|
||
const checkMail = (data, filteredTeamspace) => { | ||
const expectedLogExcerpt = data.map(({ teamspace, models, drawings }) => { | ||
if (!filteredTeamspace || teamspace === filteredTeamspace) { | ||
const expectedModels = models.map(({ model, status }) => ( | ||
(status !== processStatuses.OK && status !== processStatuses.FAILED) | ||
? `${teamspace}, model, ${model}, ${status}, ${recentDate}` : '')); | ||
const expectedDrawings = drawings.map(({ drawing, status }) => ( | ||
(status !== processStatuses.OK && status !== processStatuses.FAILED) | ||
? `${teamspace}, drawing, ${drawing}, ${status}, ${recentDate}` : '')); | ||
return [...expectedModels, ...expectedDrawings]; | ||
} | ||
return undefined; | ||
}).flat().filter(Boolean); | ||
const expectedData = { | ||
script: Path.basename(__filename, Path.extname(__filename)).replace(/\.test/, ''), | ||
title: 'Zombie processing statuses found', | ||
message: `${expectedLogExcerpt.length} zombie processing statuses found`, | ||
}; | ||
expect(Mailer.sendSystemEmail).toHaveBeenCalledWith( | ||
emailTemplates.ZOMBIE_PROCESSING_STATUSES.name, | ||
expect.objectContaining(expectedData), | ||
); | ||
const actualLogExcerpt = JSON.parse(Mailer.sendSystemEmail.mock.calls[0][1].logExcerpt); | ||
expect(actualLogExcerpt).toEqual(expect.arrayContaining(expectedLogExcerpt)); | ||
}; | ||
|
||
const runTest = () => { | ||
describe('Detect zombie processing', () => { | ||
let data; | ||
beforeEach(async () => { | ||
await resetDB(); | ||
data = await setupData(); | ||
}); | ||
|
||
test('Should do nothing if notify is not set', async () => { | ||
await DetectZombieProcessing.run(); | ||
expect(Mailer.sendSystemEmail).toHaveBeenCalledTimes(0); | ||
}); | ||
|
||
test('Should send system mail if notify is set', async () => { | ||
await DetectZombieProcessing.run(undefined, undefined, true); | ||
expect(Mailer.sendSystemEmail).toHaveBeenCalledTimes(1); | ||
checkMail(data); | ||
}); | ||
|
||
test('Should send system mail if the predefined teamspace exists', async () => { | ||
await DetectZombieProcessing.run(data[0].teamspace, undefined, true); | ||
expect(Mailer.sendSystemEmail).toHaveBeenCalledTimes(1); | ||
checkMail(data, data[0].teamspace); | ||
}); | ||
|
||
test('Should do nothing if teamspace is not found', async () => { | ||
await DetectZombieProcessing.run(generateRandomString(), undefined, true); | ||
expect(Mailer.sendSystemEmail).toHaveBeenCalledTimes(0); | ||
}); | ||
|
||
test('Should do nothing if time limit is extended', async () => { | ||
await DetectZombieProcessing.run(undefined, 48 * 60 * 60 * 1000, true); | ||
expect(Mailer.sendSystemEmail).toHaveBeenCalledTimes(0); | ||
}); | ||
}); | ||
}; | ||
|
||
describe(determineTestGroup(__filename), () => { | ||
runTest(); | ||
afterAll(disconnect); | ||
}); |
51 changes: 51 additions & 0 deletions
51
backend/tests/v5/unit/services/mailer/templates/zombieProcessingStatuses.test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
/** | ||
* Copyright (C) 2024 3D Repo Ltd | ||
* | ||
* This program is free software: you can redistribute it and/or modify | ||
* it under the terms of the GNU Affero General Public License as | ||
* published by the Free Software Foundation, either version 3 of the | ||
* License, or (at your option) any later version. | ||
* | ||
* This program is distributed in the hope that it will be useful, | ||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
* GNU Affero General Public License for more details. | ||
* | ||
* You should have received a copy of the GNU Affero General Public License | ||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
*/ | ||
|
||
const { src } = require('../../../../helper/path'); | ||
const { determineTestGroup, generateRandomString } = require('../../../../helper/services'); | ||
const isHtml = require('is-html-content'); | ||
|
||
const ZombieProcessingStatuses = require(`${src}/services/mailer/templates/zombieProcessingStatuses`); | ||
|
||
const testHtml = () => { | ||
describe('get template html', () => { | ||
test('should get zombieProcessingStatuses template html', async () => { | ||
const res = await ZombieProcessingStatuses.html({ | ||
message: generateRandomString(), | ||
domain: generateRandomString(), | ||
logExcerpt: generateRandomString(), | ||
}); | ||
expect(isHtml(res)).toEqual(true); | ||
}); | ||
}); | ||
}; | ||
|
||
const testSubject = () => { | ||
describe.each([ | ||
['data object is empty', {}], | ||
['data object is not empty', { domain: generateRandomString(), title: generateRandomString(), script: generateRandomString() }], | ||
])('get subject', (desc, data) => { | ||
test(`should succeed if ${desc}`, () => { | ||
expect(ZombieProcessingStatuses.subject(data).length).not.toEqual(0); | ||
}); | ||
}); | ||
}; | ||
|
||
describe(determineTestGroup(__filename), () => { | ||
testHtml(); | ||
testSubject(); | ||
}); |
Oops, something went wrong.