From 85b2144d318ea423fb58c2a07f001dd81750d267 Mon Sep 17 00:00:00 2001 From: Jameson Z <89222642+VpOfEngineering@users.noreply.github.com> Date: Wed, 10 May 2023 09:53:28 -0500 Subject: [PATCH] 4.21.1 hotfix (#9268) * Switching the worker indexing to use hosting config feature flag (#9245) * Initial commit. * fixing issue with worker indexing, dotnet-isolated, and placeholders (#9253) * fixing issue with worker indexing, dotnet-isolated, and placeholders (#9253) * Update Python Worker Version to 4.14.0 (#9261) * Update Python Worker Version to 4.14.0 * Update release_notes.md --------- Co-authored-by: AzureFunctionsPython * Setting up workerConfig with ConfigureOptionsWithChangeTokenSource (#9264) * Sending metadata request once per scriptHost instance (#9248) Co-authored-by: Brett Samblanet * Restart workers when we need to refresh the worker metadata (#9244) * Incrementing patch version to 4.21.1 * Removing duplicate mention of #9253 --------- Co-authored-by: Naren Soni Co-authored-by: Yogesh Co-authored-by: Brett Samblanet Co-authored-by: gavin-aguiar <80794152+gavin-aguiar@users.noreply.github.com> Co-authored-by: AzureFunctionsPython Co-authored-by: azfuncgh --- NuGet.config | 1 + build/common.props | 2 +- build/python.props | 2 +- release_notes.md | 3 + .../Channel/GrpcWorkerChannel.cs | 41 ++- .../Models/AzureStorageInfoValue.cs | 7 +- .../WebHostServiceCollectionExtensions.cs | 12 +- .../Config/FunctionsHostingConfigOptions.cs | 22 ++ .../ScriptStartupTypeLocator.cs | 2 +- .../Host/FunctionMetadataManager.cs | 25 +- .../Host/FunctionMetadataProvider.cs | 11 +- .../Host/IFunctionMetadataManager.cs | 4 +- src/WebJobs.Script/Host/ScriptHost.cs | 2 +- .../Host/WorkerFunctionMetadataProvider.cs | 20 +- src/WebJobs.Script/Utility.cs | 27 +- .../RpcFunctionInvocationDispatcher.cs | 2 +- .../Rpc/IWebHostRpcWorkerChannelManager.cs | 2 +- .../Workers/Rpc/RpcInitializationService.cs | 7 +- .../Workers/Rpc/RpcWorkerConstants.cs | 3 + .../Workers/Rpc/RpcWorkerProcess.cs | 6 +- .../Workers/Rpc/RpcWorkerProcessFactory.cs | 7 +- .../Rpc/WebHostRpcWorkerChannelManager.cs | 12 +- test/DotNetIsolated60/DotNetIsolated60.csproj | 27 ++ test/DotNetIsolated60/DotNetIsolated60.sln | 25 ++ test/DotNetIsolated60/Function1.cs | 30 ++ test/DotNetIsolated60/Program.cs | 11 + .../Properties/serviceDependencies.json | 11 + .../Properties/serviceDependencies.local.json | 11 + test/DotNetIsolated60/host.json | 11 + .../StandbyManagerE2ETests_Linux.cs | 6 +- .../Management/AzureStorageInfoValueTests.cs | 22 +- .../Management/FunctionsSyncManagerTests.cs | 5 +- .../TestFunctionHost.cs | 5 +- .../HttpTriggerNoAuth/function.json | 16 + .../HttpTriggerNoAuth/index.js | 25 ++ .../TestScripts/NodeWithBundles/host.json | 7 + .../WebHostEndToEnd/ProxyEndToEndTests.cs | 4 +- .../WebHostEndToEnd/SpecializationE2ETests.cs | 315 +++++++++++++++++- .../TestFunctionMetadataManager.cs | 3 +- .../TestHostBuilderExtensions.cs | 4 +- .../Description/FunctionInvokerBaseTests.cs | 3 +- .../FunctionMetadataProviderTests.cs | 15 +- .../Managment/WebFunctionsManagerTests.cs | 3 +- .../Middleware/HostWarmupMiddlewareTests.cs | 2 +- .../ScriptStartupTypeDiscovererTests.cs | 38 +-- test/WebJobs.Script.Tests/UtilityTests.cs | 87 ++++- .../WebJobs.Script.Tests.csproj | 2 +- .../Workers/Rpc/GrpcWorkerChannelTests.cs | 48 +++ .../RpcFunctionInvocationDispatcherTests.cs | 4 +- .../Rpc/RpcInitializationServiceTests.cs | 43 +-- .../Workers/Rpc/RpcWorkerProcessTests.cs | 3 +- .../Rpc/TestRpcWorkerChannelManager.cs | 3 +- .../WebHostRpcWorkerChannelManagerTests.cs | 50 ++- 53 files changed, 890 insertions(+), 169 deletions(-) create mode 100644 test/DotNetIsolated60/DotNetIsolated60.csproj create mode 100644 test/DotNetIsolated60/DotNetIsolated60.sln create mode 100644 test/DotNetIsolated60/Function1.cs create mode 100644 test/DotNetIsolated60/Program.cs create mode 100644 test/DotNetIsolated60/Properties/serviceDependencies.json create mode 100644 test/DotNetIsolated60/Properties/serviceDependencies.local.json create mode 100644 test/DotNetIsolated60/host.json create mode 100644 test/WebJobs.Script.Tests.Integration/TestScripts/NodeWithBundles/HttpTriggerNoAuth/function.json create mode 100644 test/WebJobs.Script.Tests.Integration/TestScripts/NodeWithBundles/HttpTriggerNoAuth/index.js create mode 100644 test/WebJobs.Script.Tests.Integration/TestScripts/NodeWithBundles/host.json diff --git a/NuGet.config b/NuGet.config index 822ccc026a..67fe8f1f51 100644 --- a/NuGet.config +++ b/NuGet.config @@ -9,6 +9,7 @@ + diff --git a/build/common.props b/build/common.props index 3da66fe546..a30aad6c7f 100644 --- a/build/common.props +++ b/build/common.props @@ -5,7 +5,7 @@ latest 4 21 - 0 + 1 0 diff --git a/build/python.props b/build/python.props index 5a94d306e3..127c413ee9 100644 --- a/build/python.props +++ b/build/python.props @@ -1,5 +1,5 @@ - + diff --git a/release_notes.md b/release_notes.md index 17f1230f82..1ca4ef22b6 100644 --- a/release_notes.md +++ b/release_notes.md @@ -3,7 +3,10 @@ +- Update Python Worker Version to [4.14.0](https://github.com/Azure/azure-functions-python-worker/releases/tag/4.14.0) - Update Java Worker Version to [2.11.0](https://github.com/Azure/azure-functions-java-worker/releases/tag/2.11.0) **Release sprint:** Sprint 142 [ [bugs](https://github.com/Azure/azure-functions-host/issues?q=is%3Aissue+milestone%3A%22Functions+Sprint+143%22+label%3Abug+is%3Aclosed) | [features](https://github.com/Azure/azure-functions-host/issues?q=is%3Aissue+milestone%3A%22Functions+Sprint+143%22+label%3Afeature+is%3Aclosed) ] +- Fixing bug with placeholder misses in dotnet-isolated #9253 +- (Update Python Worker Version to 4.14.0 (#9261)) diff --git a/src/WebJobs.Script.Grpc/Channel/GrpcWorkerChannel.cs b/src/WebJobs.Script.Grpc/Channel/GrpcWorkerChannel.cs index 753224972c..3c956decf8 100644 --- a/src/WebJobs.Script.Grpc/Channel/GrpcWorkerChannel.cs +++ b/src/WebJobs.Script.Grpc/Channel/GrpcWorkerChannel.cs @@ -52,6 +52,7 @@ internal class GrpcWorkerChannel : IRpcWorkerChannel, IDisposable private readonly IOptions _workerConcurrencyOptions; private readonly WaitCallback _processInbound; private readonly object _syncLock = new object(); + private readonly object _metadataLock = new object(); private readonly Dictionary> _pendingActions = new(); private readonly ChannelWriter _outbound; private readonly ChannelReader _inbound; @@ -85,6 +86,7 @@ internal class GrpcWorkerChannel : IRpcWorkerChannel, IDisposable private IHttpProxyService _httpProxyService; private Uri _httpProxyEndpoint; private System.Timers.Timer _timer; + private bool _functionMetadataRequestSent = false; internal GrpcWorkerChannel( string workerId, @@ -540,6 +542,8 @@ internal FunctionLoadRequestCollection GetFunctionLoadRequestCollection(IEnumera public Task SendFunctionEnvironmentReloadRequest() { _functionsIndexingTask = new TaskCompletionSource>(TaskCreationOptions.RunContinuationsAsynchronously); + _functionMetadataRequestSent = false; + _workerChannelLogger.LogDebug("Sending FunctionEnvironmentReloadRequest to WorkerProcess with Pid: '{0}'", _rpcWorkerProcess.Id); IDisposable latencyEvent = _metricsLogger.LatencyEvent(MetricEventNames.SpecializationEnvironmentReloadRequestResponse); @@ -795,22 +799,32 @@ public Task> GetFunctionMetadata() internal Task> SendFunctionMetadataRequest() { - // reset indexing task when in case we need to send another request - _functionsIndexingTask = new TaskCompletionSource>(TaskCreationOptions.RunContinuationsAsynchronously); + _workerChannelLogger.LogDebug("Fetching worker metadata, FunctionMetadataReceived set to: {functionMetadataReceived}", _functionMetadataRequestSent); + if (!_functionMetadataRequestSent) + { + lock (_metadataLock) + { + if (!_functionMetadataRequestSent) + { + RegisterCallbackForNextGrpcMessage(MsgType.FunctionMetadataResponse, _functionLoadTimeout, 1, + msg => ProcessFunctionMetadataResponses(msg.Message.FunctionMetadataResponse), HandleWorkerMetadataRequestError); - RegisterCallbackForNextGrpcMessage(MsgType.FunctionMetadataResponse, _functionLoadTimeout, 1, - msg => ProcessFunctionMetadataResponses(msg.Message.FunctionMetadataResponse), HandleWorkerMetadataRequestError); + _workerChannelLogger.LogDebug("Sending WorkerMetadataRequest to {language} worker with worker ID {workerID}", _runtime, _workerId); - _workerChannelLogger.LogDebug("Sending WorkerMetadataRequest to {language} worker with worker ID {workerID}", _runtime, _workerId); + // sends the function app directory path to worker for indexing + SendStreamingMessage(new StreamingMessage + { + FunctionsMetadataRequest = new FunctionsMetadataRequest() + { + FunctionAppDirectory = _applicationHostOptions.CurrentValue.ScriptPath + } + }); - // sends the function app directory path to worker for indexing - SendStreamingMessage(new StreamingMessage - { - FunctionsMetadataRequest = new FunctionsMetadataRequest() - { - FunctionAppDirectory = _applicationHostOptions.CurrentValue.ScriptPath + _functionMetadataRequestSent = true; + } } - }); + } + return _functionsIndexingTask.Task; } @@ -845,7 +859,8 @@ internal void ProcessFunctionMetadataResponses(FunctionMetadataResponse function FunctionDirectory = metadata.Directory, ScriptFile = metadata.ScriptFile, EntryPoint = metadata.EntryPoint, - Name = metadata.Name + Name = metadata.Name, + Language = metadata.Language }; functionMetadata.SetFunctionId(metadata.FunctionId); diff --git a/src/WebJobs.Script.WebHost/Models/AzureStorageInfoValue.cs b/src/WebJobs.Script.WebHost/Models/AzureStorageInfoValue.cs index c56499095e..35b546a68d 100644 --- a/src/WebJobs.Script.WebHost/Models/AzureStorageInfoValue.cs +++ b/src/WebJobs.Script.WebHost/Models/AzureStorageInfoValue.cs @@ -83,7 +83,12 @@ public static AzureStorageInfoValue FromEnvironmentVariable(KeyValuePair(sp => - { - return new FunctionMetadataProvider( - sp.GetRequiredService>(), - ActivatorUtilities.CreateInstance(sp), - ActivatorUtilities.CreateInstance(sp)); - }); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); // Core script host services services.AddSingleton(); @@ -208,7 +204,7 @@ public static void AddWebJobsScriptHost(this IServiceCollection services, IConfi services.AddSingleton, ScriptApplicationHostOptionsChangeTokenSource>(); services.ConfigureOptions(); - services.ConfigureOptions(); + services.ConfigureOptionsWithChangeTokenSource>(); services.ConfigureOptionsWithChangeTokenSource>(); services.ConfigureOptionsWithChangeTokenSource>(); services.ConfigureOptions(); diff --git a/src/WebJobs.Script/Config/FunctionsHostingConfigOptions.cs b/src/WebJobs.Script/Config/FunctionsHostingConfigOptions.cs index 982163d2fb..7ffd6431d5 100644 --- a/src/WebJobs.Script/Config/FunctionsHostingConfigOptions.cs +++ b/src/WebJobs.Script/Config/FunctionsHostingConfigOptions.cs @@ -43,6 +43,28 @@ public bool WorkerWarmupEnabled } } + /// + /// Gets a value indicating whether worker indexing feature is enabled in the hosting config. + /// + public bool WorkerIndexingEnabled + { + get + { + return GetFeature(RpcWorkerConstants.WorkerIndexingEnabled) == "1"; + } + } + + /// + /// Gets a string delimited by '|' that contains the name of the apps with worker indexing disabled. + /// + public string WorkerIndexingDisabledApps + { + get + { + return GetFeature(RpcWorkerConstants.WorkerIndexingDisabledApps) ?? string.Empty; + } + } + /// /// Gets a value indicating whether Linux Log Backoff is disabled in the hosting config. /// diff --git a/src/WebJobs.Script/DependencyInjection/ScriptStartupTypeLocator.cs b/src/WebJobs.Script/DependencyInjection/ScriptStartupTypeLocator.cs index 597ea6e8a0..0732a6cd13 100644 --- a/src/WebJobs.Script/DependencyInjection/ScriptStartupTypeLocator.cs +++ b/src/WebJobs.Script/DependencyInjection/ScriptStartupTypeLocator.cs @@ -89,7 +89,7 @@ public async Task> GetExtensionsStartupTypesAsync() ExtensionBundleDetails bundleDetails = await _extensionBundleManager.GetExtensionBundleDetails(); ValidateBundleRequirements(bundleDetails); - var functionMetadataCollection = _functionMetadataManager.GetFunctionMetadata(forceRefresh: true, includeCustomProviders: false); + var functionMetadataCollection = _functionMetadataManager.GetFunctionMetadata(forceRefresh: true, includeCustomProviders: false, workerConfigs: workerConfigs); bindingsSet = new HashSet(StringComparer.OrdinalIgnoreCase); // Generate a Hashset of all the binding types used in the function app diff --git a/src/WebJobs.Script/Host/FunctionMetadataManager.cs b/src/WebJobs.Script/Host/FunctionMetadataManager.cs index 77c04283fe..da7f387d07 100644 --- a/src/WebJobs.Script/Host/FunctionMetadataManager.cs +++ b/src/WebJobs.Script/Host/FunctionMetadataManager.cs @@ -30,17 +30,15 @@ public class FunctionMetadataManager : IFunctionMetadataManager private bool _servicesReset = false; private ILogger _logger; private IOptions _scriptOptions; - private IOptionsMonitor _languageWorkerOptions; private ImmutableArray _functionMetadataArray; private Dictionary> _functionErrors = new Dictionary>(); private ConcurrentDictionary _functionMetadataMap = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); public FunctionMetadataManager(IOptions scriptOptions, IFunctionMetadataProvider functionMetadataProvider, IOptions httpWorkerOptions, IScriptHostManager scriptHostManager, ILoggerFactory loggerFactory, - IOptionsMonitor languageWorkerOptions, IEnvironment environment) + IEnvironment environment) { _scriptOptions = scriptOptions; - _languageWorkerOptions = languageWorkerOptions; _serviceProvider = scriptHostManager as IServiceProvider; _functionMetadataProvider = functionMetadataProvider; _loggerFactory = loggerFactory; @@ -84,11 +82,11 @@ public bool TryGetFunctionMetadata(string functionName, out FunctionMetadata fun /// Apply functions allow list filter. /// Include any metadata provided by IFunctionProvider when loading the metadata /// An Immmutable array of FunctionMetadata. - public ImmutableArray GetFunctionMetadata(bool forceRefresh, bool applyAllowList = true, bool includeCustomProviders = true) + public ImmutableArray GetFunctionMetadata(bool forceRefresh, bool applyAllowList = true, bool includeCustomProviders = true, IList workerConfigs = null) { if (forceRefresh || _servicesReset || _functionMetadataArray.IsDefaultOrEmpty) { - _functionMetadataArray = LoadFunctionMetadata(forceRefresh, includeCustomProviders); + _functionMetadataArray = LoadFunctionMetadata(forceRefresh, includeCustomProviders, workerConfigs: workerConfigs); _logger.FunctionMetadataManagerFunctionsLoaded(ApplyAllowList(_functionMetadataArray).Count()); _servicesReset = false; } @@ -114,7 +112,6 @@ private void InitializeServices() _isHttpWorker = _serviceProvider.GetService>()?.Value?.Description != null; _scriptOptions = _serviceProvider.GetService>(); - _languageWorkerOptions = _serviceProvider.GetService>(); // Resetting the logger switches the logger scope to Script Host level, // also making the logs available to Application Insights @@ -122,20 +119,30 @@ private void InitializeServices() _servicesReset = true; } + /// + /// This is the worker configuration created in the jobhost scope during placeholder initialization + /// This is used as a fallback incase the config is not passed down from previous method call. + /// + private IList GetFallbackWorkerConfig() + { + return _serviceProvider.GetService>().CurrentValue.WorkerConfigs; + } + /// /// Read all functions and populate function metadata. /// - internal ImmutableArray LoadFunctionMetadata(bool forceRefresh = false, bool includeCustomProviders = true, IFunctionInvocationDispatcher dispatcher = null) + internal ImmutableArray LoadFunctionMetadata(bool forceRefresh = false, bool includeCustomProviders = true, IFunctionInvocationDispatcher dispatcher = null, IList workerConfigs = null) { + workerConfigs ??= GetFallbackWorkerConfig(); + _functionMetadataMap.Clear(); ICollection functionsAllowList = _scriptOptions?.Value?.Functions; _logger.FunctionMetadataManagerLoadingFunctionsMetadata(); ImmutableArray immutableFunctionMetadata; - var workerConfigs = _languageWorkerOptions.CurrentValue.WorkerConfigs; - immutableFunctionMetadata = _functionMetadataProvider.GetFunctionMetadataAsync(workerConfigs, SystemEnvironment.Instance, forceRefresh).GetAwaiter().GetResult(); + immutableFunctionMetadata = _functionMetadataProvider.GetFunctionMetadataAsync(workerConfigs, _environment, forceRefresh).GetAwaiter().GetResult(); var functionMetadataList = new List(); _functionErrors = new Dictionary>(); diff --git a/src/WebJobs.Script/Host/FunctionMetadataProvider.cs b/src/WebJobs.Script/Host/FunctionMetadataProvider.cs index 948e1b1010..d74a4e2ef7 100644 --- a/src/WebJobs.Script/Host/FunctionMetadataProvider.cs +++ b/src/WebJobs.Script/Host/FunctionMetadataProvider.cs @@ -1,10 +1,10 @@ // 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.Collections.Immutable; using System.Threading.Tasks; +using Microsoft.Azure.WebJobs.Script.Config; using Microsoft.Azure.WebJobs.Script.Description; using Microsoft.Azure.WebJobs.Script.Workers.Rpc; using Microsoft.Extensions.Logging; @@ -16,22 +16,25 @@ internal class FunctionMetadataProvider : IFunctionMetadataProvider { private readonly IEnvironment _environment; private readonly ILogger _logger; + private readonly FunctionsHostingConfigOptions _functionsHostingConfigOptions; private IWorkerFunctionMetadataProvider _workerFunctionMetadataProvider; private IHostFunctionMetadataProvider _hostFunctionMetadataProvider; - public FunctionMetadataProvider(ILogger logger, IWorkerFunctionMetadataProvider workerFunctionMetadataProvider, IHostFunctionMetadataProvider hostFunctionMetadataProvider) + public FunctionMetadataProvider(ILogger logger, IWorkerFunctionMetadataProvider workerFunctionMetadataProvider, IHostFunctionMetadataProvider hostFunctionMetadataProvider, + IOptions functionsHostingConfigOptions, IEnvironment environment) { _logger = logger; _workerFunctionMetadataProvider = workerFunctionMetadataProvider; _hostFunctionMetadataProvider = hostFunctionMetadataProvider; - _environment = SystemEnvironment.Instance; + _functionsHostingConfigOptions = functionsHostingConfigOptions.Value; + _environment = environment; } public ImmutableDictionary> FunctionErrors { get; private set; } public async Task> GetFunctionMetadataAsync(IEnumerable workerConfigs, IEnvironment environment, bool forceRefresh = false) { - bool workerIndexing = Utility.CanWorkerIndex(workerConfigs, _environment); + bool workerIndexing = Utility.CanWorkerIndex(workerConfigs, _environment, _functionsHostingConfigOptions); if (!workerIndexing) { return await GetMetadataFromHostProvider(workerConfigs, environment, forceRefresh); diff --git a/src/WebJobs.Script/Host/IFunctionMetadataManager.cs b/src/WebJobs.Script/Host/IFunctionMetadataManager.cs index 442bfb7ad6..bf42513863 100644 --- a/src/WebJobs.Script/Host/IFunctionMetadataManager.cs +++ b/src/WebJobs.Script/Host/IFunctionMetadataManager.cs @@ -1,9 +1,11 @@ // 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 System.Collections.Immutable; using Microsoft.Azure.WebJobs.Script.Description; using Microsoft.Azure.WebJobs.Script.Workers; +using Microsoft.Azure.WebJobs.Script.Workers.Rpc; namespace Microsoft.Azure.WebJobs.Script { @@ -11,7 +13,7 @@ public interface IFunctionMetadataManager { ImmutableDictionary> Errors { get; } - ImmutableArray GetFunctionMetadata(bool forceRefresh = false, bool applyAllowlist = true, bool includeCustomProviders = true); + ImmutableArray GetFunctionMetadata(bool forceRefresh = false, bool applyAllowlist = true, bool includeCustomProviders = true, IList workerConfigs = null); bool TryGetFunctionMetadata(string functionName, out FunctionMetadata functionMetadata, bool forceRefresh = false); } diff --git a/src/WebJobs.Script/Host/ScriptHost.cs b/src/WebJobs.Script/Host/ScriptHost.cs index 9841d402e2..7214b73d82 100644 --- a/src/WebJobs.Script/Host/ScriptHost.cs +++ b/src/WebJobs.Script/Host/ScriptHost.cs @@ -367,7 +367,7 @@ private IEnumerable GetFunctionsMetadata() { IEnumerable functionMetadata; - functionMetadata = _functionMetadataManager.GetFunctionMetadata(forceRefresh: false); + functionMetadata = _functionMetadataManager.GetFunctionMetadata(forceRefresh: false, workerConfigs: _languageWorkerOptions.CurrentValue.WorkerConfigs); _workerRuntime ??= Utility.GetWorkerRuntime(functionMetadata); foreach (var error in _functionMetadataManager.Errors) { diff --git a/src/WebJobs.Script/Host/WorkerFunctionMetadataProvider.cs b/src/WebJobs.Script/Host/WorkerFunctionMetadataProvider.cs index 41642cf11f..91967ddaad 100644 --- a/src/WebJobs.Script/Host/WorkerFunctionMetadataProvider.cs +++ b/src/WebJobs.Script/Host/WorkerFunctionMetadataProvider.cs @@ -20,7 +20,7 @@ namespace Microsoft.Azure.WebJobs.Script internal class WorkerFunctionMetadataProvider : IWorkerFunctionMetadataProvider { private readonly Dictionary> _functionErrors = new Dictionary>(); - private readonly IOptions _scriptOptions; + private readonly IOptionsMonitor _scriptOptions; private readonly ILogger _logger; private readonly IEnvironment _environment; private readonly IWebHostRpcWorkerChannelManager _channelManager; @@ -28,7 +28,7 @@ internal class WorkerFunctionMetadataProvider : IWorkerFunctionMetadataProvider private ImmutableArray _functions; public WorkerFunctionMetadataProvider( - IOptions scriptOptions, + IOptionsMonitor scriptOptions, ILogger logger, IEnvironment environment, IWebHostRpcWorkerChannelManager webHostRpcWorkerChannelManager) @@ -61,11 +61,21 @@ public async Task GetFunctionMetadataAsync(IEnumerable GetFunctionMetadataAsync(IEnumerable ValidateFunctionAppFormat(_scriptOptions.Value.RootScriptPath, _logger, _environment)); + _ = Task.Delay(TimeSpan.FromMinutes(1)).ContinueWith(t => ValidateFunctionAppFormat(_scriptOptions.CurrentValue.ScriptPath, _logger, _environment)); break; } @@ -162,7 +172,7 @@ internal IEnumerable ValidateMetadata(IEnumerable workerConfigs, IEnvironment environment) + // EnableWorkerIndexing set through AzureWebjobsFeatuerFlag always take precdence + // if AzureWebjobsFeatuerFlag is not set then WORKER_INDEXING_ENABLED hosting config controls stamplevel enablement + // if WORKER_INDEXING_ENABLED is set and WORKER_INDEXING_DISABLED contains the customers app name worker indexing is then disabled for that customer only + // Also Worker indexing is disabled for Logic apps + public static bool CanWorkerIndex(IEnumerable workerConfigs, IEnvironment environment, FunctionsHostingConfigOptions functionsHostingConfigOptions) { - if (!FeatureFlags.IsEnabled(ScriptConstants.FeatureFlagEnableWorkerIndexing, environment)) + string appName = environment.GetAzureWebsiteUniqueSlotName(); + bool workerIndexingEnabled = FeatureFlags.IsEnabled(ScriptConstants.FeatureFlagEnableWorkerIndexing, environment) + || (functionsHostingConfigOptions.WorkerIndexingEnabled + && !functionsHostingConfigOptions.WorkerIndexingDisabledApps.ToLowerInvariant().Split("|").Contains(appName) + && !environment.IsLogicApp()); + + if (!workerIndexingEnabled) { return false; } - if (workerConfigs != null && !environment.IsMultiLanguageRuntimeEnvironment()) + bool workerIndexingAvailable = false; + if (workerConfigs != null) { var workerRuntime = environment.GetEnvironmentVariable(EnvironmentSettingNames.FunctionWorkerRuntime); var workerConfig = workerConfigs.FirstOrDefault(c => c.Description?.Language != null && c.Description.Language.Equals(workerRuntime, StringComparison.InvariantCultureIgnoreCase)); // if feature flag is enabled and workerConfig.WorkerIndexing == true, then return true - if (workerConfig != null + workerIndexingAvailable = workerConfig != null && workerConfig.Description != null && workerConfig.Description.WorkerIndexing != null - && workerConfig.Description.WorkerIndexing.Equals("true", StringComparison.OrdinalIgnoreCase)) - { - return true; - } + && workerConfig.Description.WorkerIndexing.Equals("true", StringComparison.OrdinalIgnoreCase); } - return false; + + return workerIndexingEnabled && workerIndexingAvailable; } public static void LogAutorestGeneratedJsonIfExists(string rootScriptPath, ILogger logger) diff --git a/src/WebJobs.Script/Workers/Rpc/FunctionRegistration/RpcFunctionInvocationDispatcher.cs b/src/WebJobs.Script/Workers/Rpc/FunctionRegistration/RpcFunctionInvocationDispatcher.cs index 4040cf4f77..a8ddda17a7 100644 --- a/src/WebJobs.Script/Workers/Rpc/FunctionRegistration/RpcFunctionInvocationDispatcher.cs +++ b/src/WebJobs.Script/Workers/Rpc/FunctionRegistration/RpcFunctionInvocationDispatcher.cs @@ -172,7 +172,7 @@ internal async Task InitializeWebhostLanguageWorkerChannel(IEnumerable l foreach (string language in languages) { _logger.LogDebug("Creating new webhost language worker channel for runtime:{workerRuntime}.", language); - IRpcWorkerChannel workerChannel = await _webHostLanguageWorkerChannelManager.InitializeChannelAsync(language); + IRpcWorkerChannel workerChannel = await _webHostLanguageWorkerChannelManager.InitializeChannelAsync(_workerConfigs, language); // if the worker is indexing, we will not have function metadata yet. So, we cannot set up invocation buffers or send load requests workerChannel.SetupFunctionInvocationBuffers(_functions); diff --git a/src/WebJobs.Script/Workers/Rpc/IWebHostRpcWorkerChannelManager.cs b/src/WebJobs.Script/Workers/Rpc/IWebHostRpcWorkerChannelManager.cs index 39fd85ad38..e8969ef218 100644 --- a/src/WebJobs.Script/Workers/Rpc/IWebHostRpcWorkerChannelManager.cs +++ b/src/WebJobs.Script/Workers/Rpc/IWebHostRpcWorkerChannelManager.cs @@ -9,7 +9,7 @@ namespace Microsoft.Azure.WebJobs.Script.Workers.Rpc { public interface IWebHostRpcWorkerChannelManager { - Task InitializeChannelAsync(string language); + Task InitializeChannelAsync(IEnumerable workerConfigs, string language); Dictionary> GetChannels(string language); diff --git a/src/WebJobs.Script/Workers/Rpc/RpcInitializationService.cs b/src/WebJobs.Script/Workers/Rpc/RpcInitializationService.cs index 8b77d7a778..33191ee40a 100644 --- a/src/WebJobs.Script/Workers/Rpc/RpcInitializationService.cs +++ b/src/WebJobs.Script/Workers/Rpc/RpcInitializationService.cs @@ -18,12 +18,14 @@ public class RpcInitializationService : IManagedHostedService private readonly IWebHostRpcWorkerChannelManager _webHostRpcWorkerChannelManager; private readonly IRpcServer _rpcServer; private readonly ILogger _logger; + private readonly IOptionsMonitor _languageWorkerOptions; private readonly string _workerRuntime; private readonly int _rpcServerShutdownTimeoutInMilliseconds; private HashSet _placeholderLanguageWorkersList = new HashSet(); - public RpcInitializationService(IOptionsMonitor applicationHostOptions, IEnvironment environment, IRpcServer rpcServer, IWebHostRpcWorkerChannelManager rpcWorkerChannelManager, ILogger logger) + public RpcInitializationService(IOptionsMonitor applicationHostOptions, IEnvironment environment, IRpcServer rpcServer, + IWebHostRpcWorkerChannelManager rpcWorkerChannelManager, ILogger logger, IOptionsMonitor languageWorkerOptions) { _applicationHostOptions = applicationHostOptions ?? throw new ArgumentNullException(nameof(applicationHostOptions)); _logger = logger; @@ -33,6 +35,7 @@ public RpcInitializationService(IOptionsMonitor ap _webHostRpcWorkerChannelManager = rpcWorkerChannelManager ?? throw new ArgumentNullException(nameof(rpcWorkerChannelManager)); _workerRuntime = _environment.GetEnvironmentVariable(RpcWorkerConstants.FunctionWorkerRuntimeSettingName); _placeholderLanguageWorkersList = _environment.GetLanguageWorkerListToStartInPlaceholder(); + _languageWorkerOptions = languageWorkerOptions; } public async Task StartAsync(CancellationToken cancellationToken) @@ -116,7 +119,7 @@ internal Task InitializeChannelsAsync() if (_placeholderLanguageWorkersList.Count() != 0) { return Task.WhenAll(_placeholderLanguageWorkersList.Select(runtime => - _webHostRpcWorkerChannelManager.InitializeChannelAsync(runtime))); + _webHostRpcWorkerChannelManager.InitializeChannelAsync(_languageWorkerOptions.CurrentValue.WorkerConfigs, runtime))); } } return Task.CompletedTask; diff --git a/src/WebJobs.Script/Workers/Rpc/RpcWorkerConstants.cs b/src/WebJobs.Script/Workers/Rpc/RpcWorkerConstants.cs index ec7894afdc..468e418bfd 100644 --- a/src/WebJobs.Script/Workers/Rpc/RpcWorkerConstants.cs +++ b/src/WebJobs.Script/Workers/Rpc/RpcWorkerConstants.cs @@ -81,5 +81,8 @@ public static class RpcWorkerConstants public const string DotNetCoreDebugEngine = ".NETCore"; public const string DotNetFrameworkDebugEngine = ".NETFramework"; public const string DotNetFramework = "Framework"; + + public const string WorkerIndexingEnabled = "WORKER_INDEXING_ENABLED"; + public const string WorkerIndexingDisabledApps = "WORKER_INDEXING_DISABLED_APPS"; } } \ No newline at end of file diff --git a/src/WebJobs.Script/Workers/Rpc/RpcWorkerProcess.cs b/src/WebJobs.Script/Workers/Rpc/RpcWorkerProcess.cs index 498d86f18f..d5e1cbcc95 100644 --- a/src/WebJobs.Script/Workers/Rpc/RpcWorkerProcess.cs +++ b/src/WebJobs.Script/Workers/Rpc/RpcWorkerProcess.cs @@ -23,6 +23,7 @@ internal class RpcWorkerProcess : WorkerProcess private readonly WorkerProcessArguments _workerProcessArguments; private readonly string _workerDirectory; private readonly IOptions _hostingConfigOptions; + private readonly IEnvironment _environment; internal RpcWorkerProcess(string runtime, string workerId, @@ -36,7 +37,8 @@ internal RpcWorkerProcess(string runtime, IWorkerConsoleLogSource consoleLogSource, IMetricsLogger metricsLogger, IServiceProvider serviceProvider, - IOptions hostingConfigOptions) + IOptions hostingConfigOptions, + IEnvironment environment) : base(eventManager, processRegistry, workerProcessLogger, consoleLogSource, metricsLogger, serviceProvider, rpcWorkerConfig.Description.UseStdErrorStreamForErrorsOnly) { _runtime = runtime; @@ -49,6 +51,7 @@ internal RpcWorkerProcess(string runtime, _workerProcessArguments = rpcWorkerConfig.Arguments; _workerDirectory = rpcWorkerConfig.Description.WorkerDirectory; _hostingConfigOptions = hostingConfigOptions; + _environment = environment; } internal override Process CreateWorkerProcess() @@ -56,6 +59,7 @@ internal override Process CreateWorkerProcess() var workerContext = new RpcWorkerContext(Guid.NewGuid().ToString(), RpcWorkerConstants.DefaultMaxMessageLengthBytes, _workerId, _workerProcessArguments, _scriptRootPath, _serverUri); workerContext.EnvironmentVariables.Add(WorkerConstants.FunctionsWorkerDirectorySettingName, _workerDirectory); workerContext.EnvironmentVariables.Add(WorkerConstants.FunctionsApplicationDirectorySettingName, _scriptRootPath); + workerContext.EnvironmentVariables.Add(RpcWorkerConstants.FunctionWorkerRuntimeVersionSettingName, _environment.GetEnvironmentVariable(RpcWorkerConstants.FunctionWorkerRuntimeVersionSettingName)); foreach (var pair in _hostingConfigOptions.Value.Features) { workerContext.EnvironmentVariables[pair.Key] = pair.Value; diff --git a/src/WebJobs.Script/Workers/Rpc/RpcWorkerProcessFactory.cs b/src/WebJobs.Script/Workers/Rpc/RpcWorkerProcessFactory.cs index 75491bf44b..db19fbee59 100644 --- a/src/WebJobs.Script/Workers/Rpc/RpcWorkerProcessFactory.cs +++ b/src/WebJobs.Script/Workers/Rpc/RpcWorkerProcessFactory.cs @@ -21,6 +21,7 @@ internal class RpcWorkerProcessFactory : IRpcWorkerProcessFactory private readonly IMetricsLogger _metricsLogger; private readonly IServiceProvider _serviceProvider; private readonly IOptions _hostingConfigOptions; + private readonly IEnvironment _environment; public RpcWorkerProcessFactory(IRpcServer rpcServer, IScriptEventManager eventManager, @@ -30,7 +31,8 @@ public RpcWorkerProcessFactory(IRpcServer rpcServer, IWorkerConsoleLogSource consoleLogSource, IMetricsLogger metricsLogger, IServiceProvider serviceProvider, - IOptions hostingConfigOptions) + IOptions hostingConfigOptions, + IEnvironment environment) { _loggerFactory = loggerFactory; _eventManager = eventManager; @@ -41,12 +43,13 @@ public RpcWorkerProcessFactory(IRpcServer rpcServer, _metricsLogger = metricsLogger; _serviceProvider = serviceProvider; _hostingConfigOptions = hostingConfigOptions; + _environment = environment; } public IWorkerProcess Create(string workerId, string runtime, string scriptRootPath, RpcWorkerConfig workerConfig) { ILogger workerProcessLogger = _loggerFactory.CreateLogger($"Worker.rpcWorkerProcess.{runtime}.{workerId}"); - return new RpcWorkerProcess(runtime, workerId, scriptRootPath, _rpcServer.Uri, workerConfig, _eventManager, _workerProcessFactory, _processRegistry, workerProcessLogger, _consoleLogSource, _metricsLogger, _serviceProvider, _hostingConfigOptions); + return new RpcWorkerProcess(runtime, workerId, scriptRootPath, _rpcServer.Uri, workerConfig, _eventManager, _workerProcessFactory, _processRegistry, workerProcessLogger, _consoleLogSource, _metricsLogger, _serviceProvider, _hostingConfigOptions, _environment); } } } diff --git a/src/WebJobs.Script/Workers/Rpc/WebHostRpcWorkerChannelManager.cs b/src/WebJobs.Script/Workers/Rpc/WebHostRpcWorkerChannelManager.cs index 1b7599a8f6..80bdd08e6e 100644 --- a/src/WebJobs.Script/Workers/Rpc/WebHostRpcWorkerChannelManager.cs +++ b/src/WebJobs.Script/Workers/Rpc/WebHostRpcWorkerChannelManager.cs @@ -21,7 +21,6 @@ public class WebHostRpcWorkerChannelManager : IWebHostRpcWorkerChannelManager private readonly ILogger _logger = null; private readonly TimeSpan workerInitTimeout = TimeSpan.FromSeconds(30); private readonly IOptionsMonitor _applicationHostOptions = null; - private readonly IOptionsMonitor _languageWorkerOptions = null; private readonly IScriptEventManager _eventManager = null; private readonly IEnvironment _environment; private readonly ILoggerFactory _loggerFactory = null; @@ -39,7 +38,7 @@ public WebHostRpcWorkerChannelManager(IScriptEventManager eventManager, ILoggerFactory loggerFactory, IRpcWorkerChannelFactory rpcWorkerChannelFactory, IOptionsMonitor applicationHostOptions, - IMetricsLogger metricsLogger, IOptionsMonitor languageWorkerOptions, + IMetricsLogger metricsLogger, IConfiguration config, IWorkerProfileManager workerProfileManager) { @@ -52,26 +51,25 @@ public WebHostRpcWorkerChannelManager(IScriptEventManager eventManager, _rpcWorkerChannelFactory = rpcWorkerChannelFactory; _logger = loggerFactory.CreateLogger(); _applicationHostOptions = applicationHostOptions; - _languageWorkerOptions = languageWorkerOptions; _shutdownStandbyWorkerChannels = ScheduleShutdownStandbyChannels; _shutdownStandbyWorkerChannels = _shutdownStandbyWorkerChannels.Debounce(milliseconds: 5000); } - public Task InitializeChannelAsync(string runtime) + public Task InitializeChannelAsync(IEnumerable workerConfigs, string runtime) { _logger?.LogDebug("Initializing language worker channel for runtime:{runtime}", runtime); - return InitializeLanguageWorkerChannel(runtime, _applicationHostOptions.CurrentValue.ScriptPath); + return InitializeLanguageWorkerChannel(workerConfigs, runtime, _applicationHostOptions.CurrentValue.ScriptPath); } - internal async Task InitializeLanguageWorkerChannel(string runtime, string scriptRootPath) + internal async Task InitializeLanguageWorkerChannel(IEnumerable workerConfigs, string runtime, string scriptRootPath) { IRpcWorkerChannel rpcWorkerChannel = null; string workerId = Guid.NewGuid().ToString(); _logger.LogDebug("Creating language worker channel for runtime:{runtime}", runtime); try { - rpcWorkerChannel = _rpcWorkerChannelFactory.Create(scriptRootPath, runtime, _metricsLogger, 0, _languageWorkerOptions.CurrentValue.WorkerConfigs); + rpcWorkerChannel = _rpcWorkerChannelFactory.Create(scriptRootPath, runtime, _metricsLogger, 0, workerConfigs); AddOrUpdateWorkerChannels(runtime, rpcWorkerChannel); await rpcWorkerChannel.StartWorkerProcessAsync().ContinueWith(processStartTask => { diff --git a/test/DotNetIsolated60/DotNetIsolated60.csproj b/test/DotNetIsolated60/DotNetIsolated60.csproj new file mode 100644 index 0000000000..dc2326ed03 --- /dev/null +++ b/test/DotNetIsolated60/DotNetIsolated60.csproj @@ -0,0 +1,27 @@ + + + net6.0 + v4 + Exe + enable + enable + True + + + + + + + + + PreserveNewest + + + PreserveNewest + Never + + + + + + \ No newline at end of file diff --git a/test/DotNetIsolated60/DotNetIsolated60.sln b/test/DotNetIsolated60/DotNetIsolated60.sln new file mode 100644 index 0000000000..aa1dfcfd2d --- /dev/null +++ b/test/DotNetIsolated60/DotNetIsolated60.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.33627.172 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetIsolated60", "DotNetIsolated60.csproj", "{1DA92227-F28E-408D-96B1-20C72571E4AE}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1DA92227-F28E-408D-96B1-20C72571E4AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1DA92227-F28E-408D-96B1-20C72571E4AE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1DA92227-F28E-408D-96B1-20C72571E4AE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1DA92227-F28E-408D-96B1-20C72571E4AE}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {2AC2402B-48F8-46BF-B275-AEDFE2400EB6} + EndGlobalSection +EndGlobal diff --git a/test/DotNetIsolated60/Function1.cs b/test/DotNetIsolated60/Function1.cs new file mode 100644 index 0000000000..dc8d94d012 --- /dev/null +++ b/test/DotNetIsolated60/Function1.cs @@ -0,0 +1,30 @@ +using System.Net; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; + +namespace DotNetIsolated60 +{ + public class Function1 + { + private readonly ILogger _logger; + + public Function1(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + + [Function("Function1")] + public HttpResponseData Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req) + { + _logger.LogInformation("C# HTTP trigger function processed a request."); + + var response = req.CreateResponse(HttpStatusCode.OK); + response.Headers.Add("Content-Type", "text/plain; charset=utf-8"); + + response.WriteString("Welcome to Azure Functions!"); + + return response; + } + } +} diff --git a/test/DotNetIsolated60/Program.cs b/test/DotNetIsolated60/Program.cs new file mode 100644 index 0000000000..6e03f927b6 --- /dev/null +++ b/test/DotNetIsolated60/Program.cs @@ -0,0 +1,11 @@ +using Microsoft.Azure.Functions.Worker; +using Microsoft.Extensions.Hosting; + +//Debugger.Launch(); + +var host = new HostBuilder() + .ConfigureFunctionsWorkerDefaults() + .ConfigureGeneratedFunctionMetadataProvider() + .Build(); + +host.Run(); diff --git a/test/DotNetIsolated60/Properties/serviceDependencies.json b/test/DotNetIsolated60/Properties/serviceDependencies.json new file mode 100644 index 0000000000..df4dcc9d88 --- /dev/null +++ b/test/DotNetIsolated60/Properties/serviceDependencies.json @@ -0,0 +1,11 @@ +{ + "dependencies": { + "appInsights1": { + "type": "appInsights" + }, + "storage1": { + "type": "storage", + "connectionId": "AzureWebJobsStorage" + } + } +} \ No newline at end of file diff --git a/test/DotNetIsolated60/Properties/serviceDependencies.local.json b/test/DotNetIsolated60/Properties/serviceDependencies.local.json new file mode 100644 index 0000000000..b804a28939 --- /dev/null +++ b/test/DotNetIsolated60/Properties/serviceDependencies.local.json @@ -0,0 +1,11 @@ +{ + "dependencies": { + "appInsights1": { + "type": "appInsights.sdk" + }, + "storage1": { + "type": "storage.emulator", + "connectionId": "AzureWebJobsStorage" + } + } +} \ No newline at end of file diff --git a/test/DotNetIsolated60/host.json b/test/DotNetIsolated60/host.json new file mode 100644 index 0000000000..beb2e4020b --- /dev/null +++ b/test/DotNetIsolated60/host.json @@ -0,0 +1,11 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + } + } + } +} \ No newline at end of file diff --git a/test/WebJobs.Script.Tests.Integration/Host/StandbyManager/StandbyManagerE2ETests_Linux.cs b/test/WebJobs.Script.Tests.Integration/Host/StandbyManager/StandbyManagerE2ETests_Linux.cs index 941e3c28e5..bc4602bc11 100644 --- a/test/WebJobs.Script.Tests.Integration/Host/StandbyManager/StandbyManagerE2ETests_Linux.cs +++ b/test/WebJobs.Script.Tests.Integration/Host/StandbyManager/StandbyManagerE2ETests_Linux.cs @@ -125,9 +125,9 @@ public async Task StandbyModeE2E_LinuxContainer() Assert.Equal(1, logLines.Count(p => p.Contains("Validating host assignment context"))); Assert.Equal(1, logLines.Count(p => p.Contains("Starting Assignment"))); Assert.Equal(1, logLines.Count(p => p.Contains("Applying 3 app setting(s)"))); - Assert.Equal(1, logLines.Count(p => p.Contains("Skipping WorkerConfig for language: python"))); - Assert.Equal(1, logLines.Count(p => p.Contains("Skipping WorkerConfig for language: powershell"))); - Assert.Equal(1, logLines.Count(p => p.Contains("Skipping WorkerConfig for language: java"))); + Assert.Equal(2, logLines.Count(p => p.Contains("Skipping WorkerConfig for language: python"))); + Assert.Equal(2, logLines.Count(p => p.Contains("Skipping WorkerConfig for language: powershell"))); + Assert.Equal(2, logLines.Count(p => p.Contains("Skipping WorkerConfig for language: java"))); Assert.Equal(1, logLines.Count(p => p.Contains($"Extracting files to '{_expectedScriptPath}'"))); Assert.Equal(1, logLines.Count(p => p.Contains("Zip extraction complete"))); Assert.Equal(1, logLines.Count(p => p.Contains("Triggering specialization"))); diff --git a/test/WebJobs.Script.Tests.Integration/Management/AzureStorageInfoValueTests.cs b/test/WebJobs.Script.Tests.Integration/Management/AzureStorageInfoValueTests.cs index 6b4253b12e..34631f5df1 100644 --- a/test/WebJobs.Script.Tests.Integration/Management/AzureStorageInfoValueTests.cs +++ b/test/WebJobs.Script.Tests.Integration/Management/AzureStorageInfoValueTests.cs @@ -10,18 +10,20 @@ namespace Microsoft.Azure.WebJobs.Script.Tests.Integration.Management public class AzureStorageInfoValueTests { [Theory] - [InlineData(null, null, null, null, null, null, false)] - [InlineData("", null, "", "", "", "", false)] - [InlineData("AZUREFILESSTORAGE_storageid", AzureStorageType.AzureFiles, "", "", "", "", false)] - [InlineData("AZUREBLOBSTORAGE_storageid", AzureStorageType.AzureBlob, "", "", "", "", false)] - [InlineData("AZUREFILESSTORAGE_storageid", AzureStorageType.AzureFiles, "", "sharename", "accesskey", "mountpath", false)] - [InlineData("AZUREFILESSTORAGE_storageid", AzureStorageType.AzureFiles, "accountname", "sharename", "accesskey", "mountpath", true)] - [InlineData("AZUREBLOBSTORAGE_storageid", AzureStorageType.AzureBlob, "accountname", "", "accesskey", "mountpath", false)] - [InlineData("AZUREBLOBSTORAGE_storageid", AzureStorageType.AzureBlob, "accountname", "sharename", "accesskey", "mountpath", true)] - public void Builds_AzureStorageInfoValue(string id, AzureStorageType? type, string accountName, string shareName, string accessKey, string mountPath, bool isValid) + [InlineData(null, null, null, null, null, null, null, false)] + [InlineData("", null, "", "", "", "", "", false)] + [InlineData("AZUREFILESSTORAGE_storageid", AzureStorageType.AzureFiles, "", "", "", "", "", false)] + [InlineData("AZUREBLOBSTORAGE_storageid", AzureStorageType.AzureBlob, "", "", "", "", "", false)] + [InlineData("AZUREFILESSTORAGE_storageid", AzureStorageType.AzureFiles, "", "sharename", "accesskey", "mountpath", "", false)] + [InlineData("AZUREFILESSTORAGE_storageid", AzureStorageType.AzureFiles, "accountname", "sharename", "accesskey", "mountpath", "", true)] + [InlineData("AZUREFILESSTORAGE_storageid", AzureStorageType.AzureFiles, "accountname", "sharename", "accesskey", "mountpath", "smb", true)] + [InlineData("AZUREBLOBSTORAGE_storageid", AzureStorageType.AzureBlob, "accountname", "", "accesskey", "mountpath", "", false)] + [InlineData("AZUREBLOBSTORAGE_storageid", AzureStorageType.AzureBlob, "accountname", "sharename", "accesskey", "mountpath", "", true)] + [InlineData("AZUREFILESSTORAGE_storageid", AzureStorageType.AzureFiles, "accountname", "sharename", "accesskey", "mountpath", "http", true)] + public void Builds_AzureStorageInfoValue(string id, AzureStorageType? type, string accountName, string shareName, string accessKey, string mountPath, string protocol, bool isValid) { var key = id; - var value = $"{accountName}|{shareName}|{accessKey}|{mountPath}"; + var value = $"{accountName}|{shareName}|{accessKey}|{mountPath}|{protocol}"; var environmentVariable = new KeyValuePair(key, value); var azureStorageInfoValue = AzureStorageInfoValue.FromEnvironmentVariable(environmentVariable); if (isValid) diff --git a/test/WebJobs.Script.Tests.Integration/Management/FunctionsSyncManagerTests.cs b/test/WebJobs.Script.Tests.Integration/Management/FunctionsSyncManagerTests.cs index 3136df2cdc..de35470fe9 100644 --- a/test/WebJobs.Script.Tests.Integration/Management/FunctionsSyncManagerTests.cs +++ b/test/WebJobs.Script.Tests.Integration/Management/FunctionsSyncManagerTests.cs @@ -136,11 +136,14 @@ public FunctionsSyncManagerTests() _mockEnvironment.Setup(p => p.GetEnvironmentVariable(EnvironmentSettingNames.KubernetesServiceHost)).Returns(""); _mockEnvironment.Setup(p => p.GetEnvironmentVariable(EnvironmentSettingNames.PodNamespace)).Returns(""); _mockEnvironment.Setup(p => p.GetEnvironmentVariable(EnvironmentSettingNames.ManagedEnvironment)).Returns((string)null); + _mockEnvironment.Setup(p => p.GetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteName)).Returns((string)null); + _mockEnvironment.Setup(p => p.GetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteSlotName)).Returns((string)null); + _mockEnvironment.Setup(p => p.GetEnvironmentVariable(EnvironmentSettingNames.AzureWebJobsFeatureFlags)).Returns((string)null); _hostNameProvider = new HostNameProvider(_mockEnvironment.Object); var functionMetadataProvider = new HostFunctionMetadataProvider(optionsMonitor, NullLogger.Instance, new TestMetricsLogger()); - var defaultProvider = new FunctionMetadataProvider(NullLogger.Instance, null, functionMetadataProvider); + var defaultProvider = new FunctionMetadataProvider(NullLogger.Instance, null, functionMetadataProvider, new OptionsWrapper(new FunctionsHostingConfigOptions()), _mockEnvironment.Object); var functionMetadataManager = TestFunctionMetadataManager.GetFunctionMetadataManager(new OptionsWrapper(jobHostOptions), defaultProvider, null, new OptionsWrapper(new HttpWorkerOptions()), loggerFactory, new TestOptionsMonitor(CreateLanguageWorkerConfigSettings())); _scriptHostManager = new TestScriptHostService(configuration); diff --git a/test/WebJobs.Script.Tests.Integration/TestFunctionHost.cs b/test/WebJobs.Script.Tests.Integration/TestFunctionHost.cs index 87c3c6d612..e0ceeb60eb 100644 --- a/test/WebJobs.Script.Tests.Integration/TestFunctionHost.cs +++ b/test/WebJobs.Script.Tests.Integration/TestFunctionHost.cs @@ -15,6 +15,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; using Microsoft.Azure.WebJobs.Host.Executors; +using Microsoft.Azure.WebJobs.Script.Config; using Microsoft.Azure.WebJobs.Script.ExtensionBundle; using Microsoft.Azure.WebJobs.Script.Models; using Microsoft.Azure.WebJobs.Script.WebHost; @@ -477,9 +478,9 @@ private FunctionMetadataManager GetMetadataManager(IOptionsMonitor.Instance, new TestMetricsLogger()); - var defaultProvider = new FunctionMetadataProvider(NullLogger.Instance, null, metadataProvider); + var defaultProvider = new FunctionMetadataProvider(NullLogger.Instance, null, metadataProvider, new OptionsWrapper(new FunctionsHostingConfigOptions()), SystemEnvironment.Instance); var metadataManager = new FunctionMetadataManager(managerServiceProvider.GetService>(), defaultProvider, - managerServiceProvider.GetService>(), manager, factory, new TestOptionsMonitor(workerOptions), environment); + managerServiceProvider.GetService>(), manager, factory, environment); return metadataManager; } diff --git a/test/WebJobs.Script.Tests.Integration/TestScripts/NodeWithBundles/HttpTriggerNoAuth/function.json b/test/WebJobs.Script.Tests.Integration/TestScripts/NodeWithBundles/HttpTriggerNoAuth/function.json new file mode 100644 index 0000000000..e8d500a16a --- /dev/null +++ b/test/WebJobs.Script.Tests.Integration/TestScripts/NodeWithBundles/HttpTriggerNoAuth/function.json @@ -0,0 +1,16 @@ +{ + "bindings": [ + { + "type": "httpTrigger", + "name": "request", + "direction": "in", + "methods": [ "get", "post" ], + "authLevel": "anonymous" + }, + { + "type": "http", + "name": "response", + "direction": "out" + } + ] +} diff --git a/test/WebJobs.Script.Tests.Integration/TestScripts/NodeWithBundles/HttpTriggerNoAuth/index.js b/test/WebJobs.Script.Tests.Integration/TestScripts/NodeWithBundles/HttpTriggerNoAuth/index.js new file mode 100644 index 0000000000..1e66a843c0 --- /dev/null +++ b/test/WebJobs.Script.Tests.Integration/TestScripts/NodeWithBundles/HttpTriggerNoAuth/index.js @@ -0,0 +1,25 @@ +var util = require('util'); + +module.exports = function (context, req) { + context.log('Node.js HttpTrigger function invoked.'); + + context.res = { + status: 200, + body: { + reqBodyType: 'Node.js HttpTrigger function invoked.', + reqBodyIsArray: util.isArray(req.body), + reqBody: req.body, + reqRawBodyType: typeof req.rawBody, + reqRawBody: req.rawBody, + reqHeaders: req.headers, + bindingData: context.bindingData, + reqOriginalUrl: req.originalUrl + }, + headers: { + 'test-header': 'Test Response Header', + "Content-Type": "application/json; charset=utf-8" + } + }; + + context.done(); +}; \ No newline at end of file diff --git a/test/WebJobs.Script.Tests.Integration/TestScripts/NodeWithBundles/host.json b/test/WebJobs.Script.Tests.Integration/TestScripts/NodeWithBundles/host.json new file mode 100644 index 0000000000..1469c8e37c --- /dev/null +++ b/test/WebJobs.Script.Tests.Integration/TestScripts/NodeWithBundles/host.json @@ -0,0 +1,7 @@ +{ + "version": "2.0", + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[3.*, 4.0.0)" + } +} \ No newline at end of file diff --git a/test/WebJobs.Script.Tests.Integration/WebHostEndToEnd/ProxyEndToEndTests.cs b/test/WebJobs.Script.Tests.Integration/WebHostEndToEnd/ProxyEndToEndTests.cs index fe0c5b9439..3f1cf3d3cc 100644 --- a/test/WebJobs.Script.Tests.Integration/WebHostEndToEnd/ProxyEndToEndTests.cs +++ b/test/WebJobs.Script.Tests.Integration/WebHostEndToEnd/ProxyEndToEndTests.cs @@ -9,8 +9,6 @@ using System.Net.Http; using System.Threading.Tasks; using System.Web.Http; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.TestHost; using Microsoft.Azure.WebJobs.Script.Config; using Microsoft.Azure.WebJobs.Script.Management.Models; using Microsoft.Azure.WebJobs.Script.WebHost; @@ -414,7 +412,7 @@ public TestFixture() }; var hostProvider = new HostFunctionMetadataProvider(optionsMonitor, NullLogger.Instance, new TestMetricsLogger()); - var provider = new FunctionMetadataProvider(NullLogger.Instance, null, hostProvider); + var provider = new FunctionMetadataProvider(NullLogger.Instance, null, hostProvider, new OptionsWrapper(new FunctionsHostingConfigOptions()), SystemEnvironment.Instance); TestHost = new TestFunctionHost(HostOptions.ScriptPath, HostOptions.LogPath, configureScriptHostServices: services => diff --git a/test/WebJobs.Script.Tests.Integration/WebHostEndToEnd/SpecializationE2ETests.cs b/test/WebJobs.Script.Tests.Integration/WebHostEndToEnd/SpecializationE2ETests.cs index 71b27f6f1f..00d1777788 100644 --- a/test/WebJobs.Script.Tests.Integration/WebHostEndToEnd/SpecializationE2ETests.cs +++ b/test/WebJobs.Script.Tests.Integration/WebHostEndToEnd/SpecializationE2ETests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Net; @@ -31,6 +32,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.WebJobs.Script.Tests; +using TestFunctions; using Xunit; using Xunit.Abstractions; using IApplicationLifetime = Microsoft.AspNetCore.Hosting.IApplicationLifetime; @@ -44,6 +46,10 @@ public class SpecializationE2ETests private static SemaphoreSlim _buildCount; private static readonly string _standbyPath = Path.Combine(Path.GetTempPath(), "functions", "standby", "wwwroot"); + private static readonly string _scriptRootConfigPath = ConfigurationPath.Combine(ConfigurationSectionNames.WebHost, nameof(ScriptApplicationHostOptions.ScriptPath)); + + private static readonly string _dotnetIsolated60Path = Path.GetFullPath(@"..\..\..\..\DotNetIsolated60\bin\Debug\net6.0"); + private const string _specializedScriptRoot = @"TestScripts\CSharp"; private readonly TestEnvironment _environment; @@ -244,6 +250,164 @@ public async Task Specialization_ResetsSharedLoadContext() } } + [Fact] + public async Task ForNonReadOnlyFileSystem_RestartWorkerForSpecializationAndHotReload() + { + _environment.SetEnvironmentVariable(RpcWorkerConstants.FunctionWorkerRuntimeSettingName, "node"); + _environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebJobsFeatureFlags, ScriptConstants.FeatureFlagEnableWorkerIndexing); + + var builder = CreateStandbyHostBuilder("HttpTriggerNoAuth"); + + builder.ConfigureAppConfiguration(config => + { + string scriptRootConfigPath = ConfigurationPath.Combine(ConfigurationSectionNames.WebHost, nameof(ScriptApplicationHostOptions.ScriptPath)); + config.AddInMemoryCollection(new Dictionary + { + { _scriptRootConfigPath, Path.GetFullPath(@"TestScripts\NodeWithBundles") } + }); + }); + + using var testServer = new TestServer(builder); + + var client = testServer.CreateClient(); + + var response = await client.GetAsync("api/warmup"); + response.EnsureSuccessStatusCode(); + + var webChannelManager = testServer.Services.GetService(); + var channel = await webChannelManager.GetChannels("node").Single().Value.Task; + var processId = channel.WorkerProcess.Process.Id; + + _environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteContainerReady, "1"); + _environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsitePlaceholderMode, "0"); + + response = await client.GetAsync("api/HttpTriggerNoAuth"); + response.EnsureSuccessStatusCode(); + var responseContent = await response.Content.ReadAsStringAsync(); + + string content = "Node.js HttpTrigger function invoked."; + responseContent.Contains(content); + + channel = await webChannelManager.GetChannels("node").Single().Value.Task; + var newProcessId = channel.WorkerProcess.Process.Id; + Assert.NotEqual(processId, newProcessId); + Assert.Contains(content, responseContent); + + var indexJS = Path.GetFullPath(@"TestScripts\NodeWithBundles\HttpTriggerNoAuth\index.js"); + + string fileContent = File.ReadAllText(indexJS); + string newContent = "Updated Node.js HttpTrigger function invoked."; + string updatedContent = fileContent.Replace(content, newContent); + File.WriteAllText(indexJS, updatedContent); + + var manager = testServer.Host.Services.GetService(); + var hostService = manager as WebJobsScriptHostService; + + await TestHelpers.Await(() => + { + return hostService.State == ScriptHostState.Default; + }, 5000); + + await TestHelpers.Await(() => + { + return hostService.State == ScriptHostState.Running; + }, 5000); + + response = await client.GetAsync("api/HttpTriggerNoAuth"); + response.EnsureSuccessStatusCode(); + responseContent = await response.Content.ReadAsStringAsync(); + responseContent.Contains(newContent); + + channel = await webChannelManager.GetChannels("node").Single().Value.Task; + var hotReloadProcessId = channel.WorkerProcess.Process.Id; + Assert.NotEqual(hotReloadProcessId, newProcessId); + Assert.Contains(newContent, responseContent); + } + + [Fact] + public async Task Specialization_RestartsWorkerForNonReadOnlyFileSystem() + { + _environment.SetEnvironmentVariable(RpcWorkerConstants.FunctionWorkerRuntimeSettingName, "node"); + _environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebJobsFeatureFlags, ScriptConstants.FeatureFlagEnableWorkerIndexing); + + var builder = CreateStandbyHostBuilder("HttpTriggerNoAuth"); + + builder.ConfigureAppConfiguration(config => + { + string scriptRootConfigPath = ConfigurationPath.Combine(ConfigurationSectionNames.WebHost, nameof(ScriptApplicationHostOptions.ScriptPath)); + config.AddInMemoryCollection(new Dictionary + { + { _scriptRootConfigPath, Path.GetFullPath(@"TestScripts\NodeWithBundles") } + }); + }); + + using var testServer = new TestServer(builder); + + var client = testServer.CreateClient(); + + var response = await client.GetAsync("api/warmup"); + response.EnsureSuccessStatusCode(); + + var placeholderContext = FunctionAssemblyLoadContext.Shared; + + var webChannelManager = testServer.Services.GetService(); + var channel = await webChannelManager.GetChannels("node").Single().Value.Task; + var processId = channel.WorkerProcess.Process.Id; + + _environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteContainerReady, "1"); + _environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsitePlaceholderMode, "0"); + + //await _pauseBeforeHostBuild.WaitAsync(10000); + response = await client.GetAsync("api/HttpTriggerNoAuth"); + response.EnsureSuccessStatusCode(); + + channel = await webChannelManager.GetChannels("node").Single().Value.Task; + var newProcessId = channel.WorkerProcess.Process.Id; + Assert.NotEqual(processId, newProcessId); + } + + + [Fact] + public async Task Specialization_UsePlaceholderWorkerforReadOnlyFileSystem() + { + _environment.SetEnvironmentVariable(RpcWorkerConstants.FunctionWorkerRuntimeSettingName, "node"); + _environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebJobsFeatureFlags, ScriptConstants.FeatureFlagEnableWorkerIndexing); + + var builder = CreateStandbyHostBuilder("HttpTriggerNoAuth"); + string isFileSystemReadOnly = ConfigurationPath.Combine(ConfigurationSectionNames.WebHost, nameof(ScriptApplicationHostOptions.IsFileSystemReadOnly)); + + builder.ConfigureAppConfiguration(config => + { + config.AddInMemoryCollection(new Dictionary + { + { _scriptRootConfigPath, Path.GetFullPath(@"TestScripts\NodeWithBundles") } + }); + }); + + + using var testServer = new TestServer(builder); + + var client = testServer.CreateClient(); + + var response = await client.GetAsync("api/warmup"); + response.EnsureSuccessStatusCode(); + + var webChannelManager = testServer.Services.GetService(); + var channel = await webChannelManager.GetChannels("node").Single().Value.Task; + var processId = channel.WorkerProcess.Process.Id; + + _environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteRunFromPackage, "1"); + _environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteContainerReady, "1"); + _environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsitePlaceholderMode, "0"); + + response = await client.GetAsync("api/HttpTriggerNoAuth"); + response.EnsureSuccessStatusCode(); + + channel = await webChannelManager.GetChannels("node").Single().Value.Task; + var newProcessId = channel.WorkerProcess.Process.Id; + Assert.Equal(processId, newProcessId); + } + [Fact] public async Task Specialization_GCMode() { @@ -564,7 +728,6 @@ public async Task Specialization_JobHostInternalStorageOptionsUpdatesWithActiveH _environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteContainerReady, "1"); _environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsitePlaceholderMode, "0"); - // This value is available now using (new TestScopedEnvironmentVariable("AzureFunctionsJobHost__InternalSasBlobContainer", fakeSasUri.ToString())) using (new TestScopedEnvironmentVariable("AzureWebJobsStorage", storageValue)) @@ -586,20 +749,159 @@ public async Task Specialization_JobHostInternalStorageOptionsUpdatesWithActiveH } } - private IWebHostBuilder CreateStandbyHostBuilder(params string[] functions) + [Fact] + public async Task DotNetIsolated_PlaceholderHit() + { + var builder = InitializeDotNetIsolatedPlaceholderBuilder(); + + using var testServer = new TestServer(builder); + + var client = testServer.CreateClient(); + var response = await client.GetAsync("api/warmup"); + response.EnsureSuccessStatusCode(); + + // Validate that the channel is set up with native worker + var webChannelManager = testServer.Services.GetService(); + + var placeholderChannel = await webChannelManager.GetChannels("dotnet-isolated").Single().Value.Task; + Assert.Contains("FunctionsNetHost.exe", placeholderChannel.WorkerProcess.Process.StartInfo.FileName); + Assert.NotNull(placeholderChannel.WorkerProcess.Process.Id); + var runningProcess = Process.GetProcessById(placeholderChannel.WorkerProcess.Id); + Assert.Contains(runningProcess.ProcessName, "FunctionsNetHost"); + + _environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteContainerReady, "1"); + _environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsitePlaceholderMode, "0"); + + response = await client.GetAsync("api/function1"); + response.EnsureSuccessStatusCode(); + + // Placeholder hit; these should match + var specializedChannel = await webChannelManager.GetChannels("dotnet-isolated").Single().Value.Task; + Assert.Same(placeholderChannel, specializedChannel); + runningProcess = Process.GetProcessById(placeholderChannel.WorkerProcess.Id); + Assert.Contains(runningProcess.ProcessName, "FunctionsNetHost"); + + var log = _loggerProvider.GetLog(); + Assert.Contains("UsePlaceholderDotNetIsolated: True", log); + Assert.Contains("Placeholder runtime version: '6.0'. Site runtime version: '6.0'. Match: True", log); + Assert.DoesNotContain("Shutting down placeholder worker.", log); + } + + [Fact] + public async Task DotNetIsolated_PlaceholderMiss_EnvVar() + { + // Placeholder miss if the WEBSITE_USE_PLACEHOLDER_DOTNETISOLATED env var is not set + await DotNetIsolatedPlaceholderMiss(); + + var log = _loggerProvider.GetLog(); + Assert.Contains("UsePlaceholderDotNetIsolated: False", log); + Assert.Contains("Shutting down placeholder worker. Worker is not compatible for runtime: dotnet-isolated", log); + } + + [Fact] + public async Task DotNetIsolated_PlaceholderMiss_DotNetVer() + { + // Even with placeholders enabled via the WEBSITE_USE_PLACEHOLDER_DOTNETISOLATED env var, + // if the dotnet version does not match, we should not use the placeholder + await DotNetIsolatedPlaceholderMiss(() => + { + _environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteUsePlaceholderDotNetIsolated, "1"); + _environment.SetEnvironmentVariable(RpcWorkerConstants.FunctionWorkerRuntimeVersionSettingName, "7.0"); + }); + + var log = _loggerProvider.GetLog(); + Assert.Contains("UsePlaceholderDotNetIsolated: True", log); + Assert.Contains("Placeholder runtime version: '6.0'. Site runtime version: '7.0'. Match: False", log); + Assert.Contains("Shutting down placeholder worker. Worker is not compatible for runtime: dotnet-isolated", log); + } + + private async Task DotNetIsolatedPlaceholderMiss(Action additionalSpecializedSetup = null) { - string scriptRootConfigPath = ConfigurationPath.Combine(ConfigurationSectionNames.WebHost, nameof(ScriptApplicationHostOptions.ScriptPath)); + var builder = InitializeDotNetIsolatedPlaceholderBuilder(() => + { + // remove WEBSITE_USE_PLACEHOLDER_DOTNETISOLATED + _environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteUsePlaceholderDotNetIsolated, null); + }); + + using var testServer = new TestServer(builder); + var client = testServer.CreateClient(); + var response = await client.GetAsync("api/warmup"); + response.EnsureSuccessStatusCode(); + + // Validate that the channel is set up with native worker + var webChannelManager = testServer.Services.GetService(); + + var placeholderChannel = await webChannelManager.GetChannels("dotnet-isolated").Single().Value.Task; + Assert.Contains("FunctionsNetHost.exe", placeholderChannel.WorkerProcess.Process.StartInfo.FileName); + Assert.NotNull(placeholderChannel.WorkerProcess.Process.Id); + var runningProcess = Process.GetProcessById(placeholderChannel.WorkerProcess.Id); + Assert.Contains(runningProcess.ProcessName, "FunctionsNetHost"); + + _environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteContainerReady, "1"); + _environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsitePlaceholderMode, "0"); + + additionalSpecializedSetup?.Invoke(); + + response = await client.GetAsync("api/function1"); + response.EnsureSuccessStatusCode(); + + // Placeholder miss; new channel should be started using the deployed worker directly + var specializedChannel = await webChannelManager.GetChannels("dotnet-isolated").Single().Value.Task; + Assert.Contains("dotnet.exe", specializedChannel.WorkerProcess.Process.StartInfo.FileName); + Assert.Contains("DotNetIsolated60", specializedChannel.WorkerProcess.Process.StartInfo.Arguments); + runningProcess = Process.GetProcessById(specializedChannel.WorkerProcess.Id); + Assert.Contains(runningProcess.ProcessName, "dotnet"); + + // Ensure other process is gone. + Assert.DoesNotContain(Process.GetProcesses(), p => p.ProcessName.Contains("FunctionsNetHost")); + Assert.Throws(() => placeholderChannel.WorkerProcess.Process.Id); + } + + private static void BuildDotnetIsolated60() + { + var p = Process.Start("dotnet", $"build {_dotnetIsolated60Path}/../../.."); + p.WaitForExit(); + } + + private IWebHostBuilder InitializeDotNetIsolatedPlaceholderBuilder(Action additionalSetup = null) + { + BuildDotnetIsolated60(); + + _environment.SetEnvironmentVariable(RpcWorkerConstants.FunctionWorkerRuntimeSettingName, "dotnet-isolated"); + _environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteUsePlaceholderDotNetIsolated, "1"); + _environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebJobsFeatureFlags, ScriptConstants.FeatureFlagEnableWorkerIndexing); + _environment.SetEnvironmentVariable(RpcWorkerConstants.FunctionWorkerRuntimeVersionSettingName, "6.0"); + + additionalSetup?.Invoke(); + + var builder = CreateStandbyHostBuilder("Function1"); + + builder.ConfigureAppConfiguration(config => + { + config.AddInMemoryCollection(new Dictionary + { + { _scriptRootConfigPath, _dotnetIsolated60Path }, + }); + }); + + return builder; + } + + private IWebHostBuilder CreateStandbyHostBuilder(params string[] functions) + { var builder = Program.CreateWebHostBuilder() .ConfigureLogging(b => { b.AddProvider(_loggerProvider); + b.AddFilter("Microsoft.Azure.WebJobs", LogLevel.Debug); + b.AddFilter("Worker", LogLevel.Debug); }) .ConfigureAppConfiguration(c => { c.AddInMemoryCollection(new Dictionary { - { scriptRootConfigPath, _specializedScriptRoot } + { _scriptRootConfigPath, _specializedScriptRoot } }); }) .ConfigureServices((bc, s) => @@ -614,6 +916,11 @@ private IWebHostBuilder CreateStandbyHostBuilder(params string[] functions) }) .ConfigureScriptHostServices(s => { + s.AddLogging(logging => + { + logging.AddProvider(_loggerProvider); + }); + s.PostConfigure(o => { // Only load the function we care about, but not during standby diff --git a/test/WebJobs.Script.Tests.Shared/TestFunctionMetadataManager.cs b/test/WebJobs.Script.Tests.Shared/TestFunctionMetadataManager.cs index 453fc126a6..28a2767e0c 100644 --- a/test/WebJobs.Script.Tests.Shared/TestFunctionMetadataManager.cs +++ b/test/WebJobs.Script.Tests.Shared/TestFunctionMetadataManager.cs @@ -9,6 +9,7 @@ using Microsoft.Azure.WebJobs.Script.Grpc; using Microsoft.Azure.WebJobs.Script.Workers.Http; using Microsoft.Azure.WebJobs.Script.Workers.Rpc; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Moq; @@ -51,7 +52,7 @@ public static FunctionMetadataManager GetFunctionMetadataManager(IOptions