Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Changelog

## Unreleased
- Validate explicit `UseWorkItemFilters(filters)` filter names against the worker's `DurableTaskRegistry`. Filters that reference an orchestration, activity, or entity name not registered with the worker now throw `OptionsValidationException` at worker startup instead of silently waiting for work items that will never arrive. No customer-side validation call is required. ([#719](https://github.com/microsoft/durabletask-dotnet/pull/719))

## 1.24.1
- Add retry to grpc calls that failed due to transient errors by @sophiatev ([#714](https://github.com/microsoft/durabletask-dotnet/pull/714))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using Microsoft.DurableTask.Worker.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using static Microsoft.DurableTask.Worker.DurableTaskWorkerOptions;

Expand Down Expand Up @@ -78,7 +79,7 @@
where TTarget : DurableTaskWorker
where TOptions : DurableTaskWorkerOptions
{
builder.UseBuildTarget(typeof(TTarget));

Check warning on line 82 in src/Worker/Core/DependencyInjection/DurableTaskWorkerBuilderExtensions.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

Prefer the generic overload 'Microsoft.DurableTask.Worker.DurableTaskWorkerBuilderExtensions.UseBuildTarget<TTarget>(Microsoft.DurableTask.Worker.IDurableTaskWorkerBuilder)' instead of 'Microsoft.DurableTask.Worker.DurableTaskWorkerBuilderExtensions.UseBuildTarget(Microsoft.DurableTask.Worker.IDurableTaskWorkerBuilder, System.Type)' (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2263)

Check warning on line 82 in src/Worker/Core/DependencyInjection/DurableTaskWorkerBuilderExtensions.cs

View workflow job for this annotation

GitHub Actions / build

Prefer the generic overload 'Microsoft.DurableTask.Worker.DurableTaskWorkerBuilderExtensions.UseBuildTarget<TTarget>(Microsoft.DurableTask.Worker.IDurableTaskWorkerBuilder)' instead of 'Microsoft.DurableTask.Worker.DurableTaskWorkerBuilderExtensions.UseBuildTarget(Microsoft.DurableTask.Worker.IDurableTaskWorkerBuilder, System.Type)' (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2263)

Check warning on line 82 in src/Worker/Core/DependencyInjection/DurableTaskWorkerBuilderExtensions.cs

View workflow job for this annotation

GitHub Actions / smoke-tests

Prefer the generic overload 'Microsoft.DurableTask.Worker.DurableTaskWorkerBuilderExtensions.UseBuildTarget<TTarget>(Microsoft.DurableTask.Worker.IDurableTaskWorkerBuilder)' instead of 'Microsoft.DurableTask.Worker.DurableTaskWorkerBuilderExtensions.UseBuildTarget(Microsoft.DurableTask.Worker.IDurableTaskWorkerBuilder, System.Type)' (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2263)
builder.Services.AddOptions<TOptions>(builder.Name)
.PostConfigure<IOptionsMonitor<DurableTaskWorkerOptions>>((options, baseOptions) =>
{
Expand Down Expand Up @@ -145,8 +146,18 @@
/// <param name="workItemFilters">The instance of a <see cref="DurableTaskWorkerWorkItemFilters"/> to use.
/// If <c>null</c>, any previously configured filters will be cleared and filtering will be disabled.</param>
/// <returns>The same <see cref="IDurableTaskWorkerBuilder"/> instance, allowing for method chaining.</returns>
/// <remarks>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 <c>null</c> to disable filtering.</remarks>
/// <remarks>
/// <para>
/// 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 <c>null</c> to disable filtering.
/// </para>
/// <para>
/// The supplied filter names are validated against the worker's <see cref="DurableTaskRegistry"/>
/// when the worker is built. If any filter references an orchestration, activity, or entity name
/// that is not registered with this worker, an <see cref="OptionsValidationException"/> is thrown
/// at startup. This prevents the worker from silently waiting on work items it cannot handle.
/// </para>
/// </remarks>
public static IDurableTaskWorkerBuilder UseWorkItemFilters(this IDurableTaskWorkerBuilder builder, DurableTaskWorkerWorkItemFilters? workItemFilters)
{
Check.NotNull(builder);
Expand All @@ -170,6 +181,26 @@
}
});

// Register a single global validator that fails fast at worker build time if any filter
// references a task name that isn't registered with the corresponding worker. The validator
// dispatches per named-options instance via the 'name' parameter that the Options framework
// passes to IValidateOptions.Validate, so a single registration covers every named worker.
// TryAddEnumerable de-duplicates by implementation type, making repeat calls idempotent
// across all UseWorkItemFilters invocations.
//
// Skip registration when the caller passed null (opt-out) or an empty filter set: there is
// nothing to validate, and registering would otherwise pull IOptionsMonitor<DurableTaskRegistry>
// into the resolution chain for workers that have explicitly opted out of filtering.
if (workItemFilters is not null
&& (workItemFilters.Orchestrations.Count > 0
|| workItemFilters.Activities.Count > 0
|| workItemFilters.Entities.Count > 0))
{
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<
IValidateOptions<DurableTaskWorkerWorkItemFilters>,
DurableTaskWorkerWorkItemFiltersValidator>());
}

return builder;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Text;
using Microsoft.Extensions.Options;

namespace Microsoft.DurableTask.Worker;

/// <summary>
/// Validates that every name configured on <see cref="DurableTaskWorkerWorkItemFilters"/> matches a
/// task registered with the corresponding named worker's <see cref="DurableTaskRegistry"/>.
/// </summary>
/// <remarks>
/// Registered as a single global <see cref="IValidateOptions{TOptions}"/> via
/// <see cref="DurableTaskWorkerBuilderExtensions.UseWorkItemFilters(IDurableTaskWorkerBuilder, DurableTaskWorkerWorkItemFilters?)"/>.
/// The Options framework dispatches the named-options name to <see cref="Validate(string?, DurableTaskWorkerWorkItemFilters)"/>,
/// which is then used to resolve the matching <see cref="DurableTaskRegistry"/>. Validation runs
/// lazily when <see cref="IOptionsMonitor{TOptions}.Get(string?)"/> is first called for a worker
/// (effectively at worker construction), so callers do not need to invoke validation explicitly.
/// </remarks>
sealed class DurableTaskWorkerWorkItemFiltersValidator : IValidateOptions<DurableTaskWorkerWorkItemFilters>
{
readonly IOptionsMonitor<DurableTaskRegistry> registryMonitor;

/// <summary>
/// Initializes a new instance of the <see cref="DurableTaskWorkerWorkItemFiltersValidator"/> class.
/// </summary>
/// <param name="registryMonitor">The monitor used to resolve the worker's <see cref="DurableTaskRegistry"/> at validation time.</param>
public DurableTaskWorkerWorkItemFiltersValidator(IOptionsMonitor<DurableTaskRegistry> registryMonitor)
{
this.registryMonitor = Check.NotNull(registryMonitor);
}

/// <inheritdoc/>
public ValidateOptionsResult Validate(string? name, DurableTaskWorkerWorkItemFilters options)
{
Check.NotNull(options);

// The validator is registered globally, so the Options framework dispatches every named
// worker's filter options through it -- including workers that never opted into filtering
// and therefore have no filter entries to validate. Skip those cases so the validator only
// reports a verdict for workers that actually configured filters.
if (options.Orchestrations.Count == 0
&& options.Activities.Count == 0
&& options.Entities.Count == 0)
{
return ValidateOptionsResult.Skip;
}

DurableTaskRegistry registry = this.registryMonitor.Get(name);

List<string> unknownOrchestrations = FindUnknown(
options.Orchestrations.Select(o => o.Name), n => registry.Orchestrators.ContainsKey(n));
List<string> unknownActivities = FindUnknown(
options.Activities.Select(a => a.Name), n => registry.Activities.ContainsKey(n));
List<string> unknownEntities = FindUnknown(
options.Entities.Select(e => e.Name), n => registry.Entities.ContainsKey(n));

if (unknownOrchestrations.Count == 0
&& unknownActivities.Count == 0
&& unknownEntities.Count == 0)
{
return ValidateOptionsResult.Success;
}

StringBuilder sb = new();
string displayName = string.IsNullOrEmpty(name) ? "<default>" : name!;
sb.Append("Cannot configure work item filters for worker '").Append(displayName)
.Append("': the following filter names do not match any registered task. ")
.Append("Register them on the worker (via AddTasks/AddOrchestrator/AddActivity/AddEntity) ")
.Append("or remove them from the filters.");
Comment thread
YunchuWang marked this conversation as resolved.
AppendCategory(sb, "Orchestrations", unknownOrchestrations);
AppendCategory(sb, "Activities", unknownActivities);
AppendCategory(sb, "Entities", unknownEntities);

return ValidateOptionsResult.Fail(sb.ToString());
}

static List<string> FindUnknown(IEnumerable<string> names, Func<TaskName, bool> isRegistered)
{
List<string> unknown = [];
foreach (string name in names)
{
if (string.IsNullOrEmpty(name))
{
unknown.Add("<empty>");
continue;
}

// TaskName equality is OrdinalIgnoreCase, mirroring how registered keys are compared.
// Construct the TaskName explicitly so the conversion is not dependent on the implicit
// string -> TaskName operator (which could be removed/changed independently).
if (!isRegistered(new TaskName(name)))
{
unknown.Add(name);
}
}

return unknown;
}
Comment thread
YunchuWang marked this conversation as resolved.

static void AppendCategory(StringBuilder sb, string category, List<string> unknown)
{
if (unknown.Count == 0)
{
return;
}

sb.Append(' ').Append(category).Append(": [").Append(string.Join(", ", unknown)).Append(']');
}
}
Loading
Loading