diff --git a/extensions/Worker.Extensions.Tables/release_notes.md b/extensions/Worker.Extensions.Tables/release_notes.md index 80dd76122..62b6451d2 100644 --- a/extensions/Worker.Extensions.Tables/release_notes.md +++ b/extensions/Worker.Extensions.Tables/release_notes.md @@ -4,6 +4,6 @@ - My change description (#PR/#issue) --> -### Microsoft.Azure.Functions.Worker.Extensions.Tables 1.2.0 +### Microsoft.Azure.Functions.Worker.Extensions.Tables -- Add ability to bind table input to TableClient, TableEntity, and IEnumerable (#1470) +- diff --git a/host/src/FunctionsNetHost/Grpc/Exceptions/EnvironmentReloadNotSupportedException.cs b/host/src/FunctionsNetHost/Grpc/Exceptions/EnvironmentReloadNotSupportedException.cs new file mode 100644 index 000000000..a0996236e --- /dev/null +++ b/host/src/FunctionsNetHost/Grpc/Exceptions/EnvironmentReloadNotSupportedException.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace Microsoft.Azure.Functions.Worker +{ + /// + /// The exception that is thrown when the current function app payload does not support environment reload. + /// + public sealed class EnvironmentReloadNotSupportedException : NotSupportedException + { + public EnvironmentReloadNotSupportedException() { } + + public EnvironmentReloadNotSupportedException(string message) : base(message) { } + } +} diff --git a/host/src/FunctionsNetHost/Grpc/Exceptions/FunctionAppPayloadNotFoundException.cs b/host/src/FunctionsNetHost/Grpc/Exceptions/FunctionAppPayloadNotFoundException.cs new file mode 100644 index 000000000..97bfac80a --- /dev/null +++ b/host/src/FunctionsNetHost/Grpc/Exceptions/FunctionAppPayloadNotFoundException.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace Microsoft.Azure.Functions.Worker +{ + /// + /// The exception that is thrown when there is no function app payload found. + /// + public sealed class FunctionAppPayloadNotFoundException : Exception + { + public FunctionAppPayloadNotFoundException() { } + + public FunctionAppPayloadNotFoundException(string message) : base(message) { } + } +} diff --git a/host/src/FunctionsNetHost/Grpc/IncomingGrpcMessageHandler.cs b/host/src/FunctionsNetHost/Grpc/IncomingGrpcMessageHandler.cs index 4beb947ad..55b088487 100644 --- a/host/src/FunctionsNetHost/Grpc/IncomingGrpcMessageHandler.cs +++ b/host/src/FunctionsNetHost/Grpc/IncomingGrpcMessageHandler.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. +using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.Functions.Worker.Grpc.Messages; namespace FunctionsNetHost.Grpc @@ -50,14 +51,33 @@ private async Task Process(StreamingMessage msg) Logger.LogTrace("Specialization request received."); var envReloadRequest = msg.FunctionEnvironmentReloadRequest; - foreach (var kv in envReloadRequest.EnvironmentVariables) + + var workerConfig = await WorkerConfigUtils.GetWorkerConfig(envReloadRequest.FunctionAppDirectory); + + if (workerConfig?.Description is null) { - EnvironmentUtils.SetValue(kv.Key, kv.Value); + Logger.LogTrace($"Could not find a worker config in {envReloadRequest.FunctionAppDirectory}"); + responseMessage.FunctionEnvironmentReloadResponse = BuildFailedEnvironmentReloadResponse(); + break; + } + + // function app payload which uses an older version of Microsoft.Azure.Functions.Worker package does not support specialization. + if (!workerConfig.Description.CanUsePlaceholder) + { + Logger.LogTrace("App payload uses an older version of Microsoft.Azure.Functions.Worker SDK which does not support placeholder."); + var e = new EnvironmentReloadNotSupportedException("This app is not using the latest version of Microsoft.Azure.Functions.Worker SDK and therefore does not leverage all performance optimizations. See https://aka.ms/azure-functions/dotnet/placeholders for more information."); + responseMessage.FunctionEnvironmentReloadResponse = BuildFailedEnvironmentReloadResponse(e); + break; } - var applicationExePath = PathUtils.GetApplicationExePath(envReloadRequest.FunctionAppDirectory); + var applicationExePath = Path.Combine(envReloadRequest.FunctionAppDirectory, workerConfig.Description.DefaultWorkerPath!); Logger.LogTrace($"application path {applicationExePath}"); + foreach (var kv in envReloadRequest.EnvironmentVariables) + { + EnvironmentUtils.SetValue(kv.Key, kv.Value); + } + #pragma warning disable CS4014 Task.Run(() => #pragma warning restore CS4014 @@ -74,9 +94,43 @@ private async Task Process(StreamingMessage msg) break; } - await MessageChannel.Instance.SendOutboundAsync(responseMessage); + if (responseMessage.ContentCase != StreamingMessage.ContentOneofCase.None) + { + await MessageChannel.Instance.SendOutboundAsync(responseMessage); + } } + private static FunctionEnvironmentReloadResponse BuildFailedEnvironmentReloadResponse(Exception? exception = null) + { + var response = new FunctionEnvironmentReloadResponse + { + Result = new StatusResult + { + Status = StatusResult.Types.Status.Failure + } + }; + + response.Result.Exception = ToUserRpcException(exception); + + return response; + } + + internal static RpcException? ToUserRpcException(Exception? exception) + { + if (exception is null) + { + return null; + } + + return new RpcException + { + Message = exception.Message, + Source = exception.Source ?? string.Empty, + StackTrace = exception.StackTrace ?? string.Empty, + Type = exception.GetType().FullName ?? string.Empty, + IsUserException = true + }; + } private static FunctionMetadataResponse BuildFunctionMetadataResponse() { var metadataResponse = new FunctionMetadataResponse @@ -94,6 +148,7 @@ private static WorkerInitResponse BuildWorkerInitResponse() { Result = new StatusResult { Status = StatusResult.Types.Status.Success } }; + response.Capabilities[WorkerCapabilities.EnableUserCodeException] = bool.TrueString; return response; } diff --git a/host/src/FunctionsNetHost/Grpc/PathUtils.cs b/host/src/FunctionsNetHost/Grpc/PathUtils.cs deleted file mode 100644 index 839d79d0d..000000000 --- a/host/src/FunctionsNetHost/Grpc/PathUtils.cs +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. - -using System.Text.Json; -using System.Text.Json.Nodes; - -namespace FunctionsNetHost.Grpc -{ - internal static class PathUtils - { - /// - /// Gets the absolute path to worker application executable. - /// Builds the path by reading the worker.config.json - /// - /// The FunctionAppDirectory value from environment reload request. - internal static string? GetApplicationExePath(string applicationDirectory) - { - string jsonString = string.Empty; - string workerConfigPath = string.Empty; - try - { - workerConfigPath = Path.Combine(applicationDirectory, "worker.config.json"); - - jsonString = File.ReadAllText(workerConfigPath); - var workerConfigJsonNode = JsonNode.Parse(jsonString)!; - var executableName = workerConfigJsonNode["description"]?["defaultWorkerPath"]?.ToString(); - - if (executableName == null) - { - Logger.Log($"Invalid worker configuration. description > defaultWorkerPath property value is null. jsonString:{jsonString}"); - return null; - } - - return Path.Combine(applicationDirectory, executableName); - } - catch (FileNotFoundException ex) - { - Logger.Log($"{workerConfigPath} file not found.{ex}"); - return null; - } - catch (JsonException ex) - { - Logger.Log($"Error parsing JSON in GetApplicationExePath.{ex}. jsonString:{jsonString}"); - return null; - } - catch (Exception ex) - { - Logger.Log($"Error in GetApplicationExePath.{ex}. jsonString:{jsonString}"); - return null; - } - } - } -} diff --git a/host/src/FunctionsNetHost/Grpc/WorkerCapabilities.cs b/host/src/FunctionsNetHost/Grpc/WorkerCapabilities.cs new file mode 100644 index 000000000..749788fc2 --- /dev/null +++ b/host/src/FunctionsNetHost/Grpc/WorkerCapabilities.cs @@ -0,0 +1,10 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace FunctionsNetHost.Grpc +{ + internal static class WorkerCapabilities + { + internal const string EnableUserCodeException = "EnableUserCodeException"; + } +} diff --git a/host/src/FunctionsNetHost/Grpc/WorkerConfig/WorkerConfig.cs b/host/src/FunctionsNetHost/Grpc/WorkerConfig/WorkerConfig.cs new file mode 100644 index 000000000..b15b46e3f --- /dev/null +++ b/host/src/FunctionsNetHost/Grpc/WorkerConfig/WorkerConfig.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace FunctionsNetHost.Grpc +{ + /// + /// Represents a worker configuration instance. + /// + public sealed class WorkerConfig + { + public WorkerDescription? Description { set; get; } + } +} diff --git a/host/src/FunctionsNetHost/Grpc/WorkerConfig/WorkerConfigSerializerContext.cs b/host/src/FunctionsNetHost/Grpc/WorkerConfig/WorkerConfigSerializerContext.cs new file mode 100644 index 000000000..d37bcd11e --- /dev/null +++ b/host/src/FunctionsNetHost/Grpc/WorkerConfig/WorkerConfigSerializerContext.cs @@ -0,0 +1,11 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Text.Json.Serialization; + +namespace FunctionsNetHost.Grpc +{ + [JsonSerializable(typeof(WorkerConfig))] + [JsonSourceGenerationOptions(IncludeFields = true, PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] + internal partial class WorkerConfigSerializerContext : JsonSerializerContext { } +} diff --git a/host/src/FunctionsNetHost/Grpc/WorkerConfig/WorkerConfigUtils.cs b/host/src/FunctionsNetHost/Grpc/WorkerConfig/WorkerConfigUtils.cs new file mode 100644 index 000000000..98c430718 --- /dev/null +++ b/host/src/FunctionsNetHost/Grpc/WorkerConfig/WorkerConfigUtils.cs @@ -0,0 +1,39 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Text.Json; + +namespace FunctionsNetHost.Grpc +{ + internal static class WorkerConfigUtils + { + /// + /// Builds and returns an instance of from the worker.config.json file if present in the application directory. + /// + /// The directory where function app deployed payload is present. + internal static async Task GetWorkerConfig(string applicationDirectory) + { + string workerConfigPath = string.Empty; + + try + { + workerConfigPath = Path.Combine(applicationDirectory, "worker.config.json"); + + using Stream stream = File.OpenRead(workerConfigPath); + var workerConfig = await JsonSerializer.DeserializeAsync(stream, WorkerConfigSerializerContext.Default.WorkerConfig); + + return workerConfig; + } + catch (FileNotFoundException) + { + Logger.Log($"worker.config.json not found at {workerConfigPath}. This may indicate missing app payload."); + return null; + } + catch (Exception ex) + { + Logger.Log($"Error in WorkerConfigUtils.GetWorkerConfig.{ex}"); + return null; + } + } + } +} diff --git a/host/src/FunctionsNetHost/Grpc/WorkerConfig/WorkerDescription.cs b/host/src/FunctionsNetHost/Grpc/WorkerConfig/WorkerDescription.cs new file mode 100644 index 000000000..5ad21dd0a --- /dev/null +++ b/host/src/FunctionsNetHost/Grpc/WorkerConfig/WorkerDescription.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace FunctionsNetHost.Grpc +{ + public sealed class WorkerDescription + { + public string? DefaultWorkerPath { set; get; } + + public bool CanUsePlaceholder { set; get; } + } +} diff --git a/host/src/FunctionsNetHost/global.json b/host/src/FunctionsNetHost/global.json index f735d6ed6..27928313f 100644 --- a/host/src/FunctionsNetHost/global.json +++ b/host/src/FunctionsNetHost/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.100-rc.1.23455.8", + "version": "8.0.100-rc.2.23502.2", "allowPrerelease": true, "rollForward": "latestMinor" } diff --git a/host/tools/build/Microsoft.Azure.Functions.DotnetIsolatedNativeHost.nuspec b/host/tools/build/Microsoft.Azure.Functions.DotnetIsolatedNativeHost.nuspec index c2ab2b44b..0f198f5a2 100644 --- a/host/tools/build/Microsoft.Azure.Functions.DotnetIsolatedNativeHost.nuspec +++ b/host/tools/build/Microsoft.Azure.Functions.DotnetIsolatedNativeHost.nuspec @@ -4,7 +4,7 @@ Microsoft.Azure.Functions.DotNetIsolatedNativeHost Microsoft Azure Functions dotnet-isolated native host dotnet-isolated azure-functions azure - 1.0.0 + 1.0.2 Microsoft Microsoft https://github.com/Azure/azure-functions-dotnet-worker diff --git a/samples/WorkerBindingSamples/Table/TableSamples.cs b/samples/WorkerBindingSamples/Table/TableSamples.cs deleted file mode 100644 index 8eab43106..000000000 --- a/samples/WorkerBindingSamples/Table/TableSamples.cs +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. - -using Azure.Data.Tables; -using Microsoft.Azure.Functions.Worker; -using Microsoft.Azure.Functions.Worker.Http; -using Microsoft.Extensions.Logging; - -namespace WorkerBindingSamples.Table -{ - /// - /// Samples demonstrating binding to , and types. - /// - public class TableSamples - { - private readonly ILogger _logger; - - public TableSamples(ILogger logger) - { - _logger = logger; - } - - /// - /// This function demonstrates binding to a single . - /// - [Function(nameof(TableClientFunction))] - public async Task TableClientFunction( - [HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req, - [TableInput("TableName")] TableClient table) - { - var tableEntity = table.QueryAsync(); - - await foreach (TableEntity entity in tableEntity) - { - _logger.LogInformation("PK={pk}, RK={rk}, Text={t}", entity.PartitionKey, entity.RowKey, entity["Text"]); - } - } - - /// - /// This function demonstrates binding to a single , using the - /// and properties. - /// - [Function(nameof(TableEntityFunction))] - public void TableEntityFunction( - [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = "items/{partitionKey}/{rowKey}")] HttpRequestData req, - [TableInput("TableName", "{partitionKey}", "{rowKey}")] TableEntity entity) - { - _logger.LogInformation("PK={pk}, RK={rk}, Text={t}", entity.PartitionKey, entity.RowKey, entity["Text"]); - } - - /// - /// This function demonstrates binding to a collection of , using the . - /// Note that when the is not provided, you are able to bind to a collection. - /// - [Function(nameof(TableEntityCollectionFunction))] - public void TableEntityCollectionFunction( - [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = "items/{partitionKey}")] HttpRequestData req, - [TableInput("TableName", "{partitionKey}")] IEnumerable entities) - { - foreach (var entity in entities) - { - _logger.LogInformation("PK={pk}, RK={rk}, Text={t}", entity.PartitionKey, entity.RowKey, entity["Text"]); - } - } - - /// - /// This function demonstrates binding to a collection of , using - /// to filter on the row key. This sample also demonstrates using - /// to limit the number of entities returned. - /// - [Function(nameof(TableEntityWithFilterFunction))] - public void TableEntityWithFilterFunction( - [HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req, - [TableInput("TableName", "PartitionKey", 2, Filter = "RowKey ne 'value'")] IEnumerable entities) - { - foreach (var entity in entities) - { - _logger.LogInformation("PK={pk}, RK={rk}, Text={t}", entity.PartitionKey, entity.RowKey, entity["Text"]); - } - } - - /// - /// This function demonstrates binding to a collection of - /// - [Function(nameof(TablePocoFunction))] - public void TablePocoFunction( - [HttpTrigger(AuthorizationLevel.Function, "get","post", Route = null)] HttpRequestData req, - [TableInput("TableName")] IEnumerable entities) - { - foreach (var entity in entities) - { - _logger.LogInformation("PK={pk}, RK={rk}, Text={t}", entity.PartitionKey, entity.RowKey, entity.Text); - } - } - } - - public class MyEntity - { - public string? Text { get; set; } - public string? PartitionKey { get; set; } - public string? RowKey { get; set; } - } -} diff --git a/sdk/Sdk/worker.config.json b/sdk/Sdk/worker.config.json index 1c86e746e..6d6c64b28 100644 --- a/sdk/Sdk/worker.config.json +++ b/sdk/Sdk/worker.config.json @@ -4,6 +4,7 @@ "extensions": [ ".dll" ], "defaultExecutablePath": "$functionExe$", "defaultWorkerPath": "$functionWorker$", - "workerIndexing": "$enableWorkerIndexing$" + "workerIndexing": "$enableWorkerIndexing$", + "canUsePlaceholder": true } } \ No newline at end of file diff --git a/sdk/release_notes.md b/sdk/release_notes.md index e1b6c7ece..d45386eca 100644 --- a/sdk/release_notes.md +++ b/sdk/release_notes.md @@ -20,8 +20,5 @@ ### Microsoft.Azure.Functions.Worker.Sdk.Generators -- Parse named arguments by type (#1877) -- Refactor source gen to walk dependent assemblies (#1896) -- Add diagnostic descriptor logs for parsing binding arguments in source gen (#1882) -- Use project root namespace for generated types (#1158) +-