Skip to content

Commit c6354f2

Browse files
authored
Updating service registration methods to ensure idempotency (#2820)
1 parent 274b9b8 commit c6354f2

10 files changed

+262
-101
lines changed

release_notes.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
### Microsoft.Azure.Functions.Worker.Core 1.19.0
1414

1515
- Updating `Azure.Core` to 1.41.0
16+
- Updated service registrations for bootstrapping methods to ensure idempotency.
1617

1718
### Microsoft.Azure.Functions.Worker.Grpc 1.17.0
1819

1920
- Updating `Azure.Core` to 1.41.0
21+
- Updated service registrations for bootstrapping methods to ensure idempotency.

src/DotNetWorker.Core/Hosting/ServiceCollectionExtensions.cs

Lines changed: 30 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -41,52 +41,47 @@ public static IFunctionsWorkerApplicationBuilder AddFunctionsWorkerCore(this ISe
4141
}
4242

4343
// Request handling
44-
services.AddSingleton<IFunctionsApplication, FunctionsApplication>();
44+
services.TryAddSingleton<IFunctionsApplication, FunctionsApplication>();
4545

4646
// Execution
47-
services.AddSingleton<IMethodInfoLocator, DefaultMethodInfoLocator>();
48-
services.AddSingleton<IFunctionInvokerFactory, DefaultFunctionInvokerFactory>();
49-
services.AddSingleton<IMethodInvokerFactory, DefaultMethodInvokerFactory>();
50-
services.AddSingleton<IFunctionActivator, DefaultFunctionActivator>();
51-
services.AddSingleton<IFunctionExecutor, DefaultFunctionExecutor>();
47+
services.TryAddSingleton<IMethodInfoLocator, DefaultMethodInfoLocator>();
48+
services.TryAddSingleton<IFunctionInvokerFactory, DefaultFunctionInvokerFactory>();
49+
services.TryAddSingleton<IMethodInvokerFactory, DefaultMethodInvokerFactory>();
50+
services.TryAddSingleton<IFunctionActivator, DefaultFunctionActivator>();
51+
services.TryAddSingleton<IFunctionExecutor, DefaultFunctionExecutor>();
5252

5353
// Function Execution Contexts
54-
services.AddSingleton<IFunctionContextFactory, DefaultFunctionContextFactory>();
54+
services.TryAddSingleton<IFunctionContextFactory, DefaultFunctionContextFactory>();
5555

5656
// Invocation Features
5757
services.TryAddSingleton<IInvocationFeaturesFactory, DefaultInvocationFeaturesFactory>();
58-
services.AddSingleton<IInvocationFeatureProvider, DefaultBindingFeatureProvider>();
58+
services.TryAddSingleton<IInvocationFeatureProvider, DefaultBindingFeatureProvider>();
5959

6060
// Input conversion feature
61-
services.AddSingleton<IConverterContextFactory, DefaultConverterContextFactory>();
62-
services.AddSingleton<IInputConversionFeatureProvider, DefaultInputConversionFeatureProvider>();
63-
services.AddSingleton<IInputConverterProvider, DefaultInputConverterProvider>();
61+
services.TryAddSingleton<IConverterContextFactory, DefaultConverterContextFactory>();
62+
services.TryAddSingleton<IInputConversionFeatureProvider, DefaultInputConversionFeatureProvider>();
63+
services.TryAddSingleton<IInputConverterProvider, DefaultInputConverterProvider>();
6464

6565
// Input binding cache
66-
services.AddScoped<IBindingCache<ConversionResult>, DefaultBindingCache<ConversionResult>>();
66+
services.TryAddScoped<IBindingCache<ConversionResult>, DefaultBindingCache<ConversionResult>>();
6767

6868
// Output Bindings
69-
services.AddSingleton<IOutputBindingsInfoProvider, DefaultOutputBindingsInfoProvider>();
69+
services.TryAddSingleton<IOutputBindingsInfoProvider, DefaultOutputBindingsInfoProvider>();
7070

7171
// Worker initialization service
72-
services.AddSingleton<IHostedService, WorkerHostedService>();
72+
services.AddHostedService<WorkerHostedService>();
7373

7474
// Default serializer settings
75-
services.AddOptions<WorkerOptions>()
76-
.PostConfigure<IOptions<JsonSerializerOptions>>((workerOptions, serializerOptions) =>
77-
{
78-
if (workerOptions.Serializer is null)
79-
{
80-
workerOptions.Serializer = new JsonObjectSerializer(serializerOptions.Value);
81-
}
82-
});
75+
services.AddOptions();
76+
services.TryAddEnumerable(ServiceDescriptor.Transient<IPostConfigureOptions<WorkerOptions>, WorkerOptionsSetup>());
8377

8478
services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerProvider, WorkerLoggerProvider>());
85-
services.AddSingleton(NullLogWriter.Instance);
86-
services.AddSingleton<IUserLogWriter>(s => s.GetRequiredService<NullLogWriter>());
87-
services.AddSingleton<ISystemLogWriter>(s => s.GetRequiredService<NullLogWriter>());
88-
services.AddSingleton<IUserMetricWriter>(s => s.GetRequiredService<NullLogWriter>());
89-
services.AddSingleton<FunctionActivitySourceFactory>();
79+
80+
services.TryAddSingleton(NullLogWriter.Instance);
81+
services.TryAddSingleton<IUserLogWriter>(s => s.GetRequiredService<NullLogWriter>());
82+
services.TryAddSingleton<ISystemLogWriter>(s => s.GetRequiredService<NullLogWriter>());
83+
services.TryAddSingleton<IUserMetricWriter>(s => s.GetRequiredService<NullLogWriter>());
84+
services.TryAddSingleton<FunctionActivitySourceFactory>();
9085

