Skip to content

Commit f8abda9

Browse files
authored
Merge pull request #40 from Blazor-Data-Orchestrator/feature/agent-single-job-execution-guard
Prevent concurrent job execution on single agent and ensure memory cl…
2 parents f01bdcf + b94060a commit f8abda9

2 files changed

Lines changed: 38 additions & 0 deletions

File tree

src/BlazorDataOrchestrator.Core/Services/CodeExecutorService.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -775,6 +775,24 @@ import json
775775

776776
return null;
777777
}
778+
779+
/// <summary>
780+
/// Cleans up state between job executions to prevent memory leaks and state bleed.
781+
/// Resets the CSScript evaluator and forces unloading of collectible AssemblyLoadContexts.
782+
/// Must be called after every job execution, regardless of success or failure.
783+
/// </summary>
784+
public void CleanupAfterExecution()
785+
{
786+
try
787+
{
788+
// Reset the CSScript evaluator to clear all cached references and compiled assemblies
789+
CSScript.Evaluator.Reset();
790+
}
791+
catch
792+
{
793+
// Evaluator may not have been initialized — safe to ignore
794+
}
795+
}
778796
}
779797

780798
/// <summary>

src/BlazorOrchestrator.Agent/Worker.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ public class Worker : BackgroundService
2626
// Visibility timeout configuration for long-running jobs
2727
private static readonly TimeSpan VisibilityTimeout = TimeSpan.FromMinutes(5);
2828
private static readonly TimeSpan RenewalInterval = TimeSpan.FromMinutes(3);
29+
30+
// Ensures only one job runs at a time per agent instance.
31+
// Prevents picking up a new job while the current one is still executing.
32+
private static readonly SemaphoreSlim _jobExecutionLock = new(1, 1);
2933

3034
public Worker(
3135
ILogger<Worker> logger,
@@ -96,6 +100,10 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
96100
using var renewalCts = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
97101
Task<string>? renewalTask = null;
98102

103+
// Acquire the execution lock — blocks if a previous job is somehow still running
104+
await _jobExecutionLock.WaitAsync(stoppingToken);
105+
_logger.LogInformation("Execution lock acquired for message {MessageId}", message.MessageId);
106+
99107
try
100108
{
101109
// Parse the queue message, handling both Base64-encoded and plain text formats
@@ -183,6 +191,18 @@ await _jobManager.ProcessJobInstanceAsync(
183191
// Message will become visible again after visibility timeout
184192
// After too many failures, it will go to poison queue (if configured)
185193
}
194+
finally
195+
{
196+
// Release the execution lock so the next job can be picked up
197+
_jobExecutionLock.Release();
198+
_logger.LogInformation("Execution lock released for message {MessageId}", message.MessageId);
199+
200+
// Force cleanup between jobs to prevent memory leaks and state bleed
201+
_codeExecutor.CleanupAfterExecution();
202+
GC.Collect(2, GCCollectionMode.Aggressive, blocking: true);
203+
GC.WaitForPendingFinalizers();
204+
_logger.LogInformation("Post-job memory cleanup completed");
205+
}
186206
}
187207
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
188208
{

0 commit comments

Comments
 (0)