Skip to content

Commit c006a08

Browse files
authored
Fallback behavior to ensure in-proc payload compatibility with dotnet-isolated as the FUNCTIONS_WORKER_RUNTIME value (#10439)
* Adding diagnostic warning/error when function worker runtime does not match with deployed payload * Making the new condition specific to "dotnet-isolated" worker to reduce impact. * Adding a test * PR feedback fixes * Missed release notes in last push * Removed an unnecessary code change. * Minor cleanup * Minor cleanup- made a method private and vairable renaming * Updating HostingConfig test to reflect the new hosting config property addition. * PR Feedback updates. * Fix test to reflect renaming changes from PR feedback.
1 parent ff0dea7 commit c006a08

File tree

9 files changed

+113
-7
lines changed

9 files changed

+113
-7
lines changed

release_notes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,6 @@
1818
- Worker termination path updated with sanitized logging (#10367)
1919
- Avoid redundant DiagnosticEvents error message (#10395)
2020
- Added logic to shim older versions of the .NET Worker JsonFunctionProvider to ensure backwards compatibility (#10410)
21+
- Added fallback behavior to ensure in-proc payload compatibility with "dotnet-isolated" as the `FUNCTIONS_WORKER_RUNTIME` value (#10439)
2122
- Migrated Scale Metrics to use `Azure.Data.Tables` SDK (#10276)
2223
- Added support for Identity-based connections

src/WebJobs.Script/Config/FunctionsHostingConfigOptions.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,14 @@ internal bool ThrowOnMissingFunctionsWorkerRuntime
183183
}
184184
}
185185

186+
internal bool WorkerRuntimeStrictValidationEnabled
187+
{
188+
get
189+
{
190+
return GetFeatureAsBooleanOrDefault(RpcWorkerConstants.WorkerRuntimeStrictValidationEnabled, false);
191+
}
192+
}
193+
186194
/// <summary>
187195
/// Gets feature by name.
188196
/// </summary>

src/WebJobs.Script/Diagnostics/DiagnosticEventConstants.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,8 @@ internal static class DiagnosticEventConstants
3131

3232
public const string NonHISSecretLoaded = "AZFD0012";
3333
public const string NonHISSecretLoadedHelpLink = "https://aka.ms/functions-non-his-secrets";
34+
35+
public const string WorkerRuntimeDoesNotMatchWithFunctionMetadataErrorCode = "AZFD0013";
36+
public const string WorkerRuntimeDoesNotMatchWithFunctionMetadataHelpLink = "https://aka.ms/functions-invalid-worker-runtime";
3437
}
3538
}

src/WebJobs.Script/Host/ScriptHost.cs

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -773,14 +773,50 @@ private void TrySetDirectType(FunctionMetadata metadata)
773773
}
774774
}
775775

