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 { 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()
|
||||||
|
try {
|
||||||
await runner.run(reservationToPerform)
|
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,
|
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')
|
||||||
|
try {
|
||||||
if (!this.browser) {
|
if (!this.browser) {
|
||||||
this.browser = await puppeteer.launch(this.options)
|
this.browser = await puppeteer.launch(this.options)
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw new PuppeteerBrowserLaunchError(error as Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
this.page = await this.browser?.newPage()
|
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 {}
|
||||||
|
|
@ -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,17 +22,20 @@ export const startTasks = () => {
|
||||||
asyncLocalStorage.run(
|
asyncLocalStorage.run(
|
||||||
new Logger('cron', v4(), LogLevel.DEBUG),
|
new Logger('cron', v4(), LogLevel.DEBUG),
|
||||||
async () => {
|
async () => {
|
||||||
|
if (shouldContinue) {
|
||||||
const childLogger = asyncLocalStorage.getStore()
|
const childLogger = asyncLocalStorage.getStore()
|
||||||
childLogger?.info('Running cron job', { timestamp })
|
childLogger?.info('Running cron job', { timestamp })
|
||||||
try {
|
try {
|
||||||
await reserve()
|
shouldContinue = await reserve()
|
||||||
childLogger?.info('Completed running cron job')
|
childLogger?.info('Completed running cron job')
|
||||||
} catch (error: any) {
|
} catch (error) {
|
||||||
|
shouldContinue = false
|
||||||
childLogger?.error('Error running cron job', {
|
childLogger?.error('Error running cron job', {
|
||||||
error: error.message,
|
error: (error as Error).message,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
getTaskConfig('reserver cron')
|
getTaskConfig('reserver cron')
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue