fix: 改进候选随机性与并发配额稳定性

This commit is contained in:
root
2026-09-15 15:14:01 +08:00
parent 69fc8d51ce
commit 0c5cbd75d9
5 changed files with 384 additions and 69 deletions
+92 -60
View File
@@ -1,7 +1,7 @@
import { randomBytes, randomUUID } from "node:crypto";
import { randomBytes, randomInt, randomUUID } from "node:crypto";
import { HttpStatus, Inject, Injectable } from "@nestjs/common";
import { ErrorCode } from "@drift/contracts";
import type { Prisma } from "@prisma/client";
import { Prisma } from "@prisma/client";
import { DomainException } from "../common/domain.exception.js";
import { PrismaService } from "../database/prisma.service.js";
import { utc8UsageDate } from "../bottle/usage-date.js";
@@ -56,69 +56,101 @@ export class MatchService {
) {}
async pick(pickerId: string, requestId: string) {
return this.prisma.$transaction(
async (tx) => {
await tx.$executeRaw`SELECT pg_advisory_xact_lock(hashtextextended(${`pick:${pickerId}`}, 0))`;
const previous = await tx.bottlePickRequest.findUnique({
where: { pickerId_requestId: { pickerId, requestId } },
include: {
lease: {
for (let attempt = 0; attempt < 100; attempt += 1) {
try {
const result = await this.prisma.$transaction(
async (tx) => {
const [lock] = await tx.$queryRaw<Array<{ locked: boolean }>>`
SELECT pg_try_advisory_xact_lock(
hashtextextended(${`pick:${pickerId}`}, 0)
) AS "locked"`;
if (!lock?.locked) return null;
const previous = await tx.bottlePickRequest.findUnique({
where: { pickerId_requestId: { pickerId, requestId } },
include: {
bottle: {
include: { author: { include: { anonymousProfile: true } } },
lease: {
include: {
bottle: {
include: {
author: { include: { anonymousProfile: true } },
},
},
},
},
},
},
},
});
if (previous?.lease) {
let token: string;
try {
token = decryptLeaseToken(
previous.lease.leaseTokenCiphertext,
previous.lease.id,
);
} catch {
throw new DomainException(
ErrorCode.SERVICE_UNAVAILABLE,
"Bottle pick temporarily unavailable",
HttpStatus.SERVICE_UNAVAILABLE,
);
}
return this.response(previous.lease, token);
}
await this.assertEligible(tx, pickerId);
const usageDate = utc8UsageDate(new Date());
const current = await tx.dailyUsage.findUnique({
where: {
accountId_usageDate: {
accountId: pickerId,
usageDate: new Date(`${usageDate}T00:00:00.000Z`),
},
},
});
if ((current?.bottlesPicked ?? 0) >= 20) this.limit();
});
if (previous?.lease) {
let token: string;
try {
token = decryptLeaseToken(
previous.lease.leaseTokenCiphertext,
previous.lease.id,
);
} catch {
throw new DomainException(
ErrorCode.SERVICE_UNAVAILABLE,
"Bottle pick temporarily unavailable",
HttpStatus.SERVICE_UNAVAILABLE,
);
}
return this.response(previous.lease, token);
}
await this.assertEligible(tx, pickerId);
const usageDate = utc8UsageDate(new Date());
const current = await tx.dailyUsage.findUnique({
where: {
accountId_usageDate: {
accountId: pickerId,
usageDate: new Date(`${usageDate}T00:00:00.000Z`),
},
},
});
if ((current?.bottlesPicked ?? 0) >= 20) this.limit();
const batch = await this.candidates.findBatch(tx, pickerId);
while (batch.length) {
const index = Math.floor(Math.random() * batch.length);
const candidate = batch.splice(index, 1)[0]!;
const result = await this.claim(
tx,
pickerId,
requestId,
usageDate,
candidate,
);
if (result) return result;
}
throw new DomainException(
ErrorCode.BOTTLE_POOL_EMPTY,
"Bottle pool empty",
HttpStatus.NOT_FOUND,
const batch = await this.candidates.findBatch(tx, pickerId);
while (batch.length) {
const index = Math.floor(Math.random() * batch.length);
const candidate = batch.splice(index, 1)[0]!;
const result = await this.claim(
tx,
pickerId,
requestId,
usageDate,
candidate,
);
if (result) return result;
}
throw new DomainException(
ErrorCode.BOTTLE_POOL_EMPTY,
"Bottle pool empty",
HttpStatus.NOT_FOUND,
);
},
{
isolationLevel: "ReadCommitted",
maxWait: 10_000,
timeout: 10_000,
},
);
},
{ isolationLevel: "ReadCommitted" },
if (result) return result;
} catch (error) {
if (!this.isContention(error)) throw error;
}
await new Promise((resolve) => setTimeout(resolve, randomInt(5, 21)));
}
throw new DomainException(
ErrorCode.SERVICE_UNAVAILABLE,
"Bottle pick temporarily unavailable",
HttpStatus.SERVICE_UNAVAILABLE,
);
}
private isContention(error: unknown) {
return (
error instanceof Prisma.PrismaClientKnownRequestError &&
(error.code === "P2028" ||
error.code === "P2034" ||
error.code === "P2002")
);
}