diff --git a/build/common.props b/build/common.props index bbf48b9159..afa45088b8 100644 --- a/build/common.props +++ b/build/common.props @@ -5,7 +5,7 @@ latest 4 27 - 4 + 5 0 diff --git a/release_notes.md b/release_notes.md index 60f1211c12..b39d1d8dd7 100644 --- a/release_notes.md +++ b/release_notes.md @@ -5,16 +5,11 @@ --> - Update Python Worker Version to [4.20.0](https://github.com/Azure/azure-functions-python-worker/releases/tag/4.20.0) - Update Java Worker Version to [2.13.0](https://github.com/Azure/azure-functions-java-worker/releases/tag/2.13.0) -- Update WebJobsScriptHostService to remove hardcoded sleep during application shut down (#9520) +- Increased maximum HTTP request content size to 210000000 Bytes (~200MB) +- Update Node.js Worker Version to [3.8.1](https://github.com/Azure/azure-functions-nodejs-worker/releases/tag/v3.8.1) +- Update PowerShell 7.4 Worker Version to [4.0.2975](https://github.com/Azure/azure-functions-powershell-worker/releases/tag/v4.0.2975) - Update PowerShell 7.2 Worker Version to [4.0.2974](https://github.com/Azure/azure-functions-powershell-worker/releases/tag/v4.0.2974) - Update PowerShell 7.0 Worker Version to [4.0.2973](https://github.com/Azure/azure-functions-powershell-worker/releases/tag/v4.0.2973) -- Add support for standalone executable (ie: `dotnet build --standalone`) for out-of-proc workers in Linux Consumption. (#9550) -- Bug fix: Do not restart worker channels or JobHost when an API request is made to get or update the function metadata (unless the config was changed) (#9510) - - This fixes a bug where requests to 'admin/functions' lead to a "Did not find initialized workers" error when - worker indexing is enabled. -- Bug fix: If there are no channels created and the host is running, restart the JobHost instead of shutting down worker channels (#9510) - - This fixes a bug with worker indexing where we are shutting down worker channels and creating a new channel that never - gets properly initialized as the invocation buffers are not created - this leads to a "Did not find initialized workers" error. -- Check if a blob container or table exists before trying to create it (#9555) - Limit dotnet-isolated specialization to 64 bit host process (#9548) - Sending command line arguments to language workers with `functions-` prefix to prevent conflicts (#9514) +- Update WebJobsScriptHostService to remove hardcoded sleep during application shut down (#9520) diff --git a/src/WebJobs.Script.Grpc/Channel/GrpcWorkerChannel.cs b/src/WebJobs.Script.Grpc/Channel/GrpcWorkerChannel.cs index f9a77c497e..9366a3b83e 100644 --- a/src/WebJobs.Script.Grpc/Channel/GrpcWorkerChannel.cs +++ b/src/WebJobs.Script.Grpc/Channel/GrpcWorkerChannel.cs @@ -374,12 +374,18 @@ internal void FunctionEnvironmentReloadResponse(FunctionEnvironmentReloadRespons ApplyCapabilities(res.Capabilities, res.CapabilitiesUpdateStrategy.ToGrpcCapabilitiesUpdateStrategy()); - if (res.Result.IsFailure(out Exception reloadEnvironmentVariablesException)) + if (res.Result.IsFailure(IsUserCodeExceptionCapabilityEnabled(), out var reloadEnvironmentVariablesException)) { - _workerChannelLogger.LogError(reloadEnvironmentVariablesException, "Failed to reload environment variables"); - _reloadTask.SetException(reloadEnvironmentVariablesException); + if (res.Result.Exception is not null && reloadEnvironmentVariablesException is not null) + { + _workerChannelLogger.LogWarning(reloadEnvironmentVariablesException, reloadEnvironmentVariablesException.Message); + } + _reloadTask.SetResult(false); + } + else + { + _reloadTask.SetResult(true); } - _reloadTask.SetResult(true); latencyEvent.Dispose(); } @@ -414,6 +420,15 @@ internal void WorkerInitResponse(GrpcEvent initEvent) _workerInitTask.TrySetResult(true); } + private bool IsUserCodeExceptionCapabilityEnabled() + { + var enableUserCodeExceptionCapability = string.Equals( + _workerCapabilities.GetCapabilityState(RpcWorkerConstants.EnableUserCodeException), bool.TrueString, + StringComparison.OrdinalIgnoreCase); + + return enableUserCodeExceptionCapability; + } + private void LogWorkerMetadata(WorkerMetadata workerMetadata) { if (workerMetadata == null) @@ -546,7 +561,7 @@ internal FunctionLoadRequestCollection GetFunctionLoadRequestCollection(IEnumera return functionLoadRequestCollection; } - public Task SendFunctionEnvironmentReloadRequest() + public Task SendFunctionEnvironmentReloadRequest() { _functionsIndexingTask = new TaskCompletionSource>(TaskCreationOptions.RunContinuationsAsynchronously); _functionMetadataRequestSent = false; @@ -1576,4 +1591,4 @@ private void OnTimeout() } } } -} +} \ No newline at end of file diff --git a/src/WebJobs.Script.Grpc/MessageExtensions/StatusResultExtensions.cs b/src/WebJobs.Script.Grpc/MessageExtensions/StatusResultExtensions.cs index b555e6c192..077a0185e3 100644 --- a/src/WebJobs.Script.Grpc/MessageExtensions/StatusResultExtensions.cs +++ b/src/WebJobs.Script.Grpc/MessageExtensions/StatusResultExtensions.cs @@ -3,20 +3,18 @@ using System; using System.Threading.Tasks; -using Grpc.Core; -using Microsoft.Azure.WebJobs.Script.Config; using Microsoft.Azure.WebJobs.Script.Grpc.Messages; namespace Microsoft.Azure.WebJobs.Script.Grpc { internal static class StatusResultExtensions { - public static bool IsFailure(this StatusResult statusResult, out Exception exception) + public static bool IsFailure(this StatusResult statusResult, bool enableUserCodeExceptionCapability, out Exception exception) { switch (statusResult.Status) { case StatusResult.Types.Status.Failure: - exception = GetRpcException(statusResult); + exception = GetRpcException(statusResult, enableUserCodeExceptionCapability); return true; case StatusResult.Types.Status.Cancelled: @@ -29,6 +27,11 @@ public static bool IsFailure(this StatusResult statusResult, out Exception excep } } + public static bool IsFailure(this StatusResult statusResult, out Exception exception) + { + return IsFailure(statusResult, false, out exception); + } + /// /// This method is only hit on the invocation code path. /// enableUserCodeExceptionCapability = feature flag exposed as a capability that is set by the worker. @@ -68,4 +71,4 @@ public static Workers.Rpc.RpcException GetRpcException(StatusResult statusResult return new Workers.Rpc.RpcException(status, string.Empty, string.Empty); } } -} +} \ No newline at end of file diff --git a/src/WebJobs.Script/Environment/IEnvironment.cs b/src/WebJobs.Script/Environment/IEnvironment.cs index 2889e3b7e2..e502b8effd 100644 --- a/src/WebJobs.Script/Environment/IEnvironment.cs +++ b/src/WebJobs.Script/Environment/IEnvironment.cs @@ -12,6 +12,11 @@ namespace Microsoft.Azure.WebJobs.Script /// public interface IEnvironment { + /// + /// Gets a value indicating whether the current process is a 64-bit process. + /// + public bool Is64BitProcess { get; } + /// /// Returns the value of an environment variable for the current . /// @@ -20,7 +25,7 @@ public interface IEnvironment string GetEnvironmentVariable(string name); /// - /// Creates, modifies, or deletes an environment variable stored in the current + /// Creates, modifies, or deletes an environment variable stored in the current . /// /// The environment variable name. /// The value to assign to the variable. diff --git a/src/WebJobs.Script/Environment/SystemEnvironment.cs b/src/WebJobs.Script/Environment/SystemEnvironment.cs index ed3e862225..d8ac86e36a 100644 --- a/src/WebJobs.Script/Environment/SystemEnvironment.cs +++ b/src/WebJobs.Script/Environment/SystemEnvironment.cs @@ -17,6 +17,8 @@ private SystemEnvironment() public static SystemEnvironment Instance => _instance.Value; + public bool Is64BitProcess => Environment.Is64BitProcess; + private static SystemEnvironment CreateInstance() { return new SystemEnvironment(); diff --git a/src/WebJobs.Script/ExtensionBundle/ExtensionBundleManager.cs b/src/WebJobs.Script/ExtensionBundle/ExtensionBundleManager.cs index afd3563a3a..3c88df31f3 100644 --- a/src/WebJobs.Script/ExtensionBundle/ExtensionBundleManager.cs +++ b/src/WebJobs.Script/ExtensionBundle/ExtensionBundleManager.cs @@ -71,7 +71,7 @@ public bool IsLegacyExtensionBundle() /// /// Attempts to locate the extension bundle inside the probing paths and download paths. If the extension bundle is not found then it will download the extension bundle. /// - /// Path of the extension bundle + /// Path of the extension bundle. public async Task GetExtensionBundlePath() { using (var httpClient = new HttpClient()) @@ -83,8 +83,8 @@ public async Task GetExtensionBundlePath() /// /// Attempts to locate the extension bundle inside the probing paths and download paths. If the extension bundle is not found then it will download the extension bundle. /// - /// HttpClient used to download the extension bundle - /// Path of the extension bundle + /// HttpClient used to download the extension bundle. + /// Path of the extension bundle. public async Task GetExtensionBundlePath(HttpClient httpClient) { return await GetBundle(httpClient); diff --git a/src/WebJobs.Script/ScriptConstants.cs b/src/WebJobs.Script/ScriptConstants.cs index 75b2275166..b5d982a4e8 100644 --- a/src/WebJobs.Script/ScriptConstants.cs +++ b/src/WebJobs.Script/ScriptConstants.cs @@ -88,6 +88,7 @@ public static class ScriptConstants public const string DefaultMasterKeyName = "master"; public const string DefaultFunctionKeyName = "default"; public const string ColdStartEventName = "ColdStart"; + public const string PlaceholderMissDueToBitnessEventName = "PlaceholderMissDueToBitness"; public const string FunctionsUserAgent = "AzureFunctionsRuntime"; public const string HttpScaleUserAgent = "HttpScaleManager"; diff --git a/src/WebJobs.Script/WebJobs.Script.csproj b/src/WebJobs.Script/WebJobs.Script.csproj index 413bd27b75..7c00814776 100644 --- a/src/WebJobs.Script/WebJobs.Script.csproj +++ b/src/WebJobs.Script/WebJobs.Script.csproj @@ -47,7 +47,7 @@ - + @@ -57,11 +57,11 @@ - + - - - + + + diff --git a/src/WebJobs.Script/Workers/Rpc/IRpcWorkerChannel.cs b/src/WebJobs.Script/Workers/Rpc/IRpcWorkerChannel.cs index df9bde5278..d9d3e6eb9c 100644 --- a/src/WebJobs.Script/Workers/Rpc/IRpcWorkerChannel.cs +++ b/src/WebJobs.Script/Workers/Rpc/IRpcWorkerChannel.cs @@ -22,7 +22,7 @@ public interface IRpcWorkerChannel : IWorkerChannel void SendFunctionLoadRequests(ManagedDependencyOptions managedDependencyOptions, TimeSpan? functionTimeout); - Task SendFunctionEnvironmentReloadRequest(); + Task SendFunctionEnvironmentReloadRequest(); void SendWorkerWarmupRequest(); diff --git a/src/WebJobs.Script/Workers/Rpc/RpcWorkerContext.cs b/src/WebJobs.Script/Workers/Rpc/RpcWorkerContext.cs index 50dd7d54ee..e39ce55adb 100644 --- a/src/WebJobs.Script/Workers/Rpc/RpcWorkerContext.cs +++ b/src/WebJobs.Script/Workers/Rpc/RpcWorkerContext.cs @@ -44,7 +44,10 @@ public RpcWorkerContext(string requestId, public override string GetFormattedArguments() { - return $" --host {ServerUri.Host} --port {ServerUri.Port} --workerId {WorkerId} --requestId {RequestId} --grpcMaxMessageLength {MaxMessageLength}"; + // Adding a second copy of the commandline arguments with the "functions-" prefix to prevent any conflicts caused by the existing generic names. + // Language workers are advised to use the "functions-" prefix ones and if not present fallback to existing ones. + + return $" --host {ServerUri.Host} --port {ServerUri.Port} --workerId {WorkerId} --requestId {RequestId} --grpcMaxMessageLength {MaxMessageLength} --functions-uri {ServerUri.AbsoluteUri} --functions-worker-id {WorkerId} --functions-request-id {RequestId} --functions-grpc-max-message-length {MaxMessageLength}"; } } } diff --git a/src/WebJobs.Script/Workers/Rpc/WebHostRpcWorkerChannelManager.cs b/src/WebJobs.Script/Workers/Rpc/WebHostRpcWorkerChannelManager.cs index af2863f7cc..8e0ce86d50 100644 --- a/src/WebJobs.Script/Workers/Rpc/WebHostRpcWorkerChannelManager.cs +++ b/src/WebJobs.Script/Workers/Rpc/WebHostRpcWorkerChannelManager.cs @@ -123,12 +123,14 @@ public async Task SpecializeAsync() if (_workerRuntime != null && rpcWorkerChannel != null) { + bool envReloadRequestResultSuccessful = false; if (UsePlaceholderChannel(rpcWorkerChannel)) { _logger.LogDebug("Loading environment variables for runtime: {runtime}", _workerRuntime); - await rpcWorkerChannel.SendFunctionEnvironmentReloadRequest(); + envReloadRequestResultSuccessful = await rpcWorkerChannel.SendFunctionEnvironmentReloadRequest(); } - else + + if (envReloadRequestResultSuccessful == false) { _logger.LogDebug("Shutting down placeholder worker. Worker is not compatible for runtime: {runtime}", _workerRuntime); // If we need to allow file edits, we should shutdown the webhost channel on specialization. @@ -181,6 +183,15 @@ private bool UsePlaceholderChannel(IRpcWorkerChannel channel) return false; } + // We support specialization of dotnet-isolated only on 64bit host process. + if (!_environment.Is64BitProcess) + { + _logger.LogInformation(new EventId(421, ScriptConstants.PlaceholderMissDueToBitnessEventName), + "This app is configured as 32-bit and therefore does not leverage all performance optimizations. See https://aka.ms/azure-functions/dotnet/placeholders for more information."); + + return false; + } + // Do not specialize if the placeholder is 6.0 but the site is 7.0 (for example). var currentWorkerRuntimeVersion = _environment.GetEnvironmentVariable(RpcWorkerConstants.FunctionWorkerRuntimeVersionSettingName); channel.WorkerProcess.Process.StartInfo.Environment.TryGetValue(RpcWorkerConstants.FunctionWorkerRuntimeVersionSettingName, out string placeholderWorkerRuntimeVersion); diff --git a/test/DotNetIsolated60/DotNetIsolated60.csproj b/test/DotNetIsolated60/DotNetIsolated60.csproj index 8e602a3cd7..f6a818fb17 100644 --- a/test/DotNetIsolated60/DotNetIsolated60.csproj +++ b/test/DotNetIsolated60/DotNetIsolated60.csproj @@ -6,13 +6,15 @@ enable enable True + True + True - - - + + + - + diff --git a/test/DotNetIsolated60/DotNetIsolated60.sln b/test/DotNetIsolated60/DotNetIsolated60.sln index 1f839fdf73..286c56b973 100644 --- a/test/DotNetIsolated60/DotNetIsolated60.sln +++ b/test/DotNetIsolated60/DotNetIsolated60.sln @@ -5,6 +5,8 @@ VisualStudioVersion = 17.5.33627.172 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetIsolated60", "DotNetIsolated60.csproj", "{1DA92227-F28E-408D-96B1-20C72571E4AE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetIsolatedUnsupportedWorker", "..\DotNetIsolatedUnsupportedWorker\DotNetIsolatedUnsupportedWorker.csproj", "{3F15B936-6365-447E-9EC6-4E996B30C55F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,6 +17,10 @@ Global {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 + {3F15B936-6365-447E-9EC6-4E996B30C55F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3F15B936-6365-447E-9EC6-4E996B30C55F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3F15B936-6365-447E-9EC6-4E996B30C55F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3F15B936-6365-447E-9EC6-4E996B30C55F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/test/DotNetIsolated60/Program.cs b/test/DotNetIsolated60/Program.cs index 2d2691d7d4..29dd670851 100644 --- a/test/DotNetIsolated60/Program.cs +++ b/test/DotNetIsolated60/Program.cs @@ -11,14 +11,12 @@ if (useProxy) { hostBuilder - .ConfigureFunctionsWebApplication() - .ConfigureGeneratedFunctionMetadataProvider(); + .ConfigureFunctionsWebApplication(); } else { hostBuilder - .ConfigureFunctionsWorkerDefaults() - .ConfigureGeneratedFunctionMetadataProvider(); + .ConfigureFunctionsWorkerDefaults(); } var host = hostBuilder.Build(); diff --git a/test/DotNetIsolatedUnsupportedWorker/DotNetIsolatedUnsupportedWorker.csproj b/test/DotNetIsolatedUnsupportedWorker/DotNetIsolatedUnsupportedWorker.csproj new file mode 100644 index 0000000000..81c3461eaf --- /dev/null +++ b/test/DotNetIsolatedUnsupportedWorker/DotNetIsolatedUnsupportedWorker.csproj @@ -0,0 +1,29 @@ + + + net6.0 + v4 + Exe + enable + enable + True + + + + + + + + + + + PreserveNewest + + + PreserveNewest + Never + + + + + + \ No newline at end of file diff --git a/test/DotNetIsolatedUnsupportedWorker/HttpRequestDataFunction.cs b/test/DotNetIsolatedUnsupportedWorker/HttpRequestDataFunction.cs new file mode 100644 index 0000000000..f494114440 --- /dev/null +++ b/test/DotNetIsolatedUnsupportedWorker/HttpRequestDataFunction.cs @@ -0,0 +1,30 @@ +using System.Net; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; + +namespace DotNetIsolatedUnsupportedWorker +{ + public class HttpRequestDataFunction + { + private readonly ILogger _logger; + + public HttpRequestDataFunction(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + + [Function("HttpRequestDataFunction")] + 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/DotNetIsolatedUnsupportedWorker/HttpRequestFunction.cs b/test/DotNetIsolatedUnsupportedWorker/HttpRequestFunction.cs new file mode 100644 index 0000000000..ba6df7f15b --- /dev/null +++ b/test/DotNetIsolatedUnsupportedWorker/HttpRequestFunction.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Extensions.Logging; + +namespace DotNetIsolatedUnsupportedWorker +{ + public class HttpRequestFunction + { + private readonly ILogger _logger; + + public HttpRequestFunction(ILogger logger) + { + _logger = logger; + } + + [Function(nameof(HttpRequestFunction))] + public Task Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequest req) + { + _logger.LogInformation("C# HTTP trigger function processed a request."); + return req.HttpContext.Response.WriteAsync("Welcome to Azure Functions!"); + } + } +} diff --git a/test/DotNetIsolatedUnsupportedWorker/Program.cs b/test/DotNetIsolatedUnsupportedWorker/Program.cs new file mode 100644 index 0000000000..29dd670851 --- /dev/null +++ b/test/DotNetIsolatedUnsupportedWorker/Program.cs @@ -0,0 +1,23 @@ +using Microsoft.Azure.Functions.Worker; +using Microsoft.Extensions.Hosting; + +//Debugger.Launch(); + +// Tests can set an env var that will swap this to use the proxy +bool useProxy = Environment.GetEnvironmentVariable("UseProxyInTest")?.Contains("1") ?? false; + +var hostBuilder = new HostBuilder(); + +if (useProxy) +{ + hostBuilder + .ConfigureFunctionsWebApplication(); +} +else +{ + hostBuilder + .ConfigureFunctionsWorkerDefaults(); +} + +var host = hostBuilder.Build(); +host.Run(); diff --git a/test/DotNetIsolatedUnsupportedWorker/QueueFunction.cs b/test/DotNetIsolatedUnsupportedWorker/QueueFunction.cs new file mode 100644 index 0000000000..69dd183369 --- /dev/null +++ b/test/DotNetIsolatedUnsupportedWorker/QueueFunction.cs @@ -0,0 +1,21 @@ +using Microsoft.Azure.Functions.Worker; +using Microsoft.Extensions.Logging; + +namespace DotNetIsolatedUnsupportedWorker +{ + public class QueueFunction + { + private readonly ILogger _logger; + + public QueueFunction(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + + [Function("QueueFunction")] + public void Run([QueueTrigger("myqueue-items", Connection = "AzureWebJobsStorage")] string myQueueItem) + { + _logger.LogInformation($"C# Queue trigger function processed: {myQueueItem}"); + } + } +} diff --git a/test/DotNetIsolatedUnsupportedWorker/host.json b/test/DotNetIsolatedUnsupportedWorker/host.json new file mode 100644 index 0000000000..ee5cf5f83f --- /dev/null +++ b/test/DotNetIsolatedUnsupportedWorker/host.json @@ -0,0 +1,12 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + }, + "enableLiveMetricsFilters": true + } + } +} \ No newline at end of file diff --git a/test/EmptyScriptRoot/host.json b/test/EmptyScriptRoot/host.json new file mode 100644 index 0000000000..55d16424d6 --- /dev/null +++ b/test/EmptyScriptRoot/host.json @@ -0,0 +1,7 @@ +{ + "version": "2.0", + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[4.*, 5.0.0)" + } +} \ No newline at end of file diff --git a/test/WebJobs.Script.Tests.Integration/WebHostEndToEnd/SpecializationE2ETests.cs b/test/WebJobs.Script.Tests.Integration/WebHostEndToEnd/SpecializationE2ETests.cs index ceb4160c8c..79ca16577d 100644 --- a/test/WebJobs.Script.Tests.Integration/WebHostEndToEnd/SpecializationE2ETests.cs +++ b/test/WebJobs.Script.Tests.Integration/WebHostEndToEnd/SpecializationE2ETests.cs @@ -51,6 +51,8 @@ public class SpecializationE2ETests 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 static readonly string _dotnetIsolatedUnsuppportedPath = Path.GetFullPath(@"..\..\..\..\DotNetIsolatedUnsupportedWorker\bin\Debug\net6.0"); + private static readonly string _dotnetIsolatedEmptyScriptRoot = Path.GetFullPath(@"..\..\..\..\EmptyScriptRoot"); private const string _specializedScriptRoot = @"TestScripts\CSharp"; @@ -799,7 +801,7 @@ public async Task Specialization_JobHostInternalStorageOptionsUpdatesWithActiveH [Fact] public async Task DotNetIsolated_PlaceholderHit() { - var builder = InitializeDotNetIsolatedPlaceholderBuilder("HttpRequestDataFunction"); + var builder = InitializeDotNetIsolatedPlaceholderBuilder(_dotnetIsolated60Path, "HttpRequestDataFunction"); using var testServer = new TestServer(builder); @@ -839,7 +841,7 @@ public async Task DotNetIsolated_PlaceholderHit_WithProxies() { // This test ensures that capabilities are correctly applied in EnvironmentReload during // specialization - var builder = InitializeDotNetIsolatedPlaceholderBuilder("HttpRequestFunction"); + var builder = InitializeDotNetIsolatedPlaceholderBuilder(_dotnetIsolated60Path, "HttpRequestFunction"); using var testServer = new TestServer(builder); @@ -888,19 +890,37 @@ public async Task DotNetIsolated_PlaceholderHit_WithProxies() public async Task DotNetIsolated_PlaceholderMiss_EnvVar() { // Placeholder miss if the WEBSITE_USE_PLACEHOLDER_DOTNETISOLATED env var is not set - await DotNetIsolatedPlaceholderMiss(); + await DotNetIsolatedPlaceholderMiss(_dotnetIsolated60Path); 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_Not64Bit() + { + _environment.SetProcessBitness(is64Bitness: false); + + // We only specialize when host process is 64 bit process. + await DotNetIsolatedPlaceholderMiss(_dotnetIsolated60Path, () => + { + _environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteUsePlaceholderDotNetIsolated, "1"); + _environment.SetEnvironmentVariable(RpcWorkerConstants.FunctionWorkerRuntimeVersionSettingName, "6.0"); + }); + + var log = _loggerProvider.GetLog(); + Assert.Contains("UsePlaceholderDotNetIsolated: True", log); + Assert.Contains("This app is configured as 32-bit and therefore does not leverage all performance optimizations. See https://aka.ms/azure-functions/dotnet/placeholders for more information.", 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(() => + await DotNetIsolatedPlaceholderMiss(_dotnetIsolated60Path, () => { _environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteUsePlaceholderDotNetIsolated, "1"); _environment.SetEnvironmentVariable(RpcWorkerConstants.FunctionWorkerRuntimeVersionSettingName, "7.0"); @@ -912,6 +932,36 @@ await DotNetIsolatedPlaceholderMiss(() => Assert.Contains("Shutting down placeholder worker. Worker is not compatible for runtime: dotnet-isolated", log); } + [Fact] + public async Task DotNetIsolated_PlaceholderMiss_UnsupportedWorkerPackage() + { + await DotNetIsolatedPlaceholderMiss(_dotnetIsolatedUnsuppportedPath, () => + { + _environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteUsePlaceholderDotNetIsolated, "1"); + _environment.SetEnvironmentVariable(RpcWorkerConstants.FunctionWorkerRuntimeVersionSettingName, "6.0"); + }); + + 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.Contains("Shutting down placeholder worker. Worker is not compatible for runtime: dotnet-isolated", log); + } + + [Fact] + public async Task DotNetIsolated_PlaceholderMiss_EmptyScriptRoot() + { + await DotNetIsolatedPlaceholderMiss(_dotnetIsolatedEmptyScriptRoot, () => + { + _environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteUsePlaceholderDotNetIsolated, "1"); + _environment.SetEnvironmentVariable(RpcWorkerConstants.FunctionWorkerRuntimeVersionSettingName, "6.0"); + }); + + 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.Contains("Shutting down placeholder worker. Worker is not compatible for runtime: dotnet-isolated", log); + } + [Fact] // Fix for https://github.com/Azure/azure-functions-host/issues/9288 public async Task SpecializedSite_StopsHostBeforeWorker() @@ -925,7 +975,7 @@ public async Task SpecializedSite_StopsHostBeforeWorker() await queue.CreateIfNotExistsAsync(); await queue.ClearAsync(); - var builder = InitializeDotNetIsolatedPlaceholderBuilder("HttpRequestDataFunction", "QueueFunction"); + var builder = InitializeDotNetIsolatedPlaceholderBuilder(_dotnetIsolated60Path, "HttpRequestDataFunction", "QueueFunction"); using var testServer = new TestServer(builder); @@ -987,9 +1037,9 @@ await TestHelpers.Await(() => Assert.Empty(completedLogs.Where(p => p.Level == LogLevel.Error)); } - private async Task DotNetIsolatedPlaceholderMiss(Action additionalSpecializedSetup = null) + private async Task DotNetIsolatedPlaceholderMiss(string scriptRootPath, Action additionalSpecializedSetup = null) { - var builder = InitializeDotNetIsolatedPlaceholderBuilder("HttpRequestDataFunction"); + var builder = InitializeDotNetIsolatedPlaceholderBuilder(scriptRootPath, "HttpRequestDataFunction"); // remove WEBSITE_USE_PLACEHOLDER_DOTNETISOLATED _environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteUsePlaceholderDotNetIsolated, null); @@ -1015,18 +1065,26 @@ private async Task DotNetIsolatedPlaceholderMiss(Action additionalSpecializedSet additionalSpecializedSetup?.Invoke(); response = await client.GetAsync("api/HttpRequestDataFunction"); - response.EnsureSuccessStatusCode(); + if (scriptRootPath == _dotnetIsolatedEmptyScriptRoot) + { + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + else + { + 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); + var expectedProcessName = scriptRootPath == _dotnetIsolated60Path ? "DotNetIsolated60" : "DotNetIsolatedUnsupported"; + // 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(expectedProcessName, 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() @@ -1035,7 +1093,7 @@ private static void BuildDotnetIsolated60() p.WaitForExit(); } - private IWebHostBuilder InitializeDotNetIsolatedPlaceholderBuilder(params string[] functions) + private IWebHostBuilder InitializeDotNetIsolatedPlaceholderBuilder(string scriptRootPath, params string[] functions) { BuildDotnetIsolated60(); @@ -1050,7 +1108,7 @@ private IWebHostBuilder InitializeDotNetIsolatedPlaceholderBuilder(params string { config.AddInMemoryCollection(new Dictionary { - { _scriptRootConfigPath, _dotnetIsolated60Path }, + { _scriptRootConfigPath, scriptRootPath }, }); }); diff --git a/test/WebJobs.Script.Tests.Integration/WebJobs.Script.Tests.Integration.csproj b/test/WebJobs.Script.Tests.Integration/WebJobs.Script.Tests.Integration.csproj index 72548c0bce..48052e5676 100644 --- a/test/WebJobs.Script.Tests.Integration/WebJobs.Script.Tests.Integration.csproj +++ b/test/WebJobs.Script.Tests.Integration/WebJobs.Script.Tests.Integration.csproj @@ -44,7 +44,7 @@ - + diff --git a/test/WebJobs.Script.Tests.Shared/TestEnvironment.cs b/test/WebJobs.Script.Tests.Shared/TestEnvironment.cs index 1bd73bea94..d8af0c5f26 100644 --- a/test/WebJobs.Script.Tests.Shared/TestEnvironment.cs +++ b/test/WebJobs.Script.Tests.Shared/TestEnvironment.cs @@ -12,17 +12,26 @@ namespace Microsoft.Azure.WebJobs.Script.Tests public class TestEnvironment : IEnvironment { private readonly IDictionary _variables; + private bool _is64BitProcess; public TestEnvironment() - : this(new Dictionary()) + : this(new Dictionary()) { } public TestEnvironment(IDictionary variables) + : this(variables, is64BitProcess: true) + { + } + + public TestEnvironment(IDictionary variables, bool is64BitProcess) { _variables = variables ?? throw new ArgumentNullException(nameof(variables)); + _is64BitProcess = is64BitProcess; } + public bool Is64BitProcess => _is64BitProcess; + public void Clear() { _variables.Clear(); @@ -54,5 +63,10 @@ public static TestEnvironment FromEnvironmentVariables() return new TestEnvironment(variables); } + + public void SetProcessBitness(bool is64Bitness) + { + _is64BitProcess = is64Bitness; + } } } diff --git a/test/WebJobs.Script.Tests/Workers/DefaultWorkerProcessFactoryTests.cs b/test/WebJobs.Script.Tests/Workers/DefaultWorkerProcessFactoryTests.cs index 2286612603..5952481759 100644 --- a/test/WebJobs.Script.Tests/Workers/DefaultWorkerProcessFactoryTests.cs +++ b/test/WebJobs.Script.Tests/Workers/DefaultWorkerProcessFactoryTests.cs @@ -82,7 +82,7 @@ public void DefaultWorkerProcessFactory_Returns_ExpectedProcess(WorkerContext wo } if (workerContext is RpcWorkerContext) { - Assert.Equal(" httpvalue1 TestVal httpvalue2 --host localhost --port 80 --workerId testWorkerId --requestId testId --grpcMaxMessageLength 2147483647", childProcess.StartInfo.Arguments); + Assert.Equal(" httpvalue1 TestVal httpvalue2 --host localhost --port 80 --workerId testWorkerId --requestId testId --grpcMaxMessageLength 2147483647 --functions-uri http://localhost/ --functions-worker-id testWorkerId --functions-request-id testId --functions-grpc-max-message-length 2147483647", childProcess.StartInfo.Arguments); } else { diff --git a/test/WebJobs.Script.Tests/Workers/Rpc/TestRpcWorkerChannel.cs b/test/WebJobs.Script.Tests/Workers/Rpc/TestRpcWorkerChannel.cs index c65e15d7fa..c7b6145565 100644 --- a/test/WebJobs.Script.Tests/Workers/Rpc/TestRpcWorkerChannel.cs +++ b/test/WebJobs.Script.Tests/Workers/Rpc/TestRpcWorkerChannel.cs @@ -74,10 +74,10 @@ public void SendFunctionLoadRequests(ManagedDependencyOptions managedDependencie _testLogger.LogInformation("RegisterFunctions called"); } - public Task SendFunctionEnvironmentReloadRequest() + public Task SendFunctionEnvironmentReloadRequest() { _testLogger.LogInformation("SendFunctionEnvironmentReloadRequest called"); - return Task.CompletedTask; + return Task.FromResult(true); } public void SendInvocationRequest(ScriptInvocationContext context)