Move all email alert logic into webserver, not relying on clients to alert. Manage a single SMTP connection and server gets alerted anyway on all relavent events

This commit is contained in:
mmcwilliams 2023-12-14 16:56:47 -05:00
parent 2316cf7d90
commit f9b69a1b26
3 changed files with 190 additions and 1 deletions

95
dist/index.js vendored
View File

@ -39,7 +39,9 @@ const multer_1 = __importDefault(require("multer"));
const uuid_1 = require("uuid"); const uuid_1 = require("uuid");
const mime_1 = require("mime"); const mime_1 = require("mime");
const log_1 = require("./log"); const log_1 = require("./log");
const mail_1 = require("./mail");
const Handlebars = __importStar(require("handlebars")); const Handlebars = __importStar(require("handlebars"));
const yoloWebUrl = typeof process.env['YOLO_WEB_URL'] !== 'undefined' ? process.env['YOLO_WEB_URL'] : 'http://localhost:3333';
const port = typeof process.env['PORT'] !== 'undefined' ? parseInt(process.env['PORT'], 10) : 3333; const port = typeof process.env['PORT'] !== 'undefined' ? parseInt(process.env['PORT'], 10) : 3333;
const data = typeof process.env['DATADIR'] !== 'undefined' ? process.env['DATADIR'] : './data'; const data = typeof process.env['DATADIR'] !== 'undefined' ? process.env['DATADIR'] : './data';
const dbPath = (0, path_1.join)(data, 'queue.sqlite'); const dbPath = (0, path_1.join)(data, 'queue.sqlite');
@ -220,6 +222,19 @@ async function claim(id) {
}); });
}); });
} }
async function get(id) {
const query = `SELECT * FROM queue WHERE id = ? LIMIT 1;`;
return new Promise((resolve, reject) => {
return db.all(query, [id], (err, rows) => {
if (err)
return reject(err);
if (rows.length < 1) {
return reject(new Error(`Job ${id} does not exist`));
}
return resolve(rows[0]);
});
});
}
async function fail(id, meta) { async function fail(id, meta) {
const query = `UPDATE queue SET failed = CURRENT_TIMESTAMP, meta = ? WHERE id = ?;`; const query = `UPDATE queue SET failed = CURRENT_TIMESTAMP, meta = ? WHERE id = ?;`;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -266,6 +281,52 @@ function annotate(row) {
} }
return row; return row;
} }
async function alertClaimed(id, model, name, email) {
const subject = `Training ${name} started`;
const body = `<div><h3>Your ${model} training job "${name}" has started!</h3>
<br />
<div>Status is available here: <a href="${yoloWebUrl}/job/${id}">${yoloWebUrl}/job/${id}</a><div>
<br />
<div>You will receive an email when the training is complete.</div>
</div>`;
try {
await (0, mail_1.sendMail)(email, subject, body);
}
catch (err) {
log.error('Error sending mail');
log.error(err);
}
}
async function alertFailed(id, model, name, email) {
const subject = `Training ${name} failed`;
const body = `<div><h3>Your ${model} training job "${name}" has failed :(</h3>
<br />
<div>Additional information is available here: <a href="${yoloWebUrl}/job/${id}">${yoloWebUrl}/job/${id}</a><div>
<br />
<div>Please contact the administrator for more information</div>
</div>`;
try {
await (0, mail_1.sendMail)(email, subject, body);
}
catch (err) {
log.error('Error sending mail');
log.error(err);
}
}
async function alertCompleted(id, model, name, email) {
const subject = `Training ${name} completed`;
const body = `<div><h3>Your ${model} training job "${name}" has completed!</h3>
<br />
<div>The model is available for download here: <a href="${yoloWebUrl}/model/${id}">${yoloWebUrl}/model/${id}</a><div>
</div>`;
try {
await (0, mail_1.sendMail)(email, subject, body);
}
catch (err) {
log.error('Error sending mail');
log.error(err);
}
}
app.get('/', async (req, res, next) => { app.get('/', async (req, res, next) => {
let html; let html;
let rows; let rows;
@ -341,6 +402,7 @@ app.post('/job/:id', uploadOnnx.single('model'), async (req, res, next) => {
let filePath; let filePath;
let meta = null; let meta = null;
let id; let id;
let jobObj;
req.setTimeout(0); req.setTimeout(0);
if (typeof req.file === 'undefined' || req.file === null) { if (typeof req.file === 'undefined' || req.file === null) {
log.error('No file in upload'); log.error('No file in upload');
@ -372,6 +434,19 @@ app.post('/job/:id', uploadOnnx.single('model'), async (req, res, next) => {
log.error(err); log.error(err);
return next(`Error completing training job ${id}`); return next(`Error completing training job ${id}`);
} }
try {
jobObj = await get(id);
}
catch (err) {
log.error('Error getting job for alertCompleted');
log.error(err);
}
try {
await alertCompleted(id, jobObj.model, jobObj.name, jobObj.email);
}
catch (err) {
log.error('Error sending alertCompleted email');
}
res.json({ id }); res.json({ id });
}); });
app.get('/job/:id', async (req, res, next) => { app.get('/job/:id', async (req, res, next) => {
@ -508,6 +583,12 @@ app.post('/job/claim/:id', async (req, res, next) => {
log.error(err); log.error(err);
return next('Error claiming job'); return next('Error claiming job');
} }
try {
await alertClaimed(id, jobObj.model, jobObj.name, jobObj.email);
}
catch (err) {
log.error(err);
}
resObj.id = id; resObj.id = id;
resObj.path = `/dataset/${id}`; resObj.path = `/dataset/${id}`;
resObj.dataset = jobObj.dataset; resObj.dataset = jobObj.dataset;
@ -519,6 +600,7 @@ app.post('/job/claim/:id', async (req, res, next) => {
app.post('/job/fail/:id', async (req, res, next) => { app.post('/job/fail/:id', async (req, res, next) => {
let id; let id;
let meta = null; let meta = null;
let jobObj;
if (typeof req.params.id === 'undefined' || req.params.id === null) { if (typeof req.params.id === 'undefined' || req.params.id === null) {
log.error(`No dataset id provided`); log.error(`No dataset id provided`);
return next('Invalid request'); return next('Invalid request');
@ -535,6 +617,19 @@ app.post('/job/fail/:id', async (req, res, next) => {
log.error(err); log.error(err);
return next('Error failing job'); return next('Error failing job');
} }
try {
jobObj = await get(id);
}
catch (err) {
log.error('Error getting job for alertFailed');
log.error(err);
}
try {
await alertFailed(id, jobObj.model, jobObj.name, jobObj.email);
}
catch (err) {
log.error('Error sending alertFailed email');
}
res.json(true); res.json(true);
}); });
app.listen(port, async () => { app.listen(port, async () => {

2
dist/index.js.map vendored

File diff suppressed because one or more lines are too long

View File

@ -16,6 +16,7 @@ import { sendMail } from './mail';
import type { Logger } from 'winston'; import type { Logger } from 'winston';
import * as Handlebars from 'handlebars'; import * as Handlebars from 'handlebars';
const yoloWebUrl : string = typeof process.env['YOLO_WEB_URL'] !== 'undefined' ? process.env['YOLO_WEB_URL'] : 'http://localhost:3333';
const port : number = typeof process.env['PORT'] !== 'undefined' ? parseInt(process.env['PORT'], 10) : 3333; const port : number = typeof process.env['PORT'] !== 'undefined' ? parseInt(process.env['PORT'], 10) : 3333;
const data : string = typeof process.env['DATADIR'] !== 'undefined' ? process.env['DATADIR'] : './data'; const data : string = typeof process.env['DATADIR'] !== 'undefined' ? process.env['DATADIR'] : './data';
const dbPath : string = join(data, 'queue.sqlite'); const dbPath : string = join(data, 'queue.sqlite');
@ -199,6 +200,19 @@ async function claim (id : string) : Promise<string> {
}); });
} }
async function get (id : string) : Promise<string> {
const query : string = `SELECT * FROM queue WHERE id = ? LIMIT 1;`;
return new Promise((resolve : Function, reject : Function) => {
return db.all(query, [id], (err : Error, rows : any) => {
if (err) return reject(err);
if (rows.length < 1) {
return reject(new Error(`Job ${id} does not exist`));
}
return resolve(rows[0]);
});
});
}
async function fail (id : string, meta : string | null) : Promise<boolean> { async function fail (id : string, meta : string | null) : Promise<boolean> {
const query : string = `UPDATE queue SET failed = CURRENT_TIMESTAMP, meta = ? WHERE id = ?;`; const query : string = `UPDATE queue SET failed = CURRENT_TIMESTAMP, meta = ? WHERE id = ?;`;
return new Promise((resolve : Function, reject : Function) => { return new Promise((resolve : Function, reject : Function) => {
@ -243,6 +257,52 @@ function annotate (row : any) {
return row; return row;
} }
async function alertClaimed (id : string, model : string, name : string, email : string) {
const subject : string = `Training ${name} started`;
const body : string = `<div><h3>Your ${model} training job "${name}" has started!</h3>
<br />
<div>Status is available here: <a href="${yoloWebUrl}/job/${id}">${yoloWebUrl}/job/${id}</a><div>
<br />
<div>You will receive an email when the training is complete.</div>
</div>`
try {
await sendMail(email, subject, body);
} catch (err) {
log.error('Error sending mail');
log.error(err);
}
}
async function alertFailed (id : string, model : string, name : string, email : string) {
const subject : string = `Training ${name} failed`;
const body : string = `<div><h3>Your ${model} training job "${name}" has failed :(</h3>
<br />
<div>Additional information is available here: <a href="${yoloWebUrl}/job/${id}">${yoloWebUrl}/job/${id}</a><div>
<br />
<div>Please contact the administrator for more information</div>
</div>`
try {
await sendMail(email, subject, body);
} catch (err) {
log.error('Error sending mail');
log.error(err);
}
}
async function alertCompleted (id : string, model : string, name : string, email : string) {
const subject : string = `Training ${name} completed`;
const body : string = `<div><h3>Your ${model} training job "${name}" has completed!</h3>
<br />
<div>The model is available for download here: <a href="${yoloWebUrl}/model/${id}">${yoloWebUrl}/model/${id}</a><div>
</div>`
try {
await sendMail(email, subject, body);
} catch (err) {
log.error('Error sending mail');
log.error(err);
}
}
app.get('/', async (req : Request, res : Response, next : NextFunction) => { app.get('/', async (req : Request, res : Response, next : NextFunction) => {
let html : string; let html : string;
let rows : any[]; let rows : any[];
@ -321,6 +381,7 @@ app.post('/job/:id', uploadOnnx.single('model'), async (req : Request, res : Res
let filePath : string; let filePath : string;
let meta : string = null; let meta : string = null;
let id : string; let id : string;
let jobObj : any;
req.setTimeout(0); req.setTimeout(0);
@ -356,6 +417,19 @@ app.post('/job/:id', uploadOnnx.single('model'), async (req : Request, res : Res
return next(`Error completing training job ${id}`); return next(`Error completing training job ${id}`);
} }
try {
jobObj = await get(id);
} catch (err) {
log.error('Error getting job for alertCompleted');
log.error(err);
}
try {
await alertCompleted(id, jobObj.model, jobObj.name, jobObj.email);
} catch (err) {
log.error('Error sending alertCompleted email');
}
res.json({ id }); res.json({ id });
}); });
@ -513,6 +587,12 @@ app.post('/job/claim/:id', async (req : Request, res: Response, next : NextFunct
return next('Error claiming job'); return next('Error claiming job');
} }
try {
await alertClaimed(id, jobObj.model, jobObj.name, jobObj.email);
} catch (err) {
log.error(err);
}
resObj.id = id; resObj.id = id;
resObj.path = `/dataset/${id}`; resObj.path = `/dataset/${id}`;
resObj.dataset = jobObj.dataset; resObj.dataset = jobObj.dataset;
@ -526,6 +606,7 @@ app.post('/job/claim/:id', async (req : Request, res: Response, next : NextFunct
app.post('/job/fail/:id', async (req : Request, res: Response, next : NextFunction) => { app.post('/job/fail/:id', async (req : Request, res: Response, next : NextFunction) => {
let id : string; let id : string;
let meta : string = null; let meta : string = null;
let jobObj : any;
if (typeof req.params.id === 'undefined' || req.params.id === null) { if (typeof req.params.id === 'undefined' || req.params.id === null) {
log.error(`No dataset id provided`); log.error(`No dataset id provided`);
@ -545,6 +626,19 @@ app.post('/job/fail/:id', async (req : Request, res: Response, next : NextFuncti
return next('Error failing job'); return next('Error failing job');
} }
try {
jobObj = await get(id);
} catch (err) {
log.error('Error getting job for alertFailed');
log.error(err);
}
try {
await alertFailed(id, jobObj.model, jobObj.name, jobObj.email);
} catch (err) {
log.error('Error sending alertFailed email');
}
res.json(true); res.json(true);
}); });