diff --git a/release_notes.md b/release_notes.md index df9fc9ab68..68af6f153a 100644 --- a/release_notes.md +++ b/release_notes.md @@ -18,5 +18,6 @@ - Worker termination path updated with sanitized logging (#10367) - Avoid redundant DiagnosticEvents error message (#10395) - Added logic to shim older versions of the .NET Worker JsonFunctionProvider to ensure backwards compatibility (#10410) +- Added fallback behavior to ensure in-proc payload compatibility with "dotnet-isolated" as the `FUNCTIONS_WORKER_RUNTIME` value (#10439) - Migrated Scale Metrics to use `Azure.Data.Tables` SDK (#10276) - Added support for Identity-based connections \ No newline at end of file diff --git a/src/WebJobs.Script/Config/FunctionsHostingConfigOptions.cs b/src/WebJobs.Script/Config/FunctionsHostingConfigOptions.cs index 844eb0992c..9e15db85fb 100644 --- a/src/WebJobs.Script/Config/FunctionsHostingConfigOptions.cs +++ b/src/WebJobs.Script/Config/FunctionsHostingConfigOptions.cs @@ -183,6 +183,14 @@ internal bool ThrowOnMissingFunctionsWorkerRuntime } } + internal bool WorkerRuntimeStrictValidationEnabled + { + get + { + return GetFeatureAsBooleanOrDefault(RpcWorkerConstants.WorkerRuntimeStrictValidationEnabled, false); + } + } + /// /// Gets feature by name. /// diff --git a/src/WebJobs.Script/Diagnostics/DiagnosticEventConstants.cs b/src/WebJobs.Script/Diagnostics/DiagnosticEventConstants.cs index f150a705a6..2669a22a6b 100644 --- a/src/WebJobs.Script/Diagnostics/DiagnosticEventConstants.cs +++ b/src/WebJobs.Script/Diagnostics/DiagnosticEventConstants.cs @@ -31,5 +31,8 @@ internal static class DiagnosticEventConstants public const string NonHISSecretLoaded = "AZFD0012"; public const string NonHISSecretLoadedHelpLink = "https://aka.ms/functions-non-his-secrets"; + + public const string WorkerRuntimeDoesNotMatchWithFunctionMetadataErrorCode = "AZFD0013"; + public const string WorkerRuntimeDoesNotMatchWithFunctionMetadataHelpLink = "https://aka.ms/functions-invalid-worker-runtime"; } } diff --git a/src/WebJobs.Script/Host/ScriptHost.cs b/src/WebJobs.Script/Host/ScriptHost.cs index dee3061b5f..9d4e5d2e68 100644 --- a/src/WebJobs.Script/Host/ScriptHost.cs +++ b/src/WebJobs.Script/Host/ScriptHost.cs @@ -773,14 +773,50 @@ private void TrySetDirectType(FunctionMetadata metadata) } } + // Ensure customer deployed application payload matches with the worker runtime configured for the function app and log a warning if not. + // If a customer has "dotnet-isolated" worker runtime configured for the function app, and then they deploy an in-proc app payload, this will warn/error + // If there is a mismatch, the method will return false, else true. + private static bool ValidateAndLogRuntimeMismatch(IEnumerable functionMetadata, string workerRuntime, IOptions hostingConfigOptions, ILogger logger) + { + if (functionMetadata != null && functionMetadata.Any() && !Utility.ContainsAnyFunctionMatchingWorkerRuntime(functionMetadata, workerRuntime)) + { + var languages = string.Join(", ", functionMetadata.Select(f => f.Language).Distinct()).Replace(DotNetScriptTypes.DotNetAssembly, RpcWorkerConstants.DotNetLanguageWorkerName); + var baseMessage = $"The '{EnvironmentSettingNames.FunctionWorkerRuntime}' is set to '{workerRuntime}', which does not match the worker runtime metadata found in the deployed function app artifacts. The deployed artifacts are for '{languages}'. See {DiagnosticEventConstants.WorkerRuntimeDoesNotMatchWithFunctionMetadataHelpLink} for more information."; + + if (hostingConfigOptions.Value.WorkerRuntimeStrictValidationEnabled) + { + logger.LogDiagnosticEventError(DiagnosticEventConstants.WorkerRuntimeDoesNotMatchWithFunctionMetadataErrorCode, baseMessage, DiagnosticEventConstants.WorkerRuntimeDoesNotMatchWithFunctionMetadataHelpLink, null); + throw new HostInitializationException(baseMessage); + } + + var warningMessage = baseMessage + " The application will continue to run, but may throw an exception in the future."; + logger.LogDiagnosticEventWarning(DiagnosticEventConstants.WorkerRuntimeDoesNotMatchWithFunctionMetadataErrorCode, warningMessage, DiagnosticEventConstants.WorkerRuntimeDoesNotMatchWithFunctionMetadataHelpLink, null); + return false; + } + + return true; + } + internal async Task> GetFunctionDescriptorsAsync(IEnumerable functions, IEnumerable descriptorProviders, string workerRuntime, CancellationToken cancellationToken) { Collection functionDescriptors = new Collection(); if (!cancellationToken.IsCancellationRequested) { + bool throwOnWorkerRuntimeAndPayloadMetadataMismatch = true; + // this dotnet isolated specific logic is temporary to ensure in-proc payload compatibility with "dotnet-isolated" as the FUNCTIONS_WORKER_RUNTIME value. + if (string.Equals(workerRuntime, RpcWorkerConstants.DotNetIsolatedLanguageWorkerName, StringComparison.OrdinalIgnoreCase)) + { + bool payloadMatchesWorkerRuntime = ValidateAndLogRuntimeMismatch(functions, workerRuntime, _hostingConfigOptions, _logger); + if (!payloadMatchesWorkerRuntime) + { + UpdateFunctionMetadataLanguageForDotnetAssembly(functions, workerRuntime); + throwOnWorkerRuntimeAndPayloadMetadataMismatch = false; // we do not want to throw an exception in this case + } + } + var httpFunctions = new Dictionary(); - Utility.VerifyFunctionsMatchSpecifiedLanguage(functions, workerRuntime, _environment.IsPlaceholderModeEnabled(), _isHttpWorker, cancellationToken); + Utility.VerifyFunctionsMatchSpecifiedLanguage(functions, workerRuntime, _environment.IsPlaceholderModeEnabled(), _isHttpWorker, cancellationToken, throwOnMismatch: throwOnWorkerRuntimeAndPayloadMetadataMismatch); foreach (FunctionMetadata metadata in functions) { @@ -819,6 +855,17 @@ internal async Task> GetFunctionDescriptorsAsync( return functionDescriptors; } + private static void UpdateFunctionMetadataLanguageForDotnetAssembly(IEnumerable functions, string workerRuntime) + { + foreach (var function in functions) + { + if (function.Language == DotNetScriptTypes.DotNetAssembly) + { + function.Language = workerRuntime; + } + } + } + internal static void ValidateFunction(FunctionDescriptor function, Dictionary httpFunctions, IEnvironment environment) { var httpTrigger = function.HttpTriggerAttribute; diff --git a/src/WebJobs.Script/Utility.cs b/src/WebJobs.Script/Utility.cs index 8c51c4a78e..8297f85ab8 100644 --- a/src/WebJobs.Script/Utility.cs +++ b/src/WebJobs.Script/Utility.cs @@ -629,7 +629,7 @@ internal static bool TryReadFunctionConfig(string scriptDir, out string json, IF return true; } - internal static void VerifyFunctionsMatchSpecifiedLanguage(IEnumerable functions, string workerRuntime, bool isPlaceholderMode, bool isHttpWorker, CancellationToken cancellationToken) + internal static void VerifyFunctionsMatchSpecifiedLanguage(IEnumerable functions, string workerRuntime, bool isPlaceholderMode, bool isHttpWorker, CancellationToken cancellationToken, bool throwOnMismatch = true) { cancellationToken.ThrowIfCancellationRequested(); @@ -646,7 +646,10 @@ internal static void VerifyFunctionsMatchSpecifiedLanguage(IEnumerable dotNetLanguages.Any(l => l.Equals(f.Language, StringComparison.OrdinalIgnoreCase))); } + + return ContainsAnyFunctionMatchingWorkerRuntime(functions, workerRuntime); + } + + /// + /// Inspect the functions metadata to determine if at least one function is of the specified worker runtime. + /// + internal static bool ContainsAnyFunctionMatchingWorkerRuntime(IEnumerable functions, string workerRuntime) + { if (functions != null && functions.Any()) { return functions.Any(f => !string.IsNullOrEmpty(f.Language) && f.Language.Equals(workerRuntime, StringComparison.OrdinalIgnoreCase)); } + return false; } diff --git a/src/WebJobs.Script/Workers/Rpc/FunctionRegistration/RpcFunctionInvocationDispatcher.cs b/src/WebJobs.Script/Workers/Rpc/FunctionRegistration/RpcFunctionInvocationDispatcher.cs index a4de3c1012..af22114a1b 100644 --- a/src/WebJobs.Script/Workers/Rpc/FunctionRegistration/RpcFunctionInvocationDispatcher.cs +++ b/src/WebJobs.Script/Workers/Rpc/FunctionRegistration/RpcFunctionInvocationDispatcher.cs @@ -40,6 +40,9 @@ internal class RpcFunctionInvocationDispatcher : IFunctionInvocationDispatcher private readonly Lazy> _maxProcessCount; private readonly IOptions _hostingConfigOptions; private readonly IHostMetrics _hostMetrics; + private readonly TimeSpan _defaultProcessStartupInterval = TimeSpan.FromSeconds(5); + private readonly TimeSpan _defaultProcessRestartInterval = TimeSpan.FromSeconds(5); + private readonly TimeSpan _defaultProcessShutdownInterval = TimeSpan.FromSeconds(5); private IScriptEventManager _eventManager; private IWebHostRpcWorkerChannelManager _webHostLanguageWorkerChannelManager; @@ -308,9 +311,9 @@ public async Task InitializeAsync(IEnumerable functions, Cance } else { - _processStartupInterval = workerConfig.CountOptions.ProcessStartupInterval; - _restartWait = workerConfig.CountOptions.ProcessRestartInterval; - _shutdownTimeout = workerConfig.CountOptions.ProcessShutdownTimeout; + _processStartupInterval = workerConfig?.CountOptions?.ProcessStartupInterval ?? _defaultProcessStartupInterval; + _restartWait = workerConfig?.CountOptions.ProcessRestartInterval ?? _defaultProcessRestartInterval; + _shutdownTimeout = workerConfig?.CountOptions.ProcessShutdownTimeout ?? _defaultProcessShutdownInterval; } ErrorEventsThreshold = 3 * await _maxProcessCount.Value; diff --git a/src/WebJobs.Script/Workers/Rpc/RpcWorkerConstants.cs b/src/WebJobs.Script/Workers/Rpc/RpcWorkerConstants.cs index daa710cda9..b7a41b980b 100644 --- a/src/WebJobs.Script/Workers/Rpc/RpcWorkerConstants.cs +++ b/src/WebJobs.Script/Workers/Rpc/RpcWorkerConstants.cs @@ -93,5 +93,6 @@ public static class RpcWorkerConstants public const string RevertWorkerShutdownBehavior = "REVERT_WORKER_SHUTDOWN_BEHAVIOR"; public const string ShutdownWebhostWorkerChannelsOnHostShutdown = "ShutdownWebhostWorkerChannelsOnHostShutdown"; public const string ThrowOnMissingFunctionsWorkerRuntime = "THROW_ON_MISSING_FUNCTIONS_WORKER_RUNTIME"; + public const string WorkerRuntimeStrictValidationEnabled = "WORKER_RUNTIME_STRICT_VALIDATION_ENABLED"; } } \ No newline at end of file diff --git a/test/WebJobs.Script.Tests.Integration/WebHostEndToEnd/WebJobsStartupEndToEndTests.cs b/test/WebJobs.Script.Tests.Integration/WebHostEndToEnd/WebJobsStartupEndToEndTests.cs index 699f911b9a..2f5ffd7054 100644 --- a/test/WebJobs.Script.Tests.Integration/WebHostEndToEnd/WebJobsStartupEndToEndTests.cs +++ b/test/WebJobs.Script.Tests.Integration/WebHostEndToEnd/WebJobsStartupEndToEndTests.cs @@ -3,6 +3,7 @@ using System.Net; using System.Threading.Tasks; using Microsoft.Azure.WebJobs.Logging; +using Microsoft.Azure.WebJobs.Script.Workers.Rpc; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -45,6 +46,34 @@ public async Task ExternalStartup_Succeeds() } } + [Fact] + public async Task InProcAppsWorkWithDotnetIsolatedAsFunctionWorkerRuntimeValue() + { + // test uses an in-proc app, but we are setting "dotnet-isolated" as functions worker runtime value. + var fixture = new CSharpPrecompiledEndToEndTestFixture(_projectName, _envVars, functionWorkerRuntime: RpcWorkerConstants.DotNetIsolatedLanguageWorkerName); + try + { + await fixture.InitializeAsync(); + var client = fixture.Host.HttpClient; + + var response = await client.GetAsync($"api/Function1"); + + // The function does all the validation internally. + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + const string expectedLogEntry = + "The 'FUNCTIONS_WORKER_RUNTIME' is set to 'dotnet-isolated', " + + "which does not match the worker runtime metadata found in the deployed function app artifacts. " + + "The deployed artifacts are for 'dotnet'. See https://aka.ms/functions-invalid-worker-runtime " + + "for more information. The application will continue to run, but may throw an exception in the future."; + Assert.Single(fixture.Host.GetScriptHostLogMessages(), p => p.FormattedMessage != null && p.FormattedMessage.EndsWith(expectedLogEntry)); + } + finally + { + await fixture.DisposeAsync(); + } + } + [Fact] public async Task ExternalStartup_InvalidOverwrite_StopsHost() { diff --git a/test/WebJobs.Script.Tests/Configuration/FunctionsHostingConfigOptionsTest.cs b/test/WebJobs.Script.Tests/Configuration/FunctionsHostingConfigOptionsTest.cs index 20259e7612..55fcb3b80e 100644 --- a/test/WebJobs.Script.Tests/Configuration/FunctionsHostingConfigOptionsTest.cs +++ b/test/WebJobs.Script.Tests/Configuration/FunctionsHostingConfigOptionsTest.cs @@ -138,7 +138,8 @@ public void Property_Validation() (nameof(FunctionsHostingConfigOptions.ThrowOnMissingFunctionsWorkerRuntime), "THROW_ON_MISSING_FUNCTIONS_WORKER_RUNTIME=1", true), (nameof(FunctionsHostingConfigOptions.WorkerIndexingDisabledApps), "WORKER_INDEXING_DISABLED_APPS=teststring", "teststring"), - (nameof(FunctionsHostingConfigOptions.WorkerIndexingEnabled), "WORKER_INDEXING_ENABLED=1", true) + (nameof(FunctionsHostingConfigOptions.WorkerIndexingEnabled), "WORKER_INDEXING_ENABLED=1", true), + (nameof(FunctionsHostingConfigOptions.WorkerRuntimeStrictValidationEnabled), "WORKER_RUNTIME_STRICT_VALIDATION_ENABLED=1", true) }; // use reflection to ensure that we have a test that uses every value exposed on FunctionsHostingConfigOptions