Fix job scheduler race condition and enhance Blazor reconnection UI
All checks were successful
Build Docker Images / test (map[name:Morska plugin_project:DiunaBI.Plugins.Morska]) (push) Successful in 1m27s
Build Docker Images / test (map[name:PedrolloPL plugin_project:DiunaBI.Plugins.PedrolloPL]) (push) Successful in 1m23s
Build Docker Images / build-and-push (map[image_suffix:morska name:Morska plugin_project:DiunaBI.Plugins.Morska]) (push) Successful in 1m43s
Build Docker Images / build-and-push (map[image_suffix:pedrollopl name:PedrolloPL plugin_project:DiunaBI.Plugins.PedrolloPL]) (push) Successful in 1m39s

This commit is contained in:
2025-12-08 22:45:31 +01:00
parent b917aa5077
commit 151ecaa98f
4 changed files with 64 additions and 5 deletions

View File

@@ -5,6 +5,30 @@
## RECENT CHANGES (This Session) ## RECENT CHANGES (This Session)
**Job Scheduler Race Condition Fix (Dec 8, 2025):**
-**In-Memory Deduplication** - Added `HashSet<Guid>` to track LayerIds scheduled within the same batch
-**Prevents Duplicate Jobs** - Fixed race condition where same layer could be scheduled multiple times during single "Run All Jobs" operation
-**Two-Level Protection** - In-memory check (HashSet) runs before database check for O(1) performance
-**Applied to Both Methods** - Fixed both ScheduleImportJobsAsync and ScheduleProcessJobsAsync
-**Better Logging** - Added debug log message "Job already scheduled in this batch" for transparency
- Root cause: When multiple layers had same ID in query results or import plugins created new layers during scheduling loop, database check couldn't detect duplicates added in same batch before SaveChangesAsync()
- Solution: Track scheduled LayerIds in HashSet during loop iteration to prevent within-batch duplicates
- Files modified: [JobSchedulerService.cs](DiunaBI.Infrastructure/Services/JobSchedulerService.cs)
- Status: Race condition resolved, duplicate job creation prevented
---
**Blazor Server Reconnection UI Customization (Dec 8, 2025):**
-**Custom Reconnection Modal** - Replaced default Blazor "Rejoin failed..." dialog with custom-styled modal
-**Theme-Matched Styling** - Changed loader and button colors from blue to app's primary red (#e7163d) matching navbar
-**Timer with Elapsed Seconds** - Added real-time timer showing elapsed reconnection time (0s, 1s, 2s...)
-**CSS Classes Integration** - Used Blazor's built-in `.components-reconnect-show/failed/rejected` classes for state management
-**MutationObserver Timer** - JavaScript watches for CSS class changes to start/stop elapsed time counter
-**Professional Design** - Modal backdrop blur, spinner animation, red reload button with hover effects
- Files modified: [App.razor](DiunaBI.UI.Web/Components/App.razor), [app.css](DiunaBI.UI.Web/wwwroot/app.css)
- Files created: [reconnect.js](DiunaBI.UI.Web/wwwroot/js/reconnect.js)
- Status: Blazor reconnection UI now matches app theme with timer indicator
**Jobs List Sorting and Multi-Select Filtering (Dec 8, 2025):** **Jobs List Sorting and Multi-Select Filtering (Dec 8, 2025):**
-**Fixed Job Sorting** - Changed from single CreatedAt DESC to Priority ASC → JobType → CreatedAt DESC -**Fixed Job Sorting** - Changed from single CreatedAt DESC to Priority ASC → JobType → CreatedAt DESC
-**Multi-Select Status Filter** - Replaced single status dropdown with multi-select supporting multiple JobStatus values -**Multi-Select Status Filter** - Replaced single status dropdown with multi-select supporting multiple JobStatus values

View File

@@ -1,7 +1,9 @@
DECLARE @JustForDebug TINYINT = 0; DECLARE @JustForDebug TINYINT = 0;
-- FIX DATAINBOX!
-- SETUP VARIABLES -- SETUP VARIABLES
DECLARE @Year INT = 2024; DECLARE @Year INT = 2025;
DECLARE @Type NVARCHAR(5) = 'B3'; DECLARE @Type NVARCHAR(5) = 'B3';
DECLARE @StartDate NVARCHAR(10) = '2025.01.02'; DECLARE @StartDate NVARCHAR(10) = '2025.01.02';
DECLARE @EndDate NVARCHAR(10) = '2026.12.31' DECLARE @EndDate NVARCHAR(10) = '2026.12.31'
@@ -22,7 +24,7 @@ SET @Plugin =
DECLARE @DataInboxName NVARCHAR(100); DECLARE @DataInboxName NVARCHAR(100);
SET @DataInboxName = SET @DataInboxName =
CASE @Type CASE @Type
WHEN 'B3' THEN 'B3_2024' WHEN 'B3' THEN 'P2_2025'
ELSE NULL -- If @Type doesn't match, set it to NULL ELSE NULL -- If @Type doesn't match, set it to NULL
END; END;

View File

@@ -1,7 +1,7 @@
DECLARE @JustForDebug TINYINT = 0; DECLARE @JustForDebug TINYINT = 0;
-- SETUP VARIABLES -- SETUP VARIABLES
DECLARE @Year INT = 2024; DECLARE @Year INT = 2025;
DECLARE @Number INT = (SELECT COUNT(id) + 1 FROM [DiunaBI-PedrolloPL].[dbo].[Layers]); DECLARE @Number INT = (SELECT COUNT(id) + 1 FROM [DiunaBI-PedrolloPL].[dbo].[Layers]);
DECLARE @CurrentTimestamp NVARCHAR(14) = FORMAT(GETDATE(), 'yyyyMMddHHmm'); DECLARE @CurrentTimestamp NVARCHAR(14) = FORMAT(GETDATE(), 'yyyyMMddHHmm');
@@ -56,3 +56,16 @@ VALUES ((SELECT NEWID()), 'Plugin', 'PedrolloPL.Process.P2', GETDATE(), GETDATE(
INSERT INTO [DiunaBI-PedrolloPL].[dbo].[Records] INSERT INTO [DiunaBI-PedrolloPL].[dbo].[Records]
([Id], [Code], [Desc1], [CreatedAt], [ModifiedAt], [CreatedById], [ModifiedById], [IsDeleted], [LayerId]) ([Id], [Code], [Desc1], [CreatedAt], [ModifiedAt], [CreatedById], [ModifiedById], [IsDeleted], [LayerId])
VALUES ((SELECT NEWID()), 'Priority', '110', GETDATE(), GETDATE(), '117be4f0-b5d1-41a1-a962-39dc30cce368', '117be4f0-b5d1-41a1-a962-39dc30cce368', 0, @LayerId); VALUES ((SELECT NEWID()), 'Priority', '110', GETDATE(), GETDATE(), '117be4f0-b5d1-41a1-a962-39dc30cce368', '117be4f0-b5d1-41a1-a962-39dc30cce368', 0, @LayerId);
--
INSERT INTO [DiunaBI-PedrolloPL].[dbo].[Records]
([Id], [Code], [Desc1], [CreatedAt], [ModifiedAt], [CreatedById], [ModifiedById], [IsDeleted], [LayerId])
VALUES ((SELECT NEWID()), 'GoogleSheetId', '1jI-3QrlBADm5slEl2Balf29cKmHwkYi4pboaHY-gRqc', GETDATE(), GETDATE(), '117be4f0-b5d1-41a1-a962-39dc30cce368', '117be4f0-b5d1-41a1-a962-39dc30cce368', 0, @LayerId);
INSERT INTO [DiunaBI-PedrolloPL].[dbo].[Records]
([Id], [Code], [Desc1], [CreatedAt], [ModifiedAt], [CreatedById], [ModifiedById], [IsDeleted], [LayerId])
VALUES ((SELECT NEWID()), 'GoogleSheetTab', 'P2_Export_DiunaBI', GETDATE(), GETDATE(), '117be4f0-b5d1-41a1-a962-39dc30cce368', '117be4f0-b5d1-41a1-a962-39dc30cce368', 0, @LayerId);
INSERT INTO [DiunaBI-PedrolloPL].[dbo].[Records]
([Id], [Code], [Desc1], [CreatedAt], [ModifiedAt], [CreatedById], [ModifiedById], [IsDeleted], [LayerId])
VALUES ((SELECT NEWID()), 'GoogleSheetRange', 'C32:O48', GETDATE(), GETDATE(), '117be4f0-b5d1-41a1-a962-39dc30cce368', '117be4f0-b5d1-41a1-a962-39dc30cce368', 0, @LayerId);

View File

@@ -40,6 +40,7 @@ public class JobSchedulerService
_logger.LogInformation("JobScheduler: Found {Count} import workers to schedule", importWorkers.Count); _logger.LogInformation("JobScheduler: Found {Count} import workers to schedule", importWorkers.Count);
var jobsCreated = 0; var jobsCreated = 0;
var scheduledLayerIds = new HashSet<Guid>(); // Track LayerIds scheduled in this batch
foreach (var worker in importWorkers) foreach (var worker in importWorkers)
{ {
@@ -61,7 +62,15 @@ public class JobSchedulerService
var maxRetriesStr = worker.Records?.FirstOrDefault(r => r.Code == "MaxRetries")?.Desc1; var maxRetriesStr = worker.Records?.FirstOrDefault(r => r.Code == "MaxRetries")?.Desc1;
var maxRetries = int.TryParse(maxRetriesStr, out var mr) ? mr : 3; var maxRetries = int.TryParse(maxRetriesStr, out var mr) ? mr : 3;
// Check if there's already a pending/running job for this layer // Check in-memory: already scheduled in this batch?
if (scheduledLayerIds.Contains(worker.Id))
{
_logger.LogDebug("JobScheduler: Job already scheduled in this batch for {LayerName} ({LayerId})",
worker.Name, worker.Id);
continue;
}
// Check if there's already a pending/running job for this layer in database
var existingJob = await _db.QueueJobs var existingJob = await _db.QueueJobs
.Where(j => j.LayerId == worker.Id && .Where(j => j.LayerId == worker.Id &&
(j.Status == JobStatus.Pending || j.Status == JobStatus.Running)) (j.Status == JobStatus.Pending || j.Status == JobStatus.Running))
@@ -91,6 +100,7 @@ public class JobSchedulerService
}; };
_db.QueueJobs.Add(job); _db.QueueJobs.Add(job);
scheduledLayerIds.Add(worker.Id); // Track that we've scheduled this layer
jobsCreated++; jobsCreated++;
_logger.LogInformation("JobScheduler: Created import job for {LayerName} ({LayerId}) with priority {Priority}", _logger.LogInformation("JobScheduler: Created import job for {LayerName} ({LayerId}) with priority {Priority}",
@@ -129,6 +139,7 @@ public class JobSchedulerService
_logger.LogInformation("JobScheduler: Found {Count} process workers to schedule", processWorkers.Count); _logger.LogInformation("JobScheduler: Found {Count} process workers to schedule", processWorkers.Count);
var jobsCreated = 0; var jobsCreated = 0;
var scheduledLayerIds = new HashSet<Guid>(); // Track LayerIds scheduled in this batch
foreach (var worker in processWorkers) foreach (var worker in processWorkers)
{ {
@@ -150,7 +161,15 @@ public class JobSchedulerService
var maxRetriesStr = worker.Records?.FirstOrDefault(r => r.Code == "MaxRetries")?.Desc1; var maxRetriesStr = worker.Records?.FirstOrDefault(r => r.Code == "MaxRetries")?.Desc1;
var maxRetries = int.TryParse(maxRetriesStr, out var mr) ? mr : 3; var maxRetries = int.TryParse(maxRetriesStr, out var mr) ? mr : 3;
// Check if there's already a pending/running job for this layer // Check in-memory: already scheduled in this batch?
if (scheduledLayerIds.Contains(worker.Id))
{
_logger.LogDebug("JobScheduler: Job already scheduled in this batch for {LayerName} ({LayerId})",
worker.Name, worker.Id);
continue;
}
// Check if there's already a pending/running job for this layer in database
var existingJob = await _db.QueueJobs var existingJob = await _db.QueueJobs
.Where(j => j.LayerId == worker.Id && .Where(j => j.LayerId == worker.Id &&
(j.Status == JobStatus.Pending || j.Status == JobStatus.Running)) (j.Status == JobStatus.Pending || j.Status == JobStatus.Running))
@@ -180,6 +199,7 @@ public class JobSchedulerService
}; };
_db.QueueJobs.Add(job); _db.QueueJobs.Add(job);
scheduledLayerIds.Add(worker.Id); // Track that we've scheduled this layer
jobsCreated++; jobsCreated++;
_logger.LogInformation("JobScheduler: Created process job for {LayerName} ({LayerId}) with priority {Priority}", _logger.LogInformation("JobScheduler: Created process job for {LayerName} ({LayerId}) with priority {Priority}",