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,
|
headDocumentBlob,
|
||||||
headTempDocumentBlob,
|
headTempDocumentBlob,
|
||||||
isMissingBlobError,
|
isMissingBlobError,
|
||||||
|
isPreconditionFailed,
|
||||||
isValidTempUploadToken,
|
isValidTempUploadToken,
|
||||||
putTempDocumentFinalizeReceipt,
|
putTempDocumentFinalizeReceipt,
|
||||||
} from '@/lib/server/documents/blobstore';
|
} from '@/lib/server/documents/blobstore';
|
||||||
|
|
@ -138,13 +139,18 @@ async function finalizeOne(input: {
|
||||||
await headDocumentBlob(documentId, input.namespace);
|
await headDocumentBlob(documentId, input.namespace);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!isMissingBlobError(error)) throw error;
|
if (!isMissingBlobError(error)) throw error;
|
||||||
await copyTempDocumentBlobToDocument(
|
try {
|
||||||
input.upload.token,
|
await copyTempDocumentBlobToDocument(
|
||||||
input.userId,
|
input.upload.token,
|
||||||
documentId,
|
input.userId,
|
||||||
input.namespace,
|
documentId,
|
||||||
temp.contentType,
|
input.namespace,
|
||||||
);
|
temp.contentType,
|
||||||
|
{ ifNoneMatch: true },
|
||||||
|
);
|
||||||
|
} catch (copyError) {
|
||||||
|
if (!isPreconditionFailed(copyError)) throw copyError;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const canonicalHead = await headDocumentBlob(documentId, input.namespace);
|
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 });
|
return NextResponse.json({ error: 'No valid uploads provided' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const stored: BaseDocument[] = [];
|
const stored = await Promise.all(
|
||||||
for (const upload of uploads) {
|
uploads.map((upload) => finalizeOne({
|
||||||
stored.push(await finalizeOne({
|
|
||||||
upload,
|
upload,
|
||||||
userId,
|
userId,
|
||||||
namespace,
|
namespace,
|
||||||
}));
|
})),
|
||||||
}
|
);
|
||||||
|
|
||||||
return NextResponse.json({ stored });
|
return NextResponse.json({ stored });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,12 @@ export function isValidDocumentId(id: string): boolean {
|
||||||
return DOCUMENT_ID_REGEX.test(id);
|
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 {
|
export function isValidTempUploadToken(token: string): boolean {
|
||||||
return TEMP_UPLOAD_TOKEN_REGEX.test(token);
|
return TEMP_UPLOAD_TOKEN_REGEX.test(token);
|
||||||
}
|
}
|
||||||
|
|
@ -455,6 +461,7 @@ export async function copyTempDocumentBlobToDocument(
|
||||||
documentId: string,
|
documentId: string,
|
||||||
namespace: string | null,
|
namespace: string | null,
|
||||||
contentType: string,
|
contentType: string,
|
||||||
|
options?: { ifNoneMatch?: boolean },
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const cfg = getS3Config();
|
const cfg = getS3Config();
|
||||||
const client = getS3ProxyClient();
|
const client = getS3ProxyClient();
|
||||||
|
|
@ -466,6 +473,7 @@ export async function copyTempDocumentBlobToDocument(
|
||||||
ContentType: contentType,
|
ContentType: contentType,
|
||||||
MetadataDirective: 'REPLACE',
|
MetadataDirective: 'REPLACE',
|
||||||
ServerSideEncryption: 'AES256',
|
ServerSideEncryption: 'AES256',
|
||||||
|
...(options?.ifNoneMatch ? { IfNoneMatch: '*' } : {}),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,14 @@ export async function registerUploadedDocument(input: RegisterUploadedDocumentIn
|
||||||
const reusableParsedPdf = input.type === 'pdf'
|
const reusableParsedPdf = input.type === 'pdf'
|
||||||
? await findReusableParsedPdfResult(input.documentId)
|
? await findReusableParsedPdfResult(input.documentId)
|
||||||
: null;
|
: 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
|
await db
|
||||||
.insert(documents)
|
.insert(documents)
|
||||||
|
|
@ -38,14 +46,8 @@ export async function registerUploadedDocument(input: RegisterUploadedDocumentIn
|
||||||
size: input.size,
|
size: input.size,
|
||||||
lastModified: input.lastModified,
|
lastModified: input.lastModified,
|
||||||
filePath: input.documentId,
|
filePath: input.documentId,
|
||||||
parseState: input.type === 'pdf'
|
parseState,
|
||||||
? stringifyDocumentParseState(
|
parsedJsonKey,
|
||||||
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,
|
|
||||||
})
|
})
|
||||||
.onConflictDoUpdate({
|
.onConflictDoUpdate({
|
||||||
target: [documents.id, documents.userId],
|
target: [documents.id, documents.userId],
|
||||||
|
|
@ -55,14 +57,8 @@ export async function registerUploadedDocument(input: RegisterUploadedDocumentIn
|
||||||
size: input.size,
|
size: input.size,
|
||||||
lastModified: input.lastModified,
|
lastModified: input.lastModified,
|
||||||
filePath: input.documentId,
|
filePath: input.documentId,
|
||||||
parseState: input.type === 'pdf'
|
parseState,
|
||||||
? stringifyDocumentParseState(
|
parsedJsonKey,
|
||||||
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,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue