Added IAsyncFitness and IMutation.MinChromosomeLength#144
Added IAsyncFitness and IMutation.MinChromosomeLength#144mikasoukhov wants to merge 3 commits intogiacomelli:masterfrom
Conversation
- IAsyncFitness interface with EvaluateAsync(chromosome, cancellationToken) - AsyncFuncFitness for lambda-based async fitness evaluation - ITaskExecutor.Add changed from Action to Func<CancellationToken, ValueTask> - CancellationToken on ITaskExecutor, passed through constructor - Executors natively support async tasks with linked cancellation - GeneticAlgorithm auto-detects IAsyncFitness and calls async method - Sync IFitness.Evaluate throws NotSupportedException in async implementations
There was a problem hiding this comment.
Pull request overview
This PR extends GeneticSharp to support asynchronous fitness evaluation (with cancellation support) and exposes a minimum chromosome length requirement on mutations (per #109). It also updates task executors to accept native async delegates (Func<CancellationToken, ValueTask>) while preserving a sync Add(Action) convenience via an extension method.
Changes:
- Added
IAsyncFitness(+ helper implementations/stubs) and updatedGeneticAlgorithmfitness evaluation to use async fitness when available. - Updated
ITaskExecutor+ executor implementations to runFunc<CancellationToken, ValueTask>tasks and propagate cancellation tokens. - Added
IMutation.MinChromosomeLength(defaulted inMutationBase, overridden for sequence mutations) and tests validating the new contract.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| src/GeneticSharp.Infrastructure.Framework/Threading/TplTaskExecutor.cs | Switches TPL executor to Parallel.ForEachAsync and introduces cancellation-token-aware execution. |
| src/GeneticSharp.Infrastructure.Framework/Threading/TaskExecutorExtensions.cs | Adds Add(Action) convenience overload via extension method. |
| src/GeneticSharp.Infrastructure.Framework/Threading/TaskExecutorBase.cs | Changes stored task type to async delegate and introduces a constructor-provided cancellation token. |
| src/GeneticSharp.Infrastructure.Framework/Threading/ParallelTaskExecutor.cs | Updates parallel execution to run async delegates and link cancellation tokens. |
| src/GeneticSharp.Infrastructure.Framework/Threading/LinearTaskExecutor.cs | Updates linear execution to run async delegates synchronously. |
| src/GeneticSharp.Infrastructure.Framework/Threading/ITaskExecutor.cs | Updates task signature and adds CancellationToken to the interface. |
| src/GeneticSharp.Infrastructure.Framework.UnitTests/Threading/StubTaskExecutor.cs | Adapts unit test stub to new task delegate type. |
| src/GeneticSharp.Domain/Mutations/SequenceMutationBase.cs | Introduces MinChromosomeLength override and uses it in validation. |
| src/GeneticSharp.Domain/Mutations/MutationBase.cs | Adds default MinChromosomeLength implementation. |
| src/GeneticSharp.Domain/Mutations/IMutation.cs | Adds MinChromosomeLength to the mutation contract. |
| src/GeneticSharp.Domain/GeneticAlgorithm.cs | Uses async fitness when available; passes cancellation token through task executor. |
| src/GeneticSharp.Domain/Fitnesses/IAsyncFitness.cs | Adds async fitness interface with cancellation support. |
| src/GeneticSharp.Domain/Fitnesses/AsyncFuncFitness.cs | Adds a Func-based async fitness implementation. |
| src/GeneticSharp.Domain.UnitTests/Populations/AsyncFitnessStub.cs | Adds async fitness stub used for GA tests. |
| src/GeneticSharp.Domain.UnitTests/Mutations/MinChromosomeLengthTest.cs | Adds tests for MinChromosomeLength on several mutations. |
| src/GeneticSharp.Domain.UnitTests/GeneticAlgorithmTest.cs | Adds tests for async fitness, parallel async fitness, cancellation, and failure wrapping. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| public override bool Start() | ||
| { | ||
| try | ||
| { | ||
| var startTime = DateTime.Now; | ||
| CancellationTokenSource = new CancellationTokenSource(); | ||
| var result = new ParallelLoopResult(); | ||
| CancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken); | ||
| var token = CancellationTokenSource.Token; |
There was a problem hiding this comment.
TplTaskExecutor.Start() never calls base.Start(), so StopRequested is not reset and IsRunning is never set to true (but is set to false in finally). This makes IsRunning/Stop semantics inconsistent with LinearTaskExecutor/ParallelTaskExecutor. Call base.Start() at the beginning of Start(), and ensure IsRunning is set/reset in a consistent place (ideally by letting TaskExecutorBase/ParallelTaskExecutor handle it).
| var startTime = DateTime.Now; | ||
| CancellationTokenSource = new CancellationTokenSource(); | ||
| var result = new ParallelLoopResult(); | ||
| CancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken); | ||
| var token = CancellationTokenSource.Token; | ||
|
|
||
| try | ||
| { | ||
| result = Parallel.For(0, Tasks.Count, new ParallelOptions() { CancellationToken = CancellationTokenSource.Token }, (i, state) => | ||
| { | ||
| // Execute the target function (fitness). | ||
| Tasks[i](); | ||
| Parallel.ForEachAsync( | ||
| System.Linq.Enumerable.Range(0, Tasks.Count), | ||
| new ParallelOptions() { CancellationToken = token }, |
There was a problem hiding this comment.
TplTaskExecutor inherits MinThreads/MaxThreads and SetThreadPoolConfig/ResetThreadPoolConfig behavior from ParallelTaskExecutor, but Start() bypasses all of that. As a result, MinThreads/MaxThreads currently have no effect for this executor. Consider mirroring ParallelTaskExecutor.Start() by calling SetThreadPoolConfig before starting and ResetThreadPoolConfig in finally (or delegating to base.Start implementation) so the thread pool configuration is applied consistently.
| CancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken); | ||
| var token = CancellationTokenSource.Token; | ||
|
|
There was a problem hiding this comment.
CreateLinkedTokenSource allocates registrations that should be disposed. Neither the previous CancellationTokenSource (if any) nor the newly-created linked source is disposed here, which can leak registrations across repeated Start/Stop cycles. Dispose the linked CancellationTokenSource when Start finishes (and consider disposing any prior instance before overwriting the field).
| base.Start(); | ||
| CancellationTokenSource = new CancellationTokenSource(); | ||
| CancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken); | ||
| var token = CancellationTokenSource.Token; | ||
| var parallelTasks = new Task[Tasks.Count]; |
There was a problem hiding this comment.
ParallelTaskExecutor creates a linked CancellationTokenSource but never disposes it. Linked CTS instances hold onto token registrations; dispose it when Start completes (and also dispose any previous instance before overwriting the property) to avoid leaks when the executor is reused.
| CancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken); | ||
| var token = CancellationTokenSource.Token; |
There was a problem hiding this comment.
ParallelTaskExecutor.Start returns false on timeout (Task.WaitAll(..., Timeout)), but it does not cancel its CancellationTokenSource when that happens. That can leave work running in the background until the caller later invokes Stop(). Consider canceling the linked CancellationTokenSource when a timeout is detected so tasks get a timely cancellation signal.
| catch (Exception ex) | ||
| { | ||
| throw new FitnessException(Fitness, "Error executing Fitness.Evaluate for chromosome: {0}".With(ex.Message), ex); |
There was a problem hiding this comment.
The async fitness path wraps exceptions using a message that still references Fitness.Evaluate. This is misleading for IAsyncFitness implementations and makes diagnosing async failures harder. Update the message to reference EvaluateAsync (and consider special-casing OperationCanceledException if you want cancellations to be distinguishable from evaluation failures).
| catch (Exception ex) | |
| { | |
| throw new FitnessException(Fitness, "Error executing Fitness.Evaluate for chromosome: {0}".With(ex.Message), ex); | |
| catch (OperationCanceledException) | |
| { | |
| // Preserve cancellation semantics without wrapping in FitnessException. | |
| throw; | |
| } | |
| catch (Exception ex) | |
| { | |
| throw new FitnessException(Fitness, "Error executing Fitness.EvaluateAsync for chromosome: {0}".With(ex.Message), ex); |
| if (chromosome.Length < 3) | ||
| if (chromosome.Length < MinChromosomeLength) | ||
| { | ||
| throw new MutationException(this, "A chromosome should have, at least, 3 genes. {0} has only {1} gene.".With(chromosome.GetType().Name, chromosome.Length)); |
There was a problem hiding this comment.
ValidateLength now uses MinChromosomeLength, but the exception message still hardcodes "at least, 3 genes". If a derived mutation overrides MinChromosomeLength, this message becomes incorrect. Build the message using MinChromosomeLength (and consider pluralization for "gene(s)") so the error stays accurate.
| throw new MutationException(this, "A chromosome should have, at least, 3 genes. {0} has only {1} gene.".With(chromosome.GetType().Name, chromosome.Length)); | |
| throw new MutationException( | |
| this, | |
| "A chromosome should have, at least, {0} {1}. {2} has only {3} {4}." | |
| .With( | |
| MinChromosomeLength, | |
| MinChromosomeLength == 1 ? "gene" : "genes", | |
| chromosome.GetType().Name, | |
| chromosome.Length, | |
| chromosome.Length == 1 ? "gene" : "genes")); |
| var cts = new CancellationTokenSource(); | ||
| var taskExecutor = new LinearTaskExecutor(cts.Token); | ||
|
|
||
| var selection = new EliteSelection(); | ||
| var crossover = new OnePointCrossover(2); | ||
| var mutation = new UniformMutation(); | ||
| var chromosome = new ChromosomeStub(); | ||
| var target = new GeneticAlgorithm(new Population(50, 50, chromosome), | ||
| new AsyncFitnessStub() { SupportsParallel = true, ParallelSleep = 5000 }, selection, crossover, mutation); | ||
| target.TaskExecutor = taskExecutor; | ||
| target.Termination = new GenerationNumberTermination(25); | ||
|
|
||
| cts.CancelAfter(100); | ||
|
|
||
| Assert.Catch<FitnessException>(() => | ||
| { | ||
| target.Start(); | ||
| }); |
There was a problem hiding this comment.
Disposable 'CancellationTokenSource' is created but not disposed.
| var cts = new CancellationTokenSource(); | |
| var taskExecutor = new LinearTaskExecutor(cts.Token); | |
| var selection = new EliteSelection(); | |
| var crossover = new OnePointCrossover(2); | |
| var mutation = new UniformMutation(); | |
| var chromosome = new ChromosomeStub(); | |
| var target = new GeneticAlgorithm(new Population(50, 50, chromosome), | |
| new AsyncFitnessStub() { SupportsParallel = true, ParallelSleep = 5000 }, selection, crossover, mutation); | |
| target.TaskExecutor = taskExecutor; | |
| target.Termination = new GenerationNumberTermination(25); | |
| cts.CancelAfter(100); | |
| Assert.Catch<FitnessException>(() => | |
| { | |
| target.Start(); | |
| }); | |
| using (var cts = new CancellationTokenSource()) | |
| { | |
| var taskExecutor = new LinearTaskExecutor(cts.Token); | |
| var selection = new EliteSelection(); | |
| var crossover = new OnePointCrossover(2); | |
| var mutation = new UniformMutation(); | |
| var chromosome = new ChromosomeStub(); | |
| var target = new GeneticAlgorithm(new Population(50, 50, chromosome), | |
| new AsyncFitnessStub() { SupportsParallel = true, ParallelSleep = 5000 }, selection, crossover, mutation); | |
| target.TaskExecutor = taskExecutor; | |
| target.Termination = new GenerationNumberTermination(25); | |
| cts.CancelAfter(100); | |
| Assert.Catch<FitnessException>(() => | |
| { | |
| target.Start(); | |
| }); | |
| } |
…eledException handling, dynamic error messages
IAsyncFitnessinterface for async fitness evaluation withCancellationTokensupportITaskExecutor.Addto acceptFunc<CancellationToken, ValueTask>for native async executionIMutation.MinChromosomeLengthproperty (IMutation. RequiredLength property #109)