diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Core/Jobs/JobCoordinator.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Core/Jobs/JobCoordinator.cs index e3ba7f1f2..be6df50c2 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Core/Jobs/JobCoordinator.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Core/Jobs/JobCoordinator.cs @@ -22,6 +22,7 @@ public sealed class JobCoordinator : IJobCoordinator private readonly TimeProvider _timeProvider; private readonly JobDiagnostics _diagnostics; private readonly string _holderId; + private readonly SemaphoreSlim _concurrencyGate; public JobCoordinator( IOptions optionsAccessor, @@ -42,6 +43,7 @@ public sealed class JobCoordinator : IJobCoordinator _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); _diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics)); _holderId = BuildHolderId(); + _concurrencyGate = new SemaphoreSlim(_options.MaxConcurrentJobs, _options.MaxConcurrentJobs); } public async Task TriggerAsync(string kind, IReadOnlyDictionary? parameters, string trigger, CancellationToken cancellationToken) @@ -437,6 +439,27 @@ public sealed class JobCoordinator : IJobCoordinator IReadOnlyDictionary parameters, string trigger, CancellationTokenSource linkedTokenSource) + { + // Limit concurrent job execution to prevent resource exhaustion (DB connections, HTTP, CPU). + // The semaphore is bounded by MaxConcurrentJobs (default: 6). + await _concurrencyGate.WaitAsync(linkedTokenSource.Token).ConfigureAwait(false); + try + { + await ExecuteJobCoreAsync(definition, lease, run, parameters, trigger, linkedTokenSource).ConfigureAwait(false); + } + finally + { + _concurrencyGate.Release(); + } + } + + private async Task ExecuteJobCoreAsync( + JobDefinition definition, + JobLease lease, + JobRunSnapshot run, + IReadOnlyDictionary parameters, + string trigger, + CancellationTokenSource linkedTokenSource) { using (linkedTokenSource) { diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Core/Jobs/JobSchedulerOptions.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Core/Jobs/JobSchedulerOptions.cs index b5db137af..383701b9b 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Core/Jobs/JobSchedulerOptions.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Core/Jobs/JobSchedulerOptions.cs @@ -9,4 +9,11 @@ public sealed class JobSchedulerOptions public TimeSpan DefaultTimeout { get; set; } = TimeSpan.FromMinutes(15); public TimeSpan DefaultLeaseDuration { get; set; } = TimeSpan.FromMinutes(5); + + /// + /// Maximum number of jobs that can execute concurrently. + /// When exceeded, new triggers will queue behind the semaphore. + /// Default: 6 (balances throughput vs. resource pressure). + /// + public int MaxConcurrentJobs { get; set; } = 6; }