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:
parent
2316cf7d90
commit
f9b69a1b26
|
@ -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 () => {
|
||||||
|
|
File diff suppressed because one or more lines are too long
94
src/index.ts
94
src/index.ts
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue