From 6dfa37272ea47949c72d2baa6afdc0ebe1ed4368 Mon Sep 17 00:00:00 2001 From: Collin Duncan <3679940+cgduncan7@users.noreply.github.com> Date: Mon, 30 Jan 2023 12:38:42 +0100 Subject: [PATCH] Updating errors and logging in a lot of places. Changing some static methods of Reservation to instance methods --- src/common/logger.ts | 6 +++ src/common/reservation.ts | 78 +++++++++++++++++++++++------ src/common/reserver.ts | 14 +++--- src/common/runner.ts | 27 +++++----- src/common/scheduler.ts | 4 +- src/server/cron.ts | 50 +++++++++--------- tests/unit/common/scheduler.test.ts | 3 +- 7 files changed, 123 insertions(+), 59 deletions(-) diff --git a/src/common/logger.ts b/src/common/logger.ts index c01acd1..792cc89 100644 --- a/src/common/logger.ts +++ b/src/common/logger.ts @@ -85,3 +85,9 @@ export class Logger { this.log(LogLevel.ERROR, message, details) } } + +export class LoggableError extends Error { + toString() { + return `${this.name} - ${this.message}\n${this.stack}` + } +} \ No newline at end of file diff --git a/src/common/reservation.ts b/src/common/reservation.ts index 2dd08c7..2197e31 100644 --- a/src/common/reservation.ts +++ b/src/common/reservation.ts @@ -76,15 +76,16 @@ export class Reservation { .subtract(RESERVATION_AVAILABLE_WITHIN_DAYS, 'days') } - public toString() { - return JSON.stringify(this.format()) + public toString(obfuscate = false) { + return JSON.stringify(this.format(obfuscate)) } - public format() { + public format(obfuscate = false) { return { + id: this.id, user: { username: this.user.username, - password: this.user.password, + password: obfuscate ? '???' : this.user.password, }, opponent: this.opponent, booked: this.booked, @@ -98,6 +99,7 @@ export class Reservation { public serializeToJson(): SerializedReservation { return { + id: this.id, user: this.user, opponent: this.opponent, booked: this.booked, @@ -126,7 +128,7 @@ export class Reservation { return dates.map((date) => dayjs(date)) } - public static async save(res: Reservation) { + public async save() { await run( ` INSERT INTO reservations @@ -151,24 +153,34 @@ export class Reservation { ) `, [ - res.id, - res.user.username, - res.user.password, - res.dateRange.start.format('YYYY-MM-DD HH:mm:ss'), - res.dateRange.end.format('YYYY-MM-DD HH:mm:ss'), - res.opponent.id, - res.opponent.name, + this.id, + this.user.username, + this.user.password, + this.dateRange.start.format('YYYY-MM-DD HH:mm:ss'), + this.dateRange.end.format('YYYY-MM-DD HH:mm:ss'), + this.opponent.id, + this.opponent.name, ] ) } - public static async delete(res: Reservation) { + public async delete() { await run( ` DELETE FROM reservations WHERE id = ? `, - [res.id] + [this.id] + ) + } + + public static async deleteById(id: string) { + await run( + ` + DELETE FROM reservations + WHERE id = ? + `, + [id] ) } @@ -273,6 +285,43 @@ export class Reservation { return [] } + + public static async fetchByPage(pageNumber: number, pageSize = 50): Promise { + const response = await all( + ` + SELECT * + FROM reservations + ORDER BY date_range_start ASC + LIMIT ? + OFFSET ?; + `, + [pageSize, pageSize * pageNumber] + ) + + if (response.length > 0) { + return response.map( + (sqlReservation) => + new Reservation( + { + username: sqlReservation.username, + password: sqlReservation.password, + }, + { + start: dayjs(sqlReservation.date_range_start), + end: dayjs(sqlReservation.date_range_end), + }, + { + id: sqlReservation.opponent_id, + name: sqlReservation.opponent_name, + }, + undefined, + sqlReservation.id + ) + ) + } + + return [] + } } export interface SerializedDateRange { @@ -281,6 +330,7 @@ export interface SerializedDateRange { } export interface SerializedReservation { + id: string user: User opponent: Opponent booked: boolean diff --git a/src/common/reserver.ts b/src/common/reserver.ts index 79dc470..cc48e3d 100644 --- a/src/common/reserver.ts +++ b/src/common/reserver.ts @@ -1,6 +1,6 @@ -import { asyncLocalStorage as l } from './logger' +import { asyncLocalStorage as l, LoggableError } from './logger' import { Reservation } from './reservation' -import { LoggableError, Runner } from './runner' +import { Runner } from './runner' let runner: Runner | undefined const getRunner = () => { @@ -24,15 +24,17 @@ export const reserve = async (reservation?: Reservation): Promise => { l.getStore()?.info('No reservation to perform') return true } - - l.getStore()?.debug('Trying to perform reservation', { reservationToPerform }) + + l.getStore()?.debug('Trying to perform reservation', { reservationToPerform: reservationToPerform.toString(true) }) const runner = getRunner() try { await runner.run(reservationToPerform) - await Reservation.delete(reservationToPerform) + await reservationToPerform.delete() return true } catch (error) { - l.getStore()?.error('Failed to perform reservation', { error: (error as LoggableError).toString() }) + l.getStore()?.error('Failed to perform reservation', { + error: (error as LoggableError).toString(), + }) return false } } diff --git a/src/common/runner.ts b/src/common/runner.ts index 5e2ec4d..f3e5a7c 100644 --- a/src/common/runner.ts +++ b/src/common/runner.ts @@ -8,7 +8,7 @@ import puppeteer, { LaunchOptions, Page, } from 'puppeteer' -import { asyncLocalStorage as l } from './logger' +import { asyncLocalStorage as l, LoggableError } from './logger' import { Opponent, Reservation } from './reservation' export class Runner { @@ -130,7 +130,7 @@ export class Runner { } private async selectAvailableTime(res: Reservation): Promise { - l.getStore()?.debug('Selecting available time', res.format()) + l.getStore()?.debug('Selecting available time', { reservation: res.toString(true) }) let freeCourt: ElementHandle | null | undefined let i = 0 while (i < res.possibleDates.length && !freeCourt) { @@ -152,7 +152,7 @@ export class Runner { } private async selectOpponent(opponent: Opponent): Promise { - l.getStore()?.debug('Selecting opponent', opponent) + l.getStore()?.debug('Selecting opponent', { opponent }) const player2Search = await this.page ?.waitForSelector('tr.res-make-player-2 > td > input') .catch((e: Error) => { @@ -173,20 +173,21 @@ export class Runner { } private async confirmReservation(): Promise { - await this.page?.$('input#__make_submit').then((b) => b?.click()) - .catch((e: Error) => { throw new RunnerReservationConfirmButtonError(e) }) + await this.page + ?.$('input#__make_submit') + .then((b) => b?.click()) + .catch((e: Error) => { + throw new RunnerReservationConfirmButtonError(e) + }) await this.page ?.waitForSelector('input#__make_submit2') .then((b) => b?.click()) - .catch((e: Error) => { throw new RunnerReservationConfirmSubmitError(e) }) - } -} - -export class LoggableError extends Error { - toString() { - return `${this.name} - ${this.message}\n${this.stack}` + .catch((e: Error) => { + throw new RunnerReservationConfirmSubmitError(e) + }) } } + export class RunnerError extends LoggableError { constructor(error: Error) { super(error.message) @@ -215,4 +216,4 @@ export class RunnerOpponentSearchNetworkError extends RunnerError {} export class RunnerOpponentSearchSelectionError extends RunnerError {} export class RunnerReservationConfirmButtonError extends RunnerError {} -export class RunnerReservationConfirmSubmitError extends RunnerError {} \ No newline at end of file +export class RunnerReservationConfirmSubmitError extends RunnerError {} diff --git a/src/common/scheduler.ts b/src/common/scheduler.ts index 95ddf39..fccc769 100644 --- a/src/common/scheduler.ts +++ b/src/common/scheduler.ts @@ -31,7 +31,7 @@ export const schedule = async ( } logger?.debug('Successfully validated request', { - reservation: reservation.format(), + reservation: reservation.toString(true), }) if (!reservation.isAvailableForReservation()) { @@ -39,7 +39,7 @@ export const schedule = async ( 'Reservation date is more than 7 days away; saving for later reservation' ) - await Reservation.save(reservation) + await reservation.save() return { scheduledReservation: { diff --git a/src/server/cron.ts b/src/server/cron.ts index 36372b6..93673d3 100644 --- a/src/server/cron.ts +++ b/src/server/cron.ts @@ -12,46 +12,50 @@ const getTaskConfig = (name: string): ScheduleOptions => ({ }) const logger = new Logger('cron', 'default', LogLevel.DEBUG) -let shouldContinue = true export const startTasks = () => { try { - const task = schedule( - '*/10 7 * * *', - async (timestamp) => { - asyncLocalStorage.run( - new Logger('cron', v4(), LogLevel.DEBUG), - async () => { - if (shouldContinue) { + if (tasks.length === 0) { + const task = schedule( + '* 7 * * *', + async (timestamp) => { + asyncLocalStorage.run( + new Logger('cron', v4(), LogLevel.DEBUG), + async () => { const childLogger = asyncLocalStorage.getStore() childLogger?.info('Running cron job', { timestamp }) try { - shouldContinue = await reserve() + const result = await reserve() + if (!result) { + throw new Error('Failed to complete reservation') + } childLogger?.info('Completed running cron job') } catch (error) { - shouldContinue = false childLogger?.error('Error running cron job', { error: (error as Error).message, }) + stopTasks() } } - } - ) - }, - getTaskConfig('reserver cron') - ) - logger.debug('Started cron task') - tasks.push(task) - } catch (error: any) { - logger.error('Failed to start tasks', { error: error.message }) + ) + }, + getTaskConfig('reserver cron') + ) + logger.debug('Started cron task') + tasks.push(task) + } + } catch (error) { + logger.error('Failed to start tasks', { error: (error as Error).message }) } } export const stopTasks = () => { try { - tasks.map((task) => task.stop()) - logger.debug('Stopped cron tasks') - } catch (error: any) { - logger.error('Failed to stop tasks', { error: error.message }) + if (tasks.length > 0) { + tasks.map((task) => task.stop()) + logger.debug('Stopped cron tasks') + } + } catch (error) { + logger.error('Failed to stop tasks', { error: (error as Error).message }) } } diff --git a/tests/unit/common/scheduler.test.ts b/tests/unit/common/scheduler.test.ts index 7c696cb..8024156 100644 --- a/tests/unit/common/scheduler.test.ts +++ b/tests/unit/common/scheduler.test.ts @@ -5,6 +5,7 @@ import { } from '../../../src/common/request' import { Reservation } from '../../../src/common/reservation' import { schedule, SchedulerInput } from '../../../src/common/scheduler' +import * as database from '../../../src/common/database' jest.mock('../../../src/common/logger') jest.mock('../../../src/common/reserver') @@ -13,7 +14,7 @@ jest.useFakeTimers().setSystemTime(new Date('2022-01-01')) describe('scheduler', () => { test('should handle valid requests within reservation window', async () => { - jest.spyOn(Reservation, 'save').mockResolvedValueOnce() + jest.spyOn(database, 'run').mockResolvedValueOnce() const start = dayjs().add(15, 'minutes') const end = start.add(15, 'minutes')