improve error handling in background jobs #3

This commit is contained in:
Michi 2025-10-11 11:56:17 +02:00
parent 6a60e3c10a
commit 04ae271e1e
8 changed files with 75 additions and 27 deletions

View file

@ -0,0 +1,49 @@
// Error classes for background jobs
// Class for custom background errors
export class BackgroundJobError extends Error{
cause?: unknown;
constructor(message: string, cause?: unknown){
super(message);
this.name = this.constructor.name;
this.cause = cause;
}
}
// Errors based on class BackgroundJobError
export class BackgroundDatabaseError extends BackgroundJobError{
constructor(cause?: unknown){
super('Database request failed.', cause);
}
}
export class BackgroundFetchError extends BackgroundJobError{
constructor(cause?: unknown){
super('Fetching data failed', cause);
}
}
export class AttractionImportError extends BackgroundJobError{
constructor(cause?: unknown){
super('Failed to import attractions into database.', cause);
}
}
export class ThemeparkUpdateError extends BackgroundJobError{
constructor(cause?: unknown){
super('Failed to update themepark data.', cause);
}
}
export class KVParseError extends BackgroundJobError{
constructor(key: string, cause?: unknown){
super(`Failed to parse JSON from KV, affected key: ${key}`, cause);
}
}
export class SendNotificationError extends BackgroundJobError{
constructor(cause?: unknown){
super('Failed to send notification.', cause);
}
}

View file

@ -3,6 +3,7 @@ import { getDbEnv } from '../db/client'
import { subscribedThemeparks, attractionSubscriptions } from "../db/schema"; import { subscribedThemeparks, attractionSubscriptions } from "../db/schema";
import { SubscribedThemeparks } from "../types/subscribed-themeparks"; import { SubscribedThemeparks } from "../types/subscribed-themeparks";
import { AttractionSubscription } from "../types/attraction-subscriptions"; import { AttractionSubscription } from "../types/attraction-subscriptions";
import { KVParseError, SendNotificationError } from "../errors/background-error";
import httpRequest from "../lib/http-request"; import httpRequest from "../lib/http-request";
import fetchAttractions from "../lib/fetch-attractions"; import fetchAttractions from "../lib/fetch-attractions";
@ -11,6 +12,7 @@ import fetchAttractions from "../lib/fetch-attractions";
* waittime in cache & send notification about changes in waittime. * waittime in cache & send notification about changes in waittime.
* @param env Connection to Cloudflare * @param env Connection to Cloudflare
*/ */
// TODO: split into batches by cron, when more than 10 parks have to be fetched -> like update-attraction-list.ts
export default async function updateWaittimes(env: Env): Promise<void>{ export default async function updateWaittimes(env: Env): Promise<void>{
const db = getDbEnv(env); const db = getDbEnv(env);
const subscribedParks = await db.select().from(subscribedThemeparks); const subscribedParks = await db.select().from(subscribedThemeparks);
@ -54,7 +56,7 @@ async function getJsonFromKV<T>(env: Env, key: string, defaultValue: T): Promise
return JSON.parse(cache) as T; return JSON.parse(cache) as T;
} }
catch(e){ catch(e){
throw new Error(`Failed to parse JSON from KV, affected key: ${key}, error: ${e}`); throw new KVParseError(key, e);
} }
} }
@ -131,7 +133,7 @@ async function sendNotification(webhookUrl: string, message: string, type: strin
}); });
} }
catch(e){ catch(e){
throw new Error(`Failed to send notification: ${e}`); throw new SendNotificationError(e);
} }
} }

View file

