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)
{