776+
// Ensure customer deployed application payload matches with the worker runtime configured for the function app and log a warning if not.
777+
// 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
778+
// If there is a mismatch, the method will return false, else true.
779+
private static bool ValidateAndLogRuntimeMismatch(IEnumerable<FunctionMetadata> functionMetadata, string workerRuntime, IOptions<FunctionsHostingConfigOptions> hostingConfigOptions, ILogger logger)
780+
{
781+
if (functionMetadata != null && functionMetadata.Any() && !Utility.ContainsAnyFunctionMatchingWorkerRuntime(functionMetadata, workerRuntime))
782+
{
783+
var languages = string.Join(", ", functionMetadata.Select(f => f.Language).Distinct()).Replace(DotNetScriptTypes.DotNetAssembly, RpcWorkerConstants.DotNetLanguageWorkerName);
784+
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.";
785+
786+
if (hostingConfigOptions.Value.WorkerRuntimeStrictValidationEnabled)
787+
{
788+
logger.LogDiagnosticEventError(DiagnosticEventConstants.WorkerRuntimeDoesNotMatchWithFunctionMetadataErrorCode, baseMessage, DiagnosticEventConstants.WorkerRuntimeDoesNotMatchWithFunctionMetadataHelpLink, null);
789+
throw new HostInitializationException(baseMessage);
790+
}
791+
792+
var warningMessage = baseMessage + " The application will continue to run, but may throw an exception in the future.";
793+
logger.LogDiagnosticEventWarning(DiagnosticEventConstants.WorkerRuntimeDoesNotMatchWithFunctionMetadataErrorCode, warningMessage, DiagnosticEventConstants.WorkerRuntimeDoesNotMatchWithFunctionMetadataHelpLink, null);
794+
return false;
795+
}
796+
797+
return true;
798+
}
799+
776800
internal async Task<Collection<FunctionDescriptor>> GetFunctionDescriptorsAsync(IEnumerable<FunctionMetadata> functions, IEnumerable<FunctionDescriptorProvider> descriptorProviders, string workerRuntime, CancellationToken cancellationToken)
777801
{
778802
Collection<FunctionDescriptor> functionDescriptors = new Collection<FunctionDescriptor>();
779803
if (!cancellationToken.IsCancellationRequested)
780804
{
805+
bool throwOnWorkerRuntimeAndPayloadMetadataMismatch = true;
806+
// this dotnet isolated specific logic is temporary to ensure in-proc payload compatibility with "dotnet-isolated" as the FUNCTIONS_WORKER_RUNTIME value.
807+
if (string.Equals(workerRuntime, RpcWorkerConstants.DotNetIsolatedLanguageWorkerName, StringComparison.OrdinalIgnoreCase))
808+
{
809+
bool payloadMatchesWorkerRuntime = ValidateAndLogRuntimeMismatch(functions, workerRuntime, _hostingConfigOptions, _logger);
810+
if (!payloadMatchesWorkerRuntime)
811+
{
812+
UpdateFunctionMetadataLanguageForDotnetAssembly(functions, workerRuntime);
813+
throwOnWorkerRuntimeAndPayloadMetadataMismatch = false; // we do not want to throw an exception in this case
814+
}
815+
}
816+
781817
var httpFunctions = new Dictionary<string, HttpTriggerAttribute>();
782818

783-
Utility.VerifyFunctionsMatchSpecifiedLanguage(functions, workerRuntime, _environment.IsPlaceholderModeEnabled(), _isHttpWorker, cancellationToken);
819+
Utility.VerifyFunctionsMatchSpecifiedLanguage(functions, workerRuntime, _environment.IsPlaceholderModeEnabled(), _isHttpWorker, cancellationToken, throwOnMismatch: throwOnWorkerRuntimeAndPayloadMetadataMismatch);
784820

785821
foreach (FunctionMetadata metadata in functions)
786822
{
@@ -819,6 +855,17 @@ internal async Task<Collection<FunctionDescriptor>> GetFunctionDescriptorsAsync(
819855
return functionDescriptors;
820856
}
821857

858+
private static void UpdateFunctionMetadataLanguageForDotnetAssembly(IEnumerable<FunctionMetadata> functions, string workerRuntime)
859+
{
860+
foreach (var function in functions)
861+
{
862+
if (function.Language == DotNetScriptTypes.DotNetAssembly)
863+
{
864+
function.Language = workerRuntime;
865+
}
866+
}
867+
}
868+
822869
internal static void ValidateFunction(FunctionDescriptor function, Dictionary<string, HttpTriggerAttribute> httpFunctions, IEnvironment environment)
823870
{
824871
var httpTrigger = function.HttpTriggerAttribute;

src/WebJobs.Script/Utility.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -629,7 +629,7 @@ internal static bool TryReadFunctionConfig(string scriptDir, out string json, IF
629629
return true;
630630
}
631631

632-
internal static void VerifyFunctionsMatchSpecifiedLanguage(IEnumerable<FunctionMetadata> functions, string workerRuntime, bool isPlaceholderMode, bool isHttpWorker, CancellationToken cancellationToken)
632+
internal static void VerifyFunctionsMatchSpecifiedLanguage(IEnumerable<FunctionMetadata> functions, string workerRuntime, bool isPlaceholderMode, bool isHttpWorker, CancellationToken cancellationToken, bool throwOnMismatch = true)
633633
{
634634
cancellationToken.ThrowIfCancellationRequested();
635635

@@ -646,7 +646,10 @@ internal static void VerifyFunctionsMatchSpecifiedLanguage(IEnumerable<FunctionM
646646
}
647647
else
648648
{
649-
throw new HostInitializationException($"Did not find functions with language [{workerRuntime}].");
649+
if (throwOnMismatch)
650+
{
651+
throw new HostInitializationException($"Did not find functions with language [{workerRuntime}].");
652+
}
650653
}
651654
}
652655
}
@@ -748,10 +751,20 @@ private static bool ContainsFunctionWithWorkerRuntime(IEnumerable<FunctionMetada
748751
{
749752
return functions.Any(f => dotNetLanguages.Any(l => l.Equals(f.Language, StringComparison.OrdinalIgnoreCase)));
750753
}
754+
755+
return ContainsAnyFunctionMatchingWorkerRuntime(functions, workerRuntime);
756+
}
757+
758+
/// <summary>
759+
/// Inspect the functions metadata to determine if at least one function is of the specified worker runtime.
760+
/// </summary>
761+
internal static bool ContainsAnyFunctionMatchingWorkerRuntime(IEnumerable<FunctionMetadata> functions, string workerRuntime)
762+
{
751763
if (functions != null && functions.Any())
752764
{
753765
return functions.Any(f => !string.IsNullOrEmpty(f.Language) && f.Language.Equals(workerRuntime, StringComparison.OrdinalIgnoreCase));
754766
}
767+
755768
return false;
756769
}
757770

src/WebJobs.Script/Workers/Rpc/FunctionRegistration/RpcFunctionInvocationDispatcher.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ internal class RpcFunctionInvocationDispatcher : IFunctionInvocationDispatcher
4040
private readonly Lazy<Task<int>> _maxProcessCount;
4141
private readonly IOptions<FunctionsHostingConfigOptions> _hostingConfigOptions;
4242
private readonly IHostMetrics _hostMetrics;
43+
private readonly TimeSpan _defaultProcessStartupInterval = TimeSpan.FromSeconds(5);
44+
private readonly TimeSpan _defaultProcessRestartInterval = TimeSpan.FromSeconds(5);
45+
private readonly TimeSpan _defaultProcessShutdownInterval = TimeSpan.FromSeconds(5);
4346

4447
private IScriptEventManager _eventManager;
4548
private IWebHostRpcWorkerChannelManager _webHostLanguageWorkerChannelManager;
@@ -308,9 +311,9 @@ public async Task InitializeAsync(IEnumerable<FunctionMetadata> functions, Cance
308311
}
309312
else
310313
{
311-
_processStartupInterval = workerConfig.CountOptions.ProcessStartupInterval;
312-
_restartWait = workerConfig.CountOptions.ProcessRestartInterval;
313-
_shutdownTimeout = workerConfig.CountOptions.ProcessShutdownTimeout;
314+
_processStartupInterval = workerConfig?.CountOptions?.ProcessStartupInterval ?? _defaultProcessStartupInterval;
315+
_restartWait = workerConfig?.CountOptions.ProcessRestartInterval ?? _defaultProcessRestartInterval;
316+
_shutdownTimeout = workerConfig?.CountOptions.ProcessShutdownTimeout ?? _defaultProcessShutdownInterval;
314317
}
315318
ErrorEventsThreshold = 3 * await _maxProcessCount.Value;
316319

src/WebJobs.Script/Workers/Rpc/RpcWorkerConstants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,5 +93,6 @@ public static class RpcWorkerConstants
9393
public const string RevertWorkerShutdownBehavior = "REVERT_WORKER_SHUTDOWN_BEHAVIOR";
9494
public const string ShutdownWebhostWorkerChannelsOnHostShutdown = "ShutdownWebhostWorkerChannelsOnHostShutdown";
9595
public const string ThrowOnMissingFunctionsWorkerRuntime = "THROW_ON_MISSING_FUNCTIONS_WORKER_RUNTIME";
96+
public const string WorkerRuntimeStrictValidationEnabled = "WORKER_RUNTIME_STRICT_VALIDATION_ENABLED";
9697
}
9798
}

test/WebJobs.Script.Tests.Integration/WebHostEndToEnd/WebJobsStartupEndToEndTests.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Net;
44
using System.Threading.Tasks;
55
using Microsoft.Azure.WebJobs.Logging;
6+
using Microsoft.Azure.WebJobs.Script.Workers.Rpc;
67
using Microsoft.Extensions.DependencyInjection;
78
using Xunit;
89

@@ -45,6 +46,34 @@ public async Task ExternalStartup_Succeeds()
4546
}
4647
}
4748

