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

Updating service registration methods to ensure idempotency #2820

Merged
merged 1 commit into from
Nov 11, 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 @@ -13,7 +13,9 @@
### Microsoft.Azure.Functions.Worker.Core 1.19.0

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

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

- Updating `Azure.Core` to 1.41.0
- Updated service registrations for bootstrapping methods to ensure idempotency.
57 changes: 30 additions & 27 deletions src/DotNetWorker.Core/Hosting/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,52 +41,47 @@ public static IFunctionsWorkerApplicationBuilder AddFunctionsWorkerCore(this ISe
}

// Request handling
services.AddSingleton<IFunctionsApplication, FunctionsApplication>();
services.TryAddSingleton<IFunctionsApplication, FunctionsApplication>();

// Execution
services.AddSingleton<IMethodInfoLocator, DefaultMethodInfoLocator>();
services.AddSingleton<IFunctionInvokerFactory, DefaultFunctionInvokerFactory>();
services.AddSingleton<IMethodInvokerFactory, DefaultMethodInvokerFactory>();
services.AddSingleton<IFunctionActivator, DefaultFunctionActivator>();
services.AddSingleton<IFunctionExecutor, DefaultFunctionExecutor>();
services.TryAddSingleton<IMethodInfoLocator, DefaultMethodInfoLocator>();
services.TryAddSingleton<IFunctionInvokerFactory, DefaultFunctionInvokerFactory>();
services.TryAddSingleton<IMethodInvokerFactory, DefaultMethodInvokerFactory>();
services.TryAddSingleton<IFunctionActivator, DefaultFunctionActivator>();
services.TryAddSingleton<IFunctionExecutor, DefaultFunctionExecutor>();

// Function Execution Contexts
services.AddSingleton<IFunctionContextFactory, DefaultFunctionContextFactory>();
services.TryAddSingleton<IFunctionContextFactory, DefaultFunctionContextFactory>();

// Invocation Features
services.TryAddSingleton<IInvocationFeaturesFactory, DefaultInvocationFeaturesFactory>();
services.AddSingleton<IInvocationFeatureProvider, DefaultBindingFeatureProvider>();
services.TryAddSingleton<IInvocationFeatureProvider, DefaultBindingFeatureProvider>();

// Input conversion feature
services.AddSingleton<IConverterContextFactory, DefaultConverterContextFactory>();
services.AddSingleton<IInputConversionFeatureProvider, DefaultInputConversionFeatureProvider>();
services.AddSingleton<IInputConverterProvider, DefaultInputConverterProvider>();
services.TryAddSingleton<IConverterContextFactory, DefaultConverterContextFactory>();
services.TryAddSingleton<IInputConversionFeatureProvider, DefaultInputConversionFeatureProvider>();
services.TryAddSingleton<IInputConverterProvider, DefaultInputConverterProvider>();

// Input binding cache
services.AddScoped<IBindingCache<ConversionResult>, DefaultBindingCache<ConversionResult>>();
services.TryAddScoped<IBindingCache<ConversionResult>, DefaultBindingCache<ConversionResult>>();

// Output Bindings
services.AddSingleton<IOutputBindingsInfoProvider, DefaultOutputBindingsInfoProvider>();
services.TryAddSingleton<IOutputBindingsInfoProvider, DefaultOutputBindingsInfoProvider>();

// Worker initialization service
services.AddSingleton<IHostedService, WorkerHostedService>();
services.AddHostedService<WorkerHostedService>();

// Default serializer settings
services.AddOptions<WorkerOptions>()
.PostConfigure<IOptions<JsonSerializerOptions>>((workerOptions, serializerOptions) =>
{
if (workerOptions.Serializer is null)
{
workerOptions.Serializer = new JsonObjectSerializer(serializerOptions.Value);
}
});
services.AddOptions();
fabiocav marked this conversation as resolved.
Show resolved Hide resolved
services.TryAddEnumerable(ServiceDescriptor.Transient<IPostConfigureOptions<WorkerOptions>, WorkerOptionsSetup>());

services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerProvider, WorkerLoggerProvider>());
services.AddSingleton(NullLogWriter.Instance);
services.AddSingleton<IUserLogWriter>(s => s.GetRequiredService<NullLogWriter>());
services.AddSingleton<ISystemLogWriter>(s => s.GetRequiredService<NullLogWriter>());
services.AddSingleton<IUserMetricWriter>(s => s.GetRequiredService<NullLogWriter>());
services.AddSingleton<FunctionActivitySourceFactory>();

