Skip to content

Commit bb017c5

Browse files
authored
Fallback behavior to ensure in-proc payload compatibility with dotnet-isolated as the FUNCTIONS_WORKER_RUNTIME value (#10439) (#10449)
* 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 f5b03de commit bb017c5

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
@@ -16,5 +16,6 @@
1616
- Resolved thread safety issue in the `GrpcWorkerChannel.LoadResponse` method. (#10352)
1717
- Worker termination path updated with sanitized logging (#10397)
1818
- Avoid redundant DiagnosticEvents error message (#10395)
19+
- Added fallback behavior to ensure in-proc payload compatibility with "dotnet-isolated" as the `FUNCTIONS_WORKER_RUNTIME` value (#10439)
1920
- Migrated Scale Metrics to use `Azure.Data.Tables` SDK (#10276)
2021
- 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
@@ -758,14 +758,50 @@ private void TrySetDirectType(FunctionMetadata metadata)
758758
}
759759
}
760760

761+
// Ensure customer deployed application payload matches with the worker runtime configured for the function app and log a warning if not.
762+
// 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
763+
// If there is a mismatch, the method will return false, else true.
764+
private static bool ValidateAndLogRuntimeMismatch(IEnumerable<FunctionMetadata> functionMetadata, string workerRuntime, IOptions<FunctionsHostingConfigOptions> hostingConfigOptions, ILogger logger)
765+
{
766+
if (functionMetadata != null && functionMetadata.Any() && !Utility.ContainsAnyFunctionMatchingWorkerRuntime(functionMetadata, workerRuntime))
767+
{
768+
var languages = string.Join(", ", functionMetadata.Select(f => f.Language).Distinct()).Replace(DotNetScriptTypes.DotNetAssembly, RpcWorkerConstants.DotNetLanguageWorkerName);
769+
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.";
770+
771+
if (hostingConfigOptions.Value.WorkerRuntimeStrictValidationEnabled)
772+
{
773+
logger.LogDiagnosticEventError(DiagnosticEventConstants.WorkerRuntimeDoesNotMatchWithFunctionMetadataErrorCode, baseMessage, DiagnosticEventConstants.WorkerRuntimeDoesNotMatchWithFunctionMetadataHelpLink, null);
774+
throw new HostInitializationException(baseMessage);
775+
}
776+
777+
var warningMessage = baseMessage + " The application will continue to run, but may throw an exception in the future.";
778+
logger.LogDiagnosticEventWarning(DiagnosticEventConstants.WorkerRuntimeDoesNotMatchWithFunctionMetadataErrorCode, warningMessage, DiagnosticEventConstants.WorkerRuntimeDoesNotMatchWithFunctionMetadataHelpLink, null);
779+
return false;
780+
}
781+
782+
return true;
783+
}
784+
761785
internal async Task<Collection<FunctionDescriptor>> GetFunctionDescriptorsAsync(IEnumerable<FunctionMetadata> functions, IEnumerable<FunctionDescriptorProvider> descriptorProviders, string workerRuntime, CancellationToken cancellationToken)
762786
{
763787
Collection<FunctionDescriptor> functionDescriptors = new Collection<FunctionDescriptor>();
764788
if (!cancellationToken.IsCancellationRequested)
765789
{
790+
bool throwOnWorkerRuntimeAndPayloadMetadataMismatch = true;
791+
// this dotnet isolated specific logic is temporary to ensure in-proc payload compatibility with "dotnet-isolated" as the FUNCTIONS_WORKER_RUNTIME value.
792+
if (string.Equals(workerRuntime, RpcWorkerConstants.DotNetIsolatedLanguageWorkerName, StringComparison.OrdinalIgnoreCase))
793+
{
794+
bool payloadMatchesWorkerRuntime = ValidateAndLogRuntimeMismatch(functions, workerRuntime, _hostingConfigOptions, _logger);
795+
if (!payloadMatchesWorkerRuntime)
796+
{
797+
UpdateFunctionMetadataLanguageForDotnetAssembly(functions, workerRuntime);
798+
throwOnWorkerRuntimeAndPayloadMetadataMismatch = false; // we do not want to throw an exception in this case
799+
}
800+
}
801+
766802
var httpFunctions = new Dictionary<string, HttpTriggerAttribute>();
767803

768-
Utility.VerifyFunctionsMatchSpecifiedLanguage(functions, workerRuntime, _environment.IsPlaceholderModeEnabled(), _isHttpWorker, cancellationToken);
804+
Utility.VerifyFunctionsMatchSpecifiedLanguage(functions, workerRuntime, _environment.IsPlaceholderModeEnabled(), _isHttpWorker, cancellationToken, throwOnMismatch: throwOnWorkerRuntimeAndPayloadMetadataMismatch);
769805

770806
foreach (FunctionMetadata metadata in functions)
771807
{
@@ -804,6 +840,17 @@ internal async Task<Collection<FunctionDescriptor>> GetFunctionDescriptorsAsync(
804840
return functionDescriptors;
805841
}
806842

843+
private static void UpdateFunctionMetadataLanguageForDotnetAssembly(IEnumerable<FunctionMetadata> functions, string workerRuntime)
844+
{
845+
foreach (var function in functions)
846+
{
847+
if (function.Language == DotNetScriptTypes.DotNetAssembly)
848+
{
849+
function.Language = workerRuntime;
850+
}
851+
}
852+
}
853+
807854
internal static void ValidateFunction(FunctionDescriptor function, Dictionary<string, HttpTriggerAttribute> httpFunctions, IEnvironment environment)
808855
{
809856
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
@@ -38,6 +38,9 @@ internal class RpcFunctionInvocationDispatcher : IFunctionInvocationDispatcher
3838
private readonly IEnumerable<RpcWorkerConfig> _workerConfigs;
3939
private readonly Lazy<Task<int>> _maxProcessCount;
4040
private readonly IOptions<FunctionsHostingConfigOptions> _hostingConfigOptions;
41+
private readonly TimeSpan _defaultProcessStartupInterval = TimeSpan.FromSeconds(5);
42+
private readonly TimeSpan _defaultProcessRestartInterval = TimeSpan.FromSeconds(5);
43+
private readonly TimeSpan _defaultProcessShutdownInterval = TimeSpan.FromSeconds(5);
4144

4245
private IScriptEventManager _eventManager;
4346
private IWebHostRpcWorkerChannelManager _webHostLanguageWorkerChannelManager;
@@ -304,9 +307,9 @@ public async Task InitializeAsync(IEnumerable<FunctionMetadata> functions, Cance
304307
}
305308
else
306309
{
307-
_processStartupInterval = workerConfig.CountOptions.ProcessStartupInterval;
308-
_restartWait = workerConfig.CountOptions.ProcessRestartInterval;
309-
_shutdownTimeout = workerConfig.CountOptions.ProcessShutdownTimeout;
310+
_processStartupInterval = workerConfig?.CountOptions?.ProcessStartupInterval ?? _defaultProcessStartupInterval;
311+
_restartWait = workerConfig?.CountOptions.ProcessRestartInterval ?? _defaultProcessRestartInterval;
312+
_shutdownTimeout = workerConfig?.CountOptions.ProcessShutdownTimeout ?? _defaultProcessShutdownInterval;
310313
}
311314
ErrorEventsThreshold = 3 * await _maxProcessCount.Value;
312315

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

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

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)