fix(gateway): stop Qdrant upsert aborts (semantic cache was being skipped)
Qdrant upserts were failing with 'This operation was aborted' ~32x/2h, so semantic moderation cache entries were silently dropped. Root cause: upsertQdrantPoint ran ensureQdrantCollection() on EVERY call — a GET (and sometimes DELETE+PUT) round-trip — while the request AbortController had only a 10s timeout. Under moderation load Qdrant is busy (the gmw_text_moderation collection is not yet HNSW-indexed, so searches are full-scans), the extra round-trips pushed the upsert past 10s, and the client aborted it. - Memoise ensureQdrantCollection() at module scope so the collection is verified exactly once per process (resetQdrantCollectionCache() for tests / config reload). - Bump the upsert request timeout 10s -> 30s so a transiently busy Qdrant no longer aborts the write. Qdrant server itself is healthy (<100ms for direct upsert; collection is green), so no server-side change is needed. Semantic cache should now populate reliably.
This commit is contained in:
@@ -17,6 +17,18 @@ import { config } from "../../shared/config/config.js";
|
|||||||
|
|
||||||
const log = createChildLogger("qdrant");
|
const log = createChildLogger("qdrant");
|
||||||
|
|
||||||
|
// ensureQdrantCollection performs a network round-trip (GET, possibly
|
||||||
|
// DELETE+PUT). Running it on every upsert adds 1-3 HTTP calls per
|
||||||
|
// moderation verdict, which under Qdrant load pushes the upsert past the
|
||||||
|
// request timeout and aborts it ("This operation was aborted"). Memoise the
|
||||||
|
// result so the collection is only verified once per process lifetime.
|
||||||
|
let ensureCollectionPromise: Promise<boolean> | null = null;
|
||||||
|
|
||||||
|
/** Reset the memoised ensure result (used by tests / config reload). */
|
||||||
|
export function resetQdrantCollectionCache(): void {
|
||||||
|
ensureCollectionPromise = null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface QdrantVerdictPayload {
|
export interface QdrantVerdictPayload {
|
||||||
text: string;
|
text: string;
|
||||||
flags: string; // JSON string of the full moderation result
|
flags: string; // JSON string of the full moderation result
|
||||||
@@ -97,13 +109,18 @@ export function qdrantPointId(cacheKey: string): number {
|
|||||||
export async function ensureQdrantCollection(
|
export async function ensureQdrantCollection(
|
||||||
vectorSize: number,
|
vectorSize: number,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
|
if (ensureCollectionPromise) return ensureCollectionPromise;
|
||||||
|
ensureCollectionPromise = (async () => {
|
||||||
try {
|
try {
|
||||||
// 404 = collection doesn't exist yet → create it.
|
// 404 = collection doesn't exist yet → create it.
|
||||||
let existing: {
|
let existing: {
|
||||||
result?: { config?: { params?: { vectors?: { size?: number } } } };
|
result?: { config?: { params?: { vectors?: { size?: number } } } };
|
||||||
} | null = null;
|
} | null = null;
|
||||||
try {
|
try {
|
||||||
existing = (await request("GET", `/collections/${collectionName()}`)) as {
|
existing = (await request(
|
||||||
|
"GET",
|
||||||
|
`/collections/${collectionName()}`,
|
||||||
|
)) as {
|
||||||
result?: { config?: { params?: { vectors?: { size?: number } } } };
|
result?: { config?: { params?: { vectors?: { size?: number } } } };
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -137,6 +154,8 @@ export async function ensureQdrantCollection(
|
|||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
})();
|
||||||
|
return ensureCollectionPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Upsert one embedding + verdict payload point. Returns false on failure. */
|
/** Upsert one embedding + verdict payload point. Returns false on failure. */
|
||||||
@@ -147,10 +166,15 @@ export async function upsertQdrantPoint(
|
|||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
if (!(await ensureQdrantCollection(vector.length))) return false;
|
if (!(await ensureQdrantCollection(vector.length))) return false;
|
||||||
await request("PUT", `/collections/${collectionName()}/points`, {
|
await request(
|
||||||
|
"PUT",
|
||||||
|
`/collections/${collectionName()}/points`,
|
||||||
|
{
|
||||||
points: [{ id: qdrantPointId(cacheKey), vector, payload }],
|
points: [{ id: qdrantPointId(cacheKey), vector, payload }],
|
||||||
wait: true,
|
wait: true,
|
||||||
});
|
},
|
||||||
|
30_000,
|
||||||
|
);
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.warn(
|
log.warn(
|
||||||
|
|||||||
Reference in New Issue
Block a user