- Notifications
You must be signed in to change notification settings - Fork 0
fix: close bug-hunter stale-state paths#137
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -16,7 +16,9 @@ export async function POST(request: Request, { params }: { params: Promise<{ id: | ||
| const { data: job, error: jobError } = await supabase | ||
| .from("ingestion_jobs") | ||
| .select("id,document_id,batch_id,status,locked_at,documents!inner(owner_id)") | ||
| .select( | ||
| "id,document_id,batch_id,status,stage,progress,error_message,attempt_count,max_attempts,locked_at,locked_by,next_run_at,completed_at,documents!inner(owner_id)", | ||
| ) | ||
| .eq("id", id) | ||
| .eq("documents.owner_id", user.id) | ||
| .maybeSingle(); | ||
| @@ -74,7 +76,27 @@ export async function POST(request: Request, { params }: { params: Promise<{ id: | ||
| .update({ status: "queued", error_message: null }) | ||
| .eq("id", job.document_id) | ||
| .eq("owner_id", user.id); | ||
| if (documentError) throw new Error(documentError.message); | ||
| if (documentError) { | ||
| const { error: rollbackError } = await supabase | ||
| .from("ingestion_jobs") | ||
| .update({ | ||
| status: job.status, | ||
| stage: job.stage, | ||
| progress: job.progress, | ||
| error_message: job.error_message, | ||
| attempt_count: job.attempt_count, | ||
| max_attempts: job.max_attempts, | ||
| locked_at: job.locked_at, | ||
| locked_by: job.locked_by, | ||
| next_run_at: job.next_run_at, | ||
| completed_at: job.completed_at, | ||
| }) | ||
| .eq("id", id); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the document update fails after the retry reset, this rollback filters only by job id. The reset to Useful? React with 👍 / 👎. | ||
| if (rollbackError) { | ||
| throw new Error(`${documentError.message}; failed to roll back retried job state: ${rollbackError.message}`); | ||
| } | ||
| throw new Error(documentError.message); | ||
| } | ||
| return NextResponse.json({ job: data }); | ||
| } catch (error) { | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -24,6 +24,8 @@ const uploadMetadataSchema = z | ||
| export async function POST(request: Request) { | ||
| let supabase: ReturnType<typeof createAdminClient> | null = null; | ||
| let uploadedPath: string | null = null; | ||
| let insertedDocumentId: string | null = null; | ||
| let insertedDocumentOwnerId: string | null = null; | ||
| try { | ||
| supabase = createAdminClient(); | ||
| @@ -132,6 +134,8 @@ export async function POST(request: Request) { | ||
| .single(); | ||
| if (documentError) throw new Error(documentError.message); | ||
| insertedDocumentId = documentId; | ||
| insertedDocumentOwnerId = user.id; | ||
| const { data: job, error: jobError } = await supabase | ||
| .from("ingestion_jobs") | ||
| @@ -146,7 +150,19 @@ export async function POST(request: Request) { | ||
| .select() | ||
| .single(); | ||
| if (jobError) throw new Error(jobError.message); | ||
| if (jobError) { | ||
| const { error: rollbackDocumentError } = await supabase | ||
| .from("documents") | ||
| .delete() | ||
| .eq("id", documentId) | ||
| .eq("owner_id", user.id); | ||
| if (rollbackDocumentError) { | ||
| throw new Error(`Failed to enqueue ingestion job: ${jobError.message}; rollback failed: ${rollbackDocumentError.message}`); | ||
| } | ||
| insertedDocumentId = null; | ||
| insertedDocumentOwnerId = null; | ||
| throw new Error(jobError.message); | ||
| } | ||
| await writeAuditLog(supabase, { | ||
| ownerId: user.id, | ||
| @@ -158,6 +174,18 @@ export async function POST(request: Request) { | ||
| return NextResponse.json({ document, job }, { status: 201 }); | ||
| } catch (error) { | ||
| if (insertedDocumentId && insertedDocumentOwnerId && supabase) { | ||
| try { | ||
| await supabase.from("documents").delete().eq("id", insertedDocumentId).eq("owner_id", insertedDocumentOwnerId); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Supabase query failures are returned in the resolved Useful? React with 👍 / 👎. | ||
| } catch (cleanupError) { | ||
| logger.error("Upload cleanup failed; document row may be orphaned", { | ||
| documentId: insertedDocumentId, | ||
| ownerId: insertedDocumentOwnerId, | ||
| message: cleanupError instanceof Error ? cleanupError.message : String(cleanupError), | ||
| }); | ||
| } | ||
| } | ||
| if (uploadedPath && supabase) { | ||
| try { | ||
| await supabase.storage.from(env.SUPABASE_DOCUMENT_BUCKET).remove([uploadedPath]); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| import { readFileSync } from "node:fs"; | ||
| import { describe, expect, it } from "vitest"; | ||
| const source = readFileSync(new URL("../src/components/forms/form-detail-page.tsx", import.meta.url), "utf8"); | ||
| describe("form detail clipboard fallback", () => { | ||
| it("falls back to selection-copy when clipboard.writeText rejects", () => { | ||
| expect(source).toContain("if (navigator.clipboard?.writeText)"); | ||
| expect(source).toContain("await navigator.clipboard.writeText(value)"); | ||
| expect(source).toContain("Fall through to the legacy selection path for restricted browser contexts."); | ||
| expect(source).toContain("document.execCommand?.(\"copy\")"); | ||
| expect(source).toContain("finally {\n document.body.removeChild(textArea);\n }"); | ||
| }); | ||
| }); |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When enqueue fails, this rollback restores the old document snapshot using only the document id and owner. If another reindex request for the same document successfully inserts a job after this request's pre-insert document update but before this rollback runs, the rollback can put the document back into the old failed/count state while a fresh pending or processing job exists. Constrain the rollback to the exact temporary state (or re-check no newer job exists) so cleanup from one failed enqueue cannot clobber a newer enqueue.
Useful? React with 👍 / 👎.