Files
habitica/website/server/middlewares/cron.js
Sabe Jones 07ae4134f3 Squashed commit of the following:
commit 0db0a69f0fa00d831b7d90340e045097e8d92343
Author: Sabe Jones <sabrecat@gmail.com>
Date:   Tue Jul 16 10:01:49 2019 -0500

    fix(groups): return 0, not empty, when skipping downscore on Dailies
    fixes #11231

commit 1bf5bc714f660345f37bcc4e3587143341521c65
Author: Sabe Jones <sabrecat@gmail.com>
Date:   Tue Jul 16 09:48:28 2019 -0500

    fix(groups): correct task creation button

commit 90908211200bc4a3eb204178df5c812ffcd09f37
Author: Sabe Jones <sabrecat@gmail.com>
Date:   Tue Jul 16 09:46:04 2019 -0500

    fix(groups): clear approval status at cron for Habits and Dailies

commit 57e8dd0252c3ea45dd6fa1a7670ee197f32e76e5
Merge: d31b0a2d0 c9a56e8f3
Author: Sabe Jones <sabrecat@gmail.com>
Date:   Tue Jul 16 09:33:27 2019 -0500

    Merge branch 'develop' into sabrecat/group-tasks

commit d31b0a2d0e3b7a46235696e9b05fa734ac29a2e5
Merge: f2185a91b 043696c22
Author: SabreCat <sabrecat@gmail.com>
Date:   Mon Jun 24 14:58:26 2019 +0000

    Merge branch 'develop' into sabrecat/group-tasks

commit f2185a91bde554c7eb22cd6bdfc1ef9227f1e765
Author: Sabe Jones <sabrecat@gmail.com>
Date:   Tue Jun 18 10:10:10 2019 -0500

    fix(groups): divide task delta by number of assigned users

commit af0cde52f22129312d0102ecc5571f59aa2e396e
Author: Sabe Jones <sabrecat@gmail.com>
Date:   Tue Jun 18 10:06:01 2019 -0500

    fix(groups): remove chat spam

commit c1c810967a9e34155948a9164862125517372268
Merge: bf4f3fb08 f987585cf
Author: Sabe Jones <sabrecat@gmail.com>
Date:   Tue Jun 18 08:15:28 2019 -0500

    Merge branch 'develop' into sabrecat/group-tasks

commit bf4f3fb08491b4c4e1dbfaa58a2a6510c3cb78aa
Author: Sabe Jones <sabrecat@gmail.com>
Date:   Thu May 23 11:29:01 2019 -0500

    fix(test): expect new task class from store

commit ae0379c80519523f68d4d606e1cc4bed8e9e49ec
Merge: ce5096e11 664cf5a47
Author: Sabe Jones <sabrecat@gmail.com>
Date:   Thu May 23 11:25:15 2019 -0500

    Merge branch 'develop' into sabrecat/group-tasks

commit ce5096e116558419e4a73e8b23a58c78a1f2ab9b
Author: Sabe Jones <sabrecat@gmail.com>
Date:   Thu May 23 11:02:01 2019 -0500

    feat(group-plans): clarify non-interactive controls on task board

commit ff3cec9530e30d51c43171d6f97d3888ff200352
Author: Sabe Jones <sabrecat@gmail.com>
Date:   Tue May 21 11:10:50 2019 -0500

    feat(group-plans): score shared task when user scores it
2019-08-02 15:49:41 -05:00

170 lines
5.2 KiB
JavaScript

