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 { Runner } from './runner'
import { LoggableError, Runner } from './runner'
let runner: Runner | undefined
const getRunner = () => {
@ -12,16 +13,25 @@ const getRunner = () => {
return runner
}
export const reserve = async (reservation?: Reservation) => {
export const reserve = async (reservation?: Reservation): Promise<boolean> => {
let reservationToPerform = reservation
if (!reservationToPerform) {
l.getStore()?.debug('No reservation provided, fetching first in database')
reservationToPerform = (await Reservation.fetchFirst()) || undefined
}
if (!reservationToPerform) {
return
l.getStore()?.info('No reservation to perform')
return true
}
l.getStore()?.debug('Trying to perform reservation', { reservationToPerform })
const runner = getRunner()
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,
Page,
} from 'puppeteer'
import { asyncLocalStorage } from './logger'
import { asyncLocalStorage as l } from './logger'
import { Opponent, Reservation } from './reservation'
export class Runner {
@ -26,41 +26,59 @@ export class Runner {
this.options = options
}
public async run(reservation: Reservation): Promise<boolean> {
asyncLocalStorage.getStore()?.debug('Launching browser')
public async run(reservation: Reservation) {
l.getStore()?.debug('Launching browser')
try {
if (!this.browser) {
this.browser = await puppeteer.launch(this.options)
}
} catch (error) {
throw new PuppeteerBrowserLaunchError(error as Error)
}
try {
this.page = await this.browser?.newPage()
} catch (error) {
throw new PuppeteerNewPageError(error as Error)
}
await this.login(reservation.user.username, reservation.user.password)
return this.makeReservation(reservation)
await this.makeReservation(reservation)
}
private async login(username: string, password: string) {
asyncLocalStorage.getStore()?.debug('Logging in', { username })
await this.page?.goto('https://squashcity.baanreserveren.nl/')
l.getStore()?.debug('Logging in', { username })
await this.page
?.goto('https://squashcity.baanreserveren.nl/')
.catch((e: Error) => {
throw new RunnerLoginNavigationError(e)
})
await this.page
?.waitForSelector('input[name=username]')
.then((i) => i?.type(username))
await this.page?.$('input[name=password]').then((i) => i?.type(password))
await this.page?.$('button').then((b) => b?.click())
.catch((e: Error) => {
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> {
try {
private async makeReservation(reservation: Reservation) {
await this.navigateToDay(reservation.dateRange.start)
await this.selectAvailableTime(reservation)
await this.selectOpponent(reservation.opponent)
await this.confirmReservation()
reservation.booked = true
return true
} catch (err: unknown) {
asyncLocalStorage.getStore()?.error('Error making reservation', {
reservation: reservation.format(),
error: err,
})
return false
}
}
private getLastVisibleDay(): Dayjs {
@ -78,17 +96,15 @@ export class Runner {
}
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)) {
asyncLocalStorage
.getStore()
?.debug('Date is on different page, increase month')
l.getStore()?.debug('Date is on different page, increase month')
await this.page
?.waitForSelector('td.month.next')
.then((d) => d?.click())
.catch(() => {
throw new Error('Could not click correct month')
.catch((e: Error) => {
throw new RunnerNavigationMonthError(e)
})
}
@ -99,8 +115,8 @@ export class Runner {
)}`
)
.then((d) => d?.click())
.catch(() => {
throw new Error('Could not click correct day')
.catch((e: Error) => {
throw new RunnerNavigationDayError(e)
})
await this.page
?.waitForSelector(
@ -108,15 +124,13 @@ export class Runner {
'date'
)}.selected`
)
.catch(() => {
throw new Error("Selected day didn't change")
.catch((e: Error) => {
throw new RunnerNavigationSelectionError(e)
})
}
private async selectAvailableTime(res: Reservation): Promise<void> {
asyncLocalStorage
.getStore()
?.debug('Selecting available time', res.format())
l.getStore()?.debug('Selecting available time', res.format())
let freeCourt: ElementHandle | null | undefined
let i = 0
while (i < res.possibleDates.length && !freeCourt) {
@ -129,28 +143,76 @@ export class Runner {
}
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> {
asyncLocalStorage.getStore()?.debug('Selecting opponent', opponent)
const player2Search = await this.page?.waitForSelector(
'tr.res-make-player-2 > td > input'
)
await player2Search?.type(opponent.name)
await this.page?.waitForNetworkIdle()
l.getStore()?.debug('Selecting opponent', opponent)
const player2Search = await this.page
?.waitForSelector('tr.res-make-player-2 > td > input')
.catch((e: Error) => {
throw new RunnerOpponentSearchError(e)
})
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
?.$('select.br-user-select[name="players[2]"]')
.then((d) => d?.select(opponent.id))
.catch((e: Error) => {
throw new RunnerOpponentSearchSelectionError(e)
})
}
private async confirmReservation(): Promise<void> {
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}`
}
}
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)
let shouldContinue = true
export const startTasks = () => {
try {
@ -21,17 +22,20 @@ export const startTasks = () => {
asyncLocalStorage.run(
new Logger('cron', v4(), LogLevel.DEBUG),
async () => {
if (shouldContinue) {
const childLogger = asyncLocalStorage.getStore()
childLogger?.info('Running cron job', { timestamp })
try {
await reserve()
shouldContinue = await reserve()
childLogger?.info('Completed running cron job')
} catch (error: any) {
} catch (error) {
shouldContinue = false
childLogger?.error('Error running cron job', {
error: error.message,
error: (error as Error).message,
})
}
}
}
)
},
getTaskConfig('reserver cron')