fix: 改进候选随机性与并发配额稳定性
This commit is contained in:
@@ -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")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user