@ -3,6 +3,7 @@ import { attraction, themepark } from '../db/schema'
import { inArray } from 'drizzle-orm' import { inArray } from 'drizzle-orm'
import { Attraction } from '../types/attraction' import { Attraction } from '../types/attraction'
import { ThemeparkSelect } from '../types/themepark' import { ThemeparkSelect } from '../types/themepark'
import { AttractionImportError, BackgroundDatabaseError } from '../errors/background-error'
import asyncBatchJob from '../lib/async-batch-job' import asyncBatchJob from '../lib/async-batch-job'
import fetchAttractions from '../lib/fetch-attractions' import fetchAttractions from '../lib/fetch-attractions'
@ -24,7 +25,7 @@ async function getThemeparks(env: Env): Promise<ThemeparkAPI[]>{
return themeparks; return themeparks;
} }
catch(e){ catch(e){
throw new Error(`Failed to get themeparks from database: ${e}`); throw new BackgroundDatabaseError(e);
} }
} }
@ -50,7 +51,7 @@ async function getAttractionsByParks(env: Env, parks: ThemeparkAPI[]): Promise<A
return attractions; return attractions;
} }
catch(e){ catch(e){
throw new Error(`Failed to get attractions from database: ${e}`); throw new BackgroundDatabaseError(e);
} }
} }
@ -86,7 +87,7 @@ async function importAttractionsByParks(env: Env, parks: ThemeparkAPI[]): Promis
} }
} }
catch(e){ catch(e){
throw new Error(`Failed to import attractions into database: ${e}`); throw new AttractionImportError(e);
} }
} }
@ -99,7 +100,6 @@ async function importAttractionsByParks(env: Env, parks: ThemeparkAPI[]): Promis
* @param cron The cron statement specified to run the background jobs; used for batch size calculation * @param cron The cron statement specified to run the background jobs; used for batch size calculation
*/ */
export async function batchAttractionImport(env: Env, timestamp: number, cron: string): Promise<void>{ export async function batchAttractionImport(env: Env, timestamp: number, cron: string): Promise<void>{
try{
const themeparks = await getThemeparks(env); // all themeparks const themeparks = await getThemeparks(env); // all themeparks
const executionHour = new Date(timestamp).getUTCHours(); // current hour, in which job is executed const executionHour = new Date(timestamp).getUTCHours(); // current hour, in which job is executed
const executionTimes = getExecutionCountFromCron(cron, 1); // how often the job is executed const executionTimes = getExecutionCountFromCron(cron, 1); // how often the job is executed
@ -116,10 +116,6 @@ export async function batchAttractionImport(env: Env, timestamp: number, cron: s
// import attractions from current time batch // import attractions from current time batch
await importAttractionsByParks(env, batch); await importAttractionsByParks(env, batch);
}
catch(e){
throw new Error(`Failed to split attraction import by time: ${e}`);
}
} }
/** /**

View file

@ -1,6 +1,7 @@
import { getDbEnv } from '../db/client' import { getDbEnv } from '../db/client'
import { themepark } from '../db/schema' import { themepark } from '../db/schema'
import { countryCodesDE } from '../lib/countries' import { countryCodesDE } from '../lib/countries'
import { BackgroundFetchError, ThemeparkUpdateError } from '../errors/background-error'
import httpRequest from '../lib/http-request' import httpRequest from '../lib/http-request'
import asyncBatchJob from '../lib/async-batch-job' import asyncBatchJob from '../lib/async-batch-job'
@ -30,7 +31,7 @@ async function fetchThemeparks(
return result; return result;
} }
catch(e){ catch(e){
throw new Error(`Fetching themeparks failed: ${e}`); throw new BackgroundFetchError(e);
} }
} }
@ -67,6 +68,6 @@ export async function updateThemeparkData(env: Env): Promise<void>{
} }
} }
catch(e){ catch(e){
console.error(`Failed to update themepark data: ${e}`); throw new ThemeparkUpdateError(e);
} }
} }

View file

@ -4,7 +4,7 @@ import { UserSelect } from "../types/user";
import { user } from "../db/schema"; import { user } from "../db/schema";
import { like } from "drizzle-orm"; import { like } from "drizzle-orm";
import { DrizzleD1Database } from "drizzle-orm/d1"; import { DrizzleD1Database } from "drizzle-orm/d1";
import { MissingMailError, UserInactiveError, DatabaseError } from "../types/error"; import { MissingMailError, UserInactiveError, DatabaseError } from "../errors/http-error";
/** /**
* Returns the details of a user from the given context * Returns the details of a user from the given context

View file

@ -2,7 +2,7 @@ import { Hono, Context } from 'hono'
import { getDbContext } from '../db/client' import { getDbContext } from '../db/client'
import { attractionNotification, notificationMethod } from '../db/schema' import { attractionNotification, notificationMethod } from '../db/schema'
import { and, eq } from 'drizzle-orm' import { and, eq } from 'drizzle-orm'
import { DatabaseError, InvalidParameter, MissingParameter } from '../types/error' import { DatabaseError, InvalidParameter, MissingParameter } from '../errors/http-error'
import { getUser } from '../lib/user-auth' import { getUser } from '../lib/user-auth'
import { Message } from '../types/response' import { Message } from '../types/response'
import { NotificationMethodSelect } from '../types/notification-method' import { NotificationMethodSelect } from '../types/notification-method'

View file

@ -3,7 +3,7 @@ import { getDbContext } from '../db/client'
import { themepark, attraction } from '../db/schema' import { themepark, attraction } from '../db/schema'
import { responseCache } from '../lib/cache' import { responseCache } from '../lib/cache'
import { eq } from 'drizzle-orm' import { eq } from 'drizzle-orm'
import { DatabaseError } from '../types/error' import { DatabaseError } from '../errors/http-error'
const app = new Hono() const app = new Hono()