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
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:
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
@@ -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}",
|
||||||
|
|||||||
Reference in New Issue
Block a user