From c95d3993cdf82ee7974ae386b91003eaab7f2b36 Mon Sep 17 00:00:00 2001 From: julian-wasmeier-titanom Date: Tue, 21 May 2024 19:50:48 +0200 Subject: [PATCH] feat: scheduling logic --- backend/src/assignment-scheduler.service.ts | 122 +++++++++++------- .../src/db/functions/assignment-task-group.ts | 51 -------- backend/src/db/functions/assignment.ts | 46 ++++++- 3 files changed, 121 insertions(+), 98 deletions(-) delete mode 100644 backend/src/db/functions/assignment-task-group.ts diff --git a/backend/src/assignment-scheduler.service.ts b/backend/src/assignment-scheduler.service.ts index 823df4d..5853bc8 100644 --- a/backend/src/assignment-scheduler.service.ts +++ b/backend/src/assignment-scheduler.service.ts @@ -1,63 +1,93 @@ import { Injectable } from '@nestjs/common'; import { Cron, CronExpression } from '@nestjs/schedule'; +import { eq, sql } from 'drizzle-orm'; +import { db } from './db'; import { - dbGetTaskGroupUsers, - dbGetTasksOfTaskGroup, -} from './db/functions/task-group'; -import { - dbCreateTaskGroupAssignment, dbGetAssignmentsForTaskGroup, - dbGetTaskGroupsToAssignForCurrentInterval, -} from './db/functions/assignment-task-group'; + dbGetTasksToAssignForCurrentInterval, +} from './db/functions/assignment'; +import { dbGetTaskGroupUsers } from './db/functions/task-group'; +import { assignmentTable, taskGroupTable, taskTable } from './db/schema'; import { randomFromArray } from './utils/array'; -import { db } from './db'; -import { assignmentTable } from './db/schema'; @Injectable() export class AssignmentSchedulerService { - @Cron(CronExpression.EVERY_5_SECONDS) + @Cron(CronExpression.EVERY_10_SECONDS) async handleCron() { - console.debug('CRON job running'); - const taskGroupsToCreateAssignmentsFor = - await dbGetTaskGroupsToAssignForCurrentInterval(); - console.debug({ taskGroupsToCreateAssignmentsFor }); - for (const { taskGroupId } of taskGroupsToCreateAssignmentsFor) { - const userIds = await dbGetTaskGroupUsers(taskGroupId); + const tasksToCreateAssignmentsFor = + await dbGetTasksToAssignForCurrentInterval(); + + console.debug( + 'Running task scheduling cron job for', + tasksToCreateAssignmentsFor, + ); + + const tasksByGroup = tasksToCreateAssignmentsFor.reduce< + Record + >((acc, curr) => { + if (!acc[curr.taskGroupId]) { + acc[curr.taskGroupId] = []; + } + acc[curr.taskGroupId].push(curr.taskId); + return acc; + }, {}); + + for (const [taskGroupId, taskIds] of Object.entries(tasksByGroup)) { + // FIXME: ugly type conversion, maybe use a map + const userIds = await dbGetTaskGroupUsers(Number(taskGroupId)); if (userIds.length === 0) { continue; } - const lastAssignments = await dbGetAssignmentsForTaskGroup( - taskGroupId, - userIds.length, - ); - const firstTimeAssignmentUsers = userIds.filter( - ({ userId }) => - !lastAssignments.some((assignment) => assignment.userId === userId), - ); - console.debug({ userIds }); - console.debug({ firstTimeAssignmentUsers }); - console.debug({ lastAssignments, taskGroupId }); - - let nextResponsibleUserId: number; - if (firstTimeAssignmentUsers.length === 0) { - nextResponsibleUserId = - lastAssignments[lastAssignments.length - 1].userId; - } else { - nextResponsibleUserId = randomFromArray( - firstTimeAssignmentUsers, - ).userId; - } + const nextResponsibleUserId = await getNextResponsibleUserId( + Number(taskGroupId), + userIds.map(({ userId }) => userId), + ); - await dbCreateTaskGroupAssignment(taskGroupId, nextResponsibleUserId); - const tasksOfTaskGroup = await dbGetTasksOfTaskGroup(taskGroupId); - const assignmentsToCreate = tasksOfTaskGroup.map((task) => { - return { - taskId: task.id, - userId: nextResponsibleUserId, - }; - }); - await db.insert(assignmentTable).values(assignmentsToCreate); + await db + .insert(assignmentTable) + .values( + taskIds.map((taskId) => ({ taskId, userId: nextResponsibleUserId })), + ); } } } + +async function getNextResponsibleUserId( + taskGroupId: number, + userIds: number[], +) { + const currentAssignments = await db + .select() + .from(assignmentTable) + .innerJoin(taskTable, eq(taskTable.id, assignmentTable.taskId)) + .innerJoin(taskGroupTable, eq(taskGroupTable.id, taskTable.taskGroupId)) + .where( + sql`${assignmentTable.createdAt} >= NOW() - ${taskGroupTable.interval} AND ${taskGroupTable.id} = ${taskGroupId}`, + ); + + /* If there already are current assignments, return the userId of one of the current assignments + (It doesn't matter which one, they should all be assigned to the same user) */ + if (currentAssignments.length != 0) { + return currentAssignments[0].assignment.userId; + } + + const lastAssignments = await dbGetAssignmentsForTaskGroup( + taskGroupId, + userIds.length, + ); + + const userIdsWithoutAnyAssignments = userIds.filter( + (userId) => + !lastAssignments.some(({ assignment }) => assignment.userId === userId), + ); + + let nextResponsibleUserId: number; + if (userIdsWithoutAnyAssignments.length === 0) { + nextResponsibleUserId = + lastAssignments[lastAssignments.length - 1].assignment.userId; + } else { + nextResponsibleUserId = randomFromArray(userIdsWithoutAnyAssignments); + } + return nextResponsibleUserId; +} diff --git a/backend/src/db/functions/assignment-task-group.ts b/backend/src/db/functions/assignment-task-group.ts deleted file mode 100644 index df84356..0000000 --- a/backend/src/db/functions/assignment-task-group.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { count, desc, eq, or, sql } from 'drizzle-orm'; -import { db } from '..'; -import { taskGroupAssignmentTable, taskGroupTable } from '../schema'; - -export async function dbGetAssignmentsForTaskGroup( - taskGroupId: number, - limit?: number, -) { - const result = db - .select() - .from(taskGroupAssignmentTable) - .where(eq(taskGroupAssignmentTable.taskGroupId, taskGroupId)) - .orderBy(desc(taskGroupAssignmentTable.createdAt)); - if (limit === undefined) { - return await result; - } - return await result.limit(limit); -} - -export async function dbCreateTaskGroupAssignment( - taskGroupId: number, - userId: number, -) { - await db.insert(taskGroupAssignmentTable).values({ taskGroupId, userId }); -} - -export async function dbGetTaskGroupsToAssignForCurrentInterval() { - try { - const taskGroupIdsToCreateAssignmentsFor = await db - .select({ - taskGroupId: taskGroupTable.id, - }) - .from(taskGroupTable) - .leftJoin( - taskGroupAssignmentTable, - eq(taskGroupTable.id, taskGroupAssignmentTable.taskGroupId), - ) - .groupBy(taskGroupTable.id) - .having( - or( - eq(count(taskGroupAssignmentTable.id), 0), - sql`MAX(${taskGroupAssignmentTable.createdAt}) < (NOW() - ${taskGroupTable.interval})`, - ), - ); - - return taskGroupIdsToCreateAssignmentsFor; - } catch (error) { - console.error({ error }); - throw error; - } -} diff --git a/backend/src/db/functions/assignment.ts b/backend/src/db/functions/assignment.ts index 5a87600..dbbda28 100644 --- a/backend/src/db/functions/assignment.ts +++ b/backend/src/db/functions/assignment.ts @@ -3,10 +3,11 @@ import { db } from '..'; import { AssignmentState, assignmentTable, + taskGroupTable, taskTable, userTable, } from '../schema'; -import { eq } from 'drizzle-orm'; +import { count, desc, eq, or, sql } from 'drizzle-orm'; export async function dbGetAllAssignments(): Promise { try { @@ -47,3 +48,46 @@ export async function dbChangeAssignmentState( throw error; } } + +export async function dbGetAssignmentsForTaskGroup( + taskGroupId: number, + limit?: number, +) { + const result = db + .select({ assignment: { ...assignmentTable } }) + .from(taskGroupTable) + .innerJoin(taskTable, eq(taskGroupTable.id, taskTable.taskGroupId)) + .innerJoin(assignmentTable, eq(taskTable.id, assignmentTable.taskId)) + .where(eq(taskGroupTable.id, taskGroupId)) + .orderBy(desc(assignmentTable.createdAt)); + if (limit === undefined) { + return await result; + } + return await result.limit(limit); +} + +export async function dbGetTasksToAssignForCurrentInterval() { + try { + // Get all tasks that either have no assignments yet or don't have an assignment in the current period + const taskIdsToCreateAssignmentsFor = await db + .select({ + taskId: taskTable.id, + taskGroupId: taskGroupTable.id, + }) + .from(taskGroupTable) + .innerJoin(taskTable, eq(taskGroupTable.id, taskTable.taskGroupId)) + .leftJoin(assignmentTable, eq(taskTable.id, assignmentTable.taskId)) + .groupBy(taskGroupTable.id, taskTable.id) + .having( + or( + eq(count(assignmentTable.id), 0), + sql`MAX(${assignmentTable.createdAt}) <= (NOW() - ${taskGroupTable.interval})`, + ), + ); + + return taskIdsToCreateAssignmentsFor; + } catch (error) { + console.error({ error }); + throw error; + } +}