fix(documents): handle precondition failures and optimize upload finalization
Add detection for precondition failures during blob copy to prevent race conditions when finalizing uploads. Refactor upload finalization to use Promise.all for concurrent processing. Extract parse state and parsed JSON key logic to variables for clarity and to avoid duplication in document registration.
This commit is contained in:
parent
6c86720926
commit
cc868511d9
3 changed files with 37 additions and 28 deletions
|
|
@ -10,6 +10,7 @@ import {
|
|||
headDocumentBlob,
|
||||
headTempDocumentBlob,
|
||||
isMissingBlobError,
|
||||
isPreconditionFailed,
|
||||
isValidTempUploadToken,
|
||||
putTempDocumentFinalizeReceipt,
|
||||
} from '@/lib/server/documents/blobstore';
|
||||
|
|
@ -138,13 +139,18 @@ async function finalizeOne(input: {
|
|||
await headDocumentBlob(documentId, input.namespace);
|
||||
} catch (error) {
|
||||
if (!isMissingBlobError(error)) throw error;
|
||||
await copyTempDocumentBlobToDocument(
|
||||
input.upload.token,
|
||||
input.userId,
|
||||
documentId,
|
||||
input.namespace,
|
||||
temp.contentType,
|
||||
);
|
||||
try {
|
||||
await copyTempDocumentBlobToDocument(
|
||||
input.upload.token,
|
||||
input.userId,
|
||||
documentId,
|
||||
input.namespace,
|
||||
temp.contentType,
|
||||
{ ifNoneMatch: true },
|
||||
);
|
||||
} catch (copyError) {
|
||||
if (!isPreconditionFailed(copyError)) throw copyError;
|
||||
}
|
||||
}
|
||||
|
||||
const canonicalHead = await headDocumentBlob(documentId, input.namespace);
|
||||
|
|
@ -194,14 +200,13 @@ export async function POST(req: NextRequest) {
|
|||
return NextResponse.json({ error: 'No valid uploads provided' }, { status: 400 });
|
||||
}
|
||||
|
||||
const stored: BaseDocument[] = [];
|
||||
for (const upload of uploads) {
|
||||
stored.push(await finalizeOne({
|
||||
const stored = await Promise.all(
|
||||
uploads.map((upload) => finalizeOne({
|
||||
upload,
|
||||
userId,
|
||||
namespace,
|
||||
}));
|
||||
}
|
||||
})),
|
||||
);
|
||||
|
||||
return NextResponse.json({ stored });
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -73,6 +73,12 @@ export function isValidDocumentId(id: string): boolean {
|
|||
return DOCUMENT_ID_REGEX.test(id);
|
||||
}
|
||||
|
||||
export function isPreconditionFailed(error: unknown): boolean {
|
||||
if (!error || typeof error !== 'object') return false;
|
||||
const maybe = error as { name?: string; $metadata?: { httpStatusCode?: number } };
|
||||
return maybe.$metadata?.httpStatusCode === 412 || maybe.name === 'PreconditionFailed';
|
||||
}
|
||||
|
||||
export function isValidTempUploadToken(token: string): boolean {
|
||||
return TEMP_UPLOAD_TOKEN_REGEX.test(token);
|
||||
}
|
||||
|
|
@ -455,6 +461,7 @@ export async function copyTempDocumentBlobToDocument(
|
|||
documentId: string,
|
||||
namespace: string | null,
|
||||
contentType: string,
|
||||
options?: { ifNoneMatch?: boolean },
|
||||
): Promise<void> {
|
||||
const cfg = getS3Config();
|
||||
const client = getS3ProxyClient();
|
||||
|
|
@ -466,6 +473,7 @@ export async function copyTempDocumentBlobToDocument(
|
|||
ContentType: contentType,
|
||||
MetadataDirective: 'REPLACE',
|
||||
ServerSideEncryption: 'AES256',
|
||||
...(options?.ifNoneMatch ? { IfNoneMatch: '*' } : {}),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,14 @@ export async function registerUploadedDocument(input: RegisterUploadedDocumentIn
|
|||
const reusableParsedPdf = input.type === 'pdf'
|
||||
? await findReusableParsedPdfResult(input.documentId)
|
||||
: null;
|
||||
const parsedJsonKey = reusableParsedPdf?.parsedJsonKey ?? null;
|
||||
const parseState = input.type === 'pdf'
|
||||
? stringifyDocumentParseState(
|
||||
reusableParsedPdf
|
||||
? { status: 'ready', progress: null, updatedAt: Date.now(), parserVersion: PDF_PARSER_VERSION }
|
||||
: { status: 'pending', progress: null, updatedAt: Date.now(), parserVersion: PDF_PARSER_VERSION },
|
||||
)
|
||||
: null;
|
||||
|
||||
await db
|
||||
.insert(documents)
|
||||
|
|
@ -38,14 +46,8 @@ export async function registerUploadedDocument(input: RegisterUploadedDocumentIn
|
|||
size: input.size,
|
||||
lastModified: input.lastModified,
|
||||
filePath: input.documentId,
|
||||
parseState: input.type === 'pdf'
|
||||
? stringifyDocumentParseState(
|
||||
reusableParsedPdf
|
||||
? { status: 'ready', progress: null, updatedAt: Date.now(), parserVersion: PDF_PARSER_VERSION }
|
||||
: { status: 'pending', progress: null, updatedAt: Date.now(), parserVersion: PDF_PARSER_VERSION },
|
||||
)
|
||||
: null,
|
||||
parsedJsonKey: reusableParsedPdf?.parsedJsonKey ?? null,
|
||||
parseState,
|
||||
parsedJsonKey,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [documents.id, documents.userId],
|
||||
|
|
@ -55,14 +57,8 @@ export async function registerUploadedDocument(input: RegisterUploadedDocumentIn
|
|||
size: input.size,
|
||||
lastModified: input.lastModified,
|
||||
filePath: input.documentId,
|
||||
parseState: input.type === 'pdf'
|
||||
? stringifyDocumentParseState(
|
||||
reusableParsedPdf
|
||||
? { status: 'ready', progress: null, updatedAt: Date.now(), parserVersion: PDF_PARSER_VERSION }
|
||||
: { status: 'pending', progress: null, updatedAt: Date.now(), parserVersion: PDF_PARSER_VERSION },
|
||||
)
|
||||
: null,
|
||||
parsedJsonKey: reusableParsedPdf?.parsedJsonKey ?? null,
|
||||
parseState,
|
||||
parsedJsonKey,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue