Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ type StoryReviewEmailTemplateData = {
storiesCount: number;
reviewStoriesUrl: string;
stats: { reviewer: string; count: number }[];
// Switches the wording between the early warning and the missed-SLO version.
isApproachingSlo: boolean;
};

class StoriesReviewQueueStaleTemplate extends EmailTemplate<
Expand Down
37 changes: 24 additions & 13 deletions backend/src/cron/checkStaleStories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@ import ReviewQueueStaleTemplate from '../business/email/templates/StoriesReviewQ
import StoryState from '../enum/StoryState';
import StoryRepository from '../repositories/StoryRepository';

const STALENESS_THRESHOLD_MS = 1000 * 60 * 60 * 24 * 2;
const ONE_DAY_MS = 1000 * 60 * 60 * 24;

// Stories should be reviewed within two days of submission.
const SLO_THRESHOLD_MS = ONE_DAY_MS * 2;

// Warn a day before the SLO is missed so moderators have time to act.
const WARNING_THRESHOLD_MS = SLO_THRESHOLD_MS - ONE_DAY_MS;

function forgeReviewStoriesUrl(): string {
const storyEditUrl: URL = new URL(
Expand All @@ -24,19 +30,23 @@ async function getReviewerStats(): Promise<
return stats;
}

// How long the story at the head of the review queue has been waiting, or 0
// when the queue is empty.
async function getLongestWaitMs(): Promise<number> {
const oldestStory = await StoryRepository()
.createQueryBuilder('story')
.where({ state: StoryState.SUBMITTED })
.orderBy('story.updated_at', 'ASC')
.getOne();

return oldestStory ? Date.now() - oldestStory.updatedAt.getTime() : 0;
}

export default async function checkStaleStories(): Promise<void> {
const hasStaleStories =
(await StoryRepository()
.createQueryBuilder('story')
.where({ state: StoryState.SUBMITTED })
.andWhere('story.updated_at < :stalenessThreshold', {
stalenessThreshold: new Date(Date.now() - STALENESS_THRESHOLD_MS),
})
.orderBy('story.updated_at', 'ASC')
.getCount()) > 0;

if (!hasStaleStories) {
console.log('No stale stories found');
const longestWaitMs = await getLongestWaitMs();

if (longestWaitMs < WARNING_THRESHOLD_MS) {
console.log('No stories approaching the review SLO');
return;
}

Expand All @@ -51,6 +61,7 @@ export default async function checkStaleStories(): Promise<void> {
reviewStoriesUrl,
storiesCount,
stats,
isApproachingSlo: longestWaitMs < SLO_THRESHOLD_MS,
},
metadata: {},
to: process.env.MODERATORS_TO_EMAIL ?? '',
Expand Down
Loading