fix(gateway): un-claim budget-overflow messages stuck in processing
getPendingMessagesByConversation() flips every fetched pending row to 'processing', then pickBatchWithinBudget() may stop early on the token budget. The tail rows that did NOT make the batch were never un-claimed, so they stayed 'processing' forever — the recovery worker reverted them (120s) only for the next wave to re-claim them, an infinite loop of stuck messages that never get analyzed (saw 22 rows, some recycled for 40+ minutes). - add computeBudgetOverflowMessages() pure helper (batchBudget.ts) - batchScheduler un-claims overflow rows back to 'pending' before dispatching the trimmed batch - recovery-worker now reverts stuck processing unconditionally (the old 'conversationProcessing.size > 0' guard skipped the revert when the in-memory lock map was empty, e.g. fresh boot — exactly when stranded rows from a previous process need rescuing) - 3 regression tests for the overflow helper
This commit is contained in:
1 parent
ef7708bf7d
commit
80daa9f045
4 files changed
+148
-14
No files matched your search
@@ -3,6 +3,7 @@ import { config } from "../../shared/config/index.js";
|
||||
import { messageStore } from "../message-capture/messageStore.js";
|
||||
import type { MessageRecord } from "../message-capture/types.js";
|
||||
import { type AnalysisLane, splitMessagesByLane } from "./analysisLanes.js";
|
||||
import { computeBudgetOverflowMessages } from "./batchBudget.js";
|
||||
import {
|
||||
pickBatchWithinBudget,
|
||||
processBatch,
|
||||
@@ -147,6 +148,60 @@ function scheduleLaneTimer(
|
||||
);
|
||||
}
|
||||
|
||||
// Un-claim messages that did NOT make it into the trimmed batch.
|
||||
// getPendingMessagesByConversation() flips EVERY fetched pending row
|
||||
// to `processing`; pickBatchWithinBudget() may then stop early on the
|
||||
// token budget, leaving the tail rows stuck in `processing` forever
|
||||
// (recovery only reverts rows older than 120s, and these keep getting
|
||||
// re-claimed each wave). Return them to `pending` so the next wave
|
||||
// picks them up instead of leaking processing slots.
|
||||
const unclaimed = computeBudgetOverflowMessages(
|
||||
processableMessages,
|
||||
trimmed,
|
||||
);
|
||||
if (unclaimed.length > 0) {
|
||||
const unclaimedRows = await messageStore
|
||||
.updateMessagesAIAnalysisBulk(
|
||||
unclaimed.map((msg) => ({
|
||||
messageId: msg.id,
|
||||
result: {
|
||||
status: "pending",
|
||||
flags: null,
|
||||
score: null,
|
||||
analysis: null,
|
||||
categories: null,
|
||||
severity: null,
|
||||
confidence: null,
|
||||
recommendedAction: null,
|
||||
analyzedAt: null,
|
||||
error: null,
|
||||
},
|
||||
})),
|
||||
)
|
||||
.catch((err: unknown) => {
|
||||
logger.error(
|
||||
{
|
||||
conversationKey,
|
||||
lane,
|
||||
count: unclaimed.length,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
},
|
||||
"Failed to un-claim budget-overflow messages back to pending",
|
||||
);
|
||||
return null;
|
||||
});
|
||||
if (unclaimedRows) {
|
||||
logger.debug(
|
||||
{
|
||||
conversationKey,
|
||||
lane,
|
||||
unclaimedCount: unclaimed.length,
|
||||
},
|
||||
"Returned budget-overflow messages to pending for next wave",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// processBatch releases THIS lane's lock the moment its worker job
|
||||
// finishes and re-schedules the same lane — independent of the other
|
||||
// lane's (possibly much slower) media batch.
|
||||
|
||||
Reference in new issue
Block a user