Introducing new interfaces to clean up ScaleHost DI model

This commit is contained in:
Mathew Charles 2023-10-24 15:50:46 -07:00
Родитель 1768447932
Коммит 963a7f6262
10 изменённых файлов: 251 добавлений и 137 удалений

Просмотреть файл

@ -15,5 +15,6 @@ namespace Microsoft.Azure.WebJobs.Host
public const string AzureWebsiteInstanceId = "WEBSITE_INSTANCE_ID";
public const string AzureWebsiteContainerName = "CONTAINER_NAME";
public const string DateTimeFormatString = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffK";
public const string ScaleHostEnvironmentSettingName = "IsAzureWebJobsScaleHost";
}
}

Просмотреть файл

@ -1,30 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Azure.WebJobs.Host.Scale;
namespace Microsoft.Azure.WebJobs.Host.Hosting
{
/// <summary>
/// Context containing triggers metadata needed to be confiured for scale in an extension.
/// </summary>
public class ScaleHostBuilderContext : WebJobsBuilderContext
{
private IEnumerable<TriggerMetadata> _triggersMetadata;
public ScaleHostBuilderContext(IEnumerable<TriggerMetadata> triggersMetadata)
{
_triggersMetadata = triggersMetadata;
}
/// <summary>
/// Returns <see cref="IEnumerable"> of <see cref="TriggerMetadata"/> by trigger type.
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
public IEnumerable<TriggerMetadata> GetTriggersMetadata(string type) => _triggersMetadata.Where(x => x.Type == type);
}
}

Просмотреть файл

@ -1,6 +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 System.Collections.Generic;
using Microsoft.Extensions.Configuration;
namespace Microsoft.Azure.WebJobs
@ -10,6 +12,11 @@ namespace Microsoft.Azure.WebJobs
/// </summary>
public class WebJobsBuilderContext
{
public WebJobsBuilderContext()
{
Properties = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
}
/// <summary>
/// Gets or sets the <see cref="IConfiguration"/> containing the merged configuration of the application and the host.
/// </summary>
@ -24,5 +31,10 @@ namespace Microsoft.Azure.WebJobs
/// Gets or sets the absolute path to the directory that contains the application content files.
/// </summary>
public string ApplicationRootPath { get; set; }
/// <summary>
/// A central location for sharing state between components during the host building process.
/// </summary>
public IDictionary<string, object> Properties { get; }
}
}

Просмотреть файл

@ -0,0 +1,19 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
using System.Collections.Generic;
namespace Microsoft.Azure.WebJobs.Host.Scale
{
/// <summary>
/// Provider interface for returning a collection of <see cref="IScaleMonitor"/> instances.
/// </summary>
public interface IScaleMonitorCollectionProvider
{
/// <summary>
/// Gets the collection of <see cref="IScaleMonitor"/> instances.
/// </summary>
/// <returns>The collection of <see cref="IScaleMonitor"/> instances.</returns>
IEnumerable<IScaleMonitor> GetScaleMonitors();
}
}

Просмотреть файл

@ -0,0 +1,19 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
using System.Collections.Generic;
namespace Microsoft.Azure.WebJobs.Host.Scale
{
/// <summary>
/// Provider interface for returning a collection of <see cref="ITargetScaler"/> instances.
/// </summary>
public interface ITargetScalerCollectionProvider
{
/// <summary>
/// Gets the collection of <see cref="ITargetScaler"/> instances.
/// </summary>
/// <returns>The collection of <see cref="ITargetScaler"/> instances.</returns>
IEnumerable<ITargetScaler> GetTargetScalers();
}
}

Просмотреть файл

@ -0,0 +1,19 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
using System.Collections.Generic;
namespace Microsoft.Azure.WebJobs.Host.Scale
{
/// <summary>
/// Provider interface for returning a collection of <see cref="TriggerMetadata"/> instances.
/// </summary>
public interface ITriggerMetadataProvider
{
/// <summary>
/// Gets the collection of <see cref="TriggerMetadata"/> instances.
/// </summary>
/// <returns>The collection of <see cref="TriggerMetadata"/> instances.</returns>
public IEnumerable<TriggerMetadata> GetTriggerMetadata();
}
}

Просмотреть файл

@ -15,7 +15,7 @@ namespace Microsoft.Azure.WebJobs.Host.Scale
{
}
public ScaleMonitorManager(IEnumerable<IScaleMonitor> monitors, IEnumerable<IScaleMonitorProvider> monitorProviders)
public ScaleMonitorManager(IEnumerable<IScaleMonitor> monitors, IEnumerable<IScaleMonitorProvider> monitorProviders, IEnumerable<IScaleMonitorCollectionProvider> monitorCollectionProviders)
{
// add any initial monitors coming from DI
// additional monitors can be added at runtime
@ -24,6 +24,9 @@ namespace Microsoft.Azure.WebJobs.Host.Scale
// add monitors coming from any registered providers
_monitors.AddRange(monitorProviders.Select(p => p.GetMonitor()));
// add monitors coming from any registered collection providers
_monitors.AddRange(monitorCollectionProviders.SelectMany(p => p.GetScaleMonitors()));
}
public void Register(IScaleMonitor monitor)

Просмотреть файл

@ -15,7 +15,7 @@ namespace Microsoft.Azure.WebJobs.Host.Scale
{
}
public TargetScalerManager(IEnumerable<ITargetScaler> targetScalers, IEnumerable<ITargetScalerProvider> scalerProviders)
public TargetScalerManager(IEnumerable<ITargetScaler> targetScalers, IEnumerable<ITargetScalerProvider> scalerProviders, IEnumerable<ITargetScalerCollectionProvider> scalerCollectionProvider)
{
// add any initial target scalers coming from DI
// additional monitors can be added at runtime
@ -24,6 +24,9 @@ namespace Microsoft.Azure.WebJobs.Host.Scale
// add scalers coming from any registered providers
_targetScalers.AddRange(scalerProviders.Select(p => p.GetTargetScaler()));
// add scalers coming from any registered collection providers
_targetScalers.AddRange(scalerCollectionProvider.SelectMany(p => p.GetTargetScalers()));
}
public void Register(ITargetScaler targetScaler)

Просмотреть файл

@ -10,7 +10,6 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs.Host.EndToEndTests.Scale;
using Microsoft.Azure.WebJobs.Host.Hosting;
using Microsoft.Azure.WebJobs.Host.Scale;
using Microsoft.Azure.WebJobs.Host.TestCommon;
using Microsoft.Azure.WebJobs.Hosting;
@ -25,8 +24,8 @@ using Newtonsoft.Json.Linq;
using Xunit;
using static Microsoft.Azure.WebJobs.Host.EndToEndTests.Scale.ScaleHostEndToEndTests;
[assembly: WebJobsStartup(typeof(TestExtensionA))]
[assembly: WebJobsStartup(typeof(TestExtensionB))]
[assembly: WebJobsStartup(typeof(TestExtensionAStartup))]
[assembly: WebJobsStartup(typeof(TestExtensionBStartup))]
namespace Microsoft.Azure.WebJobs.Host.EndToEndTests.Scale
{
@ -37,8 +36,8 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests.Scale
private const string Function2Name = "Function2";
private const string Function3Name = "Function3";
internal const string ATriggerType = "testExtensionATrigger";
internal const string BTriggerType = "testExtensionBTrigger";
internal const string ExtensionTypeA = "testExtensionATrigger";
internal const string ExtensionTypeB = "testExtensionBTrigger";
[Theory]
[InlineData(false)]
@ -47,9 +46,9 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests.Scale
{
var triggerMetadata = new List<TriggerMetadata>()
{
new TriggerMetadata(new JObject { { "functionName", $"{Function1Name}" }, { "type", $"{ATriggerType}" } }),
new TriggerMetadata(new JObject { { "functionName", $"{Function2Name}" }, { "type", $"{ATriggerType}" } }),
new TriggerMetadata(new JObject { { "functionName", $"{Function3Name}" }, { "type", $"{BTriggerType}" } }, new Dictionary<string, object> { { "foo", "bar" } })
new TriggerMetadata(new JObject { { "functionName", $"{Function1Name}" }, { "type", $"{ExtensionTypeA}" } }),
new TriggerMetadata(new JObject { { "functionName", $"{Function2Name}" }, { "type", $"{ExtensionTypeA}" } }),
new TriggerMetadata(new JObject { { "functionName", $"{Function3Name}" }, { "type", $"{ExtensionTypeB}" } }, new Dictionary<string, object> { { "foo", "bar" } })
};
string hostJson =
@ -92,16 +91,19 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests.Scale
services.AddAzureStorageScaleServices();
services.AddSingleton<IConcurrencyStatusRepository, TestConcurrencyStatusRepository>();
services.AddSingleton<ITriggerMetadataProvider>(new TestTriggerMetadataProvider(triggerMetadata));
})
.ConfigureWebJobsScale((context, builder) =>
{
builder.UseHostId(hostId);
Assembly assembly = Assembly.GetExecutingAssembly();
var scaleHostBuilderContext = new ScaleHostBuilderContext(triggerMetadata);
new DefaultStartupTypeLocator(assembly);
builder.UseExternalStartup(new DefaultStartupTypeLocator(assembly), scaleHostBuilderContext, NullLoggerFactory.Instance);
var webJobsBuilderContext = new WebJobsBuilderContext
{
Configuration = context.Configuration,
EnvironmentName = context.HostingEnvironment.EnvironmentName
};
webJobsBuilderContext.Properties["IsAzureWebJobsScaleHost"] = true;
builder.UseExternalStartup(new DefaultStartupTypeLocator(Assembly.GetExecutingAssembly()), webJobsBuilderContext, NullLoggerFactory.Instance);
},
scaleOptions =>
{
@ -114,6 +116,12 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests.Scale
IHost scaleHost = hostBuilder.Build();
await scaleHost.StartAsync();
VerifyHostConfiguration(scaleHost);
await VerifyScaleLogsAsync(scaleHost, tbsEnabled, loggerProvider);
}
private void VerifyHostConfiguration(IHost scaleHost)
{
IHostedService scaleMonitorService = scaleHost.Services.GetService<IHostedService>();
Assert.NotNull(scaleMonitorService);
@ -125,10 +133,13 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests.Scale
// Validate IConfiguration
var config = scaleHost.Services.GetService<IConfiguration>();
Assert.False(config.GetValue<string>("sovemalue") == "1");
Assert.False(config.GetValue<string>("somevalue") == "1");
Assert.True(config.GetValue<string>("Microsoft.Azure.WebJobs.Host.EndToEndTests") == "1");
Assert.True(config.GetValue<string>("microsoft.azure.webJobs.host.endtoendtests") == "1");
}
private async Task VerifyScaleLogsAsync(IHost scaleHost, bool tbsEnabled, TestLoggerProvider loggerProvider)
{
await TestHelpers.Await(async () =>
{
IScaleStatusProvider scaleManager = scaleHost.Services.GetService<IScaleStatusProvider>();
@ -174,7 +185,7 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests.Scale
}
return scaledOut;
}, pollingInterval: 1000);
}, timeout: 5000, pollingInterval: 1000);
}
private class TestConcurrencyStatusRepository : IConcurrencyStatusRepository
@ -200,43 +211,9 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests.Scale
});
}
}
internal abstract class TestExtensionScalerProvider : IScaleMonitorProvider, ITargetScalerProvider
{
private IOptions<ScaleOptions> _scaleOptions;
private IScaleMonitor _scaleMonitor;
private ITargetScaler _targetScaler;
private TriggerMetadata _triggerMetadata;
public TestExtensionScalerProvider(IConfiguration config, IOptions<ScaleOptions> scaleOptions, TriggerMetadata triggerMetadata)
{
Assert.Equal(scaleOptions.Value.ScaleMetricsMaxAge, TimeSpan.FromMinutes(4));
_scaleOptions = scaleOptions;
_triggerMetadata = triggerMetadata;
}
public IScaleMonitor GetMonitor()
{
if (_scaleMonitor == null)
{
_scaleMonitor = new TestScaleMonitor(_triggerMetadata.FunctionName, _triggerMetadata.FunctionName);
}
return _scaleMonitor;
}
public ITargetScaler GetTargetScaler()
{
if (_targetScaler == null)
{
_targetScaler = new TestTargetScaler(_triggerMetadata.FunctionName);
}
return _targetScaler;
}
}
}
public class TestExtensionA : IWebJobsStartup2
public class TestExtensionAStartup : IWebJobsStartup2
{
public void Configure(IWebJobsBuilder builder)
{
@ -244,46 +221,19 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests.Scale
public void Configure(WebJobsBuilderContext context, IWebJobsBuilder builder)
{
var scaleHostBuilderContext = context as ScaleHostBuilderContext;
if (scaleHostBuilderContext != null)
if (context.Properties.ContainsKey("IsAzureWebJobsScaleHost"))
{
foreach (var triggerMetadata in scaleHostBuilderContext.GetTriggersMetadata(ATriggerType))
{
builder.Services.AddSingleton<IScaleMonitorProvider>(serviceProvider =>
{
IConfiguration config = serviceProvider.GetService<IConfiguration>();
IOptions<ScaleOptions> scaleOptions = serviceProvider.GetService<IOptions<ScaleOptions>>();
return new TestExtensionAScalerProvider(config, scaleOptions, triggerMetadata);
});
builder.Services.AddSingleton<ITargetScalerProvider>(serviceProvider =>
{
IConfiguration config = serviceProvider.GetService<IConfiguration>();
IOptions<ScaleOptions> scaleOptions = serviceProvider.GetService<IOptions<ScaleOptions>>();
return new TestExtensionAScalerProvider(config, scaleOptions, triggerMetadata);
});
}
builder.Services.AddSingleton<ITargetScalerCollectionProvider, TestExtensionATargetScalerCollectionProvider>();
builder.Services.AddSingleton<IScaleMonitorCollectionProvider, TestExtensionAScaleMonitorCollectionProvider>();
}
else
{
Configure(builder);
}
}
private class TestExtensionAScalerProvider : TestExtensionScalerProvider
{
public TestExtensionAScalerProvider(IConfiguration config, IOptions<ScaleOptions> scaleOptions, TriggerMetadata triggerMetadata)
: base(config, scaleOptions, triggerMetadata)
{
// verify we can access configuration settings
var appSetting = config.GetValue<string>("app_setting1");
Assert.NotNull(appSetting);
var hostJsonSetting = config.GetValue<int>("extensions:testExtensionA:foo");
Assert.NotNull(hostJsonSetting);
}
}
}
public class TestExtensionB : IWebJobsStartup2
public class TestExtensionBStartup : IWebJobsStartup2
{
public void Configure(IWebJobsBuilder builder)
{
@ -291,39 +241,155 @@ namespace Microsoft.Azure.WebJobs.Host.EndToEndTests.Scale
public void Configure(WebJobsBuilderContext context, IWebJobsBuilder builder)
{
var scaleHostBuilderContext = context as ScaleHostBuilderContext;
if (scaleHostBuilderContext != null)
if (context.Properties.ContainsKey("IsAzureWebJobsScaleHost"))
{
foreach (var triggerMetadata in scaleHostBuilderContext.GetTriggersMetadata(BTriggerType))
{
builder.Services.AddSingleton<IScaleMonitorProvider>(serviceProvider =>
{
IConfiguration config = serviceProvider.GetService<IConfiguration>();
IOptions<ScaleOptions> scaleOptions = serviceProvider.GetService<IOptions<ScaleOptions>>();
return new TestExtensionBScalerProvider(config, scaleOptions, triggerMetadata);
});
builder.Services.AddSingleton<ITargetScalerProvider>(serviceProvider =>
{
IConfiguration config = serviceProvider.GetService<IConfiguration>();
IOptions<ScaleOptions> scaleOptions = serviceProvider.GetService<IOptions<ScaleOptions>>();
return new TestExtensionBScalerProvider(config, scaleOptions, triggerMetadata);
});
}
builder.Services.AddSingleton<ITargetScalerCollectionProvider, TestExtensionBTargetScalerCollectionProvider>();
builder.Services.AddSingleton<IScaleMonitorCollectionProvider, TestExtensionBScaleMonitorCollectionProvider>();
}
else
{
Configure(builder);
}
}
}
private class TestExtensionBScalerProvider : TestExtensionScalerProvider
internal class TestTriggerMetadataProvider : ITriggerMetadataProvider
{
private readonly IReadOnlyCollection<TriggerMetadata> _triggerMetadata;
public TestTriggerMetadataProvider(List<TriggerMetadata> triggerMetadata)
{
public TestExtensionBScalerProvider(IConfiguration config, IOptions<ScaleOptions> scaleOptions, TriggerMetadata triggerMetadata)
: base(config, scaleOptions, triggerMetadata)
{
}
_triggerMetadata = triggerMetadata.AsReadOnly();
}
public IEnumerable<TriggerMetadata> GetTriggerMetadata()
{
return _triggerMetadata;
}
}
internal abstract class TestTargetScalerCollectionProvider : ITargetScalerCollectionProvider
{
private readonly TriggerMetadata[] _triggerMetadata;
private readonly object _lock = new object();
private ITargetScaler[] _targetScalers;
public TestTargetScalerCollectionProvider(IConfiguration config, IOptions<ScaleOptions> scaleOptions, ITriggerMetadataProvider triggerMetadataProvider)
{
Assert.Equal(scaleOptions.Value.ScaleMetricsMaxAge, TimeSpan.FromMinutes(4));
_triggerMetadata = triggerMetadataProvider.GetTriggerMetadata().Where(p => string.Compare(p.Type, ExtensionType, StringComparison.OrdinalIgnoreCase) == 0).ToArray();
}
public abstract string ExtensionType { get; }
public IEnumerable<ITargetScaler> GetTargetScalers()
{
if (_targetScalers == null)
{
lock (_lock)
{
if (_targetScalers == null)
{
List<ITargetScaler> targetScalers = new List<ITargetScaler>();
foreach (var triggerMetadata in _triggerMetadata)
{
var targetScaler = new TestTargetScaler(triggerMetadata.FunctionName);
targetScalers.Add(targetScaler);
}
_targetScalers = targetScalers.ToArray();
}
}
}
return _targetScalers;
}
}
internal abstract class TestScaleMonitorCollectionProvider : IScaleMonitorCollectionProvider
{
private readonly TriggerMetadata[] _triggerMetadata;
private readonly object _lock = new object();
private IScaleMonitor[] _scaleMonitors;
public TestScaleMonitorCollectionProvider(IConfiguration config, IOptions<ScaleOptions> scaleOptions, ITriggerMetadataProvider triggerMetadataProvider)
{
Assert.Equal(scaleOptions.Value.ScaleMetricsMaxAge, TimeSpan.FromMinutes(4));
_triggerMetadata = triggerMetadataProvider.GetTriggerMetadata().Where(p => string.Compare(p.Type, ExtensionType, StringComparison.OrdinalIgnoreCase) == 0).ToArray();
}
public abstract string ExtensionType { get; }
public IEnumerable<IScaleMonitor> GetScaleMonitors()
{
if (_scaleMonitors == null)
{
lock (_lock)
{
if (_scaleMonitors == null)
{
List<IScaleMonitor> scaleMonitors = new List<IScaleMonitor>();
foreach (var triggerMetadata in _triggerMetadata)
{
var monitor = new TestScaleMonitor(triggerMetadata.FunctionName, triggerMetadata.FunctionName);
scaleMonitors.Add(monitor);
}
_scaleMonitors = scaleMonitors.ToArray();
}
}
}
return _scaleMonitors;
}
}
internal class TestExtensionATargetScalerCollectionProvider : TestTargetScalerCollectionProvider
{
public TestExtensionATargetScalerCollectionProvider(IConfiguration config, IOptions<ScaleOptions> scaleOptions, ITriggerMetadataProvider triggerMetadataProvider)
: base(config, scaleOptions, triggerMetadataProvider)
{
// verify we can access configuration settings
var appSetting = config.GetValue<string>("app_setting1");
Assert.NotNull(appSetting);
var hostJsonSetting = config.GetValue<int>("extensions:testExtensionA:foo");
Assert.NotNull(hostJsonSetting);
}
public override string ExtensionType => ExtensionTypeA;
}
internal class TestExtensionAScaleMonitorCollectionProvider : TestScaleMonitorCollectionProvider
{
public TestExtensionAScaleMonitorCollectionProvider(IConfiguration config, IOptions<ScaleOptions> scaleOptions, ITriggerMetadataProvider triggerMetadataProvider)
: base(config, scaleOptions, triggerMetadataProvider)
{
// verify we can access configuration settings
var appSetting = config.GetValue<string>("app_setting1");
Assert.NotNull(appSetting);
var hostJsonSetting = config.GetValue<int>("extensions:testExtensionA:foo");
Assert.NotNull(hostJsonSetting);
}
public override string ExtensionType => ExtensionTypeA;
}
internal class TestExtensionBTargetScalerCollectionProvider : TestTargetScalerCollectionProvider
{
public TestExtensionBTargetScalerCollectionProvider(IConfiguration config, IOptions<ScaleOptions> scaleOptions, ITriggerMetadataProvider triggerMetadataProvider)
: base(config, scaleOptions, triggerMetadataProvider)
{
}
public override string ExtensionType => ExtensionTypeB;
}
internal class TestExtensionBScaleMonitorCollectionProvider : TestScaleMonitorCollectionProvider
{
public TestExtensionBScaleMonitorCollectionProvider(IConfiguration config, IOptions<ScaleOptions> scaleOptions, ITriggerMetadataProvider triggerMetadataProvider)
: base(config, scaleOptions, triggerMetadataProvider)
{
}
public override string ExtensionType => ExtensionTypeB;
}
internal class TestScaleMonitor : IScaleMonitor

Просмотреть файл

@ -313,7 +313,9 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests
"AggregateScaleStatus",
"IListenerDecorator",
"ListenerDecoratorContext",
"ScaleHostBuilderContext"
"ITriggerMetadataProvider",
"ITargetScalerCollectionProvider",
"IScaleMonitorCollectionProvider"
};
TestHelpers.AssertPublicTypes(expected, assembly);