diff --git a/src/Worker/Core/DependencyInjection/DurableTaskWorkerBuilderExtensions.cs b/src/Worker/Core/DependencyInjection/DurableTaskWorkerBuilderExtensions.cs index d42845f60..972fdcf3a 100644 --- a/src/Worker/Core/DependencyInjection/DurableTaskWorkerBuilderExtensions.cs +++ b/src/Worker/Core/DependencyInjection/DurableTaskWorkerBuilderExtensions.cs @@ -143,10 +143,10 @@ public static IDurableTaskWorkerBuilder UseOrchestrationFilter(this IDurableTask /// /// The builder to set the builder target for. /// The instance of a to use. - /// If null, the auto-generated default filters will be cleared. + /// If null, any previously configured filters will be cleared and filtering will be disabled. /// The same instance, allowing for method chaining. - /// Work item filters are auto-generated from the registry by default. - /// Use this method with explicit filters to override the defaults, or with null to opt out of filtering entirely. + /// By default, no work item filters are applied and the worker processes all work items. + /// Use this method with explicit filters to enable filtering, or with null to disable filtering. public static IDurableTaskWorkerBuilder UseWorkItemFilters(this IDurableTaskWorkerBuilder builder, DurableTaskWorkerWorkItemFilters? workItemFilters) { Check.NotNull(builder); @@ -172,4 +172,43 @@ public static IDurableTaskWorkerBuilder UseWorkItemFilters(this IDurableTaskWork return builder; } + + /// + /// Enables work item filtering by auto-generating filters from the . + /// When enabled, the backend will only dispatch work items for registered orchestrations, activities, + /// and entities to this worker. + /// + /// The builder to set the builder target for. + /// The same instance, allowing for method chaining. + /// + /// + /// Work item filtering can improve efficiency in multi-worker deployments by ensuring each worker + /// only receives work items it can handle. However, if an orchestration calls a task type + /// (e.g., an entity, activity, or sub-orchestrator) that is not registered with any connected worker, + /// the call may hang indefinitely instead of failing with an error. + /// + /// + /// Only use this method when all task types referenced by orchestrations are guaranteed to be + /// registered with at least one connected worker. + /// + /// + public static IDurableTaskWorkerBuilder UseWorkItemFilters(this IDurableTaskWorkerBuilder builder) + { + Check.NotNull(builder); + + builder.Services.AddOptions(builder.Name) + .PostConfigure, IOptionsMonitor>( + (opts, registryMonitor, workerOptionsMonitor) => + { + DurableTaskRegistry registry = registryMonitor.Get(builder.Name); + DurableTaskWorkerOptions workerOptions = workerOptionsMonitor.Get(builder.Name); + DurableTaskWorkerWorkItemFilters generated = + DurableTaskWorkerWorkItemFilters.FromDurableTaskRegistry(registry, workerOptions); + opts.Orchestrations = generated.Orchestrations; + opts.Activities = generated.Activities; + opts.Entities = generated.Entities; + }); + + return builder; + } } diff --git a/src/Worker/Core/DependencyInjection/ServiceCollectionExtensions.cs b/src/Worker/Core/DependencyInjection/ServiceCollectionExtensions.cs index 9e8a93c6c..e68c550cf 100644 --- a/src/Worker/Core/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Worker/Core/DependencyInjection/ServiceCollectionExtensions.cs @@ -86,21 +86,6 @@ static IServiceCollection ConfigureDurableOptions(IServiceCollection services, s } }); - // Auto-generate work item filters from the registry by default. - // Users can override these by calling UseWorkItemFilters(customFilters) on the builder. - services.AddOptions(name) - .Configure, IOptionsMonitor>( - (opts, registryMonitor, workerOptionsMonitor) => - { - DurableTaskRegistry registry = registryMonitor.Get(name); - DurableTaskWorkerOptions workerOptions = workerOptionsMonitor.Get(name); - DurableTaskWorkerWorkItemFilters generated = - DurableTaskWorkerWorkItemFilters.FromDurableTaskRegistry(registry, workerOptions); - opts.Orchestrations = generated.Orchestrations; - opts.Activities = generated.Activities; - opts.Entities = generated.Entities; - }); - return services; } diff --git a/src/Worker/Core/DurableTaskWorkerWorkItemFilters.cs b/src/Worker/Core/DurableTaskWorkerWorkItemFilters.cs index c29ff95ca..8a5df2f1d 100644 --- a/src/Worker/Core/DurableTaskWorkerWorkItemFilters.cs +++ b/src/Worker/Core/DurableTaskWorkerWorkItemFilters.cs @@ -6,9 +6,9 @@ namespace Microsoft.DurableTask.Worker; /// /// A class that represents work item filters for a Durable Task Worker. These filters are passed to the backend /// and only work items matching the filters will be processed by the worker. If no filters are provided, -/// the worker will process all work items. By default, these are auto-generated from the registered orchestrations, -/// activities, and entities in the . To opt-out of filters, provide a null -/// value to the method when configuring the worker. +/// the worker will process all work items. To opt-in to work item filtering, call +/// on the worker builder with either +/// explicit filters or auto-generated filters from the . /// public class DurableTaskWorkerWorkItemFilters { diff --git a/test/Worker/Core.Tests/DependencyInjection/UseWorkItemFiltersTests.cs b/test/Worker/Core.Tests/DependencyInjection/UseWorkItemFiltersTests.cs index 9e325fe7b..adaac30a7 100644 --- a/test/Worker/Core.Tests/DependencyInjection/UseWorkItemFiltersTests.cs +++ b/test/Worker/Core.Tests/DependencyInjection/UseWorkItemFiltersTests.cs @@ -63,7 +63,7 @@ public void UseWorkItemFilters_ReturnsBuilder_ForChaining() } [Fact] - public void WorkItemFilters_DefaultFromRegistry_WhenNoExplicitFiltersConfigured() + public void WorkItemFilters_DefaultFromRegistry_WhenExplicitlyOptedIn() { // Arrange ServiceCollection services = new(); @@ -74,6 +74,7 @@ public void WorkItemFilters_DefaultFromRegistry_WhenNoExplicitFiltersConfigured( registry.AddOrchestrator(); registry.AddActivity(); }); + builder.UseWorkItemFilters(); }); // Act @@ -88,7 +89,7 @@ public void WorkItemFilters_DefaultFromRegistry_WhenNoExplicitFiltersConfigured( } [Fact] - public void WorkItemFilters_DefaultWithEntity_WhenNoExplicitFiltersConfigured() + public void WorkItemFilters_DefaultWithEntity_WhenExplicitlyOptedIn() { // Arrange ServiceCollection services = new(); @@ -98,6 +99,7 @@ public void WorkItemFilters_DefaultWithEntity_WhenNoExplicitFiltersConfigured() { registry.AddEntity(); }); + builder.UseWorkItemFilters(); }); // Act @@ -111,7 +113,7 @@ public void WorkItemFilters_DefaultWithEntity_WhenNoExplicitFiltersConfigured() } [Fact] - public void WorkItemFilters_DefaultNullWithVersioningCurrentOrOlder_WhenNoExplicitFiltersConfigured() + public void WorkItemFilters_DefaultNullWithVersioningCurrentOrOlder_WhenExplicitlyOptedIn() { // Arrange ServiceCollection services = new(); @@ -130,6 +132,7 @@ public void WorkItemFilters_DefaultNullWithVersioningCurrentOrOlder_WhenNoExplic MatchStrategy = DurableTaskWorkerOptions.VersionMatchStrategy.CurrentOrOlder, }; }); + builder.UseWorkItemFilters(); }); // Act @@ -144,7 +147,7 @@ public void WorkItemFilters_DefaultNullWithVersioningCurrentOrOlder_WhenNoExplic } [Fact] - public void WorkItemFilters_DefaultNullWithVersioningNone_WhenNoExplicitFiltersConfigured() + public void WorkItemFilters_DefaultNullWithVersioningNone_WhenExplicitlyOptedIn() { // Arrange ServiceCollection services = new(); @@ -163,6 +166,7 @@ public void WorkItemFilters_DefaultNullWithVersioningNone_WhenNoExplicitFiltersC MatchStrategy = DurableTaskWorkerOptions.VersionMatchStrategy.None, }; }); + builder.UseWorkItemFilters(); }); // Act @@ -177,7 +181,7 @@ public void WorkItemFilters_DefaultNullWithVersioningNone_WhenNoExplicitFiltersC } [Fact] - public void WorkItemFilters_DefaultVersionWithVersioningStrict_WhenNoExplicitFiltersConfigured() + public void WorkItemFilters_DefaultVersionWithVersioningStrict_WhenExplicitlyOptedIn() { // Arrange ServiceCollection services = new(); @@ -196,6 +200,7 @@ public void WorkItemFilters_DefaultVersionWithVersioningStrict_WhenNoExplicitFil MatchStrategy = DurableTaskWorkerOptions.VersionMatchStrategy.Strict, }; }); + builder.UseWorkItemFilters(); }); // Act @@ -232,7 +237,34 @@ public void WorkItemFilters_DefaultEmptyRegistry_ProducesEmptyFilters() } [Fact] - public void WorkItemFilters_ExplicitFiltersOverrideDefaults() + public void WorkItemFilters_DefaultNoFilters_WhenNoExplicitOptIn() + { + // Arrange - register tasks but do NOT call UseWorkItemFilters() + ServiceCollection services = new(); + services.AddDurableTaskWorker("test", builder => + { + builder.AddTasks(registry => + { + registry.AddOrchestrator(); + registry.AddActivity(); + registry.AddEntity(); + }); + }); + + // Act + ServiceProvider provider = services.BuildServiceProvider(); + IOptionsMonitor filtersMonitor = + provider.GetRequiredService>(); + DurableTaskWorkerWorkItemFilters actual = filtersMonitor.Get("test"); + + // Assert - with no explicit opt-in, filters should be empty (legacy behavior) + actual.Orchestrations.Should().BeEmpty(); + actual.Activities.Should().BeEmpty(); + actual.Entities.Should().BeEmpty(); + } + + [Fact] + public void WorkItemFilters_ExplicitFiltersOverrideAutoGenerated() { // Arrange ServiceCollection services = new(); @@ -266,7 +298,7 @@ public void WorkItemFilters_ExplicitFiltersOverrideDefaults() } [Fact] - public void WorkItemFilters_NullOverwritesDefaults() + public void WorkItemFilters_NullClearsAutoGeneratedFilters() { // Arrange ServiceCollection services = new(); @@ -277,7 +309,8 @@ public void WorkItemFilters_NullOverwritesDefaults() registry.AddOrchestrator(); registry.AddActivity(); }); - builder.UseWorkItemFilters(null); + builder.UseWorkItemFilters(); // opt-in to auto-generated filters + builder.UseWorkItemFilters(null); // then clear them }); // Act @@ -293,7 +326,7 @@ public void WorkItemFilters_NullOverwritesDefaults() } [Fact] - public void WorkItemFilters_EmptyFiltersOverrideDefaults() + public void WorkItemFilters_EmptyFiltersClearAutoGenerated() { // Arrange ServiceCollection services = new(); @@ -311,7 +344,8 @@ public void WorkItemFilters_EmptyFiltersOverrideDefaults() registry.AddOrchestrator(); registry.AddActivity(); }); - builder.UseWorkItemFilters(emptyFilters); + builder.UseWorkItemFilters(); // opt-in to auto-generated filters + builder.UseWorkItemFilters(emptyFilters); // then clear them with empty }); // Act @@ -327,17 +361,19 @@ public void WorkItemFilters_EmptyFiltersOverrideDefaults() } [Fact] - public void WorkItemFilters_NamedBuilders_HaveUniqueDefaultFilters() + public void WorkItemFilters_NamedBuilders_HaveUniqueDefaultFilters_WhenExplicitlyOptedIn() { // Arrange ServiceCollection services = new(); services.AddDurableTaskWorker("worker1", builder => { builder.AddTasks(registry => registry.AddOrchestrator()); + builder.UseWorkItemFilters(); }); services.AddDurableTaskWorker("worker2", builder => { builder.AddTasks(registry => registry.AddActivity()); + builder.UseWorkItemFilters(); }); // Act diff --git a/test/Worker/Grpc.Tests/DurableTaskWorkerWorkItemFiltersExtensionTests.cs b/test/Worker/Grpc.Tests/DurableTaskWorkerWorkItemFiltersExtensionTests.cs index 5921b4dee..e8c454831 100644 --- a/test/Worker/Grpc.Tests/DurableTaskWorkerWorkItemFiltersExtensionTests.cs +++ b/test/Worker/Grpc.Tests/DurableTaskWorkerWorkItemFiltersExtensionTests.cs @@ -300,7 +300,7 @@ public void ToGrpcWorkItemFilters_WithMixedFilters_ConvertsAll() } [Fact] - public void WorkerConstruction_DefaultFilters_FlowToWorker() + public void WorkerConstruction_NoOptIn_HasEmptyFilters() { // Arrange ServiceCollection services = new(); @@ -325,10 +325,10 @@ public void WorkerConstruction_DefaultFilters_FlowToWorker() .GetField("workItemFilters", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)! .GetValue(hosted); - // Assert + // Assert - without UseWorkItemFilters(), filters should be empty (no filtering) filters.Should().NotBeNull(); - filters!.Orchestrations.Should().ContainSingle(o => o.Name == nameof(TestOrchestrator)); - filters.Activities.Should().ContainSingle(a => a.Name == nameof(TestActivity)); + filters!.Orchestrations.Should().BeEmpty(); + filters.Activities.Should().BeEmpty(); } [Fact]