import moment from 'moment';
import * as Tasks from '../models/task';
import { model as Group } from '../models/group';
import { model as User } from '../models/user';
import { recoverCron, cron } from '../libs/cron';
// Wait this length of time in ms before attempting another cron
const CRON_TIMEOUT_WAIT = new Date(60 * 60 * 1000).getTime();
async function checkForActiveCron (user, now) {
// set _cronSignature to current time in ms since epoch time so we can make sure to wait at least CRONT_TIMEOUT_WAIT before attempting another cron
let _cronSignature = now.getTime();
// Calculate how long ago cron must have been attempted to try again
let cronRetryTime = _cronSignature - CRON_TIMEOUT_WAIT;
// To avoid double cron we first set _cronSignature and then check that it's not changed while processing
let userUpdateResult = await User.update({
_id: user._id,
$or: [ // Make sure last cron was successful or failed before cronRetryTime
{_cronSignature: 'NOT_RUNNING'},
{_cronSignature: {$lt: cronRetryTime}},
],
}, {
$set: {
_cronSignature,
'auth.timestamps.loggedin': now,
},
}).exec();
// If the cron signature is already set, cron is running in another request
// throw an error and recover later,
if (userUpdateResult.nMatched === 0 || userUpdateResult.nModified === 0) {
throw new Error('CRON_ALREADY_RUNNING');
}
}
async function updateLastCron (user, now) {
await User.update({
_id: user._id,
}, {
lastCron: now, // setting lastCron now so we don't risk re-running parts of cron if it fails
}).exec();
}
async function unlockUser (user) {
await User.update({
_id: user._id,
}, {
_cronSignature: 'NOT_RUNNING',
}).exec();
}
async function cronAsync (req, res) {
let user = res.locals.user;
if (!user) return null; // User might not be available when authentication is not mandatory
let analytics = res.analytics;
let now = new Date();
try {
await checkForActiveCron(user, now);
user = res.locals.user = await User.findOne({_id: user._id}).exec();
let {daysMissed, timezoneOffsetFromUserPrefs} = user.daysUserHasMissed(now, req);
await updateLastCron(user, now);
if (daysMissed <= 0) {
if (user.isModified()) await user.save();
await unlockUser(user);
return null;
}
let tasks = await Tasks.Task.find({
userId: user._id,
$or: [ // Exclude completed todos
{type: 'todo', completed: false},
{type: {$in: ['habit', 'daily', 'reward']}},
],
}).exec();
let tasksByType = {habits: [], dailys: [], todos: [], rewards: []};
tasks.forEach(task => tasksByType[`${task.type}s`].push(task));
// Run cron
let progress = cron({user, tasksByType, now, daysMissed, analytics, timezoneOffsetFromUserPrefs, headers: req.headers});
// Clear old completed todos - 30 days for free users, 90 for subscribers
// Do not delete challenges completed todos TODO unless the task is broken?
// Do not delete group completed todos
Tasks.Task.remove({
userId: user._id,
type: 'todo',
completed: true,
dateCompleted: {
$lt: moment(now).subtract(user.isSubscribed() ? 90 : 30, 'days').toDate(),
},
'challenge.id': {$exists: false},
'group.id': {$exists: false},
}).exec();
res.locals.wasModified = true; // TODO remove after v2 is retired
Group.tavernBoss(user, progress);
// Save user and tasks
let toSave = [user.save()];
tasks.forEach(async task => {
if (task.isModified()) toSave.push(task.save());
if (task.isModified() && task.group && task.group.taskId) {
const groupTask = await Tasks.Task.findOne({
_id: task.group.taskId,
}).exec();
if (groupTask) {
let delta = Math.pow(0.9747, task.value) * -1;
if (groupTask.group.assignedUsers) delta /= groupTask.group.assignedUsers.length;
await groupTask.scoreChallengeTask(delta, 'down');
}
}
});
await Promise.all(toSave);
await Group.processQuestProgress(user, progress);
// Set _cronSignature, lastCron and auth.timestamps.loggedin to signal end of cron
await User.update({
_id: user._id,
}, {
$set: {
_cronSignature: 'NOT_RUNNING',
},
}).exec();
// Reload user
res.locals.user = await User.findOne({_id: user._id}).exec();
return null;
} catch (err) {
// If cron was aborted for a race condition try to recover from it
if (err.message === 'CRON_ALREADY_RUNNING') {
// Recovering after abort, wait 300ms and reload user
// do it for max 5 times then reset _cronSignature so that it doesn't prevent cron from running
// at the next request
let recoveryStatus = {
times: 0,
};
await recoverCron(recoveryStatus, res.locals);
} else {
// For any other error make sure to reset _cronSignature so that it doesn't prevent cron from running
// at the next request
await User.update({
_id: user._id,
}, {
_cronSignature: 'NOT_RUNNING',
}).exec();
throw err; // re-throw the original error
}
}
}
module.exports = function cronMiddleware (req, res, next) {
cronAsync(req, res)
.then(() => {
next();
})
.catch(next);
};