Skip to content

Commit

Permalink
feat: scheduling logic
Browse files Browse the repository at this point in the history
  • Loading branch information
julian-wasmeier-titanom committed May 21, 2024
1 parent 4391b9e commit c95d399
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 98 deletions.
122 changes: 76 additions & 46 deletions backend/src/assignment-scheduler.service.ts
Original file line number Diff line number Diff line change
@@ -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<string, number[]>
>((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;
}
51 changes: 0 additions & 51 deletions backend/src/db/functions/assignment-task-group.ts

This file was deleted.

46 changes: 45 additions & 1 deletion backend/src/db/functions/assignment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<AssignmentResponse[]> {
try {
Expand Down Expand Up @@ -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;
}
}

0 comments on commit c95d399

Please sign in to comment.