diff --git a/release_notes.md b/release_notes.md index a4a17c5d39..539dd7d3c3 100644 --- a/release_notes.md +++ b/release_notes.md @@ -1,9 +1,10 @@ -### Release notes - - -- Memory allocation optimizations in `ScriptStartupTypeLocator.GetExtensionsStartupTypesAsync` (#11012) -- Fix invocation timeout when incoming request contains "x-ms-invocation-id" header (#10980) -- Warn if .azurefunctions folder does not exist (#10967) -- Memory allocation & CPU optimizations in `GrpcMessageExtensionUtilities.ConvertFromHttpMessageToExpando` (#11054) +### Release notes + + +- Memory allocation optimizations in `ScriptStartupTypeLocator.GetExtensionsStartupTypesAsync` (#11012) +- Fix invocation timeout when incoming request contains "x-ms-invocation-id" header (#10980) +- Warn if .azurefunctions folder does not exist (#10967) +- Memory allocation & CPU optimizations in `GrpcMessageExtensionUtilities.ConvertFromHttpMessageToExpando` (#11054) +- Add information diagnostics event for outdated bundle version, any bundle version < 4 (#) diff --git a/src/WebJobs.Script.SiteExtension/New-PrivateSiteExtension.ps1 b/src/WebJobs.Script.SiteExtension/New-PrivateSiteExtension.ps1 index 0ef23c159c..c367526e1b 100644 --- a/src/WebJobs.Script.SiteExtension/New-PrivateSiteExtension.ps1 +++ b/src/WebJobs.Script.SiteExtension/New-PrivateSiteExtension.ps1 @@ -38,7 +38,7 @@ param ( $normalizeBitness = @{ 'x64' = '64bit' '64bit' = '64bit' - 'x86' = '64bit' + 'x86' = '32bit' '32bit' = '32bit' } diff --git a/src/WebJobs.Script/DependencyInjection/ScriptStartupTypeLocator.cs b/src/WebJobs.Script/DependencyInjection/ScriptStartupTypeLocator.cs index 43c3758c18..e8c44f7a2d 100644 --- a/src/WebJobs.Script/DependencyInjection/ScriptStartupTypeLocator.cs +++ b/src/WebJobs.Script/DependencyInjection/ScriptStartupTypeLocator.cs @@ -30,7 +30,7 @@ namespace Microsoft.Azure.WebJobs.Script.DependencyInjection public sealed class ScriptStartupTypeLocator : IWebJobsStartupTypeLocator { private const string ApplicationInsightsStartupType = "Microsoft.Azure.WebJobs.Extensions.ApplicationInsights.ApplicationInsightsWebJobsStartup, Microsoft.Azure.WebJobs.Extensions.ApplicationInsights, Version=1.0.0.0, Culture=neutral, PublicKeyToken=9475d07f10cb09df"; - + private readonly string _rootScriptPath; private readonly ILogger _logger; private readonly IExtensionBundleManager _extensionBundleManager; diff --git a/src/WebJobs.Script/Diagnostics/DiagnosticEventConstants.cs b/src/WebJobs.Script/Diagnostics/DiagnosticEventConstants.cs index 2669a22a6b..777b0a2bb7 100644 --- a/src/WebJobs.Script/Diagnostics/DiagnosticEventConstants.cs +++ b/src/WebJobs.Script/Diagnostics/DiagnosticEventConstants.cs @@ -34,5 +34,8 @@ internal static class DiagnosticEventConstants public const string WorkerRuntimeDoesNotMatchWithFunctionMetadataErrorCode = "AZFD0013"; public const string WorkerRuntimeDoesNotMatchWithFunctionMetadataHelpLink = "https://aka.ms/functions-invalid-worker-runtime"; + + public const string OutdatedBundlesVersionErrorCode = "AZFD0014"; + public const string OutdatedBundlesVersionHelpLink = "https://aka.ms/functions-outdated-bundles"; } } diff --git a/src/WebJobs.Script/ExtensionBundle/ExtensionBundleManager.cs b/src/WebJobs.Script/ExtensionBundle/ExtensionBundleManager.cs index 3c3c1dd597..ecce4a6d06 100644 --- a/src/WebJobs.Script/ExtensionBundle/ExtensionBundleManager.cs +++ b/src/WebJobs.Script/ExtensionBundle/ExtensionBundleManager.cs @@ -10,8 +10,10 @@ using System.Threading.Tasks; using Microsoft.Azure.WebJobs.Script.Config; using Microsoft.Azure.WebJobs.Script.Configuration; +using Microsoft.Azure.WebJobs.Script.Diagnostics; using Microsoft.Azure.WebJobs.Script.Diagnostics.Extensions; using Microsoft.Azure.WebJobs.Script.Models; +using Microsoft.Azure.WebJobs.Script.Properties; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using NuGet.Versioning; @@ -375,5 +377,20 @@ public async Task GetExtensionBundleBinPathAsync() // if no bin directory is present something is wrong return FileUtility.DirectoryExists(binPath) ? binPath : null; } + + public void CompareWithLatestMajorVersion() + { + string majorVersionStr = _extensionBundleVersion?.Split('.')?.FirstOrDefault() ?? string.Empty; + int majorVersion = int.TryParse(majorVersionStr, out int result) ? result : 0; + + int latestMajorVersion = ScriptConstants.ExtensionBundleV4MajorVersion; + if (string.Compare(_options?.Id, ScriptConstants.DefaultExtensionBundleId, StringComparison.OrdinalIgnoreCase) == 0 + && majorVersion != 0 + && majorVersion < latestMajorVersion) + { + string message = string.Format(Resources.OutdatedExtensionBundlesVersionInfoFormat, _extensionBundleVersion, latestMajorVersion, latestMajorVersion + 1); + DiagnosticEventLoggerExtensions.LogDiagnosticEventWarning(_logger, DiagnosticEventConstants.OutdatedBundlesVersionErrorCode, message, DiagnosticEventConstants.OutdatedBundlesVersionHelpLink, null); + } + } } } \ No newline at end of file diff --git a/src/WebJobs.Script/ExtensionBundle/IExtensionBundleManager.cs b/src/WebJobs.Script/ExtensionBundle/IExtensionBundleManager.cs index 9a63b25a07..7ae30590cf 100644 --- a/src/WebJobs.Script/ExtensionBundle/IExtensionBundleManager.cs +++ b/src/WebJobs.Script/ExtensionBundle/IExtensionBundleManager.cs @@ -20,5 +20,7 @@ public interface IExtensionBundleManager bool IsLegacyExtensionBundle(); Task GetExtensionBundleDetails(); + + void CompareWithLatestMajorVersion(); } } \ No newline at end of file diff --git a/src/WebJobs.Script/Host/FunctionAppValidationService.cs b/src/WebJobs.Script/Host/FunctionAppValidationService.cs index 582da52dac..9cdc300fed 100644 --- a/src/WebJobs.Script/Host/FunctionAppValidationService.cs +++ b/src/WebJobs.Script/Host/FunctionAppValidationService.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Azure.WebJobs.Script.Diagnostics.Extensions; +using Microsoft.Azure.WebJobs.Script.ExtensionBundle; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -22,15 +23,18 @@ internal sealed class FunctionAppValidationService : BackgroundService private readonly IEnvironment _environment; private readonly ILogger _logger; private readonly IOptions _scriptOptions; + private readonly IExtensionBundleManager _extensionBundleManager; public FunctionAppValidationService( ILogger logger, IOptions scriptOptions, + IExtensionBundleManager extensionBundleManager, IEnvironment environment) { _scriptOptions = scriptOptions ?? throw new ArgumentNullException(nameof(scriptOptions)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _environment = environment ?? throw new ArgumentNullException(nameof(environment)); + _extensionBundleManager = extensionBundleManager ?? throw new ArgumentNullException(nameof(extensionBundleManager)); } protected override async Task ExecuteAsync(CancellationToken cancellationToken) @@ -41,6 +45,8 @@ protected override async Task ExecuteAsync(CancellationToken cancellationToken) Utility.ExecuteAfterColdStartDelay(_environment, Validate, cancellationToken); } + // Validate the extension bundle and throw warning for outdated bundles + _extensionBundleManager.CompareWithLatestMajorVersion(); await Task.CompletedTask; } diff --git a/src/WebJobs.Script/Properties/Resources.Designer.cs b/src/WebJobs.Script/Properties/Resources.Designer.cs index 83b6069f1b..78857472b9 100644 --- a/src/WebJobs.Script/Properties/Resources.Designer.cs +++ b/src/WebJobs.Script/Properties/Resources.Designer.cs @@ -209,6 +209,15 @@ internal static string MatchingBundleNotFound { } } + /// + /// Looks up a localized string similar to You are currently using an outdated version - {0} of the extension bundle. To ensure optimal performance and access to the latest features, please update to the latest version range: [{1}.*, {2}.0.0). + /// + internal static string OutdatedExtensionBundlesVersionInfoFormat { + get { + return ResourceManager.GetString("OutdatedExtensionBundlesVersionInfoFormat", resourceCulture); + } + } + /// /// Looks up a localized string similar to SAS token within '{0}' setting has expired. Please generate a new SAS token or switch to using identites instead. For more information, see https://go.microsoft.com/fwlink/?linkid=2244092.. /// diff --git a/src/WebJobs.Script/Properties/Resources.resx b/src/WebJobs.Script/Properties/Resources.resx index 84231352bf..b39a1fc622 100644 --- a/src/WebJobs.Script/Properties/Resources.resx +++ b/src/WebJobs.Script/Properties/Resources.resx @@ -179,4 +179,7 @@ The environment variables 'WEBSITE_TIME_ZONE' and 'TZ' are not supported on this platform. For more information, see https://go.microsoft.com/fwlink/?linkid=2250165. + + You are currently using an outdated version — {0} — of the extension bundle, which is deprecated as of 2026-05-30. To ensure optimal performance and access to the latest features, please update to a supported version in the range: [{1}.*, {2}.0.0). + \ No newline at end of file diff --git a/test/WebJobs.Script.Tests/Description/FunctionAppValidationServiceTests.cs b/test/WebJobs.Script.Tests/Description/FunctionAppValidationServiceTests.cs index cd1848e234..4840554656 100644 --- a/test/WebJobs.Script.Tests/Description/FunctionAppValidationServiceTests.cs +++ b/test/WebJobs.Script.Tests/Description/FunctionAppValidationServiceTests.cs @@ -1,13 +1,15 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. - using System; using System.Collections.Immutable; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.Azure.WebJobs.Script.Config; +using Microsoft.Azure.WebJobs.Script.Configuration; using Microsoft.Azure.WebJobs.Script.Description; +using Microsoft.Azure.WebJobs.Script.ExtensionBundle; using Microsoft.Azure.WebJobs.Script.Host; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -21,8 +23,10 @@ public class FunctionAppValidationServiceTests { private readonly ILogger _testLogger; private readonly Mock> _scriptOptionsMock; + private readonly IExtensionBundleManager _extensionBundleManager; private readonly ScriptJobHostOptions _scriptJobHostOptions; private readonly TestLoggerProvider _testLoggerProvider; + private readonly ILoggerFactory _loggerFactory; public FunctionAppValidationServiceTests() { @@ -36,9 +40,10 @@ public FunctionAppValidationServiceTests() _scriptOptionsMock.Setup(o => o.Value).Returns(_scriptJobHostOptions); _testLoggerProvider = new TestLoggerProvider(); - var factory = new LoggerFactory(); - factory.AddProvider(_testLoggerProvider); - _testLogger = factory.CreateLogger(); + _loggerFactory = new LoggerFactory(); + _loggerFactory.AddProvider(_testLoggerProvider); + _testLogger = _loggerFactory.CreateLogger(); + _extensionBundleManager = new Mock().Object; } [Fact] @@ -49,6 +54,7 @@ public async Task StartAsync_NotDotnetIsolatedApp_DoesNotLogError() var service = new FunctionAppValidationService( _testLogger, _scriptOptionsMock.Object, + _extensionBundleManager, new TestEnvironment()); // Act @@ -72,6 +78,7 @@ public async Task StartAsync_PlaceholderMode_DoesNotLogError() var service = new FunctionAppValidationService( _testLogger, _scriptOptionsMock.Object, + _extensionBundleManager, environment); // Act @@ -105,6 +112,7 @@ public async Task StartAsync_NewAppWithNoPayload_DoesNotLogError() var service = new FunctionAppValidationService( _testLogger, scriptOptionsMock.Object, + _extensionBundleManager, environment); // Act @@ -136,6 +144,7 @@ public async Task StartAsync_MissingAzureFunctionsFolder_LogsWarning() var service = new FunctionAppValidationService( _testLogger, _scriptOptionsMock.Object, + _extensionBundleManager, environment); // Act @@ -147,5 +156,38 @@ await TestHelpers.Await(() => return completed > 0; }); } + + [Theory] + [InlineData("Microsoft.Azure.Functions.ExtensionBundle", "3.36.0", true)] + [InlineData("Microsoft.Azure.Functions.ExtensionBundle", "2.25.0", true)] + [InlineData("Microsoft.Azure.Functions.ExtensionBundle", "4.22.0", false)] + [InlineData("Microsoft.Azure.Functions.ExtensionBundle.Preview", "4.29.0", false)] + [InlineData("Microsoft.Azure.Functions.ExtensionBundle.Preview", "3.2.0", false)] + public void CompareWithLatestMajorVersion_LogsExpectedDiagnosticEvents(string bundleId, string bundleVersion, bool shouldLogEvent) + { + // Arrange + _testLoggerProvider.ClearAllLogMessages(); + + var options = new ExtensionBundleOptions { Id = bundleId }; + var env = new TestEnvironment(); + var config = new FunctionsHostingConfigOptions(); + var manager = new ExtensionBundleManager(options, env, _loggerFactory, config); + + // Set the private _extensionBundleVersion field using reflection + typeof(ExtensionBundleManager) + .GetField("_extensionBundleVersion", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) + .SetValue(manager, bundleVersion); + + // Act + manager.CompareWithLatestMajorVersion(); + + // Assert + var logMessages = _testLoggerProvider.GetAllLogMessages(); + bool hasOutdatedBundleLog = logMessages.Any(m => m.FormattedMessage.Contains(bundleVersion) && + m.FormattedMessage.Contains("outdated version") && + m.FormattedMessage.Contains("of the extension bundle")); + + Assert.Equal(shouldLogEvent, hasOutdatedBundleLog); + } } } \ No newline at end of file diff --git a/test/WebJobs.Script.Tests/ExtensionBundle/ExtensionBundleContentProviderTests.cs b/test/WebJobs.Script.Tests/ExtensionBundle/ExtensionBundleContentProviderTests.cs index 2d11c01efd..bdc9ec2474 100644 --- a/test/WebJobs.Script.Tests/ExtensionBundle/ExtensionBundleContentProviderTests.cs +++ b/test/WebJobs.Script.Tests/ExtensionBundle/ExtensionBundleContentProviderTests.cs @@ -142,6 +142,10 @@ public TestExtensionBundleManager(string bundlePath = null, bool isExtensionBund _isExtensionBundleConfigured = isExtensionBundleConfigured; _isLegacyExtensionBundle = isLegacyExtensionBundle; } + public void CompareWithLatestMajorVersion() + { + // No-op for test stub. This can be extended for test verifications if needed. + } public Task GetExtensionBundleBinPathAsync() { diff --git a/test/WebJobs.Script.Tests/ExtensionManagerTests.cs b/test/WebJobs.Script.Tests/ExtensionManagerTests.cs index 891ca6ffdc..cdf287e240 100644 --- a/test/WebJobs.Script.Tests/ExtensionManagerTests.cs +++ b/test/WebJobs.Script.Tests/ExtensionManagerTests.cs @@ -193,6 +193,11 @@ public TestExtensionBundleManager(string bundlePath = null, bool isExtensionBund _isLegacyExtensionBundle = isLegacyExtensionBundle; } + public void CompareWithLatestMajorVersion() + { + // No-op for test stub. This can be extended for test verifications if needed. + } + public Task GetExtensionBundleBinPathAsync() { return Task.FromResult(Path.Combine(_bundlePath, "bin")); diff --git a/test/WebJobs.Script.Tests/ScriptStartupTypeDiscovererTests.cs b/test/WebJobs.Script.Tests/ScriptStartupTypeDiscovererTests.cs index 0372156aa6..a924e0b995 100644 --- a/test/WebJobs.Script.Tests/ScriptStartupTypeDiscovererTests.cs +++ b/test/WebJobs.Script.Tests/ScriptStartupTypeDiscovererTests.cs @@ -780,7 +780,7 @@ public async Task GetExtensionsStartupTypes_WorkerRuntimeNotSetForNodeApp_LoadsE //Assert var traces = testLoggerProvider.GetAllLogMessages(); - var traceMessage = traces.FirstOrDefault(val => val.EventId.Name.Equals("ScriptStartNotLoadingExtensionBundle")); + var traceMessage = traces.FirstOrDefault(val => string.Equals(val.EventId.Name, "ScriptStartNotLoadingExtensionBundle")); bool loadingExtensionBundle = traceMessage == null; Assert.True(loadingExtensionBundle);