Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(datatrak): RN-1398: Setup task scheduler #5841

Merged
merged 7 commits into from
Aug 15, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/central-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"build-dev": "npm run build",
"lint": "yarn package:lint",
"lint:fix": "yarn lint --fix",
"scheduled-task": "babel-node ./scripts/scheduledTask.js --config-file '../../babel.config.json'",
"start": "node dist",
"start-dev": "yarn package:start:backend-start-dev 9999",
"start-verbose": "LOG_LEVEL=debug yarn start-dev",
Expand Down Expand Up @@ -47,6 +48,7 @@
"compare-versions": "^6.1.0",
"cors": "^2.8.5",
"countrynames": "^0.1.1",
"date-fns": "^2.29.2",
"del": "^2.2.2",
"express": "^4.19.2",
"form-data": "^2.3.3",
Expand All @@ -62,6 +64,7 @@
"moment-timezone": "^0.5.45",
"morgan": "^1.9.0",
"multer": "^1.4.3",
"node-schedule": "^2.1.1",
"public-ip": "^2.5.0",
"react-autobind": "^1.0.6",
"react-native-uuid": "^1.4.9",
Expand All @@ -72,6 +75,7 @@
"xlsx": "^0.10.9"
},
"devDependencies": {
"@babel/node": "^7.10.5",
"chai": "^4.1.2",
"chai-as-promised": "^7.1.1",
"chai-subset": "^1.6.0",
Expand Down
48 changes: 48 additions & 0 deletions packages/central-server/scripts/scheduledTask.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Tupaia
* Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
*/
import '@babel/polyfill';
import { configureEnv } from '../src/configureEnv';
import { ModelRegistry, TupaiaDatabase } from '@tupaia/database';
import winston from '../src/log';
import { TaskOverdueChecker } from '../src/scheduledTasks';
import * as modelClasses from '../src/database/models';

const TASKS = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is picky but I can just see us getting confused between tasks and schedules tasks. Perhaps we can call these scheduled jobs/events instead so the distinction is clear?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep good point

TaskOverdueChecker,
};

configureEnv();

const getTaskArg = argv => {
const taskAgr = argv[4];
if (!taskAgr || !Object.keys(TASKS).find(t => t === taskAgr)) {
const availableOptions = Object.keys(TASKS).join(', ');
throw new Error(`You need to specify one of the following tasks to run: ${availableOptions}`);
}

return argv[4];
};

(async () => {
const database = new TupaiaDatabase();
try {
winston.info('Starting scheduled task script');
const start = Date.now();
const taskArg = getTaskArg(process.argv);
const taskKey = Object.keys(TASKS).find(t => t === taskArg);
const taskModule = taskKey && TASKS[taskKey];
winston.info(`Running ${taskArg} module`);
const models = new ModelRegistry(database, modelClasses, true);
const taskInstance = new taskModule(models);
await taskInstance.run();
const end = Date.now();
winston.info(`Completed in ${end - start}ms`);
} catch (error) {
winston.error(error.message);
winston.error(error.stack);
} finally {
await database.closeConnections();
}
})();
1 change: 0 additions & 1 deletion packages/central-server/src/createApp.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { buildBasicBearerAuthMiddleware } from '@tupaia/server-boilerplate';
import { handleError } from './apiV2/middleware';

import { apiV2 } from './apiV2';

/**
* Set up express server with middleware,
*/
Expand Down
15 changes: 14 additions & 1 deletion packages/central-server/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import '@babel/polyfill';
import http from 'http';
import nodeSchedule from 'node-schedule';
import {
AnalyticsRefresher,
EntityHierarchyCacher,
Expand All @@ -24,7 +25,7 @@ import { startSyncWithMs1 } from './ms1';
import { startSyncWithKoBo } from './kobo';
import { startFeedScraper } from './social';
import { createApp } from './createApp';

import { TaskOverdueChecker } from './scheduledTasks';
import winston from './log';
import { configureEnv } from './configureEnv';

Expand Down Expand Up @@ -69,6 +70,11 @@ configureEnv();
const taskAssigneeEmailer = new TaskAssigneeEmailer(models);
taskAssigneeEmailer.listenForChanges();

/**
* Scheduled tasks
*/
new TaskOverdueChecker(models).init();

/**
* Set up actual app with routes etc.
*/
Expand Down Expand Up @@ -117,4 +123,11 @@ configureEnv();
winston.error(error.message);
}
}