9186
if (configure != null)
9287
{
@@ -152,5 +147,13 @@ private static void RunExtensionStartupCode(IFunctionsWorkerApplicationBuilder b
152147
Activator.CreateInstance(startupCodeExecutorInfoAttr.StartupCodeExecutorType) as WorkerExtensionStartup;
153148
startupCodeExecutorInstance!.Configure(builder);
154149
}
150+
151+
private sealed class WorkerOptionsSetup(IOptions<JsonSerializerOptions> serializerOptions) : IPostConfigureOptions<WorkerOptions>
152+
{
153+
public void PostConfigure(string? name, WorkerOptions options)
154+
{
155+
options.Serializer ??= new JsonObjectSerializer(serializerOptions.Value);
156+
}
157+
}
155158
}
156159
}

src/DotNetWorker.Grpc/GrpcServiceCollectionExtensions.cs

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,15 @@
1212
using Microsoft.Extensions.Configuration;
1313
using Microsoft.Extensions.DependencyInjection.Extensions;
1414
using Microsoft.Azure.Functions.Worker.Handlers;
15+
using Microsoft.Extensions.Options;
1516

1617
namespace Microsoft.Extensions.DependencyInjection
1718
{
1819
internal static class GrpcServiceCollectionExtensions
1920
{
2021
internal static IServiceCollection RegisterOutputChannel(this IServiceCollection services)
2122
{
22-
return services.AddSingleton<GrpcHostChannel>(s =>
23+
services.TryAddSingleton<GrpcHostChannel>(s =>
2324
{
2425
UnboundedChannelOptions outputOptions = new UnboundedChannelOptions
2526
{
@@ -30,6 +31,8 @@ internal static IServiceCollection RegisterOutputChannel(this IServiceCollection
3031

3132
return new GrpcHostChannel(Channel.CreateUnbounded<StreamingMessage>(outputOptions));
3233
});
34+
35+
return services;
3336
}
3437

3538
public static IServiceCollection AddGrpc(this IServiceCollection services)
@@ -38,42 +41,36 @@ public static IServiceCollection AddGrpc(this IServiceCollection services)
3841
services.RegisterOutputChannel();
3942

4043
// Internal logging
41-
services.AddSingleton<GrpcFunctionsHostLogWriter>();
42-
services.AddSingleton<IUserLogWriter>(p => p.GetRequiredService<GrpcFunctionsHostLogWriter>());
43-
services.AddSingleton<ISystemLogWriter>(p => p.GetRequiredService<GrpcFunctionsHostLogWriter>());
44-
services.AddSingleton<IUserMetricWriter>(p => p.GetRequiredService<GrpcFunctionsHostLogWriter>());
45-
services.AddSingleton<IWorkerDiagnostics, GrpcWorkerDiagnostics>();
44+
services.TryAddSingleton<GrpcFunctionsHostLogWriter>();
45+
services.TryAddEnumerable(ServiceDescriptor.Singleton<IUserLogWriter, GrpcFunctionsHostLogWriter>(p => p.GetRequiredService<GrpcFunctionsHostLogWriter>()));
46+
services.TryAddEnumerable(ServiceDescriptor.Singleton<ISystemLogWriter, GrpcFunctionsHostLogWriter>(p => p.GetRequiredService<GrpcFunctionsHostLogWriter>()));
47+
services.TryAddEnumerable(ServiceDescriptor.Singleton<IUserMetricWriter, GrpcFunctionsHostLogWriter>(p => p.GetRequiredService<GrpcFunctionsHostLogWriter>()));
48+
services.TryAddSingleton<IWorkerDiagnostics, GrpcWorkerDiagnostics>();
4649

4750
// FunctionMetadataProvider for worker driven function-indexing
4851
services.TryAddSingleton<IFunctionMetadataProvider, DefaultFunctionMetadataProvider>();
4952

5053
// gRPC Core services
51-
services.AddSingleton<IWorker, GrpcWorker>();
54+
services.TryAddSingleton<IWorker, GrpcWorker>();
5255
services.TryAddSingleton<IInvocationHandler, InvocationHandler>();
5356

5457
#if NET5_0_OR_GREATER
5558
// If we are running in the native host process, use the native client
5659
// for communication (interop). Otherwise; use the gRPC client.
5760
if (AppContext.GetData("AZURE_FUNCTIONS_NATIVE_HOST") is not null)
5861
{
59-
services.AddSingleton<IWorkerClientFactory, Azure.Functions.Worker.Grpc.NativeHostIntegration.NativeWorkerClientFactory>();
62+
services.TryAddSingleton<IWorkerClientFactory, Azure.Functions.Worker.Grpc.NativeHostIntegration.NativeWorkerClientFactory>();
6063
}
6164
else
6265
{
63-
services.AddSingleton<IWorkerClientFactory, GrpcWorkerClientFactory>();
66+
services.TryAddSingleton<IWorkerClientFactory, GrpcWorkerClientFactory>();
6467
}
6568
#else
6669
services.AddSingleton<IWorkerClientFactory, GrpcWorkerClientFactory>();
6770
#endif
6871

69-
services.AddOptions<GrpcWorkerStartupOptions>()
70-
.Configure<IConfiguration>((grpcWorkerStartupOption, config) =>
71-
{
72-
grpcWorkerStartupOption.HostEndpoint = GetFunctionsHostGrpcUri(config);
73-
grpcWorkerStartupOption.RequestId = config["Functions:Worker:RequestId"] ?? config["requestId"];
74-
grpcWorkerStartupOption.WorkerId = config["Functions:Worker:WorkerId"] ?? config["workerId"];
75-
grpcWorkerStartupOption.GrpcMaxMessageLength = config.GetValue<int?>("Functions:Worker:GrpcMaxMessageLength", null) ?? config.GetValue<int>("grpcMaxMessageLength");
76-
});
72+
services.AddOptions();
73+
services.TryAddEnumerable(ServiceDescriptor.Transient<IConfigureOptions<GrpcWorkerStartupOptions>, GrpcWorkerStartupOptionsSetup>());
7774

7875
return services;
7976
}
@@ -100,5 +97,16 @@ private static Uri GetFunctionsHostGrpcUri(IConfiguration configuration)
10097

10198
return grpcUri;
10299
}
100+
101+
private sealed class GrpcWorkerStartupOptionsSetup(IConfiguration configuration) : IConfigureOptions<GrpcWorkerStartupOptions>
102+
{
103+
public void Configure(GrpcWorkerStartupOptions options)
104+
{
105+
options.HostEndpoint = GetFunctionsHostGrpcUri(configuration);
106+
options.RequestId = configuration["Functions:Worker:RequestId"];
107+
options.WorkerId = configuration["Functions:Worker:WorkerId"];
108+
options.GrpcMaxMessageLength = configuration.GetValue<int>("Functions:Worker:GrpcMaxMessageLength");
109+
}
110+
}
103111
}
104112
}

src/DotNetWorker/Hosting/ServiceCollectionExtensions.cs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
using System;
55
using System.Text.Json;
66
using Microsoft.Azure.Functions.Worker;
7+
using Microsoft.Extensions.DependencyInjection.Extensions;
8+
using Microsoft.Extensions.Options;
79

810
namespace Microsoft.Extensions.DependencyInjection
911
{
@@ -30,10 +32,7 @@ public static IFunctionsWorkerApplicationBuilder AddFunctionsWorkerDefaults(this
3032
services.AddDefaultInputConvertersToWorkerOptions();
3133

3234
// Default Json serialization should ignore casing on property names
33-
services.Configure<JsonSerializerOptions>(options =>
34-
{
35-
options.PropertyNameCaseInsensitive = true;
36-
});
35+
services.TryAddEnumerable(ServiceDescriptor.Transient<IConfigureOptions<JsonSerializerOptions>, ConfigureJsonSerializerOptions>());
3736

3837
// Core services registration
3938
var builder = services.AddFunctionsWorkerCore(configure);
@@ -43,5 +42,13 @@ public static IFunctionsWorkerApplicationBuilder AddFunctionsWorkerDefaults(this
4342

4443
return builder;
4544
}
45+
46+
private sealed class ConfigureJsonSerializerOptions : IConfigureOptions<JsonSerializerOptions>
47+
{
48+
public void Configure(JsonSerializerOptions options)
49+
{
50+
options.PropertyNameCaseInsensitive = true;
51+
}
52+
}
4653
}
4754
}

src/DotNetWorker/Hosting/WorkerHostBuilderExtensions.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public static IHostBuilder ConfigureFunctionsWorkerDefaults(this IHostBuilder bu
5656
/// <returns>The <see cref="IHostBuilder"/>.</returns>
5757
public static IHostBuilder ConfigureFunctionsWorkerDefaults(this IHostBuilder builder, Action<IFunctionsWorkerApplicationBuilder> configure)
5858
{
59-
return builder.ConfigureFunctionsWorkerDefaults(configure, o => { });
59+
return builder.ConfigureFunctionsWorkerDefaults(configure, null);
6060
}
6161

6262
/// <summary>
@@ -101,7 +101,7 @@ public static IHostBuilder ConfigureFunctionsWorkerDefaults(this IHostBuilder bu
101101
/// <param name="configure">A delegate that will be invoked to configure the provided <see cref="IFunctionsWorkerApplicationBuilder"/>.</param>
102102
/// <param name="configureOptions">A delegate that will be invoked to configure the provided <see cref="WorkerOptions"/>.</param>
103103
/// <returns>The <see cref="IHostBuilder"/>.</returns>
104-
public static IHostBuilder ConfigureFunctionsWorkerDefaults(this IHostBuilder builder, Action<IFunctionsWorkerApplicationBuilder> configure, Action<WorkerOptions> configureOptions)
104+
public static IHostBuilder ConfigureFunctionsWorkerDefaults(this IHostBuilder builder, Action<IFunctionsWorkerApplicationBuilder> configure, Action<WorkerOptions>? configureOptions)
105105
{
106106
return builder.ConfigureFunctionsWorkerDefaults((context, b) => configure(b), configureOptions);
107107
}
@@ -125,7 +125,7 @@ public static IHostBuilder ConfigureFunctionsWorkerDefaults(this IHostBuilder bu
125125
/// <returns>The <see cref="IHostBuilder"/>.</returns>
126126
public static IHostBuilder ConfigureFunctionsWorkerDefaults(this IHostBuilder builder, Action<HostBuilderContext, IFunctionsWorkerApplicationBuilder> configure)
127127
{
128-
return builder.ConfigureFunctionsWorkerDefaults(configure, o => { });
128+
return builder.ConfigureFunctionsWorkerDefaults(configure, null);
129129
}
130130

131131
/// <summary>
@@ -147,7 +147,7 @@ public static IHostBuilder ConfigureFunctionsWorkerDefaults(this IHostBuilder bu
147147
/// <param name="configure">A delegate that will be invoked to configure the provided <see cref="HostBuilderContext"/> and an <see cref="IFunctionsWorkerApplicationBuilder"/>.</param>
148148
/// <param name="configureOptions">A delegate that will be invoked to configure the provided <see cref="WorkerOptions"/>.</param>
149149
/// <returns>The <see cref="IHostBuilder"/>.</returns>
150-
public static IHostBuilder ConfigureFunctionsWorkerDefaults(this IHostBuilder builder, Action<HostBuilderContext, IFunctionsWorkerApplicationBuilder> configure, Action<WorkerOptions> configureOptions)
150+
public static IHostBuilder ConfigureFunctionsWorkerDefaults(this IHostBuilder builder, Action<HostBuilderContext, IFunctionsWorkerApplicationBuilder> configure, Action<WorkerOptions>? configureOptions)
151151
{
152152
builder
153153
.ConfigureHostConfiguration(config =>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using Microsoft.Extensions.DependencyInjection;
5+
using Xunit;
6+
7+
namespace Microsoft.Azure.Functions.Worker.Tests;
8+
9+
public class GrpcServiceCollectionExtensionsTests
10+
{
11+
[Fact]
12+
public void AddGrpc_RegistersServicesIdempotently()
13+
{
14+
ServiceCollectionExtensionsTestUtility.AssertServiceRegistrationIdempotency(services =>
15+
{
16+
services.AddGrpc();
17+
services.AddGrpc();
18+
});
19+
}
20+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Immutable;
6+
using System.Linq;
7+
using System.Text;
8+
using Microsoft.Extensions.DependencyInjection;
9+
using Xunit;
10+
11+
namespace Microsoft.Azure.Functions.Worker.Tests;
12+
13+
internal class ServiceCollectionExtensionsTestUtility
14+
{
15+
public static void AssertServiceRegistrationIdempotency(Action<IServiceCollection> configure,
16+
Func<Type, ImmutableList<ServiceDescriptor>, bool> registrationValidator = null)
17+
{
18+
var services = new ServiceCollection();
19+
20+
configure(services);
21+
22+
AssertServiceRegistrationIdempotency(services, registrationValidator);
23+
}
24+
25+
public static void AssertServiceRegistrationIdempotency(IServiceCollection services,
26+
Func<Type, ImmutableList<ServiceDescriptor>, bool> registrationValidator = null)
27+
{
28+
registrationValidator ??= (t, d) => d.Count == 1;
29+
30+
var invalidServices = services.GroupBy(s => s.ServiceType)
31+
.Where(g => !registrationValidator(g.Key, g.ToImmutableList()))
32+
.ToList();
33+
34+
static string FormatService(ServiceDescriptor service) => $"""
35+
- {service.ImplementationType ?? service.ImplementationInstance ?? service.ImplementationFactory}
36+
37+
""";
38+
39+
var stringBuilder = new StringBuilder();
40+
foreach (var service in invalidServices)
41+
{
42+
stringBuilder.AppendLine($"""
43+
Invalid service registrations for type: {service.Key}
44+
Implementation types:
45+
{service.Aggregate(string.Empty, (a, s) => a + FormatService(s))}
46+
""");
47+
}
48+
49+
Assert.True(invalidServices.Count == 0, stringBuilder.ToString());
50+
}
51+
}

0 commit comments

Comments
 (0)