Fix gsheet api request saturation caused by an infinite delayed loop

This commit is contained in:
pikiou 2022-02-22 02:10:16 +01:00
parent d8e7bb9f70
commit 968ec3b430
3 changed files with 61 additions and 32 deletions

View File

@ -11,8 +11,9 @@ export { SheetNames } from "./localDb"
const CRED_PATH = path.resolve(process.cwd(), "access/gsheets.json") const CRED_PATH = path.resolve(process.cwd(), "access/gsheets.json")
const REMOTE_UPDATE_DELAY = 80000 const REMOTE_UPDATE_DELAY = 40000
const DELAY_BETWEEN_ATTEMPTS = 10000 const DELAY_BETWEEN_ATTEMPTS = 10000
const DELAY_BETWEEN_FIRST_LOAD = 1500
let creds: string | undefined | null let creds: string | undefined | null
@ -44,7 +45,7 @@ export async function checkGSheetsAccess(): Promise<void> {
console.error(`Google Sheets: no creds found, loading local database instead`) console.error(`Google Sheets: no creds found, loading local database instead`)
} }
} }
export function getSheet< export async function getSheet<
// eslint-disable-next-line @typescript-eslint/ban-types // eslint-disable-next-line @typescript-eslint/ban-types
ElementNoId extends object, ElementNoId extends object,
Element extends ElementNoId & ElementWithId<ElementNoId> Element extends ElementNoId & ElementWithId<ElementNoId>
@ -52,18 +53,17 @@ export function getSheet<
sheetName: keyof SheetNames, sheetName: keyof SheetNames,
specimen: Element, specimen: Element,
translation: { [k in keyof Element]: string } translation: { [k in keyof Element]: string }
): Sheet<ElementNoId, Element> { ): Promise<Sheet<ElementNoId, Element>> {
let sheet: Sheet<ElementNoId, Element>
if (!sheetList[sheetName]) { if (!sheetList[sheetName]) {
sheetList[sheetName] = new Sheet<ElementNoId, Element>(sheetName, specimen, translation) sheet = new Sheet<ElementNoId, Element>(sheetName, specimen, translation)
await sheet.waitForFirstLoad()
sheetList[sheetName] = sheet
setInterval(() => sheet.dbUpdate(), REMOTE_UPDATE_DELAY)
} else {
sheet = sheetList[sheetName] as Sheet<ElementNoId, Element>
} }
const sheet = sheetList[sheetName] as Sheet<ElementNoId, Element>
setTimeout(
() => setInterval(() => sheet.dbUpdate(), REMOTE_UPDATE_DELAY),
1000 * Object.values(sheetList).length
)
return sheet return sheet
} }
@ -100,8 +100,6 @@ export class Sheet<
_.invert(translation), _.invert(translation),
(englishProp: string) => (specimen as any)[englishProp] (englishProp: string) => (specimen as any)[englishProp]
) as Element ) as Element
setTimeout(() => this.dbFirstLoad(), 100 * Object.values(sheetList).length)
} }
async getList(): Promise<Element[] | undefined> { async getList(): Promise<Element[] | undefined> {
@ -154,6 +152,15 @@ export class Sheet<
} }
} }
async waitForFirstLoad(): Promise<void> {
setTimeout(
() => this.dbFirstLoad(),
DELAY_BETWEEN_FIRST_LOAD * Object.values(sheetList).length
)
await this.waitForLoad()
}
private async waitForLoad(): Promise<void> { private async waitForLoad(): Promise<void> {
return new Promise((resolve, _reject) => { return new Promise((resolve, _reject) => {
this.addToRunAfterLoad(() => resolve(undefined)) this.addToRunAfterLoad(() => resolve(undefined))
@ -194,10 +201,9 @@ export class Sheet<
async dbLoad(): Promise<void> { async dbLoad(): Promise<void> {
try { try {
if (await hasGSheetsAccess()) { if (await hasGSheetsAccess()) {
this.dbLoadAsync().then(() => this.doRunAfterLoad()) await this.dbLoadAsync()
} else {
this.doRunAfterLoad()
} }
this.doRunAfterLoad()
} catch (e) { } catch (e) {
console.error("Error in dbLoad: ", e) console.error("Error in dbLoad: ", e)
} }
@ -214,6 +220,7 @@ export class Sheet<
if (!(await hasGSheetsAccess())) { if (!(await hasGSheetsAccess())) {
await this.loadLocalDb() await this.loadLocalDb()
} else if (this.toRunAfterLoad && __DEV__) { } else if (this.toRunAfterLoad && __DEV__) {
// Save once
this.toRunAfterLoad.push(() => this.saveLocalDb()) this.toRunAfterLoad.push(() => this.saveLocalDb())
} }
@ -288,7 +295,7 @@ export class Sheet<
private async dbLoadAsync(): Promise<void> { private async dbLoadAsync(): Promise<void> {
type StringifiedElement = Record<keyof Element, string> type StringifiedElement = Record<keyof Element, string>
const sheet = await this.getGSheet() const sheet = await this.getGSheet(20)
if (!sheet) { if (!sheet) {
return return
@ -326,7 +333,7 @@ export class Sheet<
}) })
} }
private async getGSheet(): Promise<GoogleSpreadsheetWorksheet | null> { private async getGSheet(attempts = 3): Promise<GoogleSpreadsheetWorksheet | null> {
return tryNTimes( return tryNTimes(
async () => { async () => {
if (creds === undefined) { if (creds === undefined) {
@ -347,7 +354,7 @@ export class Sheet<
return doc.sheetsByTitle[this.sheetName] return doc.sheetsByTitle[this.sheetName]
}, },
() => null, () => null,
20, attempts,
DELAY_BETWEEN_ATTEMPTS / 5 DELAY_BETWEEN_ATTEMPTS / 5
) )
} }
@ -590,7 +597,7 @@ function parseDate(value: string): Date {
async function tryNTimes<T>( async function tryNTimes<T>(
func: () => Promise<T> | T, func: () => Promise<T> | T,
failFunc?: () => Promise<T> | T, failFunc?: () => Promise<T> | T,
repeatCount = 5, repeatCount = 2,
delayBetweenAttempts = DELAY_BETWEEN_ATTEMPTS delayBetweenAttempts = DELAY_BETWEEN_ATTEMPTS
): Promise<T> { ): Promise<T> {
try { try {
@ -601,7 +608,7 @@ async function tryNTimes<T>(
await new Promise<void>((resolve) => { await new Promise<void>((resolve) => {
setTimeout(() => resolve(), delayBetweenAttempts) setTimeout(() => resolve(), delayBetweenAttempts)
}) })
if (repeatCount === 1) { if (repeatCount <= 1) {
console.error(`No more attempts left every ${delayBetweenAttempts}`) console.error(`No more attempts left every ${delayBetweenAttempts}`)
if (failFunc) { if (failFunc) {
return failFunc() return failFunc()
@ -614,7 +621,7 @@ async function tryNTimes<T>(
async function tryNTimesVoidReturn( async function tryNTimesVoidReturn(
func: () => Promise<void> | void, func: () => Promise<void> | void,
repeatCount = 5, repeatCount = 2,
delayBetweenAttempts = DELAY_BETWEEN_ATTEMPTS delayBetweenAttempts = DELAY_BETWEEN_ATTEMPTS
): Promise<void> { ): Promise<void> {
return tryNTimes(func, () => undefined, repeatCount, delayBetweenAttempts) return tryNTimes(func, () => undefined, repeatCount, delayBetweenAttempts)

View File

@ -9,14 +9,33 @@ export default class ExpressAccessors<
ElementNoId extends object, ElementNoId extends object,
Element extends ElementWithId<ElementNoId> Element extends ElementWithId<ElementNoId>
> { > {
sheet: Sheet<ElementNoId, Element> sheet?: Sheet<ElementNoId, Element>
runAfterLoad: ((value: Sheet<ElementNoId, Element>) => void)[] = []
isLoaded = false
constructor( constructor(
readonly sheetName: keyof SheetNames, readonly sheetName: keyof SheetNames,
readonly specimen: Element, readonly specimen: Element,
readonly translation: { [k in keyof Element]: string } readonly translation: { [k in keyof Element]: string }
) { ) {
this.sheet = getSheet<ElementNoId, Element>(sheetName, specimen, translation) getSheet<ElementNoId, Element>(this.sheetName, this.specimen, this.translation).then(
(sheet) => {
this.sheet = sheet
this.isLoaded = true
this.runAfterLoad.map((f) => f(sheet))
}
)
}
async getSheet(): Promise<Sheet<ElementNoId, Element>> {
if (!this.isLoaded) {
await new Promise((resolve) => {
this.runAfterLoad.push(resolve)
})
}
return this.sheet as Sheet<ElementNoId, Element>
} }
listGet() { listGet() {
@ -26,7 +45,7 @@ export default class ExpressAccessors<
_next: NextFunction _next: NextFunction
): Promise<void> => { ): Promise<void> => {
try { try {
const elements = await this.sheet.getList() const elements = await (await this.getSheet()).getList()
if (elements) { if (elements) {
response.status(200).json(elements) response.status(200).json(elements)
} }
@ -42,7 +61,8 @@ export default class ExpressAccessors<
) { ) {
return async (request: Request, response: Response, _next: NextFunction): Promise<void> => { return async (request: Request, response: Response, _next: NextFunction): Promise<void> => {
try { try {
const list = (await this.sheet.getList()) || [] const sheet = await this.getSheet()
const list = (await sheet.getList()) || []
let toCaller: any let toCaller: any
if (!custom) { if (!custom) {
const id = parseInt(request.query.id as string, 10) || -1 const id = parseInt(request.query.id as string, 10) || -1
@ -65,7 +85,8 @@ export default class ExpressAccessors<
add() { add() {
return async (request: Request, response: Response, _next: NextFunction): Promise<void> => { return async (request: Request, response: Response, _next: NextFunction): Promise<void> => {
try { try {
const element: Element = await this.sheet.add(request.body) const sheet = await this.getSheet()
const element: Element = await sheet.add(request.body)
if (element) { if (element) {
response.status(200).json(element) response.status(200).json(element)
} }
@ -85,15 +106,16 @@ export default class ExpressAccessors<
) { ) {
return async (request: Request, response: Response, _next: NextFunction): Promise<void> => { return async (request: Request, response: Response, _next: NextFunction): Promise<void> => {
try { try {
const sheet = await this.getSheet()
if (!custom) { if (!custom) {
await this.sheet.set(request.body) await sheet.set(request.body)
response.status(200) response.status(200)
} else { } else {
const memberId = response?.locals?.jwt?.id || -1 const memberId = response?.locals?.jwt?.id || -1
const list = (await this.sheet.getList()) || [] const list = (await sheet.getList()) || []
const { toDatabase, toCaller } = await custom(list, request.body, memberId) const { toDatabase, toCaller } = await custom(list, request.body, memberId)
if (toDatabase !== undefined) { if (toDatabase !== undefined) {
await this.sheet.set(toDatabase) await sheet.set(toDatabase)
} }
if (toCaller !== undefined) { if (toCaller !== undefined) {
response.status(200).json(toCaller) response.status(200).json(toCaller)

View File

@ -80,7 +80,7 @@ export function notificationMain(): void {
} }
async function notifyAboutAnnouncement(): Promise<void> { async function notifyAboutAnnouncement(): Promise<void> {
const announcementSheet = getSheet<AnnouncementWithoutId, Announcement>( const announcementSheet = await getSheet<AnnouncementWithoutId, Announcement>(
"Announcements", "Announcements",
new Announcement(), new Announcement(),
translationAnnouncement translationAnnouncement
@ -96,7 +96,7 @@ async function notifyAboutAnnouncement(): Promise<void> {
return return
} }
const volunteerSheet = getSheet<VolunteerWithoutId, Volunteer>( const volunteerSheet = await getSheet<VolunteerWithoutId, Volunteer>(
"Volunteers", "Volunteers",
new Volunteer(), new Volunteer(),
translationVolunteer translationVolunteer