Skip to content

Commit 8bd7c03

Browse files
thomhurstclaude
andauthored
feat: implement module scheduling constraints and enhance scheduler (#1304)
* refactor: enhance module execution with eager parallel scheduling and improved state management * feat: implement module scheduling constraints and enhance scheduler functionality * refactor: optimize blocking module search by converting values to a list * refactor: improve module state properties with volatile fields for thread safety * refactor: unify module state management by replacing boolean flags with an enum for clarity and consistency * feat: add detailed logging for module execution and queuing states * refactor: enhance constraint checking in module execution with improved logging and state consistency * fix: improve constraint checking with state snapshot and comprehensive debug logging - Snapshot all module states before filtering to ensure consistent view - Add detailed debug logging for constraint checking to diagnose race conditions - Change all diagnostic logs to LogDebug level - Log active module comparisons, lock key intersections, and blocking decisions * fix: exclude self from constraint checks in module scheduling * fix: use tracking collections for constraint checking Fixes race condition in constraint checking by using the tracking collections (_queuedModules and _executingModules) directly instead of filtering by State enum. This prevents the issue where State changes during iteration would be immediately visible through list references, causing inconsistent constraint evaluation. Changes: - Replace state-based filtering with direct collection usage - Add self-exclusion checks to prevent modules comparing against themselves - Remove snapshot approach that created references to mutable state This ensures modules with conflicting lock keys are properly blocked from parallel execution, preventing the race condition that allowed Module2 and Module3 (both with lock key "B") to execute concurrently. Fixes #1304 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: simplify long-running module execution by removing timeout handling * fix: respect engine cancellation in module execution task handling * fix: add timeout handling for LongRunningModule to ensure cancellation behavior * fix: correct await usage in timeout handling for module execution * fix: prevent unobserved task exceptions by observing executeAsyncTask's exception in timeout handling * fix: add defensive constraint check at execution boundary to prevent race conditions --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 32a2785 commit 8bd7c03

17 files changed

+1171
-310
lines changed

src/ModularPipelines/Context/IPipelineHookContext.cs

Lines changed: 120 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -11,76 +11,135 @@ namespace ModularPipelines.Context;
1111
/// <summary>
1212
/// The pipeline context, with access to configuration, DI and helper objects.
1313
/// </summary>
14+
/// <remarks>
15+
/// This interface serves as a facade providing unified access to all pipeline capabilities.
16+
/// Each property represents a focused helper for a specific domain (file system, commands, serialization, etc.).
17+
///
18+
/// The facade pattern is intentional here to:
19+
/// - Provide a single entry point for all pipeline operations
20+
/// - Allow dependency injection of all helpers
21+
/// - Support extension methods for tool-specific integrations (Git, Docker, Azure, etc.)
22+
///
23+
/// Extension Pattern:
24+
/// Tool integrations (ModularPipelines.Git, ModularPipelines.Docker, etc.) add extension methods
25+
/// like context.Git() and context.Docker() that provide strongly-typed APIs for those tools.
26+
/// </remarks>
1427
public interface IPipelineHookContext
1528
{
29+
#region DependencyInjection
30+
1631
/// <summary>
1732
/// Gets the service provider orchestrating DI within the pipeline.
1833
/// </summary>
34+
/// <remarks>
35+
/// Use the generic Get&lt;T&gt;() method instead of accessing ServiceProvider directly
36+
/// when possible for better type safety.
37+
/// </remarks>
1938
public IServiceProvider ServiceProvider { get; }
2039

40+
/// <summary>
41+
/// Helper method for retrieving services from the Service Provider.
42+
/// </summary>
43+
/// <typeparam name="T">Type to retrieve.</typeparam>
44+
/// <returns>The service instance, or null if not registered.</returns>
45+
public T? Get<T>();
46+
47+
#endregion
48+
49+
#region Configuration
50+
2151
/// <summary>
2252
/// Gets the configuration powering the pipeline.
2353
/// </summary>
54+
/// <remarks>
55+
/// Configuration is loaded from appsettings.json, environment variables, user secrets, and command line.
56+
/// </remarks>
2457
public IConfiguration Configuration { get; }
2558

2659
/// <summary>
2760
/// Gets the pipeline's options.
2861
/// </summary>
2962
public IOptions<PipelineOptions> PipelineOptions { get; }
3063

31-
/// <summary>
32-
/// Helper method for retrieving services from the Service Provider.
33-
/// </summary>
34-
/// <typeparam name="T">Type to retrieve.</typeparam>
35-
/// <returns>T.</returns>
36-
public T? Get<T>();
64+
#endregion
65+
66+
#region Logging
3767

3868
/// <summary>
3969
/// Gets the logger to be used from the pipeline.
4070
/// </summary>
71+
/// <remarks>
72+
/// This logger is module-scoped and will include the module name in log output.
73+
/// </remarks>
4174
public IModuleLogger Logger { get; }
4275

43-
#region OutOfTheBoxHelpers
76+
#endregion
77+
78+
#region EnvironmentHelpers
4479

4580
/// <summary>
4681
/// Gets helpers for getting information about the current environment.
4782
/// </summary>
83+
/// <remarks>
84+
/// Provides access to OS info, environment variables, working directory, etc.
85+
/// </remarks>
4886
public IEnvironmentContext Environment { get; }
4987

5088
/// <summary>
51-
/// Gets helpers for interacting with the filesystem.
89+
/// Gets helpers for detecting the current build system.
5290
/// </summary>
53-
public IFileSystemContext FileSystem { get; }
91+
/// <remarks>
92+
/// Detects GitHub Actions, Azure DevOps, Jenkins, GitLab CI, etc.
93+
/// </remarks>
94+
public IBuildSystemDetector BuildSystemDetector { get; }
5495

55-
/// <summary>
56-
/// Gets helpers for running commands.
57-
/// </summary>
58-
public ICommand Command { get; }
96+
#endregion
97+
98+
#region FileSystemHelpers
5999

60100
/// <summary>
61-
/// Gets helpers for installing applications.
101+
/// Gets helpers for interacting with the filesystem.
62102
/// </summary>
63-
public IInstaller Installer { get; }
103+
/// <remarks>
104+
/// Provides file/directory operations with logging and error handling.
105+
/// </remarks>
106+
public IFileSystemContext FileSystem { get; }
64107

65108
/// <summary>
66109
/// Gets helpers for zipping and unzipping files and directories.
67110
/// </summary>
68111
public IZip Zip { get; }
69112

70113
/// <summary>
71-
/// Gets helpers for converting to and from hex strings.
114+
/// Gets helpers for checking the Checksum of a file.
72115
/// </summary>
73-
public IHex Hex { get; }
116+
IChecksum Checksum { get; }
117+
118+
#endregion
119+
120+
#region CommandExecution
74121

75122
/// <summary>
76-
/// Gets helpers for converting to and from Base64 strings.
123+
/// Gets helpers for running commands.
77124
/// </summary>
78-
public IBase64 Base64 { get; }
125+
/// <remarks>
126+
/// Provides cross-platform command execution with output capture and logging.
127+
/// </remarks>
128+
public ICommand Command { get; }
79129

80130
/// <summary>
81-
/// Gets helpers for hashing data.
131+
/// Gets helpers for running powershell.
82132
/// </summary>
83-
public IHasher Hasher { get; }
133+
public IPowershell Powershell { get; }
134+
135+
/// <summary>
136+
/// Gets helpers for executing bash commands.
137+
/// </summary>
138+
public IBash Bash { get; }
139+
140+
#endregion
141+
142+
#region Serialization
84143

85144
/// <summary>
86145
/// Gets helpers for converting JSON.
@@ -97,38 +156,68 @@ public interface IPipelineHookContext
97156
/// </summary>
98157
public IYaml Yaml { get; }
99158

159+
#endregion
160+
161+
#region Encoding
162+
100163
/// <summary>
101-
/// Gets helpers for running powershell.
164+
/// Gets helpers for converting to and from hex strings.
102165
/// </summary>
103-
public IPowershell Powershell { get; }
166+
public IHex Hex { get; }
104167

105168
/// <summary>
106-
/// Gets helpers for executing bash commands.
169+
/// Gets helpers for converting to and from Base64 strings.
107170
/// </summary>
108-
public IBash Bash { get; }
171+
public IBase64 Base64 { get; }
172+
173+
#endregion
174+
175+
#region Security
109176

110177
/// <summary>
111-
/// Gets helpers for detecting the current build system.
178+
/// Gets helpers for hashing data.
112179
/// </summary>
113-
public IBuildSystemDetector BuildSystemDetector { get; }
180+
/// <remarks>
181+
/// Supports MD5, SHA1, SHA256, SHA512 hashing algorithms.
182+
/// </remarks>
183+
public IHasher Hasher { get; }
184+
185+
#endregion
186+
187+
#region NetworkHelpers
114188

115189
/// <summary>
116190
/// Gets helpers for sending HTTP requests.
117191
/// </summary>
192+
/// <remarks>
193+
/// Provides a configured HttpClient with retry policies and logging.
194+
/// </remarks>
118195
public IHttp Http { get; }
119196

120197
/// <summary>
121198
/// Gets helpers for downloading data from the web.
122199
/// </summary>
200+
/// <remarks>
201+
/// Includes progress tracking and verification helpers.
202+
/// </remarks>
123203
public IDownloader Downloader { get; }
124204

205+
#endregion
206+
207+
#region Installation
208+
125209
/// <summary>
126-
/// Gets helpers for checking the Checksum of a file.
210+
/// Gets helpers for installing applications.
127211
/// </summary>
128-
IChecksum Checksum { get; }
212+
/// <remarks>
213+
/// Supports package managers like Chocolatey, apt, brew, etc.
214+
/// </remarks>
215+
public IInstaller Installer { get; }
129216

130217
#endregion
131218

219+
#region Internal
220+
132221
/// <summary>
133222
/// Gets the detector used to detect modules which depend on each other.
134223
/// </summary>
@@ -149,4 +238,6 @@ public interface IPipelineHookContext
149238
/// Gets the cancellation token used for cancelling the pipeline on failures.
150239
/// </summary>
151240
internal EngineCancellationToken EngineCancellationToken { get; }
241+
242+
#endregion
152243
}

src/ModularPipelines/DependencyInjection/DependencyInjectionSetup.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public static void Initialize(IServiceCollection services)
2525
// Bundles
2626
services
2727
.Configure<PipelineOptions>(_ => { })
28+
.Configure<SchedulerOptions>(_ => { })
2829
.AddLogging(builder =>
2930
{
3031
builder.ClearProviders();
@@ -130,6 +131,7 @@ public static void Initialize(IServiceCollection services)
130131
.AddSingleton<IModuleDisposeExecutor, ModuleDisposeExecutor>()
131132
.AddSingleton<IPipelineExecutor, PipelineExecutor>()
132133
.AddSingleton<IModuleExecutor, ModuleExecutor>()
134+
.AddSingleton<IModuleSchedulerFactory, ModuleSchedulerFactory>()
133135
.AddSingleton<IModuleDisposer, ModuleDisposer>()
134136
.AddSingleton<ILogoPrinter, LogoPrinter>()
135137
.AddSingleton<IModuleResultRepository, NoOpModuleResultRepository>()
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using Microsoft.Extensions.Logging;
2+
3+
namespace ModularPipelines.Engine.Constraints;
4+
5+
/// <summary>
6+
/// Interface for checking module execution constraints
7+
/// </summary>
8+
/// <remarks>
9+
/// Constraints determine whether a module can execute based on current system state.
10+
/// Examples include sequential execution requirements, lock key conflicts, etc.
11+
/// </remarks>
12+
internal interface IModuleConstraint
13+
{
14+
/// <summary>
15+
/// Checks if a module can execute given current state
16+
/// </summary>
17+
/// <param name="moduleState">The module to check</param>
18+
/// <param name="stateQueries">Helper for querying module states</param>
19+
/// <returns>True if constraint is satisfied and module can execute, false otherwise</returns>
20+
/// <remarks>
21+
/// This method MUST be called while holding the scheduler's state lock
22+
/// </remarks>
23+
bool CanExecute(ModuleState moduleState, ModuleStateQueries stateQueries);
24+
25+
/// <summary>
26+
/// Logs why the constraint blocked execution (called only when CanExecute returns false)
27+
/// </summary>
28+
/// <param name="moduleState">The module that was blocked</param>
29+
/// <param name="stateQueries">Helper for querying module states</param>
30+
/// <param name="logger">Logger for output</param>
31+
void LogViolation(ModuleState moduleState, ModuleStateQueries stateQueries, ILogger logger);
32+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using Microsoft.Extensions.Logging;
2+
using ModularPipelines.Helpers;
3+
using ModularPipelines.Logging;
4+
5+
namespace ModularPipelines.Engine.Constraints;
6+
7+
/// <summary>
8+
/// Enforces lock key constraints (NotInParallel attribute with specific keys)
9+
/// </summary>
10+
internal class LockKeyConstraint : IModuleConstraint
11+
{
12+
public bool CanExecute(ModuleState moduleState, ModuleStateQueries stateQueries)
13+
{
14+
if (moduleState.RequiredLockKeys.Length == 0)
15+
{
16+
return true;
17+
}
18+
19+
return stateQueries.FindModuleWithLockConflict(moduleState) == null;
20+
}
21+
22+
public void LogViolation(ModuleState moduleState, ModuleStateQueries stateQueries, ILogger logger)
23+
{
24+
var conflictingModule = stateQueries.FindModuleWithLockConflict(moduleState);
25+
if (conflictingModule == null)
26+
{
27+
return;
28+
}
29+
30+
var conflictingKeys = string.Join(", ",
31+
moduleState.RequiredLockKeys.Intersect(conflictingModule.RequiredLockKeys));
32+
33+
logger.LogDebug(
34+
"Module {ModuleName} blocked by lock conflict with {ConflictingModule} on keys: {Keys}",
35+
MarkupFormatter.FormatModuleName(moduleState.Module.GetType().Name),
36+
MarkupFormatter.FormatModuleName(conflictingModule.Module.GetType().Name),
37+
conflictingKeys);
38+
}
39+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
using Microsoft.Extensions.Logging;
2+
using ModularPipelines.Helpers;
3+
using ModularPipelines.Logging;
4+
5+
namespace ModularPipelines.Engine.Constraints;
6+
7+
/// <summary>
8+
/// Enforces sequential execution constraints (NotInParallel attribute without keys)
9+
/// </summary>
10+
internal class SequentialExecutionConstraint : IModuleConstraint
11+
{
12+
public bool CanExecute(ModuleState moduleState, ModuleStateQueries stateQueries)
13+
{
14+
// Check if blocked by another sequential module
15+
var blockingModule = stateQueries.FindBlockingSequentialModule(moduleState);
16+
if (blockingModule != null)
17+
{
18+
return false;
19+
}
20+
21+
// Check if this is a sequential module blocked by others
22+
if (moduleState.RequiresSequentialExecution)
23+
{
24+
return !stateQueries.AreOtherModulesActive(moduleState);
25+
}
26+
27+
return true;
28+
}
29+
30+
public void LogViolation(ModuleState moduleState, ModuleStateQueries stateQueries, ILogger logger)
31+
{
32+
var blockingModule = stateQueries.FindBlockingSequentialModule(moduleState);
33+
if (blockingModule != null)
34+
{
35+
logger.LogDebug(
36+
"Module {ModuleName} blocked by sequential module {SequentialModule}",
37+
MarkupFormatter.FormatModuleName(moduleState.Module.GetType().Name),
38+
MarkupFormatter.FormatModuleName(blockingModule.Module.GetType().Name));
39+
return;
40+
}
41+
42+
if (moduleState.RequiresSequentialExecution)
43+
{
44+
logger.LogDebug(
45+
"Sequential module {ModuleName} blocked - other modules still running/queued",
46+
MarkupFormatter.FormatModuleName(moduleState.Module.GetType().Name));
47+
}
48+
}
49+
}

0 commit comments

Comments
 (0)