49+
[Fact]
50+
public async Task InProcAppsWorkWithDotnetIsolatedAsFunctionWorkerRuntimeValue()
51+
{
52+
// test uses an in-proc app, but we are setting "dotnet-isolated" as functions worker runtime value.
53+
var fixture = new CSharpPrecompiledEndToEndTestFixture(_projectName, _envVars, functionWorkerRuntime: RpcWorkerConstants.DotNetIsolatedLanguageWorkerName);
54+
try
55+
{
56+
await fixture.InitializeAsync();
57+
var client = fixture.Host.HttpClient;
58+
59+
var response = await client.GetAsync($"api/Function1");
60+
61+
// The function does all the validation internally.
62+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
63+
64+
const string expectedLogEntry =
65+
"The 'FUNCTIONS_WORKER_RUNTIME' is set to 'dotnet-isolated', " +
66+
"which does not match the worker runtime metadata found in the deployed function app artifacts. " +
67+
"The deployed artifacts are for 'dotnet'. See https://aka.ms/functions-invalid-worker-runtime " +
68+
"for more information. The application will continue to run, but may throw an exception in the future.";
69+
Assert.Single(fixture.Host.GetScriptHostLogMessages(), p => p.FormattedMessage != null && p.FormattedMessage.EndsWith(expectedLogEntry));
70+
}
71+
finally
72+
{
73+
await fixture.DisposeAsync();
74+
}
75+
}
76+
4877
[Fact]
4978
public async Task ExternalStartup_InvalidOverwrite_StopsHost()
5079
{

test/WebJobs.Script.Tests/Configuration/FunctionsHostingConfigOptionsTest.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,8 @@ public void Property_Validation()
138138

139139
(nameof(FunctionsHostingConfigOptions.ThrowOnMissingFunctionsWorkerRuntime), "THROW_ON_MISSING_FUNCTIONS_WORKER_RUNTIME=1", true),
140140
(nameof(FunctionsHostingConfigOptions.WorkerIndexingDisabledApps), "WORKER_INDEXING_DISABLED_APPS=teststring", "teststring"),
141-
(nameof(FunctionsHostingConfigOptions.WorkerIndexingEnabled), "WORKER_INDEXING_ENABLED=1", true)
141+
(nameof(FunctionsHostingConfigOptions.WorkerIndexingEnabled), "WORKER_INDEXING_ENABLED=1", true),
142+
(nameof(FunctionsHostingConfigOptions.WorkerRuntimeStrictValidationEnabled), "WORKER_RUNTIME_STRICT_VALIDATION_ENABLED=1", true)
142143
};
143144

144145
// use reflection to ensure that we have a test that uses every value exposed on FunctionsHostingConfigOptions

0 commit comments

Comments
 (0)