From 44ffa703b9b4e77deeee067956cc114a40dc1d5b Mon Sep 17 00:00:00 2001 From: Rushil Perera Date: Sat, 5 Oct 2024 14:06:57 -0400 Subject: [PATCH] refactor: replace qstash with Google Cloud Tasks --- bun.lockb | Bin 167047 -> 166627 bytes bunfig.toml | 1 - drizzle/0009_outstanding_trauma.sql | 1 + drizzle/meta/0009_snapshot.json | 138 ++++++++++++++++++ drizzle/meta/_journal.json | 7 + package.json | 1 - src/controllers/internal/new-episode/index.ts | 6 - .../internal/upcoming-titles/index.ts | 7 +- src/controllers/watch-status/index.spec.ts | 15 +- src/controllers/watch-status/index.ts | 41 +++--- src/libs/deleteMessageIdForTitle.ts | 24 --- src/libs/maybeScheduleNextAiringEpisode.ts | 25 +--- src/libs/qstash/verifyQstashHeader.ts | 35 ----- src/libs/tasks/id.ts | 7 + src/libs/tasks/queueName.ts | 1 + src/libs/tasks/queueTask.ts | 130 +++++++++++++++++ src/libs/tasks/removeTask.ts | 24 +++ src/mocks/qstash.ts | 23 --- src/models/schema.ts | 6 - src/models/titleMessages.ts | 34 ----- src/scripts/initializeNextEpisodeQueue.ts | 35 ++--- 21 files changed, 354 insertions(+), 207 deletions(-) create mode 100644 drizzle/0009_outstanding_trauma.sql create mode 100644 drizzle/meta/0009_snapshot.json delete mode 100644 src/libs/deleteMessageIdForTitle.ts delete mode 100644 src/libs/qstash/verifyQstashHeader.ts create mode 100644 src/libs/tasks/id.ts create mode 100644 src/libs/tasks/queueName.ts create mode 100644 src/libs/tasks/queueTask.ts create mode 100644 src/libs/tasks/removeTask.ts delete mode 100644 src/mocks/qstash.ts delete mode 100644 src/models/titleMessages.ts diff --git a/bun.lockb b/bun.lockb index 7e6fd7eab3b132171b574242f3b831b64f645f0c..ded1c28e64f74fedcac16ee7fc7a679885be3b33 100755 GIT binary patch delta 29226 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 AGXMYp delta 29247 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, "workerd"), c.req))) { - return c.json(ErrorResponse, { status: 401 }); - } - if (!(await isWatchingTitle(env(c, "workerd"), aniListId))) { console.log(`Title ${aniListId} is no longer being watched`); return c.json( diff --git a/src/controllers/internal/upcoming-titles/index.ts b/src/controllers/internal/upcoming-titles/index.ts index ee44111..70bc807 100644 --- a/src/controllers/internal/upcoming-titles/index.ts +++ b/src/controllers/internal/upcoming-titles/index.ts @@ -4,19 +4,14 @@ import { DateTime } from "luxon"; import { getAdminSdkCredentials } from "~/libs/gcloud/getAdminSdkCredentials"; import { sendFcmMessage } from "~/libs/gcloud/sendFcmMessage"; -import { verifyQstashHeader } from "~/libs/qstash/verifyQstashHeader"; import type { Env } from "~/types/env"; -import { ErrorResponse, SuccessResponse } from "~/types/schema"; +import { SuccessResponse } from "~/types/schema"; import { getUpcomingTitlesFromAnilist } from "./anilist"; const app = new Hono(); app.post("/", async (c) => { - if (!(await verifyQstashHeader(env(c, "workerd"), c.req))) { - return c.json(ErrorResponse, { status: 401 }); - } - const titles = await getUpcomingTitlesFromAnilist( env(c, "workerd"), c.req, diff --git a/src/controllers/watch-status/index.spec.ts b/src/controllers/watch-status/index.spec.ts index 4aefa64..c278b26 100644 --- a/src/controllers/watch-status/index.spec.ts +++ b/src/controllers/watch-status/index.spec.ts @@ -7,11 +7,7 @@ import { getTestDb } from "~/libs/test/getTestDb"; import { getTestEnv } from "~/libs/test/getTestEnv"; import { resetTestDb } from "~/libs/test/resetTestDb"; import { server } from "~/mocks"; -import { - deviceTokensTable, - titleMessagesTable, - watchStatusTable, -} from "~/models/schema"; +import { deviceTokensTable, watchStatusTable } from "~/models/schema"; server.listen(); @@ -100,9 +96,6 @@ describe("requests the /watch-status route", () => { await db .insert(deviceTokensTable) .values({ deviceId: "123", token: "asd" }); - await db - .insert(titleMessagesTable) - .values({ titleId: 10, messageId: "123" }); const res = await app.request( "/watch-status", @@ -129,9 +122,6 @@ describe("requests the /watch-status route", () => { await db .insert(deviceTokensTable) .values({ deviceId: "123", token: "asd" }); - await db - .insert(titleMessagesTable) - .values({ titleId: -1, messageId: "123" }); const res = await app.request( "/watch-status", @@ -158,9 +148,6 @@ describe("requests the /watch-status route", () => { await db .insert(deviceTokensTable) .values({ deviceId: "123", token: "asd" }); - await db - .insert(titleMessagesTable) - .values({ titleId: 139518, messageId: "123" }); const res = await app.request("/watch-status", { method: "POST", diff --git a/src/controllers/watch-status/index.ts b/src/controllers/watch-status/index.ts index a45668a..9b237c7 100644 --- a/src/controllers/watch-status/index.ts +++ b/src/controllers/watch-status/index.ts @@ -1,12 +1,10 @@ import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi"; -import { Client } from "@upstash/qstash"; import { env } from "hono/adapter"; -import { deleteMessageIdForTitle } from "~/libs/deleteMessageIdForTitle"; import { maybeScheduleNextAiringEpisode } from "~/libs/maybeScheduleNextAiringEpisode"; -import { verifyQstashHeader } from "~/libs/qstash/verifyQstashHeader"; -import { readEnvVariable } from "~/libs/readEnvVariable"; -import { deleteTitleMessage, getTitleMessage } from "~/models/titleMessages"; +import { buildNewEpisodeTaskId } from "~/libs/tasks/id"; +import { queueTask } from "~/libs/tasks/queueTask"; +import { removeTask } from "~/libs/tasks/removeTask"; import { setWatchStatus } from "~/models/watchStatus"; import type { Env } from "~/types/env"; import { @@ -75,13 +73,8 @@ app.openapi(route, async (c) => { isRetrying = false, } = await c.req.json(); const aniListToken = c.req.header("X-AniList-Token"); - const client = new Client({ token: readEnvVariable(c.env, "QSTASH_TOKEN") }); - if (isRetrying) { - if (!(await verifyQstashHeader(env(c, "workerd"), c.req))) { - return c.json(ErrorResponse, { status: 401 }); - } - } else { + if (!isRetrying) { try { const { wasAdded, wasDeleted } = await setWatchStatus( env(c, "workerd"), @@ -96,9 +89,10 @@ app.openapi(route, async (c) => { titleId, ); } else if (wasDeleted) { - await deleteMessageIdForTitle( + await removeTask( env(c, "workerd"), - titleId, + "new-episode", + buildNewEpisodeTaskId(titleId), ); } } catch (error) { @@ -118,12 +112,21 @@ app.openapi(route, async (c) => { console.error( new Error("Failed to update watch status on Anilist", { cause: error }), ); - client.publishJSON({ - url: c.req.url, - body: { deviceId, watchStatus, titleId, isRetrying: true }, - retries: 3, - delay: "1m", - }); + if (isRetrying) { + return c.json(ErrorResponse, { status: 500 }); + } + + await queueTask( + env(c, "workerd"), + "anilist", + { + deviceId, + watchStatus, + titleId, + isRetrying: true, + }, + { req: c.req, scheduleConfig: { delay: { minute: 1 } } }, + ); } return c.json(SuccessResponse, { status: 200 }); diff --git a/src/libs/deleteMessageIdForTitle.ts b/src/libs/deleteMessageIdForTitle.ts deleted file mode 100644 index f11221a..0000000 --- a/src/libs/deleteMessageIdForTitle.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Client } from "@upstash/qstash"; - -import { deleteTitleMessage, getTitleMessage } from "~/models/titleMessages"; -import type { Env } from "~/types/env"; - -import { readEnvVariable } from "./readEnvVariable"; - -export async function deleteMessageIdForTitle(env: Env, titleId: number) { - const messageId = await getTitleMessage(env, titleId); - if (!messageId) { - return; - } - - try { - const client = new Client({ token: readEnvVariable(env, "QSTASH_TOKEN") }); - await client.messages.delete(messageId); - } catch (error) { - if (!error.message.includes("not found")) { - throw error; - } - } - - await deleteTitleMessage(env, titleId); -} diff --git a/src/libs/maybeScheduleNextAiringEpisode.ts b/src/libs/maybeScheduleNextAiringEpisode.ts index 65d0ee6..62cf56d 100644 --- a/src/libs/maybeScheduleNextAiringEpisode.ts +++ b/src/libs/maybeScheduleNextAiringEpisode.ts @@ -1,7 +1,5 @@ -import { Client } from "@upstash/qstash"; import type { HonoRequest } from "hono"; -import { setTitleMessage } from "~/models/titleMessages"; import { addUnreleasedTitle, removeUnreleasedTitle, @@ -9,8 +7,8 @@ import { import type { Env } from "~/types/env"; import { getNextEpisodeTimeUntilAiring } from "./anilist/getNextEpisodeAiringAt"; -import { deleteMessageIdForTitle } from "./deleteMessageIdForTitle"; import { getCurrentDomain } from "./getCurrentDomain"; +import { queueTask } from "./tasks/queueTask"; export async function maybeScheduleNextAiringEpisode( env: Env, @@ -26,24 +24,17 @@ export async function maybeScheduleNextAiringEpisode( if (!nextAiring) { if (status === "NOT_YET_RELEASED") { await addUnreleasedTitle(env, aniListId); - } else { - await deleteMessageIdForTitle(env, aniListId); } return; } const { airingAt, episode: nextEpisode } = nextAiring; - const client = new Client({ token: env.QSTASH_TOKEN }); - - const { messageId } = await client.publishJSON({ - url: `${domain}/internal/new-episode`, - body: { aniListId, episodeNumber: nextEpisode }, - retries: 3, - notBefore: airingAt, - }); - await Promise.allSettled([ - setTitleMessage(env, aniListId, messageId), - removeUnreleasedTitle(env, aniListId), - ]); + await queueTask( + env, + "new-episode", + { aniListId, episodeNumber: nextEpisode }, + { req, scheduleConfig: { epochTime: airingAt } }, + ); + await removeUnreleasedTitle(env, aniListId); } diff --git a/src/libs/qstash/verifyQstashHeader.ts b/src/libs/qstash/verifyQstashHeader.ts deleted file mode 100644 index 8dd5c75..0000000 --- a/src/libs/qstash/verifyQstashHeader.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Receiver, SignatureError } from "@upstash/qstash"; -import type { HonoRequest } from "hono"; - -import type { Env } from "~/types/env"; - -export async function verifyQstashHeader( - env: Env, - req: HonoRequest, -): Promise { - const signature = req.header("Upstash-Signature"); - if (!signature) { - return Promise.resolve(false); - } - - try { - const receiver = new Receiver({ - currentSigningKey: env.QSTASH_CURRENT_SIGNING_KEY, - nextSigningKey: env.QSTASH_NEXT_SIGNING_KEY, - }); - - return await receiver.verify({ - body: await req.text(), - signature, - url: req.url.startsWith("http://localhost") - ? req.url - : req.url.replace("http://", "https://"), - }); - } catch (error) { - if (error instanceof SignatureError) { - return Promise.resolve(false); - } - - throw error; - } -} diff --git a/src/libs/tasks/id.ts b/src/libs/tasks/id.ts new file mode 100644 index 0000000..8810fb5 --- /dev/null +++ b/src/libs/tasks/id.ts @@ -0,0 +1,7 @@ +export function buildNewEpisodeTaskId(aniListId: number) { + return `${aniListId}`; +} + +export function buildAnilistRetryTaskId(deviceId: string, titleId: number) { + return `${deviceId}-${titleId}`; +} diff --git a/src/libs/tasks/queueName.ts b/src/libs/tasks/queueName.ts new file mode 100644 index 0000000..0e61e25 --- /dev/null +++ b/src/libs/tasks/queueName.ts @@ -0,0 +1 @@ +export type QueueName = "anilist" | "new-episode"; diff --git a/src/libs/tasks/queueTask.ts b/src/libs/tasks/queueTask.ts new file mode 100644 index 0000000..691a738 --- /dev/null +++ b/src/libs/tasks/queueTask.ts @@ -0,0 +1,130 @@ +import type { HonoRequest } from "hono"; +import { DateTime, type DurationLike } from "luxon"; + +import type { Env } from "~/types/env"; +import type { WatchStatus } from "~/types/title/watchStatus"; + +import { getAdminSdkCredentials } from "../gcloud/getAdminSdkCredentials"; +import { getGoogleAuthToken } from "../gcloud/getGoogleAuthToken"; +import { getCurrentDomain } from "../getCurrentDomain"; +import { buildAnilistRetryTaskId, buildNewEpisodeTaskId } from "./id"; +import type { QueueName } from "./queueName"; + +type QueueBody = { + anilist: { + deviceId: string; + watchStatus: WatchStatus | null; + titleId: number; + isRetrying: true; + }; + "new-episode": { aniListId: number; episodeNumber: number }; +}; + +type ScheduleConfig = + | { delay: DurationLike; epochTime: never } + | { epochTime: number; delay: never }; + +interface QueueTaskOptionalArgs { + taskId?: string; + scheduleConfig?: ScheduleConfig; + req?: HonoRequest; +} + +export async function queueTask( + env: Env, + queueName: QueueName, + body: QueueBody[QueueName], + { taskId, scheduleConfig, req }: QueueTaskOptionalArgs = {}, +) { + const domain = req + ? getCurrentDomain(req) + : "https://aniplay-v2.rururu.workers.dev"; + if (!domain) { + console.log("Skipping queue task due to local domain", queueName, body); + return; + } + + const adminSdkCredentials = getAdminSdkCredentials(env); + const { projectId } = adminSdkCredentials; + + await fetch( + `https://content-cloudtasks.googleapis.com/v2/projects/${projectId}/locations/northamerica-northeast1/queues/${queueName}/tasks?alt=json`, + { + headers: { + Authorization: `Bearer ${await getGoogleAuthToken(adminSdkCredentials)}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + task: buildTask( + projectId, + queueName, + taskId, + scheduleConfig, + domain, + body, + req.header(), + ), + }), + method: "POST", + }, + ); +} + +function buildTask( + projectId: string, + queueName: QueueName, + taskId: string | undefined, + scheduleConfig: ScheduleConfig | undefined, + domain: string, + body: QueueBody[QueueName], + headers: Record, +) { + let scheduleTime: string | undefined; + if (scheduleConfig) { + const { delay, epochTime } = scheduleConfig; + if (epochTime) { + scheduleTime = DateTime.fromSeconds(epochTime).toUTC().toISO(); + } else if (delay) { + scheduleTime = DateTime.now().plus(delay).toUTC().toISO(); + } + } + + switch (queueName) { + case "new-episode": + const { aniListId } = body as QueueBody["new-episode"]; + taskId ??= buildNewEpisodeTaskId(aniListId); + + return { + name: `projects/${projectId}/locations/northamerica-northeast1/queues/${queueName}/tasks/${taskId}`, + scheduleTime, + httpRequest: { + url: `${domain}/internal/new-episode`, + httpMethod: "POST", + body: JSON.stringify(body), + headers: { + "Content-Type": "application/json", + "X-Anilist-Token": headers["X-Anilist-Token"], + }, + }, + }; + case "anilist": + const { deviceId, titleId } = body as QueueBody["anilist"]; + taskId ??= buildAnilistRetryTaskId(deviceId, titleId); + + return { + name: `projects/${projectId}/locations/northamerica-northeast1/queues/${queueName}/tasks/${taskId}`, + scheduleTime, + httpRequest: { + url: `${domain}/watch-status`, + httpMethod: "POST", + body: JSON.stringify(body), + headers: { + "Content-Type": "application/json", + "X-Anilist-Token": headers["X-Anilist-Token"], + }, + }, + }; + default: + throw new Error(`Unknown queue name: ${queueName}`); + } +} diff --git a/src/libs/tasks/removeTask.ts b/src/libs/tasks/removeTask.ts new file mode 100644 index 0000000..1211dd9 --- /dev/null +++ b/src/libs/tasks/removeTask.ts @@ -0,0 +1,24 @@ +import type { Env } from "~/types/env"; + +import { getAdminSdkCredentials } from "../gcloud/getAdminSdkCredentials"; +import { getGoogleAuthToken } from "../gcloud/getGoogleAuthToken"; +import type { QueueName } from "./queueName"; + +export async function removeTask( + env: Env, + queueName: QueueName, + taskId: string, +) { + const adminSdkCredentials = getAdminSdkCredentials(env); + const { projectId } = adminSdkCredentials; + + await fetch( + `https://content-cloudtasks.googleapis.com/v2/projects/${projectId}/locations/northamerica-northeast1/queues/${queueName}/tasks/${taskId}`, + { + headers: { + Authorization: `Bearer ${await getGoogleAuthToken(adminSdkCredentials)}`, + }, + method: "DELETE", + }, + ); +} diff --git a/src/mocks/qstash.ts b/src/mocks/qstash.ts deleted file mode 100644 index 5141d06..0000000 --- a/src/mocks/qstash.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { SignatureError } from "@upstash/qstash"; - -import { mock } from "bun:test"; - -class MockQstashMessages { - delete = mock(); -} - -class MockQstashClient { - batchJSON = mock(); - publishJSON = mock().mockResolvedValue({ messageId: "123" }); - messages = new MockQstashMessages(); -} - -class MockQstashReceiver { - verify = mock(); -} - -mock.module("@upstash/qstash", () => ({ - Client: MockQstashClient, - Receiver: MockQstashReceiver, - SignatureError, -})); diff --git a/src/models/schema.ts b/src/models/schema.ts index a67eb1f..101fca4 100644 --- a/src/models/schema.ts +++ b/src/models/schema.ts @@ -36,11 +36,6 @@ export const keyValueTable = sqliteTable("key_value", { value: text("value").notNull(), }); -export const titleMessagesTable = sqliteTable("title_messages", { - titleId: integer("title_id").notNull().primaryKey(), - messageId: text("message_id").notNull(), -}); - /** Used to keep track of titles that haven't been released yet and the time when the first episode will be released is unknown */ export const unreleasedTitlesTable = sqliteTable("unreleased_titles", { titleId: integer("title_id").notNull().primaryKey(), @@ -51,6 +46,5 @@ export const tables = [ watchStatusTable, deviceTokensTable, keyValueTable, - titleMessagesTable, unreleasedTitlesTable, ]; diff --git a/src/models/titleMessages.ts b/src/models/titleMessages.ts deleted file mode 100644 index 9d6604f..0000000 --- a/src/models/titleMessages.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { eq } from "drizzle-orm"; - -import type { Env } from "~/types/env"; - -import { getDb } from "./db"; -import { titleMessagesTable } from "./schema"; - -export function setTitleMessage(env: Env, titleId: number, messageId: string) { - return getDb(env) - .insert(titleMessagesTable) - .values({ titleId, messageId }) - .onConflictDoUpdate({ - set: { messageId }, - target: [titleMessagesTable.titleId], - }); -} - -export function getTitleMessage( - env: Env, - titleId: number, -): Promise { - return getDb(env) - .select() - .from(titleMessagesTable) - .where(eq(titleMessagesTable.titleId, titleId)) - .then((results) => results[0]?.messageId); -} - -export function deleteTitleMessage(env: Env, titleId: number) { - return getDb(env) - .delete(titleMessagesTable) - .where(eq(titleMessagesTable.titleId, titleId)) - .run(); -} diff --git a/src/scripts/initializeNextEpisodeQueue.ts b/src/scripts/initializeNextEpisodeQueue.ts index 0fdd177..31a83fa 100644 --- a/src/scripts/initializeNextEpisodeQueue.ts +++ b/src/scripts/initializeNextEpisodeQueue.ts @@ -1,6 +1,5 @@ -import { Client } from "@upstash/qstash"; - import { fetchTitleFromAnilist } from "~/libs/anilist/getTitle"; +import { queueTask } from "~/libs/tasks/queueTask"; import { getDb } from "~/models/db"; import { watchStatusTable } from "~/models/schema"; @@ -85,15 +84,10 @@ async function triggerNextEpisodeRoute(titleId: number) { return success; }); } else { - return new Client({ token: process.env.QSTASH_TOKEN }) - .publishJSON({ - url: "https://aniplay-v2.rururu.workers.dev/internal/new-episode", - body: { - aniListId: titleId, - episodeNumber: mostRecentEpisodeNumber, - }, - retries: 3, - }) + return queueTask(process.env, "new-episode", { + aniListId: titleId, + episodeNumber: mostRecentEpisodeNumber, + }) .then(() => true) .catch((error) => { console.error( @@ -117,16 +111,15 @@ async function triggerNextEpisodeRoute(titleId: number) { } } - return new Client({ token: process.env.QSTASH_TOKEN }) - .publishJSON({ - url: "https://aniplay-v2.rururu.workers.dev/internal/new-episode", - body: { - aniListId: titleId, - episodeNumber: title.nextAiringEpisode.episode, - }, - retries: 3, - notBefore: title.nextAiringEpisode.airingAt, - }) + return queueTask( + process.env, + "new-episode", + { + aniListId: titleId, + episodeNumber: title.nextAiringEpisode.episode, + }, + { scheduleConfig: { epochTime: title.nextAiringEpisode.airingAt } }, + ) .then(() => true) .catch((error) => { console.error(