diff --git a/src/ModularPipelines/Engine/ModuleBehaviorMetadata.cs b/src/ModularPipelines/Engine/ModuleBehaviorMetadata.cs new file mode 100644 index 0000000000..2c89d8433a --- /dev/null +++ b/src/ModularPipelines/Engine/ModuleBehaviorMetadata.cs @@ -0,0 +1,73 @@ +using ModularPipelines.Modules.Behaviors; + +namespace ModularPipelines.Engine; + +/// +/// Cached metadata for module behavior interfaces. +/// +/// +/// This caches the result of runtime type checks for behavior interfaces +/// to avoid repeated is checks during module execution. +/// +/// Behavior interfaces: +/// - ISkippable: Module can be skipped based on conditions +/// - IHookable: Module has before/after execution hooks +/// - ITimeoutable: Module has a timeout configuration +/// - IRetryable: Module has a retry policy +/// - IIgnoreFailures: Module failures are non-fatal +/// - IAlwaysRun: Module runs regardless of pipeline cancellation +/// +internal sealed record ModuleBehaviorMetadata +{ + /// + /// Gets a value indicating whether the module implements ISkippable. + /// + public bool IsSkippable { get; init; } + + /// + /// Gets a value indicating whether the module implements IHookable. + /// + public bool IsHookable { get; init; } + + /// + /// Gets a value indicating whether the module implements ITimeoutable. + /// + public bool IsTimeoutable { get; init; } + + /// + /// Gets a value indicating whether the module implements IRetryable. + /// + public bool IsRetryable { get; init; } + + /// + /// Gets a value indicating whether the module implements IIgnoreFailures. + /// + public bool IsIgnoreFailures { get; init; } + + /// + /// Gets a value indicating whether the module implements IAlwaysRun. + /// + public bool IsAlwaysRun { get; init; } + + /// + /// Creates behavior metadata from a module type by checking implemented interfaces. + /// Uses type-safe IsAssignableFrom checks to avoid namespace collision issues. + /// + /// The module type to analyze. + /// Metadata containing cached behavior flags. + public static ModuleBehaviorMetadata FromType(Type moduleType) + { + // Use type-safe IsAssignableFrom instead of string-based name matching + // to avoid namespace collision issues + return new ModuleBehaviorMetadata + { + IsSkippable = typeof(ISkippable).IsAssignableFrom(moduleType), + IsHookable = typeof(IHookable).IsAssignableFrom(moduleType), + IsTimeoutable = typeof(ITimeoutable).IsAssignableFrom(moduleType), + IsRetryable = moduleType.GetInterfaces().Any(i => + i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IRetryable<>)), + IsIgnoreFailures = typeof(IIgnoreFailures).IsAssignableFrom(moduleType), + IsAlwaysRun = typeof(IAlwaysRun).IsAssignableFrom(moduleType), + }; + } +} diff --git a/src/ModularPipelines/Engine/ModuleExecutionPipeline.cs b/src/ModularPipelines/Engine/ModuleExecutionPipeline.cs index 8bae5dcf51..60b83dad2a 100644 --- a/src/ModularPipelines/Engine/ModuleExecutionPipeline.cs +++ b/src/ModularPipelines/Engine/ModuleExecutionPipeline.cs @@ -151,8 +151,8 @@ private void SetupCancellation( CancellationToken engineCancellationToken) { // AlwaysRun modules don't get cancelled when the engine cancels - // Check both the interface (composition) and the property (inheritance) for backwards compatibility - var isAlwaysRun = module is IAlwaysRun || module.ModuleRunType == ModuleRunType.AlwaysRun; + // ModuleRunType property already checks for IAlwaysRun interface (see IModule.cs) + var isAlwaysRun = module.ModuleRunType == ModuleRunType.AlwaysRun; if (!isAlwaysRun) { // Create a linked token source that cancels when: diff --git a/src/ModularPipelines/Engine/ModuleMetadataCache.cs b/src/ModularPipelines/Engine/ModuleMetadataCache.cs index 94002fb7ba..14af0be11b 100644 --- a/src/ModularPipelines/Engine/ModuleMetadataCache.cs +++ b/src/ModularPipelines/Engine/ModuleMetadataCache.cs @@ -5,24 +5,42 @@ namespace ModularPipelines.Engine; /// -/// Caches module scheduling metadata to avoid repeated reflection lookups. +/// Caches module metadata to avoid repeated reflection lookups. /// +/// +/// This cache provides unified access to both: +/// - Scheduling metadata (attributes like NotInParallel, Priority) +/// - Behavior metadata (interfaces like ISkippable, IHookable) +/// +/// Using cached metadata avoids repeated reflection and runtime type checks. +/// internal static class ModuleMetadataCache { - private static readonly ConcurrentDictionary Cache = new(); + private static readonly ConcurrentDictionary SchedulingCache = new(); + private static readonly ConcurrentDictionary BehaviorCache = new(); /// /// Gets the cached scheduling metadata for a module type. /// /// The module type to get metadata for. /// The cached metadata containing scheduling attributes. - public static ModuleSchedulingMetadata GetMetadata(Type moduleType) + public static ModuleSchedulingMetadata GetSchedulingMetadata(Type moduleType) { - return Cache.GetOrAdd(moduleType, static t => new ModuleSchedulingMetadata + return SchedulingCache.GetOrAdd(moduleType, static t => new ModuleSchedulingMetadata { NotInParallelAttribute = t.GetCustomAttribute(), PriorityAttribute = t.GetCustomAttribute(), ExecutionHintAttribute = t.GetCustomAttribute(), }); } + + /// + /// Gets the cached behavior metadata for a module type. + /// + /// The module type to get behavior metadata for. + /// The cached metadata containing behavior interface flags. + public static ModuleBehaviorMetadata GetBehaviorMetadata(Type moduleType) + { + return BehaviorCache.GetOrAdd(moduleType, ModuleBehaviorMetadata.FromType); + } } diff --git a/src/ModularPipelines/Engine/ModuleScheduler.cs b/src/ModularPipelines/Engine/ModuleScheduler.cs index efd9dbe220..371f001f3a 100644 --- a/src/ModularPipelines/Engine/ModuleScheduler.cs +++ b/src/ModularPipelines/Engine/ModuleScheduler.cs @@ -113,7 +113,7 @@ public void InitializeModules(IEnumerable modules) var moduleType = state.ModuleType; // Use cached metadata to avoid repeated reflection lookups - var metadata = ModuleMetadataCache.GetMetadata(moduleType); + var metadata = ModuleMetadataCache.GetSchedulingMetadata(moduleType); if (metadata.NotInParallelAttribute != null) {