services.TryAddSingleton(NullLogWriter.Instance);
services.TryAddSingleton<IUserLogWriter>(s => s.GetRequiredService<NullLogWriter>());
fabiocav marked this conversation as resolved.
Show resolved Hide resolved
services.TryAddSingleton<ISystemLogWriter>(s => s.GetRequiredService<NullLogWriter>());
services.TryAddSingleton<IUserMetricWriter>(s => s.GetRequiredService<NullLogWriter>());
services.TryAddSingleton<FunctionActivitySourceFactory>();

if (configure != null)
{
Expand Down Expand Up @@ -152,5 +147,13 @@ private static void RunExtensionStartupCode(IFunctionsWorkerApplicationBuilder b
Activator.CreateInstance(startupCodeExecutorInfoAttr.StartupCodeExecutorType) as WorkerExtensionStartup;
startupCodeExecutorInstance!.Configure(builder);
}

private sealed class WorkerOptionsSetup(IOptions<JsonSerializerOptions> serializerOptions) : IPostConfigureOptions<WorkerOptions>
{
public void PostConfigure(string? name, WorkerOptions options)
{
options.Serializer ??= new JsonObjectSerializer(serializerOptions.Value);
}
}
}
}
42 changes: 25 additions & 17 deletions src/DotNetWorker.Grpc/GrpcServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Azure.Functions.Worker.Handlers;
using Microsoft.Extensions.Options;

