Properly handle spans of image downloading (#1176)

This commit is contained in:
Zoe Roux 2025-11-23 19:00:54 +01:00 committed by GitHub
commit f59cb5d671
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 78 additions and 61 deletions

View File

@ -37,3 +37,4 @@ jobs:
run: bun test run: bun test
env: env:
PGHOST: localhost PGHOST: localhost
IMAGES_PATH: ./images

View File

@ -73,7 +73,7 @@ export const auth = new Elysia({ name: "auth" })
.macro({ .macro({
permissions(perms: string[]) { permissions(perms: string[]) {
return { return {
beforeHandle: ({ jwt, status }) => { beforeHandle: function permissionCheck({ jwt, status }) {
for (const perm of perms) { for (const perm of perms) {
if (!jwt!.permissions.includes(perm)) { if (!jwt!.permissions.includes(perm)) {
return status(403, { return status(403, {

View File

@ -1,4 +1,6 @@
import path from "node:path"; import path from "node:path";
import { getCurrentSpan, record, setAttributes } from "@elysiajs/opentelemetry";
import { SpanStatusCode } from "@opentelemetry/api";
import { encode } from "blurhash"; import { encode } from "blurhash";
import { and, eq, is, lt, type SQL, sql } from "drizzle-orm"; import { and, eq, is, lt, type SQL, sql } from "drizzle-orm";
import { PgColumn, type PgTable } from "drizzle-orm/pg-core"; import { PgColumn, type PgTable } from "drizzle-orm/pg-core";
@ -78,44 +80,7 @@ export const enqueueOptImage = async (
}; };
export const processImages = async () => { export const processImages = async () => {
async function processOne() { return record("download images", async () => {
return await db.transaction(async (tx) => {
const [item] = await tx
.select()
.from(mqueue)
.for("update", { skipLocked: true })
.where(and(eq(mqueue.kind, "image"), lt(mqueue.attempt, 5)))
.orderBy(mqueue.attempt, mqueue.createdAt)
.limit(1);
if (!item) return false;
const img = item.message as ImageTask;
try {
const blurhash = await downloadImage(img.id, img.url);
const ret: Image = { id: img.id, source: img.url, blurhash };
const table = sql.raw(img.table);
const column = sql.raw(img.column);
await tx.execute(sql`
update ${table} set ${column} = ${ret}
where ${column}->'id' = ${sql.raw(`'"${img.id}"'::jsonb`)}
`);
await tx.delete(mqueue).where(eq(mqueue.id, item.id));
} catch (err: any) {
console.error("Failed to download image", img.url, err.message);
// don't use the transaction here, it can be aborted.
await db
.update(mqueue)
.set({ attempt: sql`${mqueue.attempt}+1` })
.where(eq(mqueue.id, item.id));
}
return true;
});
}
let running = false; let running = false;
async function processAll() { async function processAll() {
if (running) return; if (running) return;
@ -138,8 +103,55 @@ export const processImages = async () => {
// start processing old tasks // start processing old tasks
await processAll(); await processAll();
return () => client.release(true); return () => client.release(true);
});
}; };
async function processOne() {
return record("download", async () => {
return await db.transaction(async (tx) => {
const [item] = await tx
.select()
.from(mqueue)
.for("update", { skipLocked: true })
.where(and(eq(mqueue.kind, "image"), lt(mqueue.attempt, 5)))
.orderBy(mqueue.attempt, mqueue.createdAt)
.limit(1);
if (!item) return false;
const img = item.message as ImageTask;
setAttributes({ "item.url": img.url });
try {
const blurhash = await downloadImage(img.id, img.url);
const ret: Image = { id: img.id, source: img.url, blurhash };
const table = sql.raw(img.table);
const column = sql.raw(img.column);
await tx.execute(sql`
update ${table} set ${column} = ${ret}
where ${column}->'id' = ${sql.raw(`'"${img.id}"'::jsonb`)}
`);
await tx.delete(mqueue).where(eq(mqueue.id, item.id));
} catch (err: any) {
const span = getCurrentSpan();
if (span) {
span.recordException(err);
span.setStatus({ code: SpanStatusCode.ERROR });
}
console.error("Failed to download image", img.url, err.message);
// don't use the transaction here, it can be aborted.
await db
.update(mqueue)
.set({ attempt: sql`${mqueue.attempt}+1` })
.where(eq(mqueue.id, item.id));
}
return true;
});
});
}
async function downloadImage(id: string, url: string): Promise<string> { async function downloadImage(id: string, url: string): Promise<string> {
const low = await getFile(path.join(imageDir, `${id}.low.jpg`)) const low = await getFile(path.join(imageDir, `${id}.low.jpg`))
.arrayBuffer() .arrayBuffer()

View File

@ -429,6 +429,8 @@ traefikproxy:
extraArgs: extraArgs:
- '--entryPoints.web.address=:80/tcp' - '--entryPoints.web.address=:80/tcp'
- '--entryPoints.websecure.address=:443/tcp' - '--entryPoints.websecure.address=:443/tcp'
- '--entryPoints.web.forwardedHeaders.insecure=true'
- '--entryPoints.websecure.forwardedHeaders.insecure=true'
- '--api.dashboard=true' - '--api.dashboard=true'
- '--api.insecure=true' - '--api.insecure=true'
- '--log.level=INFO' - '--log.level=INFO'

View File

@ -101,6 +101,7 @@ class RequestProcessor:
finally: finally:
self._processing = False self._processing = False
@tracer.start_as_current_span("process video")
async def process_request(self): async def process_request(self):
cur = await self._database.fetchrow( cur = await self._database.fetchrow(
""" """
@ -128,7 +129,8 @@ class RequestProcessor:
return False return False
request = Request.model_validate(cur) request = Request.model_validate(cur)
with tracer.start_as_current_span(f"process {request.title}") as span: span = trace.get_current_span()
span.update_name(f"process {request.title}")
logger.info(f"Starting to process {request.title}") logger.info(f"Starting to process {request.title}")
try: try:
show = await self._run_request(request) show = await self._run_request(request)