Adding some more error logging and handling as well as a circuit breaker to stop cron from continuously fucking up

This commit is contained in:
Collin Duncan 2023-01-29 14:34:39 +01:00
parent c027079303
commit 18a643034a
No known key found for this signature in database
3 changed files with 137 additions and 61 deletions

View file

@ -1,5 +1,6 @@
import { asyncLocalStorage as l } from './logger'
import { Reservation } from './reservation' import { Reservation } from './reservation'
import { Runner } from './runner' import { LoggableError, Runner } from './runner'
let runner: Runner | undefined let runner: Runner | undefined
const getRunner = () => { const getRunner = () => {
@ -12,16 +13,25 @@ const getRunner = () => {
return runner return runner
} }
export const reserve = async (reservation?: Reservation) => { export const reserve = async (reservation?: Reservation): Promise<boolean> => {
let reservationToPerform = reservation let reservationToPerform = reservation
if (!reservationToPerform) { if (!reservationToPerform) {
l.getStore()?.debug('No reservation provided, fetching first in database')
reservationToPerform = (await Reservation.fetchFirst()) || undefined reservationToPerform = (await Reservation.fetchFirst()) || undefined
} }
if (!reservationToPerform) { if (!reservationToPerform) {
return l.getStore()?.info('No reservation to perform')
return true
} }
l.getStore()?.debug('Trying to perform reservation', { reservationToPerform })
const runner = getRunner() const runner = getRunner()
await runner.run(reservationToPerform) try {
await runner.run(reservationToPerform)
return true
} catch (error) {
l.getStore()?.error('Failed to perform reservation', { error: (error as LoggableError).toString() })
return false
}
} }

View file

@ -8,7 +8,7 @@ import puppeteer, {
LaunchOptions, LaunchOptions,
Page, Page,
} from 'puppeteer' } from 'puppeteer'
import { asyncLocalStorage } from './logger' import { asyncLocalStorage as l } from './logger'
import { Opponent, Reservation } from './reservation' import { Opponent, Reservation } from './reservation'
export class Runner { export class Runner {
@ -26,41 +26,59 @@ export class Runner {
this.options = options this.options = options
} }
public async run(reservation: Reservation): Promise<boolean> { public async run(reservation: Reservation) {
asyncLocalStorage.getStore()?.debug('Launching browser') l.getStore()?.debug('Launching browser')
if (!this.browser) { try {
this.browser = await puppeteer.launch(this.options) if (!this.browser) {
this.browser = await puppeteer.launch(this.options)
}
} catch (error) {
throw new PuppeteerBrowserLaunchError(error as Error)
} }
this.page = await this.browser?.newPage()
try {
this.page = await this.browser?.newPage()
} catch (error) {
throw new PuppeteerNewPageError(error as Error)
}
await this.login(reservation.user.username, reservation.user.password) await this.login(reservation.user.username, reservation.user.password)
return this.makeReservation(reservation) await this.makeReservation(reservation)
} }
private async login(username: string, password: string) { private async login(username: string, password: string) {
asyncLocalStorage.getStore()?.debug('Logging in', { username }) l.getStore()?.debug('Logging in', { username })
await this.page?.goto('https://squashcity.baanreserveren.nl/') await this.page
?.goto('https://squashcity.baanreserveren.nl/')
.catch((e: Error) => {
throw new RunnerLoginNavigationError(e)
})
await this.page await this.page
?.waitForSelector('input[name=username]') ?.waitForSelector('input[name=username]')
.then((i) => i?.type(username)) .then((i) => i?.type(username))
await this.page?.$('input[name=password]').then((i) => i?.type(password)) .catch((e: Error) => {
await this.page?.$('button').then((b) => b?.click()) throw new RunnerLoginUsernameInputError(e)
})
await this.page
?.$('input[name=password]')
.then((i) => i?.type(password))
.catch((e: Error) => {
throw new RunnerLoginPasswordInputError(e)
})
await this.page
?.$('button')
.then((b) => b?.click())
.catch((e: Error) => {
throw new RunnerLoginSubmitError(e)
})
} }
private async makeReservation(reservation: Reservation): Promise<boolean> { private async makeReservation(reservation: Reservation) {
try { await this.navigateToDay(reservation.dateRange.start)
await this.navigateToDay(reservation.dateRange.start) await this.selectAvailableTime(reservation)
await this.selectAvailableTime(reservation) await this.selectOpponent(reservation.opponent)
await this.selectOpponent(reservation.opponent) await this.confirmReservation()
await this.confirmReservation() reservation.booked = true
reservation.booked = true
return true
} catch (err: unknown) {
asyncLocalStorage.getStore()?.error('Error making reservation', {
reservation: reservation.format(),
error: err,
})
return false
}
} }
private getLastVisibleDay(): Dayjs { private getLastVisibleDay(): Dayjs {
@ -78,17 +96,15 @@ export class Runner {
} }
private async navigateToDay(date: Dayjs): Promise<void> { private async navigateToDay(date: Dayjs): Promise<void> {
asyncLocalStorage.getStore()?.debug(`Navigating to ${date.format()}`) l.getStore()?.debug(`Navigating to ${date.format()}`)
if (this.getLastVisibleDay().isBefore(date)) { if (this.getLastVisibleDay().isBefore(date)) {
asyncLocalStorage l.getStore()?.debug('Date is on different page, increase month')
.getStore()
?.debug('Date is on different page, increase month')
await this.page await this.page
?.waitForSelector('td.month.next') ?.waitForSelector('td.month.next')
.then((d) => d?.click()) .then((d) => d?.click())
.catch(() => { .catch((e: Error) => {
throw new Error('Could not click correct month') throw new RunnerNavigationMonthError(e)
}) })
} }
@ -99,8 +115,8 @@ export class Runner {
)}` )}`
) )
.then((d) => d?.click()) .then((d) => d?.click())
.catch(() => { .catch((e: Error) => {
throw new Error('Could not click correct day') throw new RunnerNavigationDayError(e)
}) })
await this.page await this.page
?.waitForSelector( ?.waitForSelector(
@ -108,15 +124,13 @@ export class Runner {
'date' 'date'
)}.selected` )}.selected`
) )
.catch(() => { .catch((e: Error) => {
throw new Error("Selected day didn't change") throw new RunnerNavigationSelectionError(e)
}) })
} }
private async selectAvailableTime(res: Reservation): Promise<void> { private async selectAvailableTime(res: Reservation): Promise<void> {
asyncLocalStorage l.getStore()?.debug('Selecting available time', res.format())
.getStore()
?.debug('Selecting available time', res.format())
let freeCourt: ElementHandle | null | undefined let freeCourt: ElementHandle | null | undefined
let i = 0 let i = 0
while (i < res.possibleDates.length && !freeCourt) { while (i < res.possibleDates.length && !freeCourt) {
@ -129,28 +143,76 @@ export class Runner {
} }
if (!freeCourt) { if (!freeCourt) {
throw new Error('No free court available') throw new NoCourtAvailableError()
} }
await freeCourt.click() await freeCourt.click().catch((e: Error) => {
throw new RunnerCourtSelectionError(e)
})
} }
private async selectOpponent(opponent: Opponent): Promise<void> { private async selectOpponent(opponent: Opponent): Promise<void> {
asyncLocalStorage.getStore()?.debug('Selecting opponent', opponent) l.getStore()?.debug('Selecting opponent', opponent)
const player2Search = await this.page?.waitForSelector( const player2Search = await this.page
'tr.res-make-player-2 > td > input' ?.waitForSelector('tr.res-make-player-2 > td > input')
) .catch((e: Error) => {
await player2Search?.type(opponent.name) throw new RunnerOpponentSearchError(e)
await this.page?.waitForNetworkIdle() })
await player2Search?.type(opponent.name).catch((e: Error) => {
throw new RunnerOpponentSearchInputError(e)
})
await this.page?.waitForNetworkIdle().catch((e: Error) => {
throw new RunnerOpponentSearchNetworkError(e)
})
await this.page await this.page
?.$('select.br-user-select[name="players[2]"]') ?.$('select.br-user-select[name="players[2]"]')
.then((d) => d?.select(opponent.id)) .then((d) => d?.select(opponent.id))
.catch((e: Error) => {
throw new RunnerOpponentSearchSelectionError(e)
})
} }
private async confirmReservation(): Promise<void> { private async confirmReservation(): Promise<void> {
await this.page?.$('input#__make_submit').then((b) => b?.click()) await this.page?.$('input#__make_submit').then((b) => b?.click())
.catch((e: Error) => { throw new RunnerReservationConfirmButtonError(e) })
await this.page await this.page
?.waitForSelector('input#__make_submit2') ?.waitForSelector('input#__make_submit2')
.then((b) => b?.click()) .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}`
} }
} }
export class RunnerError extends LoggableError {
constructor(error: Error) {
super(error.message)
this.stack = error.stack
}
}
export class PuppeteerError extends RunnerError {}
export class PuppeteerBrowserLaunchError extends PuppeteerError {}
export class PuppeteerNewPageError extends PuppeteerError {}
export class RunnerLoginNavigationError extends RunnerError {}
export class RunnerLoginUsernameInputError extends RunnerError {}
export class RunnerLoginPasswordInputError extends RunnerError {}
export class RunnerLoginSubmitError extends RunnerError {}
export class RunnerNavigationMonthError extends RunnerError {}
export class RunnerNavigationDayError extends RunnerError {}
export class RunnerNavigationSelectionError extends RunnerError {}
export class RunnerCourtSelectionError extends RunnerError {}
export class NoCourtAvailableError extends LoggableError {}
export class RunnerOpponentSearchError extends RunnerError {}
export class RunnerOpponentSearchInputError extends RunnerError {}
export class RunnerOpponentSearchNetworkError extends RunnerError {}
export class RunnerOpponentSearchSelectionError extends RunnerError {}
export class RunnerReservationConfirmButtonError extends RunnerError {}
export class RunnerReservationConfirmSubmitError extends RunnerError {}

View file

@ -12,6 +12,7 @@ const getTaskConfig = (name: string): ScheduleOptions => ({
}) })
const logger = new Logger('cron', 'default', LogLevel.DEBUG) const logger = new Logger('cron', 'default', LogLevel.DEBUG)
let shouldContinue = true
export const startTasks = () => { export const startTasks = () => {
try { try {
@ -21,15 +22,18 @@ export const startTasks = () => {
asyncLocalStorage.run( asyncLocalStorage.run(
new Logger('cron', v4(), LogLevel.DEBUG), new Logger('cron', v4(), LogLevel.DEBUG),
async () => { async () => {
const childLogger = asyncLocalStorage.getStore() if (shouldContinue) {
childLogger?.info('Running cron job', { timestamp }) const childLogger = asyncLocalStorage.getStore()
try { childLogger?.info('Running cron job', { timestamp })
await reserve() try {
childLogger?.info('Completed running cron job') shouldContinue = await reserve()
} catch (error: any) { childLogger?.info('Completed running cron job')
childLogger?.error('Error running cron job', { } catch (error) {
error: error.message, shouldContinue = false
}) childLogger?.error('Error running cron job', {
error: (error as Error).message,
})
}
} }
} }
) )