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:
parent
c027079303
commit
18a643034a
3 changed files with 137 additions and 61 deletions
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
@ -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')
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue