You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

155 lines
4.4 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import { randomUUID } from "node:crypto";
import {
ADDRESS_CANDIDATES_POLL_MS,
ADDRESS_CANDIDATES_RESULT_PREFIX,
ADDRESS_CANDIDATES_RESULT_TTL_SECONDS,
} from "@/lib/constants/address-candidates-queue";
import { ADDRESS_CANDIDATES_TIMEOUT_MS } from "@/lib/constants/rpa";
import { ensureNativeInfraIfNeeded } from "@/lib/infra/ensure-native-infra";
import {
ensureRedisReady,
getRedis,
resetRedisClient,
} from "@/lib/redis";
import { isParkedSessionEnabled } from "@/lib/rpa/env";
import { spawnFetchMothershipAddressCandidates } from "@/lib/rpa/spawn-address-candidates";
import { isAnyRpaWorkerHealthy } from "@/lib/rpa/worker-health";
import type {
AddressLookupInput,
MothershipCandidatesResult,
} from "@/modules/address/types";
import { RpaError, type RpaErrorCode } from "@/modules/rpa/errors";
import { addAddressCandidatesJob } from "@/workers/rpa/queue";
import type { AddressCandidatesJobData } from "@/modules/address/candidates-queue-types";
type StoredCandidatesSuccess = {
ok: true;
data: MothershipCandidatesResult;
};
type StoredCandidatesFailure = {
ok: false;
code: RpaErrorCode;
message: string;
retryable?: boolean;
};
type StoredCandidatesResult = StoredCandidatesSuccess | StoredCandidatesFailure;
function resultKey(jobId: string): string {
return `${ADDRESS_CANDIDATES_RESULT_PREFIX}${jobId}`;
}
async function ensureCandidatesRedisReady(): Promise<void> {
try {
await ensureRedisReady(10, 800);
} catch {
ensureNativeInfraIfNeeded();
resetRedisClient();
await ensureRedisReady(15, 1_000);
}
}
/** 快速探测是否可走 BullMQRedis + Worker 心跳均可用) */
async function canUseCandidatesQueue(): Promise<boolean> {
try {
await ensureRedisReady(5, 500);
return await isAnyRpaWorkerHealthy();
} catch {
return false;
}
}
export async function saveAddressCandidatesResult(
jobId: string,
payload: StoredCandidatesResult,
): Promise<void> {
await ensureCandidatesRedisReady();
await getRedis().setex(
resultKey(jobId),
ADDRESS_CANDIDATES_RESULT_TTL_SECONDS,
JSON.stringify(payload),
);
}
async function readAddressCandidatesResult(
jobId: string,
): Promise<StoredCandidatesResult | null> {
const raw = await getRedis().get(resultKey(jobId));
if (!raw) {
return null;
}
return JSON.parse(raw) as StoredCandidatesResult;
}
async function requestViaSpawn(input: {
pickup: AddressLookupInput;
delivery: AddressLookupInput;
}): Promise<MothershipCandidatesResult> {
return spawnFetchMothershipAddressCandidates(input);
}
/** 经 RPA Worker 执行地址候选Worker/队列不可用时回退 spawn 子进程 */
export async function requestMothershipCandidatesViaWorker(input: {
pickup: AddressLookupInput;
delivery: AddressLookupInput;
}): Promise<MothershipCandidatesResult> {
if (process.env.RPA_QUEUE_ENABLED === "false") {
return requestViaSpawn(input);
}
const parkedSessionsEnabled = isParkedSessionEnabled();
const useQueue = await canUseCandidatesQueue();
if (!useQueue) {
if (parkedSessionsEnabled) {
throw new RpaError(
"PAGE_LOAD_TIMEOUT",
"RPA Worker 未运行,无法使用单会话加速。请执行 npm run worker:rpa 或 npm run dev:start",
{ retryable: true },
);
}
console.warn(
"[candidates] Redis/Worker 不可用,改用子进程执行地址联想",
);
return requestViaSpawn(input);
}
await ensureCandidatesRedisReady();
const jobId = randomUUID();
await addAddressCandidatesJob({
jobId,
pickup: input.pickup,
delivery: input.delivery,
});
const deadline = Date.now() + ADDRESS_CANDIDATES_TIMEOUT_MS;
while (Date.now() < deadline) {
try {
const stored = await readAddressCandidatesResult(jobId);
if (stored?.ok) {
return stored.data;
}
if (stored && !stored.ok) {
throw new RpaError(stored.code, stored.message, {
retryable: stored.retryable ?? false,
});
}
} catch (error) {
if (error instanceof RpaError) {
throw error;
}
ensureNativeInfraIfNeeded();
resetRedisClient();
await ensureCandidatesRedisReady();
}
await new Promise((resolve) => setTimeout(resolve, ADDRESS_CANDIDATES_POLL_MS));
}
throw new RpaError(
"PAGE_LOAD_TIMEOUT",
"MotherShip 地址联想超时,请确认 RPA Worker 与 Redis 已启动npm run dev:start",
{ retryable: true },
);
}