Skip to content

Commit c2f4e50

Browse files
Warn if .azurefunctions folder does not exist (#10967)
1 parent 006d6bb commit c2f4e50

File tree

9 files changed

+319
-20
lines changed

9 files changed

+319
-20
lines changed

release_notes.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
### Release notes
2-
3-
<!-- Please add your release notes in the following format:
4-
- My change description (#PR)
5-
-->
6-
- Memory allocation optimizations in `ScriptStartupTypeLocator.GetExtensionsStartupTypesAsync` (#11012)
7-
- Fix invocation timeout when incoming request contains "x-ms-invocation-id" header (#10980)
1+
### Release notes
2+
3+
<!-- Please add your release notes in the following format:
4+
- My change description (#PR)
5+
-->
6+
- Memory allocation optimizations in `ScriptStartupTypeLocator.GetExtensionsStartupTypesAsync` (#11012)
7+
- Fix invocation timeout when incoming request contains "x-ms-invocation-id" header (#10980)
8+
- Warn if .azurefunctions folder does not exist (#10967)

src/WebJobs.Script.WebHost/DependencyInjection/DependencyValidator/DependencyValidator.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using Microsoft.Azure.WebJobs.Script.Diagnostics;
1010
using Microsoft.Azure.WebJobs.Script.Eventing;
1111
using Microsoft.Azure.WebJobs.Script.FileProvisioning;
12+
using Microsoft.Azure.WebJobs.Script.Host;
1213
using Microsoft.Azure.WebJobs.Script.Scale;
1314
using Microsoft.Azure.WebJobs.Script.WebHost.Diagnostics;
1415
using Microsoft.Azure.WebJobs.Script.Workers;
@@ -48,6 +49,7 @@ private static ExpectedDependencyBuilder CreateExpectedDependencies()
4849
.Expect<WorkerConsoleLogService>()
4950
.Expect<FunctionInvocationDispatcherShutdownManager>()
5051
.Expect<WorkerConcurrencyManager>()
52+
.Optional<FunctionAppValidationService>() // Conditionally registered.
5153
.Optional<FuncAppFileProvisioningService>() // Used by powershell.
5254
.Optional<JobHostService>() // Missing when host is offline.
5355
.Optional<FunctionsSyncService>() // Conditionally registered.

src/WebJobs.Script/DependencyInjection/ScriptStartupTypeLocator.cs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ public async Task<IEnumerable<Type>> GetExtensionsStartupTypesAsync()
102102
}
103103
}
104104

105-
bool isDotnetIsolatedApp = IsDotnetIsolatedApp(functionMetadataCollection, SystemEnvironment.Instance);
105+
bool isDotnetIsolatedApp = Utility.IsDotnetIsolatedApp(SystemEnvironment.Instance, functionMetadataCollection);
106106
bool isDotnetApp = isPrecompiledFunctionApp || isDotnetIsolatedApp;
107107
var isLogicApp = SystemEnvironment.Instance.IsLogicApp();
108108

@@ -340,12 +340,6 @@ void CollectError(Type extensionType, Version minimumVersion, ExtensionStartupTy
340340
}
341341
}
342342

343-
private bool IsDotnetIsolatedApp(IEnumerable<FunctionMetadata> functions, IEnvironment environment)
344-
{
345-
string workerRuntime = Utility.GetWorkerRuntime(functions, environment);
346-
return workerRuntime?.Equals(RpcWorkerConstants.DotNetIsolatedLanguageWorkerName, StringComparison.OrdinalIgnoreCase) ?? false;
347-
}
348-
349343
private ExtensionRequirementsInfo GetExtensionRequirementsInfo()
350344
{
351345
ExtensionRequirementsInfo requirementsInfo = _extensionRequirementOptions.Value.Bundles != null || _extensionRequirementOptions.Value.Extensions != null

src/WebJobs.Script/Diagnostics/Extensions/LoggerExtension.cs

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -184,14 +184,24 @@ internal static class LoggerExtension
184184
"Referenced bundle {bundleId} of version {bundleVersion} does not meet the required minimum version of {minimumVersion}. Update your extension bundle reference in host.json to reference {minimumVersion2} or later.");
185185

186186
private static readonly Action<ILogger, string, Exception> _hostJsonZipDeploymentIssue =
187-
LoggerMessage.Define<string>(LogLevel.Error,
188-
new EventId(338, nameof(HostJsonZipDeploymentIssue)),
189-
"No functions were found. A valid host.json file wasn't found in the package root. However, one was located at: {hostJsonFilesPath}. This state indicates that your deployment package was created incorrectly. For deployment package requirements, see https://aka.ms/deployment-zip-push.");
187+
LoggerMessage.Define<string>(LogLevel.Error,
188+
new EventId(338, nameof(HostJsonZipDeploymentIssue)),
189+
"No functions were found. A valid host.json file wasn't found in the package root. However, one was located at: {hostJsonFilesPath}. This state indicates that your deployment package was created incorrectly. For deployment package requirements, see https://aka.ms/deployment-zip-push.");
190190

191191
private static readonly Action<ILogger, Exception> _noHostJsonFile =
192-
LoggerMessage.Define(LogLevel.Information,
193-
new EventId(339, nameof(NoHostJsonFile)),
194-
"No functions were found. This can occur before you deploy code to your function app or when the host.json file is missing from the most recent deployment. Make sure that your deployment package includes the host.json file in the root of the package. For deployment package requirements, see https://aka.ms/functions-deployment-technologies.");
192+
LoggerMessage.Define(LogLevel.Information,
193+
new EventId(339, nameof(NoHostJsonFile)),
194+
"No functions were found. This can occur before you deploy code to your function app or when the host.json file is missing from the most recent deployment. Make sure that your deployment package includes the host.json file in the root of the package. For deployment package requirements, see https://aka.ms/functions-deployment-technologies.");
195+
196+
private static readonly Action<ILogger, string, Exception> _missingAzureFunctionsFolder =
197+
LoggerMessage.Define<string>(LogLevel.Warning,
198+
new EventId(340, nameof(MissingAzureFunctionsFolder)),
199+
"Could not find the .azurefunctions folder in the deployed artifacts of a .NET isolated function app. Make sure that your deployment package includes the .azurefunctions folder at the root of the package. For deployment package requirements, see https://aka.ms/functions-deployment-technologies. If this is not intended to be a .NET isolated app, please ensure that the {functionWorkerRuntime} app setting is configured correctly.");
200+
201+
private static readonly Action<ILogger, string, string, Exception> _incorrectAzureFunctionsFolderPath =
202+
LoggerMessage.Define<string, string>(LogLevel.Warning,
203+
new EventId(341, nameof(IncorrectAzureFunctionsFolderPath)),
204+
"Could not find the .azurefunctions folder in the deployed artifacts of a .NET isolated function app. However, it is found to be located at: {path}. Make sure that your deployment package includes the .azurefunctions folder at the root of the package. For deployment package requirements, see https://aka.ms/functions-deployment-technologies. If this is not intended to be a .NET isolated app, please ensure that the {functionWorkerRuntime} app setting is configured correctly.");
195205

196206
private static readonly Action<ILogger, string, Exception> _publishingMetrics =
197207
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(338, nameof(PublishingMetrics)), "{metrics}");
@@ -387,5 +397,15 @@ public static void NoHostJsonFile(this ILogger logger)
387397
{
388398
_noHostJsonFile(logger, null);
389399
}
400+
401+
public static void MissingAzureFunctionsFolder(this ILogger logger)
402+
{
403+
_missingAzureFunctionsFolder(logger, EnvironmentSettingNames.FunctionWorkerRuntime, null);
404+
}
405+
406+
public static void IncorrectAzureFunctionsFolderPath(this ILogger logger, string path)
407+
{
408+
_incorrectAzureFunctionsFolderPath(logger, path, EnvironmentSettingNames.FunctionWorkerRuntime, null);
409+
}
390410
}
391411
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.IO;
7+
using System.Linq;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
using Microsoft.Azure.WebJobs.Script.Diagnostics.Extensions;
11+
using Microsoft.Extensions.Hosting;
12+
using Microsoft.Extensions.Logging;
13+
using Microsoft.Extensions.Options;
14+
15+
namespace Microsoft.Azure.WebJobs.Script.Host
16+
{
17+
/// <summary>
18+
/// A background service responsible for validating function app payload.
19+
/// </summary>
20+
internal sealed class FunctionAppValidationService : BackgroundService
21+
{
22+
private readonly IEnvironment _environment;
23+
private readonly ILogger<FunctionAppValidationService> _logger;
24+
private readonly IOptions<ScriptJobHostOptions> _scriptOptions;
25+
26+
public FunctionAppValidationService(
27+
ILogger<FunctionAppValidationService> logger,
28+
IOptions<ScriptJobHostOptions> scriptOptions,
29+
IEnvironment environment)
30+
{
31+
_scriptOptions = scriptOptions ?? throw new ArgumentNullException(nameof(scriptOptions));
32+
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
33+
_environment = environment ?? throw new ArgumentNullException(nameof(environment));
34+
}
35+
36+
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
37+
{
38+
if (!_scriptOptions.Value.IsStandbyConfiguration)
39+
{
40+
// Adding a delay to ensure that this validation does not impact the cold start performance
41+
Utility.ExecuteAfterColdStartDelay(_environment, Validate, cancellationToken);
42+
}
43+
44+
await Task.CompletedTask;
45+
}
46+
47+
private void Validate()
48+
{
49+
try
50+
{
51+
string azureFunctionsDirPath = Path.Combine(_scriptOptions.Value.RootScriptPath, ScriptConstants.AzureFunctionsSystemDirectoryName);
52+
53+
if (_scriptOptions.Value.RootScriptPath is not null &&
54+
!_scriptOptions.Value.IsDefaultHostConfig &&
55+
Utility.IsDotnetIsolatedApp(environment: _environment) &&
56+
!Directory.Exists(azureFunctionsDirPath))
57+
{
58+
// Search for the .azurefunctions directory within nested directories to verify scenarios where it isn't located at the root. This situation occurs when a function app has been improperly zipped.
59+
IEnumerable<string> azureFunctionsDirectories = Directory.GetDirectories(_scriptOptions.Value.RootScriptPath, ScriptConstants.AzureFunctionsSystemDirectoryName, SearchOption.AllDirectories)
60+
.Where(dir => !dir.Equals(azureFunctionsDirPath, StringComparison.OrdinalIgnoreCase));
61+
62+
if (azureFunctionsDirectories.Any())
63+
{
64+
string azureFunctionsDirectoriesPath = string.Join(", ", azureFunctionsDirectories).Replace(_scriptOptions.Value.RootScriptPath, string.Empty);
65+
_logger.IncorrectAzureFunctionsFolderPath(azureFunctionsDirectoriesPath);
66+
}
67+
else
68+
{
69+
_logger.MissingAzureFunctionsFolder();
70+
}
71+
}
72+
}
73+
catch (Exception ex)
74+
{
75+
_logger.LogTrace("Unable to validate deployed function app payload", ex);
76+
}
77+
}
78+
}
79+
}

src/WebJobs.Script/ScriptHostBuilderExtensions.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
using Microsoft.Azure.WebJobs.Script.Extensibility;
3232
using Microsoft.Azure.WebJobs.Script.ExtensionBundle;
3333
using Microsoft.Azure.WebJobs.Script.FileProvisioning;
34+
using Microsoft.Azure.WebJobs.Script.Host;
3435
using Microsoft.Azure.WebJobs.Script.Http;
3536
using Microsoft.Azure.WebJobs.Script.ManagedDependencies;
3637
using Microsoft.Azure.WebJobs.Script.Scale;
@@ -175,6 +176,11 @@ public static IHostBuilder AddScriptHostCore(this IHostBuilder builder, ScriptAp
175176

176177
builder.ConfigureServices((context, services) =>
177178
{
179+
if (!SystemEnvironment.Instance.IsPlaceholderModeEnabled())
180+
{
181+
services.AddHostedService<FunctionAppValidationService>();
182+
}
183+
178184
services.AddSingleton<ExternalConfigurationStartupValidator>();
179185
services.AddSingleton<IHostedService>(s =>
180186
{

src/WebJobs.Script/Utility.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,12 @@ internal static bool IsSingleLanguage(IEnumerable<FunctionMetadata> functions, s
671671
return ContainsFunctionWithWorkerRuntime(filteredFunctions, workerRuntime);
672672
}
673673

674+
internal static bool IsDotnetIsolatedApp(IEnvironment environment, IEnumerable<FunctionMetadata> functions = null)
675+
{
676+
string workerRuntime = GetWorkerRuntime(functions, environment);
677+
return workerRuntime?.Equals(RpcWorkerConstants.DotNetIsolatedLanguageWorkerName, StringComparison.OrdinalIgnoreCase) ?? false;
678+
}
679+
674680
internal static string GetWorkerRuntime(IEnumerable<FunctionMetadata> functions, IEnvironment environment = null)
675681
{
676682
if (environment != null)

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -873,6 +873,46 @@ public async Task ResponseCompressionWorksAfterSpecialization(string acceptEncod
873873
Assert.Equal(expectedContentEncodingResponseHeaderValue, value?.First());
874874
}
875875

876+
[Fact]
877+
public async Task Specialization_DotnetIsolatedApp_MissingAzureFunctionsDir_Logs()
878+
{
879+
Guid guid = Guid.NewGuid();
880+
string path = "test-path" + guid.ToString();
881+
882+
if (!Directory.Exists(path))
883+
{
884+
Directory.CreateDirectory(path);
885+
}
886+
887+
string json = "{\r\n \"version\": \"2.0\",\r\n \"isDefaultHostConfig\": false\r\n}";
888+
File.WriteAllText(Path.Combine(path, "host.json"), json);
889+
890+
var builder = InitializeDotNetIsolatedPlaceholderBuilder(path);
891+
892+
using var testServer = new TestServer(builder);
893+
894+
var standbyManager = testServer.Services.GetService<IStandbyManager>();
895+
Assert.NotNull(standbyManager);
896+
897+
_environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteContainerReady, "1");
898+
_environment.SetEnvironmentVariable(EnvironmentSettingNames.FunctionWorkerRuntime, "dotnet-isolated");
899+
SystemEnvironment.Instance.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsitePlaceholderMode, "0");
900+
_environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsitePlaceholderMode, "0");
901+
902+
await standbyManager.SpecializeHostAsync();
903+
904+
// Assert: Verify that the host has specialized
905+
var scriptHostManager = testServer.Services.GetService<IScriptHostManager>();
906+
Assert.NotNull(scriptHostManager);
907+
Assert.Equal(ScriptHostState.Running, scriptHostManager.State);
908+
909+
await TestHelpers.Await(() =>
910+
{
911+
int completed = _loggerProvider.GetAllLogMessages().Count(p => p.FormattedMessage.Contains("Could not find the .azurefunctions folder in the deployed artifacts of a .NET isolated function app."));
912+
return completed > 0;
913+
});
914+
}
915+
876916
[Fact]
877917
public async Task DotNetIsolated_PlaceholderHit_WithProxies()
878918
{

0 commit comments

Comments
 (0)