diff --git a/src/Managing.Application/Backtests/BacktestExecutor.cs b/src/Managing.Application/Backtests/BacktestExecutor.cs
index f1cd0b03..bbc73885 100644
--- a/src/Managing.Application/Backtests/BacktestExecutor.cs
+++ b/src/Managing.Application/Backtests/BacktestExecutor.cs
@@ -125,6 +125,7 @@ public class BacktestExecutor
/// Optional bundle request ID to update with backtest result
/// Additional metadata
/// Optional callback for progress updates (0-100)
+ /// Cancellation token to stop execution
/// The lightweight backtest result
public async Task ExecuteAsync(
TradingBotConfig config,
@@ -135,7 +136,8 @@ public class BacktestExecutor
string requestId = null,
Guid? bundleRequestId = null,
object metadata = null,
- Func progressCallback = null)
+ Func progressCallback = null,
+ CancellationToken cancellationToken = default)
{
if (candles == null || candles.Count == 0)
{
@@ -247,6 +249,9 @@ public class BacktestExecutor
// Process all candles with optimized rolling window approach
foreach (var candle in orderedCandles)
{
+ // Check for cancellation (timeout or shutdown)
+ cancellationToken.ThrowIfCancellationRequested();
+
// Add to HashSet for reuse
fixedCandles.Add(candle);
tradingBot.LastCandle = candle;
diff --git a/src/Managing.Application/Backtests/BacktestExecutorAdapter.cs b/src/Managing.Application/Backtests/BacktestExecutorAdapter.cs
index 880a7c6b..96820b75 100644
--- a/src/Managing.Application/Backtests/BacktestExecutorAdapter.cs
+++ b/src/Managing.Application/Backtests/BacktestExecutorAdapter.cs
@@ -65,7 +65,8 @@ public class BacktestExecutorAdapter : IBacktester
requestId,
bundleRequestId: null,
metadata,
- progressCallback: null);
+ progressCallback: null,
+ cancellationToken: default);
return result;
}
@@ -88,7 +89,8 @@ public class BacktestExecutorAdapter : IBacktester
requestId,
bundleRequestId: null,
metadata,
- progressCallback: null);
+ progressCallback: null,
+ cancellationToken: default);
return result;
}
diff --git a/src/Managing.Application/GeneticService.cs b/src/Managing.Application/GeneticService.cs
index 47fa02d9..461db445 100644
--- a/src/Managing.Application/GeneticService.cs
+++ b/src/Managing.Application/GeneticService.cs
@@ -1057,7 +1057,9 @@ public class TradingBotFitness : IFitness
requestId: _request.RequestId,
bundleRequestId: null, // Genetic algorithm doesn't use bundle requests
metadata: new GeneticBacktestMetadata(_geneticAlgorithm?.GenerationsNumber ?? 0,
- _request.RequestId)
+ _request.RequestId),
+ progressCallback: null,
+ cancellationToken: default
)
).GetAwaiter().GetResult();
}
diff --git a/src/Managing.Application/Workers/BacktestComputeWorker.cs b/src/Managing.Application/Workers/BacktestComputeWorker.cs
index e3f899d0..37cd4af5 100644
--- a/src/Managing.Application/Workers/BacktestComputeWorker.cs
+++ b/src/Managing.Application/Workers/BacktestComputeWorker.cs
@@ -267,18 +267,9 @@ public class BacktestComputeWorker : BackgroundService
_jobProgressTrackers.TryAdd(job.Id, progressTracker);
// Progress callback that only updates in-memory progress (non-blocking)
+ // Timeout is now enforced via CancellationToken, not by throwing in callback
Func progressCallback = (percentage) =>
{
- // Check if job has been running too long
- var elapsed = DateTime.UtcNow - jobStartTime;
- if (elapsed.TotalMinutes > _options.JobTimeoutMinutes)
- {
- _logger.LogWarning(
- "Job {JobId} has been running for {ElapsedMinutes} minutes, exceeding timeout of {TimeoutMinutes} minutes",
- job.Id, elapsed.TotalMinutes, _options.JobTimeoutMinutes);
- throw new TimeoutException($"Job exceeded timeout of {_options.JobTimeoutMinutes} minutes");
- }
-
// Update progress in memory only - persistence happens in background
progressTracker.UpdateProgress(percentage);
@@ -301,7 +292,8 @@ public class BacktestComputeWorker : BackgroundService
requestId: job.RequestId,
bundleRequestId: job.BundleRequestId,
metadata: null,
- progressCallback: progressCallback);
+ progressCallback: progressCallback,
+ cancellationToken: linkedCts.Token);
}
catch (OperationCanceledException) when (timeoutCts.Token.IsCancellationRequested && !cancellationToken.IsCancellationRequested)
{