mirror of
https://github.com/agregarr/agregarr.git
synced 2026-01-25 03:48:48 -06:00
fix(scheduler): startNow immediate sync and deadlock bugs (#348)
Bug 1: startNow doesn't trigger immediate sync - When startNow=true, firstSyncAt was set to current time - calculateNextRunFromFirstSync() returned firstSyncAt + interval (12h later) - No immediate sync was triggered - Fix: If firstSyncAt is within 60s of now, queue immediate sync Bug 2: Deadlock when scheduled sync overlaps full sync - Scheduled sync acquired API lock in queueCollectionSync() - Queued sync to process "after main sync completes" - But main sync was waiting for the same API lock - Fix: Move API lock acquisition from queue time to execution time in processLibraryQueue(), after fullSyncRunning check Co-authored-by: bitr8 <bitr8@users.noreply.github.com>
This commit is contained in:
@@ -279,6 +279,36 @@ export class IndividualCollectionScheduler {
|
||||
intervalHours
|
||||
);
|
||||
|
||||
// If firstSyncAt is very recent (within 60 seconds), trigger immediate sync
|
||||
// This handles the "startNow" case where user expects immediate execution
|
||||
const firstSyncDate = new Date(firstSyncAt);
|
||||
const now = new Date();
|
||||
const timeSinceFirstSync = now.getTime() - firstSyncDate.getTime();
|
||||
const immediateWindowMs = 60 * 1000; // 60 second grace window
|
||||
|
||||
if (
|
||||
timeSinceFirstSync >= 0 &&
|
||||
timeSinceFirstSync <= immediateWindowMs
|
||||
) {
|
||||
logger.info(
|
||||
`Triggering immediate sync for newly scheduled collection (startNow)`,
|
||||
{
|
||||
label: 'Individual Collection Scheduler',
|
||||
collectionId,
|
||||
intervalHours,
|
||||
firstSyncAt,
|
||||
timeSinceFirstSyncMs: timeSinceFirstSync,
|
||||
}
|
||||
);
|
||||
// Queue immediate sync (don't await to avoid blocking scheduler setup)
|
||||
this.queueCollectionSync(collectionId).catch((err) => {
|
||||
logger.error(`Failed to queue immediate sync: ${err}`, {
|
||||
label: 'Individual Collection Scheduler',
|
||||
collectionId,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Create a one-time job for the next calculated run, then reschedule
|
||||
const job = schedule.scheduleJob(nextRunTime, async () => {
|
||||
await this.queueCollectionSync(collectionId);
|
||||
@@ -555,14 +585,9 @@ export class IndividualCollectionScheduler {
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait for API access before proceeding
|
||||
const apiType = collectionConfig.type;
|
||||
await this.waitForApiAccess(
|
||||
apiType,
|
||||
collectionId,
|
||||
collectionConfig.name,
|
||||
collectionConfig.libraryId
|
||||
);
|
||||
// Note: API lock is acquired in processLibraryQueue() just before execution,
|
||||
// not here. Acquiring it here causes deadlock when scheduled sync triggers
|
||||
// during full sync (both waiting on same lock).
|
||||
|
||||
const libraryId = collectionConfig.libraryId;
|
||||
|
||||
@@ -669,6 +694,32 @@ export class IndividualCollectionScheduler {
|
||||
|
||||
libraryQueue.currentCollection = nextSync.collectionName;
|
||||
|
||||
// Acquire API lock here, just before execution, not at queue time.
|
||||
// This prevents deadlock when scheduled sync triggers during full sync.
|
||||
const settings = getSettings();
|
||||
const collectionConfig = settings.plex.collectionConfigs?.find(
|
||||
(config) => config.id === nextSync.collectionId
|
||||
);
|
||||
|
||||
if (!collectionConfig) {
|
||||
logger.warn(
|
||||
`Collection config not found during queue processing: ${nextSync.collectionId}`,
|
||||
{
|
||||
label: 'Individual Collection Scheduler',
|
||||
collectionId: nextSync.collectionId,
|
||||
}
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const apiType = collectionConfig.type;
|
||||
await this.waitForApiAccess(
|
||||
apiType,
|
||||
nextSync.collectionId,
|
||||
nextSync.collectionName,
|
||||
nextSync.libraryId
|
||||
);
|
||||
|
||||
logger.info(
|
||||
`Processing queued collection sync: ${nextSync.collectionName} (${libraryQueue.queue.length} remaining in queue)`,
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user