/**
* Gracefully handle shutdown of ChangeHandlers and ScheduledTasks
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Gracefully handle shutdown of ChangeHandlers and ScheduledTasks
* Gracefully handle shutdown of ScheduledTasks

*/
process.on('SIGINT', function () {
nodeSchedule.gracefulShutdown().then(() => process.exit(0));
});
})();
70 changes: 70 additions & 0 deletions packages/central-server/src/scheduledTasks/ScheduledTask.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Tupaia
* Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
*/

import { scheduleJob } from 'node-schedule';
import winston from 'winston';

/**
* Base class for scheduled tasks. Uses 'node-schedule' for scheduling based on cron tab syntax
* Subclasses should implement the run method and need to be initialised by instantiating the
* class and calling init in the central-server index.js file
*/
export class ScheduledTask {
getSchedule() {
throw new Error(`ScheduledTask::getSchedule not overridden for ${this.constructor.name}`);
}

getName() {
throw new Error(`ScheduledTask::getName not overridden for ${this.constructor.name}`);
}

constructor(models, lockKey) {
winston.info(`Initialising scheduled task ${this.getName()}`);
this.models = models;
this.lockKey = lockKey;
this.schedule = this.getSchedule();
this.name = this.getName();
this.job = null;
this.start = null;
}

async run() {
throw new Error('Any subclass of ScheduledTask must implement the "run" method');
}

async runTask() {
this.start = Date.now();

try {
await this.models.wrapInTransaction(async transactingModels => {
// Acquire a database advisory lock for the transaction
// Ensures no other server instance can execute its change handler at the same time
await transactingModels.database.acquireAdvisoryLockForTransaction(this.lockKey);
await this.run();
const durationMs = Date.now() - this.start;
winston.info(`ScheduledTask: ${this.name}: Succeeded in ${durationMs}`);
return true;
});
} catch (e) {
const durationMs = Date.now() - this.start;
winston.error(`ScheduledTask: ${this.name}: Failed`, { id: runId, durationMs });
winston.error(e.stack);

return false;
} finally {
this.start = null;
}
}

init() {
if (!this.job) {
const name = this.getName();
winston.info(`ScheduledTask: ${name}: Scheduled for ${this.schedule}`);
this.job = scheduleJob(this.schedule, async () => {
await this.runTask();
});
}
}
}
47 changes: 47 additions & 0 deletions packages/central-server/src/scheduledTasks/TaskOverdueChecker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Tupaia
* Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
*/
import { sendEmail } from '@tupaia/server-utils';
import { format } from 'date-fns';
import winston from 'winston';
import { ScheduledTask } from './ScheduledTask';

export class TaskOverdueChecker extends ScheduledTask {
getSchedule() {
return '0 * * * *'; // every hour
}

getName() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thoughts on making it possible to just set this.name = XXX and this.schedule = XXX, similar to how we set things in central-server CRUD handlers?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes good point. I liked the functions so that I could have the nice errors but I found another easy way of handling that.

return 'TaskOverdueChecker';
}

constructor(models) {
super(models, 'task-overdue-checker');
}

async run() {
const { task, user } = this.models;
const overdueTasks = await task.find({
task_status: 'overdue',
});

winston.info(`Found ${overdueTasks.length} overdue task(s)`);

for (const task of overdueTasks) {
const assignee = await user.findById(task.assignee_id);

const result = await sendEmail(assignee.email, {
subject: 'Task overdue on Tupaia.org',
templateName: 'overdueTask',
templateContext: {
userName: assignee.first_name,
surveyName: task.survey_name,
entityName: task.entity_name,
dueDate: format(new Date(task.due_date), 'do MMMM yyyy'),
},
});
winston.info(`Email sent to ${assignee.email} with status: ${result.response}`);
}
}
}
6 changes: 6 additions & 0 deletions packages/central-server/src/scheduledTasks/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/*
* Tupaia
* Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
*/

export { TaskOverdueChecker } from './TaskOverdueChecker';
18 changes: 18 additions & 0 deletions packages/server-utils/src/email/templates/content/overdueTask.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<div>
<p>Hi {{userName}},</p>
<p>
Oh no! Looks like you have an overdue task.
</p>
<p>
This is just to let you know that you have an overdue task. The task is {{surveyName}} for {{entityName}} and was due on the {{dueDate}}. To view and complete your tasks head to DataTrak.
</p>
<p>
Have fun using the platform and feel free to get in touch if you have any questions.
</p>
<p>
Cheers
</p>
<p>
The Tupaia Team
</p>
</div>
44 changes: 44 additions & 0 deletions yarn.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading