mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-10-22 10:02:41 +00:00
🗓️ fix: Classify Scheduled Run Duplicate Conflicts on DocumentDB (#15622)
This commit is contained in:
parent
d9dfdeb643
commit
a3f80e8a92
2 changed files with 130 additions and 15 deletions
|
|
@ -1889,6 +1889,105 @@ describe('reserveStartedRun duplicate reporting', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('run reservation database error formats', () => {
|
||||
const conflicts = [
|
||||
{
|
||||
conflict: 'duplicate',
|
||||
keyPattern: { scheduleId: 1, scheduledFor: 1 },
|
||||
index: 'scheduleId_1_scheduledFor_1',
|
||||
},
|
||||
{ conflict: 'overlap', keyPattern: { scheduleId: 1 }, index: 'scheduleId_1' },
|
||||
{ conflict: 'slot-taken', keyPattern: { capacitySlot: 1 }, index: 'capacitySlot_1' },
|
||||
] as const;
|
||||
|
||||
describe.each(['keyPattern', 'errmsg', 'message'] as const)('%s', (format) => {
|
||||
it.each(conflicts)(
|
||||
'classifies $conflict on admission',
|
||||
async ({ conflict, keyPattern, index }) => {
|
||||
const schedule = await methods.createSchedule(scheduleData());
|
||||
const data = runData(schedule);
|
||||
await ScheduleRun.create({ ...data, status: 'success' });
|
||||
const error = {
|
||||
code: 11000,
|
||||
...(format === 'keyPattern'
|
||||
? { keyPattern }
|
||||
: { [format]: `E11000 duplicate key error collection: scheduleruns index: ${index}` }),
|
||||
};
|
||||
// Inject the external database response shape; the occurrence lookup remains real.
|
||||
jest.spyOn(ScheduleRun, 'create').mockRejectedValueOnce(error);
|
||||
await expect(methods.reserveStartedRun(data)).resolves.toEqual(
|
||||
conflict === 'duplicate' ? { conflict, existingStatus: 'success' } : { conflict },
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(conflicts.filter(({ conflict }) => conflict !== 'duplicate'))(
|
||||
'classifies $conflict on approval resume',
|
||||
async ({ conflict, keyPattern, index }) => {
|
||||
const error = {
|
||||
code: 11000,
|
||||
...(format === 'keyPattern'
|
||||
? { keyPattern }
|
||||
: { [format]: `E11000 duplicate key error collection: scheduleruns index: ${index}` }),
|
||||
};
|
||||
jest.spyOn(ScheduleRun, 'findOneAndUpdate').mockImplementationOnce(() => {
|
||||
throw error;
|
||||
});
|
||||
await expect(methods.markRunResumeClaimed('schedule', new Date(), 0)).resolves.toEqual({
|
||||
conflict,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it.each(['started', 'requires_action', 'success'] as const)(
|
||||
'preserves the existing %s occurrence status without keyPattern or keyValue',
|
||||
async (status) => {
|
||||
const schedule = await methods.createSchedule(scheduleData());
|
||||
const data = runData(schedule);
|
||||
await ScheduleRun.create({ ...data, status });
|
||||
jest.spyOn(ScheduleRun, 'create').mockRejectedValueOnce({
|
||||
code: 11000,
|
||||
message:
|
||||
'E11000 duplicate key error collection: scheduleruns index: scheduleId_1_scheduledFor_1 dup key: { scheduleId: "schedule" }',
|
||||
});
|
||||
await expect(methods.reserveStartedRun(data)).resolves.toEqual({
|
||||
conflict: 'duplicate',
|
||||
existingStatus: status,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it('prefers keyPattern over a contradictory index name', async () => {
|
||||
jest.spyOn(ScheduleRun, 'create').mockRejectedValueOnce({
|
||||
code: 11000,
|
||||
keyPattern: { capacitySlot: 1 },
|
||||
message: 'E11000 duplicate key error collection: scheduleruns index: scheduleId_1',
|
||||
});
|
||||
await expect(methods.reserveStartedRun({})).resolves.toEqual({ conflict: 'slot-taken' });
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ code: 11000 },
|
||||
{ code: 42, message: 'index: scheduleId_1' },
|
||||
{ code: 11000, message: 'index: scheduleId_1_extra' },
|
||||
{ code: 11000, message: 'index: prefix_scheduleId_1' },
|
||||
{ code: 11000, message: 'index: unrelated dup key: { value: "index: scheduleId_1" }' },
|
||||
{ code: 11000, keyPattern: {}, message: 'index: scheduleId_1' },
|
||||
{ code: 11000, keyPattern: { scheduleId: 1, unexpected: 1 } },
|
||||
{ code: 11000, keyPattern: { scheduledFor: 1 } },
|
||||
{ code: 11000, message: 123 },
|
||||
new Error('connection lost'),
|
||||
])('rethrows unrecognized errors: %j', async (error) => {
|
||||
jest.spyOn(ScheduleRun, 'create').mockRejectedValueOnce(error);
|
||||
await expect(methods.reserveStartedRun({})).rejects.toBe(error);
|
||||
jest.spyOn(ScheduleRun, 'findOneAndUpdate').mockImplementationOnce(() => {
|
||||
throw error;
|
||||
});
|
||||
await expect(methods.markRunResumeClaimed('schedule', new Date(), 0)).rejects.toBe(error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteScheduleRun conversation fence', () => {
|
||||
it('deletes only the reservation the caller inserted', async () => {
|
||||
const schedule = await methods.createSchedule(scheduleData());
|
||||
|
|
|
|||
|
|
@ -50,31 +50,47 @@ const TERMINAL_CARD_STATUSES: ScheduleRunStatus[] = [
|
|||
'skipped_balance',
|
||||
];
|
||||
|
||||
type DuplicateKeyError = { code?: number; keyPattern?: Record<string, unknown> };
|
||||
type DuplicateKeyError = {
|
||||
code?: number;
|
||||
keyPattern?: Record<string, number>;
|
||||
errmsg?: string;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
/** DocumentDB can omit keyPattern; retain the deployed default index names as a fallback. */
|
||||
function matchesRunDuplicate(
|
||||
error: unknown,
|
||||
fields: readonly string[],
|
||||
indexName: string,
|
||||
): boolean {
|
||||
const err = error as DuplicateKeyError;
|
||||
if (err?.code !== DUPLICATE_KEY) {
|
||||
return false;
|
||||
}
|
||||
const keyPattern = err.keyPattern;
|
||||
if (keyPattern != null) {
|
||||
return (
|
||||
Object.keys(keyPattern).length === fields.length &&
|
||||
fields.every((field) => Object.prototype.hasOwnProperty.call(keyPattern, field))
|
||||
);
|
||||
}
|
||||
const message = err.errmsg || err.message;
|
||||
return typeof message === 'string' && /\bindex:\s+(\S+)/.exec(message)?.[1] === indexName;
|
||||
}
|
||||
|
||||
/** A duplicate-key error whose conflict is the {scheduleId, scheduledFor} occurrence index. */
|
||||
function isOccurrenceDuplicate(error: unknown): boolean {
|
||||
const err = error as DuplicateKeyError;
|
||||
return err?.code === DUPLICATE_KEY && err.keyPattern != null && 'scheduledFor' in err.keyPattern;
|
||||
return matchesRunDuplicate(error, ['scheduleId', 'scheduledFor'], 'scheduleId_1_scheduledFor_1');
|
||||
}
|
||||
|
||||
/** A duplicate-key error whose conflict is the single-active-run partial index
|
||||
* ({scheduleId} where status:'started'). Matched EXACTLY on scheduleId so the
|
||||
* global {capacitySlot} index below is never misread as a per-schedule overlap. */
|
||||
/** A duplicate-key error whose conflict is the single-active-run partial index. */
|
||||
function isActiveRunConflict(error: unknown): boolean {
|
||||
const err = error as DuplicateKeyError;
|
||||
return (
|
||||
err?.code === DUPLICATE_KEY &&
|
||||
err.keyPattern != null &&
|
||||
'scheduleId' in err.keyPattern &&
|
||||
!('scheduledFor' in err.keyPattern)
|
||||
);
|
||||
return matchesRunDuplicate(error, ['scheduleId'], 'scheduleId_1');
|
||||
}
|
||||
|
||||
/** A duplicate-key error whose conflict is the GLOBAL {capacitySlot} cap index. */
|
||||
function isCapacitySlotConflict(error: unknown): boolean {
|
||||
const err = error as DuplicateKeyError;
|
||||
return err?.code === DUPLICATE_KEY && err.keyPattern != null && 'capacitySlot' in err.keyPattern;
|
||||
return matchesRunDuplicate(error, ['capacitySlot'], 'capacitySlot_1');
|
||||
}
|
||||
|
||||
/** A duplicate-key error whose conflict is the per-user {user, slot} cap index. */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue