diff --git a/release_notes.md b/release_notes.md index 7f2d3d806a..80faeafbe1 100644 --- a/release_notes.md +++ b/release_notes.md @@ -6,4 +6,4 @@ - Add JitTrace Files for v4.1042 - Updating OTel related nuget packages (#11216) - Moving to version 1.5.7 of Microsoft.Azure.AppService.Middleware.Functions (https://github.com/Azure/azure-functions-host/pull/11231) - +- Refactor code to move the logic to search for WorkerConfigs to a default worker configuration resolver (#11219) \ No newline at end of file diff --git a/src/WebJobs.Script.WebHost/WebHostServiceCollectionExtensions.cs b/src/WebJobs.Script.WebHost/WebHostServiceCollectionExtensions.cs index 0d5eb6d79e..f44a660f35 100644 --- a/src/WebJobs.Script.WebHost/WebHostServiceCollectionExtensions.cs +++ b/src/WebJobs.Script.WebHost/WebHostServiceCollectionExtensions.cs @@ -29,7 +29,9 @@ using Microsoft.Azure.WebJobs.Script.WebHost.Security.Authorization.Policies; using Microsoft.Azure.WebJobs.Script.WebHost.Standby; using Microsoft.Azure.WebJobs.Script.Workers.FunctionDataCache; +using Microsoft.Azure.WebJobs.Script.Workers.Profiles; using Microsoft.Azure.WebJobs.Script.Workers.Rpc; +using Microsoft.Azure.WebJobs.Script.Workers.Rpc.Configuration; using Microsoft.Azure.WebJobs.Script.Workers.SharedMemoryDataTransfer; using Microsoft.Extensions.Azure; using Microsoft.Extensions.Configuration; @@ -228,9 +230,11 @@ public static void AddWebJobsScriptHost(this IServiceCollection services, IConfi services.AddHostingConfigOptions(configuration); services.ConfigureOptions(); - // Refresh LanguageWorkerOptions when HostBuiltChangeTokenSource is triggered. + // Refresh WorkerConfigurationResolverOptions and LanguageWorkerOptions when HostBuiltChangeTokenSource is triggered. + services.ConfigureOptionsWithChangeTokenSource>(); services.ConfigureOptionsWithChangeTokenSource>(); + services.AddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(s => DefaultMiddlewarePipeline.Empty); diff --git a/src/WebJobs.Script.WebHost/WebJobsScriptHostService.cs b/src/WebJobs.Script.WebHost/WebJobsScriptHostService.cs index 8fb10c55db..c0977defee 100644 --- a/src/WebJobs.Script.WebHost/WebJobsScriptHostService.cs +++ b/src/WebJobs.Script.WebHost/WebJobsScriptHostService.cs @@ -30,6 +30,7 @@ using Microsoft.Azure.WebJobs.Script.WebHost.Diagnostics.Extensions; using Microsoft.Azure.WebJobs.Script.Workers; using Microsoft.Azure.WebJobs.Script.Workers.Rpc; +using Microsoft.Azure.WebJobs.Script.Workers.Rpc.Configuration; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -66,6 +67,7 @@ public class WebJobsScriptHostService : IHostedService, IScriptHostManager, ISer private readonly bool _originalStandbyModeValue; private readonly string _originalFunctionsWorkerRuntime; private readonly string _originalFunctionsWorkerRuntimeVersion; + private readonly IOptionsChangeTokenSource _workerConfigResolverOptionsChangeTokenSource; private readonly IOptionsChangeTokenSource _languageWorkerOptionsChangeTokenSource; // we're only using this dictionary's keys so it acts as a "ConcurrentHashSet" @@ -89,7 +91,8 @@ public WebJobsScriptHostService(IOptionsMonitor ap HostPerformanceManager hostPerformanceManager, IOptions healthMonitorOptions, IMetricsLogger metricsLogger, IApplicationLifetime applicationLifetime, IConfiguration config, IScriptEventManager eventManager, IHostMetrics hostMetrics, IOptions hostingConfigOptions, - IOptionsChangeTokenSource languageWorkerOptionsChangeTokenSource) + IOptionsChangeTokenSource languageWorkerOptionsChangeTokenSource, + IOptionsChangeTokenSource workerConfigResolverOptionsChangeTokenSource) { ArgumentNullException.ThrowIfNull(loggerFactory); @@ -100,6 +103,7 @@ public WebJobsScriptHostService(IOptionsMonitor ap RegisterApplicationLifetimeEvents(); _metricsLogger = metricsLogger; + _workerConfigResolverOptionsChangeTokenSource = workerConfigResolverOptionsChangeTokenSource ?? throw new ArgumentNullException(nameof(workerConfigResolverOptionsChangeTokenSource)); _languageWorkerOptionsChangeTokenSource = languageWorkerOptionsChangeTokenSource ?? throw new ArgumentNullException(nameof(languageWorkerOptionsChangeTokenSource)); _applicationHostOptions = applicationHostOptions ?? throw new ArgumentNullException(nameof(applicationHostOptions)); _scriptWebHostEnvironment = scriptWebHostEnvironment ?? throw new ArgumentNullException(nameof(scriptWebHostEnvironment)); @@ -378,6 +382,11 @@ private async Task UnsynchronizedStartHostAsync(ScriptHostStartupOperation activ } } + if (_workerConfigResolverOptionsChangeTokenSource is HostBuiltChangeTokenSource { } hostBuiltChangeTokenResolverOptions) + { + hostBuiltChangeTokenResolverOptions.TriggerChange(); + } + if (_languageWorkerOptionsChangeTokenSource is HostBuiltChangeTokenSource { } hostBuiltChangeTokenSource) { hostBuiltChangeTokenSource.TriggerChange(); diff --git a/src/WebJobs.Script/Diagnostics/Extensions/LoggerExtension.cs b/src/WebJobs.Script/Diagnostics/Extensions/LoggerExtension.cs index 225f86ae84..9cea436520 100644 --- a/src/WebJobs.Script/Diagnostics/Extensions/LoggerExtension.cs +++ b/src/WebJobs.Script/Diagnostics/Extensions/LoggerExtension.cs @@ -216,6 +216,11 @@ internal static class LoggerExtension new EventId(342, nameof(OutdatedExtensionBundle)), "Your current bundle version {currentVersion} has reached end of support on Aug 4, 2026. Upgrade to [{suggestedMinVersion}.*, {suggestedMaxVersion}.0.0). For more information, see https://aka.ms/FunctionsBundlesUpgrade"); + private static readonly Action _defaultWorkersDirectoryPath = + LoggerMessage.Define(LogLevel.Debug, + new EventId(343, nameof(DefaultWorkersDirectoryPath)), + "Workers Directory set to: {workersDirPath}"); + public static void PublishingMetrics(this ILogger logger, string metrics) { _publishingMetrics(logger, metrics, null); @@ -418,6 +423,11 @@ public static void IncorrectAzureFunctionsFolderPath(this ILogger logger, string _incorrectAzureFunctionsFolderPath(logger, path, EnvironmentSettingNames.FunctionWorkerRuntime, null); } + public static void DefaultWorkersDirectoryPath(this ILogger logger, string workersDirPath) + { + _defaultWorkersDirectoryPath(logger, workersDirPath, null); + } + public static void OutdatedExtensionBundle(this ILogger logger, string currentVersion, int suggestedMinVersion, int suggestedMaxVersion) { var currentTime = DateTime.UtcNow; diff --git a/src/WebJobs.Script/ScriptConstants.cs b/src/WebJobs.Script/ScriptConstants.cs index 857c5bd912..60b4dc9dbb 100644 --- a/src/WebJobs.Script/ScriptConstants.cs +++ b/src/WebJobs.Script/ScriptConstants.cs @@ -63,6 +63,7 @@ public static class ScriptConstants public const string LogCategoryHost = "Host"; public const string LogCategoryFunction = "Function"; public const string LogCategoryWorker = "Worker"; + public const string LogCategoryWorkerConfig = "Host.LanguageWorkerConfig"; public const string SkipHostJsonConfigurationKey = "MS_SkipHostJsonConfiguration"; public const string SkipHostInitializationKey = "MS_SkipHostInitialization"; diff --git a/src/WebJobs.Script/ScriptHostBuilderExtensions.cs b/src/WebJobs.Script/ScriptHostBuilderExtensions.cs index 61ed8de519..df8b91cc9d 100644 --- a/src/WebJobs.Script/ScriptHostBuilderExtensions.cs +++ b/src/WebJobs.Script/ScriptHostBuilderExtensions.cs @@ -33,6 +33,7 @@ using Microsoft.Azure.WebJobs.Script.Workers.Http; using Microsoft.Azure.WebJobs.Script.Workers.Profiles; using Microsoft.Azure.WebJobs.Script.Workers.Rpc; +using Microsoft.Azure.WebJobs.Script.Workers.Rpc.Configuration; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -346,6 +347,7 @@ public static IHostBuilder AddScriptHostCore(this IHostBuilder builder, ScriptAp } else { + services.ConfigureOptions(); services.ConfigureOptions(); AddCommonServices(services); } diff --git a/src/WebJobs.Script/Workers/Rpc/Configuration/DefaultWorkerConfigurationResolver.cs b/src/WebJobs.Script/Workers/Rpc/Configuration/DefaultWorkerConfigurationResolver.cs new file mode 100644 index 0000000000..6882078bc7 --- /dev/null +++ b/src/WebJobs.Script/Workers/Rpc/Configuration/DefaultWorkerConfigurationResolver.cs @@ -0,0 +1,50 @@ +// 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 System.IO.Abstractions; +using Microsoft.Azure.WebJobs.Script.Diagnostics.Extensions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Microsoft.Azure.WebJobs.Script.Workers.Rpc.Configuration +{ + // This class resolves worker configurations by scanning the "workers" directory within the Host for worker config files. + internal sealed class DefaultWorkerConfigurationResolver : IWorkerConfigurationResolver + { + private readonly ILogger _logger; + private readonly IOptionsMonitor _workerConfigurationResolverOptions; + private readonly IFileSystem _fileSystem; + + public DefaultWorkerConfigurationResolver(ILoggerFactory loggerFactory, + IFileSystem fileSystem, + IOptionsMonitor workerConfigurationResolverOptions) + { + ArgumentNullException.ThrowIfNull(loggerFactory); + _logger = loggerFactory.CreateLogger(ScriptConstants.LogCategoryWorkerConfig); + _workerConfigurationResolverOptions = workerConfigurationResolverOptions ?? throw new ArgumentNullException(nameof(workerConfigurationResolverOptions)); + _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); + } + + public WorkerConfigurationInfo GetConfigurationInfo() + { + var workersRootDirPath = _workerConfigurationResolverOptions.CurrentValue.WorkersRootDirPath; + _logger.DefaultWorkersDirectoryPath(workersRootDirPath); + + var workerConfigPaths = new List(); + + foreach (var workerDir in _fileSystem.Directory.EnumerateDirectories(workersRootDirPath)) + { + string workerConfigPath = _fileSystem.Path.Combine(workerDir, RpcWorkerConstants.WorkerConfigFileName); + + if (_fileSystem.File.Exists(workerConfigPath)) + { + workerConfigPaths.Add(workerDir); + } + } + + return new WorkerConfigurationInfo(_workerConfigurationResolverOptions.CurrentValue.WorkersRootDirPath, workerConfigPaths); + } + } +} diff --git a/src/WebJobs.Script/Workers/Rpc/Configuration/LanguageWorkerOptionsSetup.cs b/src/WebJobs.Script/Workers/Rpc/Configuration/LanguageWorkerOptionsSetup.cs index 92bd078295..0b77f20844 100644 --- a/src/WebJobs.Script/Workers/Rpc/Configuration/LanguageWorkerOptionsSetup.cs +++ b/src/WebJobs.Script/Workers/Rpc/Configuration/LanguageWorkerOptionsSetup.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. using System; @@ -21,13 +21,15 @@ internal class LanguageWorkerOptionsSetup : IConfigureOptions GetConfigs() { using (_metricsLogger.LatencyEvent(MetricEventNames.GetConfigs)) @@ -68,41 +62,25 @@ public IList GetConfigs() } } - internal static string GetDefaultWorkersDirectory(Func directoryExists) - { -#pragma warning disable SYSLIB0012 // Type or member is obsolete - string assemblyLocalPath = Path.GetDirectoryName(new Uri(typeof(RpcWorkerConfigFactory).Assembly.CodeBase).LocalPath); -#pragma warning restore SYSLIB0012 // Type or member is obsolete - string workersDirPath = Path.Combine(assemblyLocalPath, RpcWorkerConstants.DefaultWorkersDirectoryName); - if (!directoryExists(workersDirPath)) - { - // Site Extension. Default to parent directory - workersDirPath = Path.Combine(Directory.GetParent(assemblyLocalPath).FullName, RpcWorkerConstants.DefaultWorkersDirectoryName); - } - return workersDirPath; - } - internal void BuildWorkerProviderDictionary() { - AddProviders(); - AddProvidersFromAppSettings(); + var workerConfigurationInfo = _workerConfigurationResolver.GetConfigurationInfo(); + + AddProviders(workerConfigurationInfo); + AddProvidersFromAppSettings(workerConfigurationInfo); } - internal void AddProviders() + internal void AddProviders(WorkerConfigurationInfo workerConfigurationInfo) { - _logger.LogDebug("Workers Directory set to: {WorkersDirPath}", WorkersDirPath); + var workerConfigs = workerConfigurationInfo.WorkerConfigPaths; - foreach (var workerDir in Directory.EnumerateDirectories(WorkersDirPath)) + foreach (var workerConfig in workerConfigs) { - string workerConfigPath = Path.Combine(workerDir, RpcWorkerConstants.WorkerConfigFileName); - if (File.Exists(workerConfigPath)) - { - AddProvider(workerDir); - } + AddProvider(workerConfig, workerConfigurationInfo.WorkersRootDirPath); } } - internal void AddProvidersFromAppSettings() + internal void AddProvidersFromAppSettings(WorkerConfigurationInfo workerConfigurationInfo) { var languagesSection = _config.GetSection($"{RpcWorkerConstants.LanguageWorkersSectionName}"); foreach (var languageSection in languagesSection.GetChildren()) @@ -111,12 +89,12 @@ internal void AddProvidersFromAppSettings() if (workerDirectorySection.Value != null) { _workerDescriptionDictionary.Remove(languageSection.Key); - AddProvider(workerDirectorySection.Value); + AddProvider(workerDirectorySection.Value, workerConfigurationInfo.WorkersRootDirPath); } } } - internal void AddProvider(string workerDir) + internal void AddProvider(string workerDir, string workersRootDirPath) { using (_metricsLogger.LatencyEvent(string.Format(MetricEventNames.AddProvider, workerDir))) { @@ -128,7 +106,7 @@ internal void AddProvider(string workerDir) string workerRuntime = Path.GetFileName(workerDir); // Only skip worker directories that don't match the current runtime. // Do not skip non-worker directories like the function app payload directory - if (!workerRuntime.Equals(_workerRuntime, StringComparison.OrdinalIgnoreCase) && workerDir.StartsWith(WorkersDirPath)) + if (!workerRuntime.Equals(_workerRuntime, StringComparison.OrdinalIgnoreCase) && workerDir.StartsWith(workersRootDirPath)) { return; } diff --git a/src/WebJobs.Script/Workers/Rpc/Configuration/WorkerConfigurationInfo.cs b/src/WebJobs.Script/Workers/Rpc/Configuration/WorkerConfigurationInfo.cs new file mode 100644 index 0000000000..f872d2d321 --- /dev/null +++ b/src/WebJobs.Script/Workers/Rpc/Configuration/WorkerConfigurationInfo.cs @@ -0,0 +1,9 @@ +// 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.Script.Workers.Rpc.Configuration +{ + internal record WorkerConfigurationInfo(string WorkersRootDirPath, IReadOnlyList WorkerConfigPaths); +} diff --git a/src/WebJobs.Script/Workers/Rpc/Configuration/WorkerConfigurationResolverOptions.cs b/src/WebJobs.Script/Workers/Rpc/Configuration/WorkerConfigurationResolverOptions.cs new file mode 100644 index 0000000000..50d1edbda8 --- /dev/null +++ b/src/WebJobs.Script/Workers/Rpc/Configuration/WorkerConfigurationResolverOptions.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Azure.WebJobs.Hosting; + +namespace Microsoft.Azure.WebJobs.Script.Workers.Rpc.Configuration +{ + public sealed class WorkerConfigurationResolverOptions : IOptionsFormatter + { + /// + /// Gets or sets the workers directory path within the Host or defined by IConfiguration. + /// + public string WorkersRootDirPath { get; set; } + + /// + /// Implements the Format method from IOptionsFormatter interface. + /// + public string Format() + { + return JsonSerializer.Serialize(this, typeof(WorkerConfigurationResolverOptions), ConfigResolverOptionsJsonSerializerContext.Default); + } + } + + [JsonSourceGenerationOptions(WriteIndented = true, GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(WorkerConfigurationResolverOptions))] + internal partial class ConfigResolverOptionsJsonSerializerContext : JsonSerializerContext; +} diff --git a/src/WebJobs.Script/Workers/Rpc/Configuration/WorkerConfigurationResolverOptionsSetup.cs b/src/WebJobs.Script/Workers/Rpc/Configuration/WorkerConfigurationResolverOptionsSetup.cs new file mode 100644 index 0000000000..313402d224 --- /dev/null +++ b/src/WebJobs.Script/Workers/Rpc/Configuration/WorkerConfigurationResolverOptionsSetup.cs @@ -0,0 +1,102 @@ +// 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.IO; +using System.IO.Abstractions; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Microsoft.Azure.WebJobs.Script.Workers.Rpc.Configuration +{ + internal sealed class WorkerConfigurationResolverOptionsSetup : IConfigureOptions + { + private readonly IConfiguration _configuration; + private readonly IScriptHostManager _scriptHostManager; + private readonly IFileSystem _fileSystem; + private readonly ILogger _logger; + + public WorkerConfigurationResolverOptionsSetup(ILoggerFactory loggerFactory, IConfiguration configuration, IScriptHostManager scriptHostManager, IFileSystem fileSystem) + { + ArgumentNullException.ThrowIfNull(loggerFactory); + _logger = loggerFactory.CreateLogger(ScriptConstants.LogCategoryWorkerConfig); + _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + _scriptHostManager = scriptHostManager ?? throw new ArgumentNullException(nameof(scriptHostManager)); + _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); + } + + public void Configure(WorkerConfigurationResolverOptions options) + { + var configuration = GetRequiredConfiguration(); + options.WorkersRootDirPath = GetWorkersRootDirPath(configuration); + } + + internal string GetDefaultWorkersDirectory() + { + var assemblyDir = AppContext.BaseDirectory; + string workersDirPath = Path.Combine(assemblyDir, RpcWorkerConstants.DefaultWorkersDirectoryName); + + if (!_fileSystem.Directory.Exists(workersDirPath)) + { + // Site Extension Path. Default to parent directory. + var parentDir = _fileSystem.Directory.GetParent(assemblyDir.TrimEnd(Path.DirectorySeparatorChar)).FullName; + workersDirPath = _fileSystem.Path.Combine(parentDir, RpcWorkerConstants.DefaultWorkersDirectoryName); + } + return workersDirPath; + } + + private string GetWorkersRootDirPath(IConfiguration configuration) + { + if (configuration is not null) + { + var workersDirectorySection = configuration.GetSection($"{RpcWorkerConstants.LanguageWorkersSectionName}:{WorkerConstants.WorkersDirectorySectionName}"); + + if (!string.IsNullOrEmpty(workersDirectorySection?.Value)) + { + return workersDirectorySection.Value; + } + } + + return GetDefaultWorkersDirectory(); + } + + private IConfiguration GetRequiredConfiguration() + { + string requiredSection = $"{RpcWorkerConstants.LanguageWorkersSectionName}:{WorkerConstants.WorkersDirectorySectionName}"; + + if (_scriptHostManager is IServiceProvider scriptHostManagerServiceProvider) + { + var latestConfiguration = scriptHostManagerServiceProvider.GetService(); + var latestConfigValue = GetConfigurationSectionValue(latestConfiguration, nameof(latestConfiguration), requiredSection); + + if (!string.IsNullOrEmpty(latestConfigValue)) + { + return latestConfiguration; + } + } + + string configSectionValue = GetConfigurationSectionValue(_configuration, nameof(_configuration), requiredSection); + if (!string.IsNullOrEmpty(configSectionValue)) + { + return _configuration; + } + + return null; + } + + private string GetConfigurationSectionValue(IConfiguration configuration, string configurationSource, string requiredSection) + { + var section = configuration?.GetSection(requiredSection); + + if (!string.IsNullOrEmpty(section?.Value)) + { + _logger.LogTrace("Found configuration section '{requiredSection}' in '{configurationSource}'.", requiredSection, configurationSource); + return section.Value; + } + + return null; + } + } +} diff --git a/src/WebJobs.Script/Workers/Rpc/IWorkerConfigurationResolver.cs b/src/WebJobs.Script/Workers/Rpc/IWorkerConfigurationResolver.cs new file mode 100644 index 0000000000..517f11f51d --- /dev/null +++ b/src/WebJobs.Script/Workers/Rpc/IWorkerConfigurationResolver.cs @@ -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 System.Collections.Generic; +using Microsoft.Azure.WebJobs.Script.Workers.Rpc.Configuration; +using Microsoft.Extensions.Options; + +namespace Microsoft.Azure.WebJobs.Script.Workers.Rpc +{ + /// + /// Provides an abstraction for retrieving worker configuration resolution details. + /// + internal interface IWorkerConfigurationResolver + { + /// + /// Retrieves the worker configuration resolution information which includes the root directory path of workers and worker configuration paths. + /// + WorkerConfigurationInfo GetConfigurationInfo(); + } +} diff --git a/test/WebJobs.Script.Tests.Shared/TestHostBuilderExtensions.cs b/test/WebJobs.Script.Tests.Shared/TestHostBuilderExtensions.cs index 9dc13e2eec..6598617861 100644 --- a/test/WebJobs.Script.Tests.Shared/TestHostBuilderExtensions.cs +++ b/test/WebJobs.Script.Tests.Shared/TestHostBuilderExtensions.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.Metrics; +using System.IO.Abstractions; using System.Threading.Tasks; using Microsoft.Azure.WebJobs; using Microsoft.Azure.WebJobs.Host.Storage; @@ -20,6 +21,7 @@ using Microsoft.Azure.WebJobs.Script.Workers; using Microsoft.Azure.WebJobs.Script.Workers.Profiles; using Microsoft.Azure.WebJobs.Script.Workers.Rpc; +using Microsoft.Azure.WebJobs.Script.Workers.Rpc.Configuration; using Microsoft.Azure.WebJobs.Script.Workers.SharedMemoryDataTransfer; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -76,6 +78,8 @@ public static IHostBuilder ConfigureDefaultTestWebScriptHost(this IHostBuilder b services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(FileUtility.Instance); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(metricsLogger); services.AddWebJobsScriptHostRouting(); @@ -83,6 +87,7 @@ public static IHostBuilder ConfigureDefaultTestWebScriptHost(this IHostBuilder b services.AddFunctionMetadataManager(); services.AddHostMetrics(); services.AddConfiguration(); + services.ConfigureOptions(); services.ConfigureOptions(); configureRootServices?.Invoke(services); diff --git a/test/WebJobs.Script.Tests/Configuration/LanguageWorkerOptionsSetupTests.cs b/test/WebJobs.Script.Tests/Configuration/LanguageWorkerOptionsSetupTests.cs index d3f2dea6c8..4c9e9ca921 100644 --- a/test/WebJobs.Script.Tests/Configuration/LanguageWorkerOptionsSetupTests.cs +++ b/test/WebJobs.Script.Tests/Configuration/LanguageWorkerOptionsSetupTests.cs @@ -4,7 +4,9 @@ using System; using Microsoft.Azure.WebJobs.Script.Workers.Profiles; using Microsoft.Azure.WebJobs.Script.Workers.Rpc; +using Microsoft.Azure.WebJobs.Script.Workers.Rpc.Configuration; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; @@ -20,6 +22,7 @@ public class LanguageWorkerOptionsSetupTests [InlineData("node")] public void LanguageWorkerOptions_Expected_ListOfConfigs(string workerRuntime) { + var loggerFactory = WorkerConfigurationResolverTestsHelper.GetTestLoggerFactory(); var testEnvironment = new TestEnvironment(); var testMetricLogger = new TestMetricsLogger(); var configurationBuilder = new ConfigurationBuilder() @@ -53,8 +56,12 @@ public void LanguageWorkerOptions_Expected_ListOfConfigs(string workerRuntime) } }); - LanguageWorkerOptionsSetup setup = new LanguageWorkerOptionsSetup(configuration, NullLoggerFactory.Instance, testEnvironment, testMetricLogger, testProfileManager.Object, testScriptHostManager.Object); - LanguageWorkerOptions options = new LanguageWorkerOptions(); + var optionsMonitor = WorkerConfigurationResolverTestsHelper.GetTestWorkerConfigurationResolverOptions(configuration, testEnvironment, testScriptHostManager.Object, null); + + var resolver = new DefaultWorkerConfigurationResolver(loggerFactory, FileUtility.Instance, optionsMonitor); + + var setup = new LanguageWorkerOptionsSetup(configuration, loggerFactory, testEnvironment, testMetricLogger, testProfileManager.Object, testScriptHostManager.Object, resolver); + var options = new LanguageWorkerOptions(); setup.Configure(options); diff --git a/test/WebJobs.Script.Tests/ScriptHostTests.cs b/test/WebJobs.Script.Tests/ScriptHostTests.cs index 7b8ac69cc6..039af78179 100644 --- a/test/WebJobs.Script.Tests/ScriptHostTests.cs +++ b/test/WebJobs.Script.Tests/ScriptHostTests.cs @@ -1604,6 +1604,8 @@ public async Task Initialize_LogsWarningForExplicitlySetHostId() [InlineData(null, "app.dll", "dotnet", DotNetScriptTypes.DotNetAssembly)] // if FUNCTIONS_WORKER_RUNTIME is missing, assume dotnet public async Task Initialize_MissingWorkerRuntime_SetsCorrectRuntimeFromFunctionMetadata(string functionsWorkerRuntime, string scriptFile, string expectedMetricLanguage, string expectedMetadataLanguage) { + string workersDirPath = Path.Combine(AppContext.BaseDirectory, RpcWorkerConstants.DefaultWorkersDirectoryName); + IFileSystem CreateFileSystem(string rootPath) { var fullFileSystem = new FileSystem(); @@ -1630,6 +1632,19 @@ IFileSystem CreateFileSystem(string rootPath) Path.Combine(rootPath, "function1"), }); + dirBase.Setup(d => d.Exists(workersDirPath)).Returns(true); + + if (functionsWorkerRuntime is not null) + { + dirBase.Setup(d => d.EnumerateDirectories(workersDirPath)) + .Returns(new[] + { + Path.Combine(workersDirPath, functionsWorkerRuntime), + }); + + fileBase.Setup(f => f.Exists(Path.Combine(workersDirPath, functionsWorkerRuntime, RpcWorkerConstants.WorkerConfigFileName))).Returns(true); + } + var function1 = $$""" { "scriptFile": "{{scriptFile}}", diff --git a/test/WebJobs.Script.Tests/WebJobsScriptHostServiceTests.cs b/test/WebJobs.Script.Tests/WebJobsScriptHostServiceTests.cs index 092beeabf5..f1bf71437d 100644 --- a/test/WebJobs.Script.Tests/WebJobsScriptHostServiceTests.cs +++ b/test/WebJobs.Script.Tests/WebJobsScriptHostServiceTests.cs @@ -16,6 +16,7 @@ using Microsoft.Azure.WebJobs.Script.WebHost.Configuration; using Microsoft.Azure.WebJobs.Script.WebHost.DependencyInjection; using Microsoft.Azure.WebJobs.Script.Workers.Rpc; +using Microsoft.Azure.WebJobs.Script.Workers.Rpc.Configuration; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -40,6 +41,7 @@ public class WebJobsScriptHostServiceTests private Mock _mockScriptWebHostEnvironment; private Mock _mockEnvironment; private HostBuiltChangeTokenSource _hostBuiltChangeTokenSource = new(); + private HostBuiltChangeTokenSource _hostBuiltChangeTokenSourceResolverOptions = new(); private IConfiguration _mockConfig; private OptionsWrapper _healthMonitorOptions; private HostPerformanceManager _hostPerformanceManager; @@ -119,7 +121,8 @@ public async Task StartAsync_Succeeds() _hostPerformanceManager, _healthMonitorOptions, metricsLogger, new Mock().Object, _mockConfig, mockEventManager.Object, _hostMetrics, _functionsHostingConfigOptions, - _hostBuiltChangeTokenSource); + _hostBuiltChangeTokenSource, + _hostBuiltChangeTokenSourceResolverOptions); await _hostService.StartAsync(CancellationToken.None); @@ -151,7 +154,8 @@ public async Task HostInitialization_OnInitializationException_MaintainsErrorInf _mockScriptWebHostEnvironment.Object, _mockEnvironment.Object, _hostPerformanceManager, _healthMonitorOptions, metricsLogger, new Mock().Object, _mockConfig, new TestScriptEventManager(), _hostMetrics, _functionsHostingConfigOptions, - _hostBuiltChangeTokenSource); + _hostBuiltChangeTokenSource, + _hostBuiltChangeTokenSourceResolverOptions); await _hostService.StartAsync(CancellationToken.None); Assert.True(AreRequiredMetricsGenerated(metricsLogger)); @@ -181,7 +185,7 @@ public async Task HostRestart_Specialization_Succeeds() _mockScriptWebHostEnvironment.Object, _mockEnvironment.Object, _hostPerformanceManager, _healthMonitorOptions, metricsLogger, new Mock().Object, - _mockConfig, new TestScriptEventManager(), _hostMetrics, _functionsHostingConfigOptions, _hostBuiltChangeTokenSource); + _mockConfig, new TestScriptEventManager(), _hostMetrics, _functionsHostingConfigOptions, _hostBuiltChangeTokenSource, _hostBuiltChangeTokenSourceResolverOptions); await _hostService.StartAsync(CancellationToken.None); Assert.True(AreRequiredMetricsGenerated(metricsLogger)); @@ -236,7 +240,7 @@ public async Task HostRestart_DuringInitializationWithError_Recovers() _mockScriptWebHostEnvironment.Object, _mockEnvironment.Object, _hostPerformanceManager, _healthMonitorOptions, metricsLogger, new Mock().Object, - _mockConfig, new TestScriptEventManager(), _hostMetrics, _functionsHostingConfigOptions, _hostBuiltChangeTokenSource); + _mockConfig, new TestScriptEventManager(), _hostMetrics, _functionsHostingConfigOptions, _hostBuiltChangeTokenSource, _hostBuiltChangeTokenSourceResolverOptions); TestLoggerProvider hostALogger = hostA.Object.GetTestLoggerProvider(); TestLoggerProvider hostBLogger = hostB.Object.GetTestLoggerProvider(); @@ -312,7 +316,7 @@ public async Task HostRestart_DuringInitialization_Cancels() _mockScriptWebHostEnvironment.Object, _mockEnvironment.Object, _hostPerformanceManager, _healthMonitorOptions, metricsLogger, new Mock().Object, - _mockConfig, new TestScriptEventManager(), _hostMetrics, _functionsHostingConfigOptions, _hostBuiltChangeTokenSource); + _mockConfig, new TestScriptEventManager(), _hostMetrics, _functionsHostingConfigOptions, _hostBuiltChangeTokenSource, _hostBuiltChangeTokenSourceResolverOptions); TestLoggerProvider hostALogger = hostA.Object.GetTestLoggerProvider(); @@ -382,7 +386,7 @@ public async Task DisposedHost_ServicesNotExposed() _mockScriptWebHostEnvironment.Object, _mockEnvironment.Object, _hostPerformanceManager, _healthMonitorOptions, metricsLogger, new Mock().Object, _mockConfig, new TestScriptEventManager(), - _hostMetrics, _functionsHostingConfigOptions, _hostBuiltChangeTokenSource); + _hostMetrics, _functionsHostingConfigOptions, _hostBuiltChangeTokenSource, _hostBuiltChangeTokenSourceResolverOptions); Task startTask = _hostService.StartAsync(CancellationToken.None); @@ -433,7 +437,7 @@ public async Task DisposesScriptHost() _mockScriptWebHostEnvironment.Object, _mockEnvironment.Object, _hostPerformanceManager, _healthMonitorOptions, metricsLogger, new Mock().Object, - _mockConfig, new TestScriptEventManager(), _hostMetrics, _functionsHostingConfigOptions, _hostBuiltChangeTokenSource); + _mockConfig, new TestScriptEventManager(), _hostMetrics, _functionsHostingConfigOptions, _hostBuiltChangeTokenSource, _hostBuiltChangeTokenSourceResolverOptions); var hostLogger = host.Object.GetTestLoggerProvider(); @@ -471,7 +475,7 @@ public async Task HostRestart_BeforeStart_WaitsForStartToContinue() _mockScriptWebHostEnvironment.Object, _mockEnvironment.Object, _hostPerformanceManager, _healthMonitorOptions, metricsLogger, new Mock().Object, _mockConfig, - new TestScriptEventManager(), _hostMetrics, _functionsHostingConfigOptions, _hostBuiltChangeTokenSource); + new TestScriptEventManager(), _hostMetrics, _functionsHostingConfigOptions, _hostBuiltChangeTokenSource, _hostBuiltChangeTokenSourceResolverOptions); // Simulate a call to specialize coming from the PlaceholderSpecializationMiddleware. This // can happen before we ever start the service, which could create invalid state. @@ -527,7 +531,7 @@ public void ShouldEnforceSequentialRestart_WithCorrectConfig(string value, bool _mockScriptWebHostEnvironment.Object, _mockEnvironment.Object, _hostPerformanceManager, _healthMonitorOptions, metricsLogger, new Mock().Object, config, new TestScriptEventManager(), - _hostMetrics, _functionsHostingConfigOptions, _hostBuiltChangeTokenSource); + _hostMetrics, _functionsHostingConfigOptions, _hostBuiltChangeTokenSource, _hostBuiltChangeTokenSourceResolverOptions); Assert.Equal(expectedResult, _hostService.ShouldEnforceSequentialRestart()); } @@ -565,7 +569,7 @@ public async Task DependencyTrackingTelemetryModule_Race() _mockScriptWebHostEnvironment.Object, _mockEnvironment.Object, _hostPerformanceManager, _healthMonitorOptions, new TestMetricsLogger(), new Mock().Object, _mockConfig, new TestScriptEventManager(), - _hostMetrics, _functionsHostingConfigOptions, _hostBuiltChangeTokenSource)) + _hostMetrics, _functionsHostingConfigOptions, _hostBuiltChangeTokenSource, _hostBuiltChangeTokenSourceResolverOptions)) { await _hostService.StartAsync(CancellationToken.None); diff --git a/test/WebJobs.Script.Tests/Workers/Rpc/RpcWorkerConfigFactoryTests.cs b/test/WebJobs.Script.Tests/Workers/Rpc/RpcWorkerConfigFactoryTests.cs index e2df3f80df..5a03b6bdcb 100644 --- a/test/WebJobs.Script.Tests/Workers/Rpc/RpcWorkerConfigFactoryTests.cs +++ b/test/WebJobs.Script.Tests/Workers/Rpc/RpcWorkerConfigFactoryTests.cs @@ -4,13 +4,18 @@ using System; using System.Collections.Generic; using System.IO; +using System.IO.Abstractions; using System.Linq; using System.Text.Json; using Microsoft.Azure.WebJobs.Script.Config; using Microsoft.Azure.WebJobs.Script.Workers; using Microsoft.Azure.WebJobs.Script.Workers.Profiles; using Microsoft.Azure.WebJobs.Script.Workers.Rpc; +using Microsoft.Azure.WebJobs.Script.Workers.Rpc.Configuration; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.WebJobs.Script.Tests; +using Moq; using Xunit; namespace Microsoft.Azure.WebJobs.Script.Tests.Workers.Rpc @@ -39,26 +44,55 @@ public void DefaultLanguageWorkersDir() var expectedWorkersDir = Path.Combine(Path.GetDirectoryName(new Uri(typeof(RpcWorkerConfigFactory).Assembly.Location).LocalPath), RpcWorkerConstants.DefaultWorkersDirectoryName); var config = new ConfigurationBuilder().Build(); var testLogger = new TestLogger("test"); - var configFactory = new RpcWorkerConfigFactory(config, testLogger, _testSysRuntimeInfo, _testEnvironment, new TestMetricsLogger(), _testWorkerProfileManager); - Assert.Equal(expectedWorkersDir, configFactory.WorkersDirPath); + + var testScriptHostManager = new Mock(); + var testLoggerFactory = WorkerConfigurationResolverTestsHelper.GetTestLoggerFactory(); + var optionsMonitor = WorkerConfigurationResolverTestsHelper.GetTestWorkerConfigurationResolverOptions(config, _testEnvironment, testScriptHostManager.Object, null); + var workerConfigurationResolver = new DefaultWorkerConfigurationResolver(testLoggerFactory, FileUtility.Instance, optionsMonitor); + + var configFactory = new RpcWorkerConfigFactory(config, testLogger, _testSysRuntimeInfo, _testEnvironment, new TestMetricsLogger(), _testWorkerProfileManager, workerConfigurationResolver); + string workersDirPath = workerConfigurationResolver.GetConfigurationInfo().WorkersRootDirPath; + + Assert.Equal(expectedWorkersDir, workersDirPath); } - [Fact] - public void GetDefaultWorkersDirectory_Returns_Expected() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void GetDefaultWorkersDirectory_Returns_Expected(bool expectedValue) { string assemblyLocalPath = Path.GetDirectoryName(new Uri(typeof(RpcWorkerConfigFactory).Assembly.Location).LocalPath); string defaultWorkersDirPath = Path.Combine(assemblyLocalPath, RpcWorkerConstants.DefaultWorkersDirectoryName); - Func testDirectoryExists = path => - { - return false; - }; + var fileSystemMock = new Mock(); var expectedWorkersDirIsCurrentDir = Path.Combine(assemblyLocalPath, RpcWorkerConstants.DefaultWorkersDirectoryName); var expectedWorkersDirIsParentDir = Path.Combine(Directory.GetParent(assemblyLocalPath).FullName, RpcWorkerConstants.DefaultWorkersDirectoryName); var config = new ConfigurationBuilder().Build(); - var testLogger = new TestLogger("test"); + var loggerFactory = WorkerConfigurationResolverTestsHelper.GetTestLoggerFactory(); + var trimmedAssemblyDir = assemblyLocalPath.TrimEnd(Path.DirectorySeparatorChar); + + var parentDirInfoMock = new Mock(); + parentDirInfoMock.Setup(d => d.FullName).Returns(Directory.GetParent(assemblyLocalPath).FullName); + + var parentDirInfoMock2 = new Mock(); + parentDirInfoMock2.Setup(d => d.FullName).Returns(Directory.GetParent(trimmedAssemblyDir).FullName); + + fileSystemMock.Setup(f => f.Directory.Exists(defaultWorkersDirPath)).Returns(expectedValue); + fileSystemMock.Setup(f => f.Directory.GetParent(trimmedAssemblyDir)).Returns(parentDirInfoMock2.Object); + fileSystemMock.Setup(f => f.Directory.GetParent(defaultWorkersDirPath)).Returns(parentDirInfoMock.Object); + fileSystemMock.Setup(f => f.Path.Combine(It.IsAny(), RpcWorkerConstants.DefaultWorkersDirectoryName)) + .Returns((string dir, string workersDirName) => Path.Combine(dir, workersDirName)); + + var mockScriptHostManager = new Mock(); + var optionsSetup = new WorkerConfigurationResolverOptionsSetup(loggerFactory, config, mockScriptHostManager.Object, fileSystemMock.Object); - Assert.Equal(expectedWorkersDirIsCurrentDir, RpcWorkerConfigFactory.GetDefaultWorkersDirectory(Directory.Exists)); - Assert.Equal(expectedWorkersDirIsParentDir, RpcWorkerConfigFactory.GetDefaultWorkersDirectory(testDirectoryExists)); + if (expectedValue) + { + Assert.Equal(expectedWorkersDirIsCurrentDir, optionsSetup.GetDefaultWorkersDirectory()); + } + else + { + Assert.Equal(expectedWorkersDirIsParentDir, optionsSetup.GetDefaultWorkersDirectory()); + } } [Fact] @@ -72,8 +106,14 @@ public void LanguageWorker_WorkersDir_Set() }) .Build(); var testLogger = new TestLogger("test"); - var configFactory = new RpcWorkerConfigFactory(config, testLogger, _testSysRuntimeInfo, _testEnvironment, new TestMetricsLogger(), _testWorkerProfileManager); - Assert.Equal(expectedWorkersDir, configFactory.WorkersDirPath); + var testScriptHostManager = new Mock(); + var mockLogger = new Mock(); + var optionsMonitor = WorkerConfigurationResolverTestsHelper.GetTestWorkerConfigurationResolverOptions(config, _testEnvironment, testScriptHostManager.Object, null); + var workerConfigurationResolver = new DefaultWorkerConfigurationResolver(mockLogger.Object, FileUtility.Instance, optionsMonitor); + + var configFactory = new RpcWorkerConfigFactory(config, testLogger, _testSysRuntimeInfo, _testEnvironment, new TestMetricsLogger(), _testWorkerProfileManager, workerConfigurationResolver); + + Assert.Equal(expectedWorkersDir, optionsMonitor.CurrentValue.WorkersRootDirPath); } [Fact] @@ -88,8 +128,16 @@ public void LanguageWorker_WorkersDir_NotSet() var config = configBuilder.Build(); var scriptSettingsManager = new ScriptSettingsManager(config); var testLogger = new TestLogger("test"); - var configFactory = new RpcWorkerConfigFactory(config, testLogger, _testSysRuntimeInfo, _testEnvironment, new TestMetricsLogger(), _testWorkerProfileManager); - Assert.Equal(expectedWorkersDir, configFactory.WorkersDirPath); + var testLoggerFactory = WorkerConfigurationResolverTestsHelper.GetTestLoggerFactory(); + var testScriptHostManager = new Mock(); + + var optionsMonitor = WorkerConfigurationResolverTestsHelper.GetTestWorkerConfigurationResolverOptions(config, _testEnvironment, testScriptHostManager.Object, null); + var workerConfigurationResolver = new DefaultWorkerConfigurationResolver(testLoggerFactory, FileUtility.Instance, optionsMonitor); + + var configFactory = new RpcWorkerConfigFactory(config, testLogger, _testSysRuntimeInfo, _testEnvironment, new TestMetricsLogger(), _testWorkerProfileManager, workerConfigurationResolver); + var workersDirPath = workerConfigurationResolver.GetConfigurationInfo().WorkersRootDirPath; + + Assert.Equal(expectedWorkersDir, workersDirPath); } [Fact] @@ -111,8 +159,15 @@ public void WorkerDescription_Skipped_When_Profile_Disables_Worker() var config = configBuilder.Build(); var scriptSettingsManager = new ScriptSettingsManager(config); var testLogger = new TestLogger("test"); + var testLoggerFactory = WorkerConfigurationResolverTestsHelper.GetTestLoggerFactory(); _testEnvironment.SetEnvironmentVariable("ENV_VAR_BAR", "True"); - var configFactory = new RpcWorkerConfigFactory(config, testLogger, _testSysRuntimeInfo, _testEnvironment, new TestMetricsLogger(), _testWorkerProfileManager); + + var loggerFactory = WorkerConfigurationResolverTestsHelper.GetTestLoggerFactory(); + var testScriptHostManager = new Mock(); + var optionsMonitor = WorkerConfigurationResolverTestsHelper.GetTestWorkerConfigurationResolverOptions(config, _testEnvironment, testScriptHostManager.Object); + var workerConfigurationResolver = new DefaultWorkerConfigurationResolver(loggerFactory, FileUtility.Instance, optionsMonitor); + + var configFactory = new RpcWorkerConfigFactory(config, testLogger, _testSysRuntimeInfo, _testEnvironment, new TestMetricsLogger(), _testWorkerProfileManager, workerConfigurationResolver); var workerConfigs = configFactory.GetConfigs(); var errors = testLogger.GetLogMessages().Where(m => m.Exception != null).ToList(); @@ -133,8 +188,15 @@ public void JavaPath_FromEnvVars() var configBuilder = ScriptSettingsManager.CreateDefaultConfigurationBuilder(); var config = configBuilder.Build(); var scriptSettingsManager = new ScriptSettingsManager(config); - var testLogger = new TestLogger("test"); - var configFactory = new RpcWorkerConfigFactory(config, testLogger, _testSysRuntimeInfo, _testEnvironment, new TestMetricsLogger(), _testWorkerProfileManager); + + var loggerFactory = WorkerConfigurationResolverTestsHelper.GetTestLoggerFactory(); + var testLogger = loggerFactory.CreateLogger("test"); + var testScriptHostManager = new Mock(); + var optionsMonitor = WorkerConfigurationResolverTestsHelper.GetTestWorkerConfigurationResolverOptions(config, _testEnvironment, testScriptHostManager.Object); + + var workerConfigurationResolver = new DefaultWorkerConfigurationResolver(loggerFactory, FileUtility.Instance, optionsMonitor); + var configFactory = new RpcWorkerConfigFactory(config, testLogger, _testSysRuntimeInfo, _testEnvironment, new TestMetricsLogger(), _testWorkerProfileManager, workerConfigurationResolver); + var workerConfigs = configFactory.GetConfigs(); var javaPath = workerConfigs.FirstOrDefault(c => c.Description.Language.Equals("java", StringComparison.OrdinalIgnoreCase)).Description.DefaultExecutablePath; Assert.DoesNotContain(@"%JAVA_HOME%", javaPath); @@ -152,11 +214,21 @@ public void DefaultWorkerConfigs_Overrides_DefaultWorkerRuntimeVersion_AppSettin .AddInMemoryCollection(testEnvVariables); var config = configBuilder.Build(); var scriptSettingsManager = new ScriptSettingsManager(config); - var testLogger = new TestLogger("test"); _testEnvironment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsitePlaceholderMode, "1"); using var variables = new TestScopedSettings(scriptSettingsManager, testEnvVariables); - var configFactory = new RpcWorkerConfigFactory(config, testLogger, _testSysRuntimeInfo, _testEnvironment, new TestMetricsLogger(), _testWorkerProfileManager); + + var loggerProvider = new TestLoggerProvider(); + var loggerFactory = new LoggerFactory(); + loggerFactory.AddProvider(loggerProvider); + var testLogger = loggerFactory.CreateLogger("test"); + + var testScriptHostManager = new Mock(); + var optionsMonitor = WorkerConfigurationResolverTestsHelper.GetTestWorkerConfigurationResolverOptions(config, _testEnvironment, testScriptHostManager.Object, null); + + var workerConfigurationResolver = new DefaultWorkerConfigurationResolver(loggerFactory, FileUtility.Instance, optionsMonitor); + var configFactory = new RpcWorkerConfigFactory(config, testLogger, _testSysRuntimeInfo, _testEnvironment, new TestMetricsLogger(), _testWorkerProfileManager, workerConfigurationResolver); + var workerConfigs = configFactory.GetConfigs(); var pythonWorkerConfig = workerConfigs.FirstOrDefault(w => w.Description.Language.Equals("python", StringComparison.OrdinalIgnoreCase)); var powershellWorkerConfig = workerConfigs.FirstOrDefault(w => w.Description.Language.Equals("powershell", StringComparison.OrdinalIgnoreCase)); @@ -176,8 +248,15 @@ public void DefaultWorkerConfigs_Overrides_VersionAppSetting() var configBuilder = ScriptSettingsManager.CreateDefaultConfigurationBuilder(); var config = configBuilder.Build(); var scriptSettingsManager = new ScriptSettingsManager(config); - var testLogger = new TestLogger("test"); - var configFactory = new RpcWorkerConfigFactory(config, testLogger, _testSysRuntimeInfo, testEnvironment, new TestMetricsLogger(), _testWorkerProfileManager); + + var loggerFactory = WorkerConfigurationResolverTestsHelper.GetTestLoggerFactory(); + var testLogger = loggerFactory.CreateLogger("test"); + var testScriptHostManager = new Mock(); + var optionsMonitor = WorkerConfigurationResolverTestsHelper.GetTestWorkerConfigurationResolverOptions(config, _testEnvironment, testScriptHostManager.Object, null); + + var resolver = new DefaultWorkerConfigurationResolver(loggerFactory, FileUtility.Instance, optionsMonitor); + var configFactory = new RpcWorkerConfigFactory(config, testLogger, _testSysRuntimeInfo, testEnvironment, new TestMetricsLogger(), _testWorkerProfileManager, resolver); + var workerConfigs = configFactory.GetConfigs(); var powershellWorkerConfig = workerConfigs.FirstOrDefault(w => w.Description.Language.Equals("powershell", StringComparison.OrdinalIgnoreCase)); Assert.Equal(1, workerConfigs.Count); @@ -203,8 +282,15 @@ public void ShouldAddProvider_Returns_Expected(string workerLanguage, string wor _testEnvironment.SetEnvironmentVariable(RpcWorkerConstants.FunctionWorkerRuntimeSettingName, workerRuntime); } var config = new ConfigurationBuilder().Build(); - var testLogger = new TestLogger("test"); - RpcWorkerConfigFactory rpcWorkerConfigFactory = new RpcWorkerConfigFactory(config, testLogger, _testSysRuntimeInfo, _testEnvironment, new TestMetricsLogger(), _testWorkerProfileManager); + var loggerFactory = WorkerConfigurationResolverTestsHelper.GetTestLoggerFactory(); + var testLogger = loggerFactory.CreateLogger("test"); + + var testScriptHostManager = new Mock(); + var optionsMonitor = WorkerConfigurationResolverTestsHelper.GetTestWorkerConfigurationResolverOptions(config, _testEnvironment, testScriptHostManager.Object, null); + + var workerConfigurationResolver = new DefaultWorkerConfigurationResolver(loggerFactory, FileUtility.Instance, optionsMonitor); + var rpcWorkerConfigFactory = new RpcWorkerConfigFactory(config, testLogger, _testSysRuntimeInfo, _testEnvironment, new TestMetricsLogger(), _testWorkerProfileManager, workerConfigurationResolver); + _testEnvironment.SetEnvironmentVariable(RpcWorkerConstants.FunctionWorkerRuntimeSettingName, workerRuntime); Assert.Equal(expectedResult, rpcWorkerConfigFactory.ShouldAddWorkerConfig(workerLanguage)); } @@ -248,7 +334,12 @@ public void GetWorkerProcessCount_Tests(bool defaultWorkerConfig, bool setProces var config = new ConfigurationBuilder().Build(); var testLogger = new TestLogger("test"); - RpcWorkerConfigFactory rpcWorkerConfigFactory = new RpcWorkerConfigFactory(config, testLogger, _testSysRuntimeInfo, _testEnvironment, new TestMetricsLogger(), _testWorkerProfileManager); + var testScriptHostManager = new Mock(); + var optionsMonitor = WorkerConfigurationResolverTestsHelper.GetTestWorkerConfigurationResolverOptions(config, _testEnvironment, testScriptHostManager.Object, null); + var mockLogger = new Mock(); + + var workerConfigurationResolver = new DefaultWorkerConfigurationResolver(mockLogger.Object, FileUtility.Instance, optionsMonitor); + var rpcWorkerConfigFactory = new RpcWorkerConfigFactory(config, testLogger, _testSysRuntimeInfo, _testEnvironment, new TestMetricsLogger(), _testWorkerProfileManager, workerConfigurationResolver); var result = rpcWorkerConfigFactory.GetWorkerProcessCount(workerConfig); if (defaultWorkerConfig) @@ -288,7 +379,14 @@ public void GetWorkerProcessCount_ThrowsException_Tests() var config = new ConfigurationBuilder().Build(); var testLogger = new TestLogger("test"); - RpcWorkerConfigFactory rpcWorkerConfigFactory = new RpcWorkerConfigFactory(config, testLogger, _testSysRuntimeInfo, _testEnvironment, new TestMetricsLogger(), _testWorkerProfileManager); + + var testScriptHostManager = new Mock(); + var optionsMonitor = WorkerConfigurationResolverTestsHelper.GetTestWorkerConfigurationResolverOptions(config, _testEnvironment, testScriptHostManager.Object, null); + var mockLogger = new Mock(); + + var workerConfigurationResolver = new DefaultWorkerConfigurationResolver(mockLogger.Object, FileUtility.Instance, optionsMonitor); + var rpcWorkerConfigFactory = new RpcWorkerConfigFactory(config, testLogger, _testSysRuntimeInfo, _testEnvironment, new TestMetricsLogger(), _testWorkerProfileManager, workerConfigurationResolver); + var resultEx1 = Assert.Throws(() => rpcWorkerConfigFactory.GetWorkerProcessCount(workerConfig)); Assert.Contains("ProcessCount must be greater than 0", resultEx1.Message); diff --git a/test/WebJobs.Script.Tests/Workers/Rpc/RpcWorkerConfigTests.cs b/test/WebJobs.Script.Tests/Workers/Rpc/RpcWorkerConfigTests.cs index 1f696ad16b..93afba2554 100644 --- a/test/WebJobs.Script.Tests/Workers/Rpc/RpcWorkerConfigTests.cs +++ b/test/WebJobs.Script.Tests/Workers/Rpc/RpcWorkerConfigTests.cs @@ -12,6 +12,7 @@ using Microsoft.Azure.WebJobs.Script.Workers; using Microsoft.Azure.WebJobs.Script.Workers.Profiles; using Microsoft.Azure.WebJobs.Script.Workers.Rpc; +using Microsoft.Azure.WebJobs.Script.Workers.Rpc.Configuration; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Moq; @@ -683,7 +684,12 @@ private IEnumerable TestReadWorkerProviderFromConfig(IEnumerabl var scriptHostOptions = new ScriptJobHostOptions(); var scriptSettingsManager = new ScriptSettingsManager(config); var workerProfileManager = new Mock(); - var configFactory = new RpcWorkerConfigFactory(config, testLogger, _testSysRuntimeInfo, _testEnvironment, testMetricsLogger, workerProfileManager.Object); + var testScriptHostManager = new Mock(); + var loggerFactory = WorkerConfigurationResolverTestsHelper.GetTestLoggerFactory(); + var optionsMonitor = WorkerConfigurationResolverTestsHelper.GetTestWorkerConfigurationResolverOptions(config, _testEnvironment, testScriptHostManager.Object, null); + var workerConfigurationResolver = new DefaultWorkerConfigurationResolver(loggerFactory, FileUtility.Instance, optionsMonitor); + + var configFactory = new RpcWorkerConfigFactory(config, testLogger, _testSysRuntimeInfo, _testEnvironment, new TestMetricsLogger(), workerProfileManager.Object, workerConfigurationResolver); if (appSvcEnv) { diff --git a/test/WebJobs.Script.Tests/Workers/Rpc/WorkerConfigurationResolverOptionsSetupTests.cs b/test/WebJobs.Script.Tests/Workers/Rpc/WorkerConfigurationResolverOptionsSetupTests.cs new file mode 100644 index 0000000000..feb150f709 --- /dev/null +++ b/test/WebJobs.Script.Tests/Workers/Rpc/WorkerConfigurationResolverOptionsSetupTests.cs @@ -0,0 +1,185 @@ +// 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 System.Linq; +using System.Text.Json; +using Microsoft.Azure.WebJobs.Script.Workers; +using Microsoft.Azure.WebJobs.Script.Workers.Rpc; +using Microsoft.Azure.WebJobs.Script.Workers.Rpc.Configuration; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.WebJobs.Script.Tests; +using Moq; +using Xunit; + +namespace Microsoft.Azure.WebJobs.Script.Tests.Workers.Rpc +{ + public class WorkerConfigurationResolverOptionsSetupTests + { + [Fact] + public void Configure_WithEnvironmentValues_SetsCorrectValues() + { + var loggerFactory = WorkerConfigurationResolverTestsHelper.GetTestLoggerFactory(); + var testEnvironment = new TestEnvironment(); + var mockScriptHostManager = new Mock(); + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + [$"{RpcWorkerConstants.LanguageWorkersSectionName}:{WorkerConstants.WorkersDirectorySectionName}"] = "/default/workers", + }).Build(); + + var setup = new WorkerConfigurationResolverOptionsSetup(loggerFactory, configuration, mockScriptHostManager.Object, FileUtility.Instance); + var options = new WorkerConfigurationResolverOptions(); + setup.Configure(options); + + Assert.Equal("/default/workers", options.WorkersRootDirPath); + } + + [Fact] + public void Configure_WithEnvironmentValues_UpdatedConfiguration_SetsCorrectValues() + { + var loggerProvider = new TestLoggerProvider(); + var loggerFactory = new LoggerFactory(); + loggerFactory.AddProvider(loggerProvider); + + var testEnvironment = new TestEnvironment(); + var mockScriptHostManager = new Mock(); + var mockServiceProvider = new Mock(); + var configuration = new ConfigurationBuilder().Build(); + + var latestConfiguration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + [$"{RpcWorkerConstants.LanguageWorkersSectionName}:{WorkerConstants.WorkersDirectorySectionName}"] = "/default/workers", + }).Build(); + + mockServiceProvider.Setup(sp => sp.GetService(typeof(IConfiguration))).Returns(latestConfiguration); + mockScriptHostManager.As() + .Setup(sp => sp.GetService(typeof(IConfiguration))) + .Returns(latestConfiguration); + + var setup = new WorkerConfigurationResolverOptionsSetup(loggerFactory, configuration, mockScriptHostManager.Object, FileUtility.Instance); + var options = new WorkerConfigurationResolverOptions(); + setup.Configure(options); + + var logs = loggerProvider.GetAllLogMessages(); + + Assert.Equal("/default/workers", options.WorkersRootDirPath); + Assert.Single(logs.Where(l => l.FormattedMessage == "Found configuration section 'languageWorkers:workersDirectory' in 'latestConfiguration'.")); + } + + [Fact] + public void Configure_WithEnvironmentValues_WithConfiguration_SetsCorrectValues() + { + var loggerProvider = new TestLoggerProvider(); + var loggerFactory = new LoggerFactory(); + loggerFactory.AddProvider(loggerProvider); + + var testEnvironment = new TestEnvironment(); + var mockScriptHostManager = new Mock(); + var mockServiceProvider = new Mock(); + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + [$"{RpcWorkerConstants.LanguageWorkersSectionName}:{WorkerConstants.WorkersDirectorySectionName}"] = "/default/workers", + }) + .Build(); + + var latestConfiguration = new ConfigurationBuilder().Build(); + + mockServiceProvider.Setup(sp => sp.GetService(typeof(IConfiguration))).Returns(latestConfiguration); + mockScriptHostManager.As() + .Setup(sp => sp.GetService(typeof(IConfiguration))) + .Returns(latestConfiguration); + + var setup = new WorkerConfigurationResolverOptionsSetup(loggerFactory, configuration, mockScriptHostManager.Object, FileUtility.Instance); + var options = new WorkerConfigurationResolverOptions(); + setup.Configure(options); + + var logs = loggerProvider.GetAllLogMessages(); + + Assert.Equal("/default/workers", options.WorkersRootDirPath); + Assert.Single(logs.Where(l => l.FormattedMessage == "Found configuration section 'languageWorkers:workersDirectory' in '_configuration'.")); + } + + [Fact] + public void Configure_WithNullConfigValues_SetsCorrectValues() + { + var testLoggerFactory = WorkerConfigurationResolverTestsHelper.GetTestLoggerFactory(); + var testEnvironment = new TestEnvironment(); + var mockScriptHostManager = new Mock(); + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + [$"{RpcWorkerConstants.LanguageWorkersSectionName}:{WorkerConstants.WorkersDirectorySectionName}"] = null, + }).Build(); + + var setup = new WorkerConfigurationResolverOptionsSetup(testLoggerFactory, configuration, mockScriptHostManager.Object, FileUtility.Instance); + var options = new WorkerConfigurationResolverOptions(); + setup.Configure(options); + + Assert.NotNull(options.WorkersRootDirPath); + Assert.Contains("workers", options.WorkersRootDirPath); + } + + [Fact] + public void Configure_WorkerConfigurationResolverOptions() + { + var testLoggerFactory = WorkerConfigurationResolverTestsHelper.GetTestLoggerFactory(); + var testEnvironment = new TestEnvironment(); + var mockScriptHostManager = new Mock(); + var configuration = new ConfigurationBuilder().Build(); + + var setup = new WorkerConfigurationResolverOptionsSetup(testLoggerFactory, configuration, mockScriptHostManager.Object, FileUtility.Instance); + var options = new WorkerConfigurationResolverOptions(); + setup.Configure(options); + + Assert.NotNull(options.WorkersRootDirPath); + Assert.Contains("workers", options.WorkersRootDirPath); + } + + [Fact] + public void Format_SerializesOptionsToJson() + { + var options = new WorkerConfigurationResolverOptions + { + WorkersRootDirPath = "/test/workers" + }; + + string json = options.Format(); + + Assert.NotNull(json); + Assert.NotEmpty(json); + + var jsonDocument = JsonDocument.Parse(json); + Assert.NotNull(jsonDocument); + + var root = jsonDocument.RootElement; + Assert.True(root.TryGetProperty("WorkersRootDirPath", out var workersDirPathProperty)); + Assert.Equal("/test/workers", workersDirPathProperty.GetString()); + } + + [Fact] + public void Format_WithNullProperties_SerializesSuccessfully() + { + var options = new WorkerConfigurationResolverOptions + { + WorkersRootDirPath = null + }; + + string json = options.Format(); + + Assert.NotNull(json); + Assert.NotEmpty(json); + + var jsonDocument = JsonDocument.Parse(json); + Assert.NotNull(jsonDocument); + + var root = jsonDocument.RootElement; + Assert.True(root.TryGetProperty("WorkersRootDirPath", out var workersDirPathProperty)); + Assert.Equal(null, workersDirPathProperty.GetString()); + } + } +} diff --git a/test/WebJobs.Script.Tests/Workers/Rpc/WorkerConfigurationResolverTestsHelper.cs b/test/WebJobs.Script.Tests/Workers/Rpc/WorkerConfigurationResolverTestsHelper.cs new file mode 100644 index 0000000000..90022356ac --- /dev/null +++ b/test/WebJobs.Script.Tests/Workers/Rpc/WorkerConfigurationResolverTestsHelper.cs @@ -0,0 +1,48 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using Microsoft.Azure.WebJobs.Script.Config; +using Microsoft.Azure.WebJobs.Script.Workers.Rpc.Configuration; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.WebJobs.Script.Tests; + +namespace Microsoft.Azure.WebJobs.Script.Tests +{ + internal static class WorkerConfigurationResolverTestsHelper + { + internal static IOptionsMonitor GetTestWorkerConfigurationResolverOptions(IConfiguration configuration, + IEnvironment environment, + IScriptHostManager scriptHostManager, + IOptions functionsHostingConfigOptions = null) + { + if (functionsHostingConfigOptions is null) + { + var hostingOptions = new FunctionsHostingConfigOptions(); + functionsHostingConfigOptions = new OptionsWrapper(hostingOptions); + } + + var testLoggerFactory = GetTestLoggerFactory(); + var resolverOptionsSetup = new WorkerConfigurationResolverOptionsSetup(testLoggerFactory, configuration, scriptHostManager, FileUtility.Instance); + var resolverOptions = new WorkerConfigurationResolverOptions(); + resolverOptionsSetup.Configure(resolverOptions); + + var factory = new TestOptionsFactory(resolverOptions); + var source = new TestChangeTokenSource(); + var changeTokens = new[] { source }; + var optionsMonitor = new OptionsMonitor(factory, changeTokens, factory); + + return optionsMonitor; + } + + internal static LoggerFactory GetTestLoggerFactory() + { + var loggerProvider = new TestLoggerProvider(); + var loggerFactory = new LoggerFactory(); + loggerFactory.AddProvider(loggerProvider); + + return loggerFactory; + } + } +}