namespace Microsoft.Extensions.DependencyInjection
{
internal static class GrpcServiceCollectionExtensions
{
internal static IServiceCollection RegisterOutputChannel(this IServiceCollection services)
{
return services.AddSingleton<GrpcHostChannel>(s =>
services.TryAddSingleton<GrpcHostChannel>(s =>
{
UnboundedChannelOptions outputOptions = new UnboundedChannelOptions
{
Expand All @@ -30,6 +31,8 @@ internal static IServiceCollection RegisterOutputChannel(this IServiceCollection

return new GrpcHostChannel(Channel.CreateUnbounded<StreamingMessage>(outputOptions));
});

return services;
}

public static IServiceCollection AddGrpc(this IServiceCollection services)
Expand All @@ -38,42 +41,36 @@ public static IServiceCollection AddGrpc(this IServiceCollection services)
services.RegisterOutputChannel();

// Internal logging
services.AddSingleton<GrpcFunctionsHostLogWriter>();
services.AddSingleton<IUserLogWriter>(p => p.GetRequiredService<GrpcFunctionsHostLogWriter>());
services.AddSingleton<ISystemLogWriter>(p => p.GetRequiredService<GrpcFunctionsHostLogWriter>());
services.AddSingleton<IUserMetricWriter>(p => p.GetRequiredService<GrpcFunctionsHostLogWriter>());
services.AddSingleton<IWorkerDiagnostics, GrpcWorkerDiagnostics>();
services.TryAddSingleton<GrpcFunctionsHostLogWriter>();
services.TryAddEnumerable(ServiceDescriptor.Singleton<IUserLogWriter, GrpcFunctionsHostLogWriter>(p => p.GetRequiredService<GrpcFunctionsHostLogWriter>()));
fabiocav marked this conversation as resolved.
Show resolved Hide resolved
services.TryAddEnumerable(ServiceDescriptor.Singleton<ISystemLogWriter, GrpcFunctionsHostLogWriter>(p => p.GetRequiredService<GrpcFunctionsHostLogWriter>()));
services.TryAddEnumerable(ServiceDescriptor.Singleton<IUserMetricWriter, GrpcFunctionsHostLogWriter>(p => p.GetRequiredService<GrpcFunctionsHostLogWriter>()));
services.TryAddSingleton<IWorkerDiagnostics, GrpcWorkerDiagnostics>();

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

// gRPC Core services
services.AddSingleton<IWorker, GrpcWorker>();
services.TryAddSingleton<IWorker, GrpcWorker>();
services.TryAddSingleton<IInvocationHandler, InvocationHandler>();

#if NET5_0_OR_GREATER
// If we are running in the native host process, use the native client
// for communication (interop). Otherwise; use the gRPC client.
if (AppContext.GetData("AZURE_FUNCTIONS_NATIVE_HOST") is not null)
{
services.AddSingleton<IWorkerClientFactory, Azure.Functions.Worker.Grpc.NativeHostIntegration.NativeWorkerClientFactory>();
services.TryAddSingleton<IWorkerClientFactory, Azure.Functions.Worker.Grpc.NativeHostIntegration.NativeWorkerClientFactory>();
}
else
{
services.AddSingleton<IWorkerClientFactory, GrpcWorkerClientFactory>();
services.TryAddSingleton<IWorkerClientFactory, GrpcWorkerClientFactory>();
}
#else
services.AddSingleton<IWorkerClientFactory, GrpcWorkerClientFactory>();
#endif

services.AddOptions<GrpcWorkerStartupOptions>()
.Configure<IConfiguration>((grpcWorkerStartupOption, config) =>
{
grpcWorkerStartupOption.HostEndpoint = GetFunctionsHostGrpcUri(config);
grpcWorkerStartupOption.RequestId = config["Functions:Worker:RequestId"] ?? config["requestId"];
grpcWorkerStartupOption.WorkerId = config["Functions:Worker:WorkerId"] ?? config["workerId"];
grpcWorkerStartupOption.GrpcMaxMessageLength = config.GetValue<int?>("Functions:Worker:GrpcMaxMessageLength", null) ?? config.GetValue<int>("grpcMaxMessageLength");
});
services.AddOptions();
services.TryAddEnumerable(ServiceDescriptor.Transient<IConfigureOptions<GrpcWorkerStartupOptions>, GrpcWorkerStartupOptionsSetup>());

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

return grpcUri;
}

private sealed class GrpcWorkerStartupOptionsSetup(IConfiguration configuration) : IConfigureOptions<GrpcWorkerStartupOptions>
{
public void Configure(GrpcWorkerStartupOptions options)
{
options.HostEndpoint = GetFunctionsHostGrpcUri(configuration);
options.RequestId = configuration["Functions:Worker:RequestId"];
options.WorkerId = configuration["Functions:Worker:WorkerId"];
options.GrpcMaxMessageLength = configuration.GetValue<int>("Functions:Worker:GrpcMaxMessageLength");
}
}
}
}
15 changes: 11 additions & 4 deletions src/DotNetWorker/Hosting/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
using System;
using System.Text.Json;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;

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

// Default Json serialization should ignore casing on property names
services.Configure<JsonSerializerOptions>(options =>
{
options.PropertyNameCaseInsensitive = true;
});
services.TryAddEnumerable(ServiceDescriptor.Transient<IConfigureOptions<JsonSerializerOptions>, ConfigureJsonSerializerOptions>());

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

return builder;
}

private sealed class ConfigureJsonSerializerOptions : IConfigureOptions<JsonSerializerOptions>
{
public void Configure(JsonSerializerOptions options)
{
options.PropertyNameCaseInsensitive = true;
}
}
}
}
8 changes: 4 additions & 4 deletions src/DotNetWorker/Hosting/WorkerHostBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public static IHostBuilder ConfigureFunctionsWorkerDefaults(this IHostBuilder bu
/// <returns>The <see cref="IHostBuilder"/>.</returns>
public static IHostBuilder ConfigureFunctionsWorkerDefaults(this IHostBuilder builder, Action<IFunctionsWorkerApplicationBuilder> configure)
{
return builder.ConfigureFunctionsWorkerDefaults(configure, o => { });
return builder.ConfigureFunctionsWorkerDefaults(configure, null);
fabiocav marked this conversation as resolved.
Show resolved Hide resolved
}

/// <summary>
Expand Down Expand Up @@ -101,7 +101,7 @@ public static IHostBuilder ConfigureFunctionsWorkerDefaults(this IHostBuilder bu
/// <param name="configure">A delegate that will be invoked to configure the provided <see cref="IFunctionsWorkerApplicationBuilder"/>.</param>
/// <param name="configureOptions">A delegate that will be invoked to configure the provided <see cref="WorkerOptions"/>.</param>
/// <returns>The <see cref="IHostBuilder"/>.</returns>
public static IHostBuilder ConfigureFunctionsWorkerDefaults(this IHostBuilder builder, Action<IFunctionsWorkerApplicationBuilder> configure, Action<WorkerOptions> configureOptions)
public static IHostBuilder ConfigureFunctionsWorkerDefaults(this IHostBuilder builder, Action<IFunctionsWorkerApplicationBuilder> configure, Action<WorkerOptions>? configureOptions)
{
return builder.ConfigureFunctionsWorkerDefaults((context, b) => configure(b), configureOptions);
}
Expand All @@ -125,7 +125,7 @@ public static IHostBuilder ConfigureFunctionsWorkerDefaults(this IHostBuilder bu
/// <returns>The <see cref="IHostBuilder"/>.</returns>
public static IHostBuilder ConfigureFunctionsWorkerDefaults(this IHostBuilder builder, Action<HostBuilderContext, IFunctionsWorkerApplicationBuilder> configure)
{
return builder.ConfigureFunctionsWorkerDefaults(configure, o => { });
return builder.ConfigureFunctionsWorkerDefaults(configure, null);
}

/// <summary>
Expand All @@ -147,7 +147,7 @@ public static IHostBuilder ConfigureFunctionsWorkerDefaults(this IHostBuilder bu
/// <param name="configure">A delegate that will be invoked to configure the provided <see cref="HostBuilderContext"/> and an <see cref="IFunctionsWorkerApplicationBuilder"/>.</param>
/// <param name="configureOptions">A delegate that will be invoked to configure the provided <see cref="WorkerOptions"/>.</param>
/// <returns>The <see cref="IHostBuilder"/>.</returns>
public static IHostBuilder ConfigureFunctionsWorkerDefaults(this IHostBuilder builder, Action<HostBuilderContext, IFunctionsWorkerApplicationBuilder> configure, Action<WorkerOptions> configureOptions)
public static IHostBuilder ConfigureFunctionsWorkerDefaults(this IHostBuilder builder, Action<HostBuilderContext, IFunctionsWorkerApplicationBuilder> configure, Action<WorkerOptions>? configureOptions)
{
builder
.ConfigureHostConfiguration(config =>
Expand Down
20 changes: 20 additions & 0 deletions test/DotNetWorkerTests/GrpcServiceCollectionExtensionsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using Microsoft.Extensions.DependencyInjection;
using Xunit;

namespace Microsoft.Azure.Functions.Worker.Tests;

public class GrpcServiceCollectionExtensionsTests
{
[Fact]
public void AddGrpc_RegistersServicesIdempotently()
{
ServiceCollectionExtensionsTestUtility.AssertServiceRegistrationIdempotency(services =>
{
services.AddGrpc();
services.AddGrpc();
});
}
}
51 changes: 51 additions & 0 deletions test/DotNetWorkerTests/ServiceCollectionExtensionsTestUtility.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// 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.Collections.Immutable;
using System.Linq;
using System.Text;
using Microsoft.Extensions.DependencyInjection;
using Xunit;

namespace Microsoft.Azure.Functions.Worker.Tests;

internal class ServiceCollectionExtensionsTestUtility
fabiocav marked this conversation as resolved.
Show resolved Hide resolved
{
public static void AssertServiceRegistrationIdempotency(Action<IServiceCollection> configure,
Func<Type, ImmutableList<ServiceDescriptor>, bool> registrationValidator = null)
{
var services = new ServiceCollection();

configure(services);

AssertServiceRegistrationIdempotency(services, registrationValidator);
}

public static void AssertServiceRegistrationIdempotency(IServiceCollection services,
Func<Type, ImmutableList<ServiceDescriptor>, bool> registrationValidator = null)
{
registrationValidator ??= (t, d) => d.Count == 1;

var invalidServices = services.GroupBy(s => s.ServiceType)
.Where(g => !registrationValidator(g.Key, g.ToImmutableList()))
.ToList();

static string FormatService(ServiceDescriptor service) => $"""
- {service.ImplementationType ?? service.ImplementationInstance ?? service.ImplementationFactory}

""";

var stringBuilder = new StringBuilder();
foreach (var service in invalidServices)
{
stringBuilder.AppendLine($"""
Invalid service registrations for type: {service.Key}
Implementation types:
{service.Aggregate(string.Empty, (a, s) => a + FormatService(s))}
""");
}

Assert.True(invalidServices.Count == 0, stringBuilder.ToString());
}
}
Loading