采购提交与报销新建/提交事务显式使用10秒上限 [09-27-purchase-submit-tx-timeout]
负载下采购提交事务超过 Prisma 交互事务默认 5000ms(P2028)返回 500。
分段计时显示无单一瓶颈(总时长中位 843ms、最大 3506ms,编号锁等待
最多 535ms),是约 20 条顺序查询整体变慢贴近上限。报销新建/提交同结构。
三处改用项目既有的 { maxWait: 5000, timeout: 10000 },事务内容不变;
新增采购与报销提交的事务选项回归断言。
Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
@@ -1701,6 +1701,8 @@ async function purchaseRequestRoutes(fastify, options = {}) {
|
||||
throw buildBusinessValidationError("purchase_request.approver_unavailable", { workflow: "no_actionable_approver" }, 409);
|
||||
}
|
||||
|
||||
// ~20 sequential queries plus document numbering; under load this exceeded
|
||||
// Prisma's 5s interactive-transaction default (P2028 -> 500).
|
||||
const submitResult = await fastify.prisma.$transaction(async (tx) => {
|
||||
const locked = await loadPurchaseRequestForUpdate(tx, purchaseRequestId);
|
||||
ensureDraftOwner(request, locked);
|
||||
@@ -1949,7 +1951,7 @@ async function purchaseRequestRoutes(fastify, options = {}) {
|
||||
budgetReservation,
|
||||
},
|
||||
};
|
||||
});
|
||||
}, { maxWait: 5000, timeout: 10000 });
|
||||
|
||||
void fastify.prisma.auditLog.create({
|
||||
data: buildAuditLogData(request, {
|
||||
|
||||
@@ -1863,7 +1863,7 @@ async function reimbursementRoutes(fastify) {
|
||||
...created,
|
||||
items: createdItems,
|
||||
};
|
||||
});
|
||||
}, { maxWait: 5000, timeout: 10000 });
|
||||
|
||||
return sendData(reply, request, {
|
||||
id: reimbursement.id,
|
||||
@@ -4498,7 +4498,7 @@ async function reimbursementRoutes(fastify) {
|
||||
actorId: request.employee.id,
|
||||
},
|
||||
};
|
||||
});
|
||||
}, { maxWait: 5000, timeout: 10000 });
|
||||
|
||||
await finalizeReimbursementSubmitSideEffects({
|
||||
prisma: fastify.prisma,
|
||||
|
||||
@@ -315,6 +315,17 @@ describe("purchase request submit budget precheck gate", () => {
|
||||
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it("runs the submit transaction with a 10s budget instead of Prisma's 5s default", async () => {
|
||||
resolveEntityPrecheckMock.mockResolvedValue(buildPrecheck({ budgetHint: { controlMode: "block" } }));
|
||||
const { app, prisma } = await buildApp({ budgetControlMode: "warn", budgetPrecheckEnabled: false });
|
||||
|
||||
const response = await app.inject({ method: "POST", url: `/api/purchase-requests/${PURCHASE_REQUEST_ID}/submit` });
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(prisma.$transaction).toHaveBeenCalledWith(expect.any(Function), { maxWait: 5000, timeout: 10000 });
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
|
||||
it.each([1, 2])("submits a stored v%s purchase draft under active policy without reclassifying it", async (version) => {
|
||||
|
||||
@@ -325,7 +325,7 @@ describe("reimbursements routes expense standard integration", () => {
|
||||
|
||||
it.each([true, false])("avoids unused bound-invoice counts: per-item guard=%s", async invoiceGuardEnabled => {
|
||||
resolveEntityPrecheckMock.mockResolvedValue(buildPrecheck());
|
||||
const { app, tx } = await buildApp({ invoiceGuardEnabled, reimbursementItems: [{
|
||||
const { app, prisma, tx } = await buildApp({ invoiceGuardEnabled, reimbursementItems: [{
|
||||
id: "item-no-invoice", amount: "180.00",
|
||||
expense_type: { code: "office", category: "office", requires_invoice: invoiceGuardEnabled },
|
||||
}] });
|
||||
@@ -336,6 +336,8 @@ describe("reimbursements routes expense standard integration", () => {
|
||||
expect(tx.reimbursementItem.findFirst).not.toHaveBeenCalled();
|
||||
expect(tx.reimbursementItem.aggregate).toHaveBeenCalledTimes(1);
|
||||
expect(tx.workflowHistory.create).toHaveBeenCalled();
|
||||
// Prisma's 5s interactive-transaction default was exceeded under load (P2028 -> 500).
|
||||
expect(prisma.$transaction).toHaveBeenCalledWith(expect.any(Function), { maxWait: 5000, timeout: 10000 });
|
||||
} finally { await app.close(); }
|
||||
});
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户