Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make durable client registration idempotent. #2950

Merged
merged 4 commits into from
Nov 8, 2024
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
2 changes: 2 additions & 0 deletions release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
### New Features

- Fail fast if extendedSessionsEnabled set to 'true' for the worker type that doesn't support extended sessions (https://github.com/Azure/azure-functions-durable-extension/pull/2732).
- Added an `IFunctionsWorkerApplicationBuilder.ConfigureDurableExtension()` extension method for cases where auto-registration does not work (no source gen running). (#2950)

### Bug Fixes

- Fix custom connection name not working when using IDurableClientFactory.CreateClient() - contributed by [@hctan](https://github.com/hctan)
- Made durable extension for isolated worker configuration idempotent, allowing multiple calls safely. (#2950)

### Breaking Changes

Expand Down
57 changes: 1 addition & 56 deletions src/Worker.Extensions.DurableTask/DurableTaskExtensionStartup.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,8 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System;
using Azure.Core.Serialization;
using Microsoft.Azure.Functions.Worker.Core;
using Microsoft.Azure.Functions.Worker.Extensions.DurableTask;
using Microsoft.DurableTask;
using Microsoft.DurableTask.Client;
using Microsoft.DurableTask.Converters;
using Microsoft.DurableTask.Worker;
using Microsoft.DurableTask.Worker.Shims;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

[assembly: WorkerExtensionStartup(typeof(DurableTaskExtensionStartup))]

Expand All @@ -28,49 +16,6 @@ public sealed class DurableTaskExtensionStartup : WorkerExtensionStartup
/// <inheritdoc/>
public override void Configure(IFunctionsWorkerApplicationBuilder applicationBuilder)
{
applicationBuilder.Services.AddSingleton<FunctionsDurableClientProvider>();
applicationBuilder.Services.AddOptions<DurableTaskClientOptions>()
.Configure(options => options.EnableEntitySupport = true)
.PostConfigure<IServiceProvider>((opt, sp) =>
{
if (GetConverter(sp) is DataConverter converter)
{
opt.DataConverter = converter;
}
});

applicationBuilder.Services.AddOptions<DurableTaskWorkerOptions>()
.Configure(options => options.EnableEntitySupport = true)
.PostConfigure<IServiceProvider>((opt, sp) =>
{
if (GetConverter(sp) is DataConverter converter)
{
opt.DataConverter = converter;
}
});

applicationBuilder.Services.TryAddSingleton(sp =>
{
DurableTaskWorkerOptions options = sp.GetRequiredService<IOptions<DurableTaskWorkerOptions>>().Value;
ILoggerFactory factory = sp.GetRequiredService<ILoggerFactory>();
return new DurableTaskShimFactory(options, factory); // For GrpcOrchestrationRunner
});

applicationBuilder.Services.Configure<WorkerOptions>(o =>
{
o.InputConverters.Register<OrchestrationInputConverter>();
});

applicationBuilder.UseMiddleware<DurableTaskFunctionsMiddleware>();
}

private static DataConverter? GetConverter(IServiceProvider services)
{
// We intentionally do not consider a DataConverter in the DI provider, or if one was already set. This is to
// ensure serialization is consistent with the rest of Azure Functions. This is particularly important because
// TaskActivity bindings use ObjectSerializer directly for the time being. Due to this, allowing DataConverter
// to be set separately from ObjectSerializer would give an inconsistent serialization solution.
WorkerOptions? worker = services.GetRequiredService<IOptions<WorkerOptions>>()?.Value;
return worker?.Serializer is not null ? new ObjectConverterShim(worker.Serializer) : null;
applicationBuilder.ConfigureDurableExtension();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System;
using System.Linq;
using Azure.Core.Serialization;
using Microsoft.Azure.Functions.Worker.Core;
using Microsoft.Azure.Functions.Worker.Extensions.DurableTask;
using Microsoft.DurableTask;
using Microsoft.DurableTask.Client;
using Microsoft.DurableTask.Converters;
using Microsoft.DurableTask.Worker;
using Microsoft.DurableTask.Worker.Shims;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Microsoft.Azure.Functions.Worker;

/// <summary>
/// Extensions for <see cref="IFunctionsWorkerApplicationBuilder"/>.
/// </summary>
public static class FunctionsWorkerApplicationBuilderExtensions
{
/// <summary>
/// Configures the Durable Functions extension for the worker.
/// </summary>
/// <param name="builder">The builder to configure.</param>
/// <returns>The <paramref name="builder"/> for call chaining.</returns>
public static IFunctionsWorkerApplicationBuilder ConfigureDurableExtension(this IFunctionsWorkerApplicationBuilder builder)
{
if (builder is null)
{
throw new ArgumentNullException(nameof(builder));
}

builder.Services.TryAddSingleton<FunctionsDurableClientProvider>();
builder.Services.TryAddEnumerable(
ServiceDescriptor.Singleton<IConfigureOptions<DurableTaskClientOptions>, ConfigureClientOptions>());
builder.Services.TryAddEnumerable(
ServiceDescriptor.Singleton<IPostConfigureOptions<DurableTaskClientOptions>, PostConfigureClientOptions>());
builder.Services.TryAddEnumerable(
ServiceDescriptor.Singleton<IConfigureOptions<DurableTaskWorkerOptions>, ConfigureWorkerOptions>());
builder.Services.TryAddEnumerable(
ServiceDescriptor.Singleton<IPostConfigureOptions<DurableTaskWorkerOptions>, PostConfigureWorkerOptions>());

builder.Services.TryAddSingleton(sp =>
{
DurableTaskWorkerOptions options = sp.GetRequiredService<IOptions<DurableTaskWorkerOptions>>().Value;
ILoggerFactory factory = sp.GetRequiredService<ILoggerFactory>();
return new DurableTaskShimFactory(options, factory); // For GrpcOrchestrationRunner
});

builder.Services.TryAddEnumerable(
ServiceDescriptor.Singleton<IConfigureOptions<WorkerOptions>, ConfigureInputConverter>());
if (!builder.Services.Any(d => d.ServiceType == typeof(DurableTaskFunctionsMiddleware)))
{
builder.UseMiddleware<DurableTaskFunctionsMiddleware>();
}

return builder;
}

private class ConfigureInputConverter : IConfigureOptions<WorkerOptions>
{
public void Configure(WorkerOptions options)
{
options.InputConverters.Register<OrchestrationInputConverter>();
}
}

private class ConfigureClientOptions : IConfigureOptions<DurableTaskClientOptions>
{
public void Configure(DurableTaskClientOptions options)
{
options.EnableEntitySupport = true;
}
}

private class PostConfigureClientOptions : IPostConfigureOptions<DurableTaskClientOptions>
{
readonly IOptionsMonitor<WorkerOptions> workerOptions;

public PostConfigureClientOptions(IOptionsMonitor<WorkerOptions> workerOptions)
{
this.workerOptions = workerOptions;
}

public void PostConfigure(string name, DurableTaskClientOptions options)
{
if (this.workerOptions.Get(name).Serializer is { } serializer)
{
options.DataConverter = new ObjectConverterShim(serializer);
}
}
}

private class ConfigureWorkerOptions : IConfigureOptions<DurableTaskWorkerOptions>
{
public void Configure(DurableTaskWorkerOptions options)
{
options.EnableEntitySupport = true;
}
}

private class PostConfigureWorkerOptions : IPostConfigureOptions<DurableTaskWorkerOptions>
{
readonly IOptionsMonitor<WorkerOptions> workerOptions;

public PostConfigureWorkerOptions(IOptionsMonitor<WorkerOptions> workerOptions)
{
this.workerOptions = workerOptions;
}

public void PostConfigure(string name, DurableTaskWorkerOptions options)
{
if (this.workerOptions.Get(name).Serializer is { } serializer)
{
options.DataConverter = new ObjectConverterShim(serializer);
}
}
}
}
Loading