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