From 2455636cca50cfb4d1543633d28657437bbb5173 Mon Sep 17 00:00:00 2001 From: DanielAves <62816402+DanielAves@users.noreply.github.com> Date: Thu, 14 Sep 2023 18:12:23 +0100 Subject: [PATCH 01/11] Update readme to include 'Worker.Extensions.DurableTask' nuget (#2565) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 7aac43a27..92e1d4214 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ Package Name | NuGet ---|--- Microsoft.Azure.WebJobs.Extensions.DurableTask | [![NuGet](https://img.shields.io/nuget/v/Microsoft.Azure.WebJobs.Extensions.DurableTask.svg)](https://www.nuget.org/packages/Microsoft.Azure.WebJobs.Extensions.DurableTask) Microsoft.Azure.WebJobs.Extensions.DurableTask.Analyzers (C# only) | [![NuGet](https://img.shields.io/nuget/v/Microsoft.Azure.WebJobs.Extensions.DurableTask.Analyzers.svg)](https://www.nuget.org/packages/Microsoft.Azure.WebJobs.Extensions.DurableTask.Analyzers) +Microsoft.Azure.Functions.Worker.Extensions.DurableTask | [![NuGet](https://img.shields.io/nuget/v/Microsoft.Azure.Functions.Worker.Extensions.DurableTask.svg)](https://www.nuget.org/packages/Microsoft.Azure.Functions.Worker.Extensions.DurableTask) ## Language support From 5c6172ca353b5ce59a301170fb3daeb57ae4f4f3 Mon Sep 17 00:00:00 2001 From: Naiyuan Tian <110135109+nytian@users.noreply.github.com> Date: Wed, 20 Sep 2023 15:09:46 -0700 Subject: [PATCH 02/11] Update Pull Request template (#2593) * Update pull_request_template.md * Update pull_request_template.md * Update pull_request_template.md * remove typo --- .github/pull_request_template.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index f1f890c4a..eedf52e97 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -20,4 +20,6 @@ resolves #issue_for_this_pr * [ ] My changes **do not** change the version of the WebJobs.Extensions.DurableTask package * [ ] Otherwise: major or minor version updates are reflected in `/src/Worker.Extensions.DurableTask/AssemblyInfo.cs` * [ ] My changes **do not** add EventIds to our EventSource logs - * [ ] Otherwise: Ensure the EventIds are within the supported range in our existing Windows infrastructure. You may validate this with a deployed app's telemetry. You may also extend the range by completing a PR such as [this one](https://msazure.visualstudio.com/One/_git/AAPT-Antares-Websites/pullrequest/7463263?_a=files). \ No newline at end of file + * [ ] Otherwise: Ensure the EventIds are within the supported range in our existing Windows infrastructure. You may validate this with a deployed app's telemetry. You may also extend the range by completing a PR such as [this one](https://msazure.visualstudio.com/One/_git/AAPT-Antares-Websites/pullrequest/7463263?_a=files). +* [ ] My changes **should** be added to v3.x branch. + * [ ] Otherwise: This change only applies to Durable Functions v2.x and **will not** be merged to branch v3.x. From 910e39b034e8fdf5e6ca1d11cecef774c0e1d0c0 Mon Sep 17 00:00:00 2001 From: Naiyuan Tian <110135109+nytian@users.noreply.github.com> Date: Fri, 22 Sep 2023 12:20:49 -0700 Subject: [PATCH 03/11] Update Release Template (#2594) * Update new-release-template.md * update typo --- .github/ISSUE_TEMPLATE/new-release-template.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/new-release-template.md b/.github/ISSUE_TEMPLATE/new-release-template.md index eb472a05c..1a52e32a1 100644 --- a/.github/ISSUE_TEMPLATE/new-release-template.md +++ b/.github/ISSUE_TEMPLATE/new-release-template.md @@ -14,6 +14,7 @@ _Due: <2-3-business-days-before-release>_ - [ ] Delete DTFx test packages from the [ADO feed](https://dev.azure.com/durabletaskframework/Durable%20Task%20Framework%20CI/_artifacts/feed/durabletask). - [ ] Run the [DTFx release pipeline](https://durabletaskframework.visualstudio.com/Durable%20Task%20Framework%20CI/_build?definitionId=21) ([defined here](https://github.com/Azure/durabletask/blob/main/azure-pipelines-release.yml)) to obtain new packages. - [ ] Publish DTFx packages to the [ADO feed](https://dev.azure.com/durabletaskframework/Durable%20Task%20Framework%20CI/_artifacts/feed/durabletask) for testing. +- [ ] Keep branch `azure-storage-v12` updated with branch `main`. **Prep Release (assigned to: )** _Due: <2-business-days-before-release>_ @@ -23,6 +24,7 @@ _Due: <2-business-days-before-release>_ - [ ] Add the Durable Functions package to the [ADO test feed](https://dev.azure.com/durabletaskframework/Durable%20Task%20Framework%20CI/_artifacts/feed/durabletask-test). - [ ] Check for package size, make sure it's not surprisingly heavier than a previous release. - [ ] Merge (**choose create a merge commit, NOT squash merge**) dev into main. Person performing validation must approve PR. +- [ ] Keep branch `v3.x` updated with branch `dev`. Do not merge PRs that are specific to Durable Functions v2. **Validation** _Due: <1-business-days-before-release>_ From 7dbbc91563f4d1e24a814f6e0d45df7f4fce034b Mon Sep 17 00:00:00 2001 From: David Justo Date: Wed, 4 Oct 2023 23:40:02 +0200 Subject: [PATCH 04/11] Add Netherite smoke test (#2597) --- .../smoketest-netherite-inproc-v4.yml | 22 ++ .../BackendSmokeTests/Netherite/.gitignore | 264 ++++++++++++++++++ .../BackendSmokeTests/Netherite/Dockerfile | 16 ++ .../DurableFunctionsOrchestrationCSharp.cs | 50 ++++ .../Netherite/Netherite.csproj | 20 ++ .../BackendSmokeTests/Netherite/Nuget.config | 6 + .../BackendSmokeTests/Netherite/host.json | 23 ++ 7 files changed, 401 insertions(+) create mode 100644 .github/workflows/smoketest-netherite-inproc-v4.yml create mode 100644 test/SmokeTests/BackendSmokeTests/Netherite/.gitignore create mode 100644 test/SmokeTests/BackendSmokeTests/Netherite/Dockerfile create mode 100644 test/SmokeTests/BackendSmokeTests/Netherite/DurableFunctionsOrchestrationCSharp.cs create mode 100644 test/SmokeTests/BackendSmokeTests/Netherite/Netherite.csproj create mode 100644 test/SmokeTests/BackendSmokeTests/Netherite/Nuget.config create mode 100644 test/SmokeTests/BackendSmokeTests/Netherite/host.json diff --git a/.github/workflows/smoketest-netherite-inproc-v4.yml b/.github/workflows/smoketest-netherite-inproc-v4.yml new file mode 100644 index 000000000..607e2ad71 --- /dev/null +++ b/.github/workflows/smoketest-netherite-inproc-v4.yml @@ -0,0 +1,22 @@ +name: Smoke Test - .NET in-proc w/ Netherite on Functions V4 + +on: + push: + branches: [ main, dev ] + paths: + - 'src/**' + - 'test/SmokeTests/BackendSmokeTests/Netherite/**' + pull_request: + branches: [ main, dev ] + paths: + - 'src/**' + - 'test/SmokeTests/BackendSmokeTests/Netherite/**' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Run V4 .NET in-proc w/ Netherite Smoke Test + run: test/SmokeTests/e2e-test.ps1 -DockerfilePath test/SmokeTests/BackendSmokeTests/Netherite/Dockerfile -HttpStartPath api/DurableFunctionsHttpStart -ContainerName NetheriteApp + shell: pwsh diff --git a/test/SmokeTests/BackendSmokeTests/Netherite/.gitignore b/test/SmokeTests/BackendSmokeTests/Netherite/.gitignore new file mode 100644 index 000000000..ff5b00c50 --- /dev/null +++ b/test/SmokeTests/BackendSmokeTests/Netherite/.gitignore @@ -0,0 +1,264 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# Azure Functions localsettings file +local.settings.json + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +#*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc \ No newline at end of file diff --git a/test/SmokeTests/BackendSmokeTests/Netherite/Dockerfile b/test/SmokeTests/BackendSmokeTests/Netherite/Dockerfile new file mode 100644 index 000000000..7daf7ffcb --- /dev/null +++ b/test/SmokeTests/BackendSmokeTests/Netherite/Dockerfile @@ -0,0 +1,16 @@ +# Copyright (c) .NET Foundation. All rights reserved. +# Licensed under the MIT License. See LICENSE in the project root for license information. + +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build-env + +# Build the app +COPY . /root +RUN cd /root/test/SmokeTests/BackendSmokeTests/Netherite && \ + mkdir -p /home/site/wwwroot && \ + dotnet publish -c Release --output /home/site/wwwroot + +# Deploy the app +FROM mcr.microsoft.com/azure-functions/dotnet:4.0 +ENV AzureWebJobsScriptRoot=/home/site/wwwroot \ + AzureFunctionsJobHost__Logging__Console__IsEnabled=true +COPY --from=build-env ["/home/site/wwwroot", "/home/site/wwwroot"] diff --git a/test/SmokeTests/BackendSmokeTests/Netherite/DurableFunctionsOrchestrationCSharp.cs b/test/SmokeTests/BackendSmokeTests/Netherite/DurableFunctionsOrchestrationCSharp.cs new file mode 100644 index 000000000..5dc6d7c3e --- /dev/null +++ b/test/SmokeTests/BackendSmokeTests/Netherite/DurableFunctionsOrchestrationCSharp.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Microsoft.Azure.WebJobs.Extensions.Http; +using Microsoft.Azure.WebJobs.Host; +using Microsoft.Extensions.Logging; + +namespace Company.Function +{ + public static class DurableFunctionsOrchestrationCSharp + { + [FunctionName("DurableFunctionsOrchestrationCSharp")] + public static async Task> RunOrchestrator( + [OrchestrationTrigger] IDurableOrchestrationContext context) + { + var outputs = new List(); + + // Replace "hello" with the name of your Durable Activity Function. + outputs.Add(await context.CallActivityAsync(nameof(SayHello), "Tokyo")); + outputs.Add(await context.CallActivityAsync(nameof(SayHello), "Seattle")); + outputs.Add(await context.CallActivityAsync(nameof(SayHello), "London")); + + // returns ["Hello Tokyo!", "Hello Seattle!", "Hello London!"] + return outputs; + } + + [FunctionName(nameof(SayHello))] + public static string SayHello([ActivityTrigger] string name, ILogger log) + { + log.LogInformation("Saying hello to {name}.", name); + return $"Hello {name}!"; + } + + [FunctionName("DurableFunctionsHttpStart")] + public static async Task HttpStart( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestMessage req, + [DurableClient] IDurableOrchestrationClient starter, + ILogger log) + { + // Function input comes from the request content. + string instanceId = await starter.StartNewAsync("DurableFunctionsOrchestrationCSharp", null); + + log.LogInformation("Started orchestration with ID = '{instanceId}'.", instanceId); + + return starter.CreateCheckStatusResponse(req, instanceId); + } + } +} \ No newline at end of file diff --git a/test/SmokeTests/BackendSmokeTests/Netherite/Netherite.csproj b/test/SmokeTests/BackendSmokeTests/Netherite/Netherite.csproj new file mode 100644 index 000000000..f1d8b2660 --- /dev/null +++ b/test/SmokeTests/BackendSmokeTests/Netherite/Netherite.csproj @@ -0,0 +1,20 @@ + + + net6.0 + v4 + + + + + + + + + PreserveNewest + + + PreserveNewest + Never + + + diff --git a/test/SmokeTests/BackendSmokeTests/Netherite/Nuget.config b/test/SmokeTests/BackendSmokeTests/Netherite/Nuget.config new file mode 100644 index 000000000..440b9b697 --- /dev/null +++ b/test/SmokeTests/BackendSmokeTests/Netherite/Nuget.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/test/SmokeTests/BackendSmokeTests/Netherite/host.json b/test/SmokeTests/BackendSmokeTests/Netherite/host.json new file mode 100644 index 000000000..35cc1adde --- /dev/null +++ b/test/SmokeTests/BackendSmokeTests/Netherite/host.json @@ -0,0 +1,23 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + }, + "enableLiveMetricsFilters": true + } + }, + "extensions": { + "durableTask": { + "useGracefulShutdown": true, + "storageProvider": { + "type": "Netherite", + "partitionCount": 12, + "StorageConnectionName": "AzureWebJobsStorage", + "EventHubsConnectionName": "SingleHost" + } + } + } +} \ No newline at end of file From 0220ee521f539b0aaf18b7d3441664041714b01a Mon Sep 17 00:00:00 2001 From: David Justo Date: Fri, 6 Oct 2023 22:59:52 +0200 Subject: [PATCH 05/11] Add target-based scaling support for Azure Storage (#2452) --- .../AzureStorageDurabilityProvider.cs | 34 +++- .../DurabilityProvider.cs | 22 +++ .../DurableTaskExtension.cs | 82 +------- ...rableTaskJobHostConfigurationExtensions.cs | 28 ++- .../Listener/DurableTaskListener.cs | 38 +++- .../Listener/DurableTaskMetricsProvider.cs | 79 ++++++++ .../Listener/DurableTaskScaleMonitor.cs | 57 ++---- .../Listener/DurableTaskTargetScaler.cs | 94 +++++++++ .../Listener/DurableTaskTriggerMetrics.cs | 7 +- ...t.Azure.WebJobs.Extensions.DurableTask.xml | 49 ++--- .../Options/DurableTaskOptions.cs | 20 ++ .../Scale/DurableTaskTriggersScaleProvider.cs | 129 +++++++++++++ .../Scale/ScaleUtils.cs | 120 ++++++++++++ .../WebJobs.Extensions.DurableTask.csproj | 2 +- test/Common/TestHelpers.cs | 11 +- .../DurableTaskScaleMonitorTests.cs | 7 + .../DurableTaskTargetScalerTests.cs | 179 ++++++++++++++++++ .../PlatformSpecificHelpers.FunctionsV2.cs | 40 ++-- ...obs.Extensions.DurableTask.Tests.V2.csproj | 2 +- 19 files changed, 829 insertions(+), 171 deletions(-) create mode 100644 src/WebJobs.Extensions.DurableTask/Listener/DurableTaskMetricsProvider.cs create mode 100644 src/WebJobs.Extensions.DurableTask/Listener/DurableTaskTargetScaler.cs create mode 100644 src/WebJobs.Extensions.DurableTask/Scale/DurableTaskTriggersScaleProvider.cs create mode 100644 src/WebJobs.Extensions.DurableTask/Scale/ScaleUtils.cs create mode 100644 test/FunctionsV2/DurableTaskTargetScalerTests.cs diff --git a/src/WebJobs.Extensions.DurableTask/AzureStorageDurabilityProvider.cs b/src/WebJobs.Extensions.DurableTask/AzureStorageDurabilityProvider.cs index f395c23de..38151e3cc 100644 --- a/src/WebJobs.Extensions.DurableTask/AzureStorageDurabilityProvider.cs +++ b/src/WebJobs.Extensions.DurableTask/AzureStorageDurabilityProvider.cs @@ -10,6 +10,7 @@ using DurableTask.AzureStorage.Tracking; using DurableTask.Core; using Microsoft.Extensions.Logging; +using Microsoft.WindowsAzure.Storage; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Newtonsoft.Json.Linq; @@ -200,6 +201,16 @@ internal static OrchestrationInstanceStatusQueryCondition ConvertWebjobsDurableC } #if !FUNCTIONS_V1 + + internal DurableTaskMetricsProvider GetMetricsProvider( + string functionName, + string hubName, + CloudStorageAccount storageAccount, + ILogger logger) + { + return new DurableTaskMetricsProvider(functionName, hubName, logger, performanceMonitor: null, storageAccount); + } + /// public override bool TryGetScaleMonitor( string functionId, @@ -208,12 +219,31 @@ public override bool TryGetScaleMonitor( string connectionName, out IScaleMonitor scaleMonitor) { + CloudStorageAccount storageAccount = this.storageAccountProvider.GetStorageAccountDetails(connectionName).ToCloudStorageAccount(); + DurableTaskMetricsProvider metricsProvider = this.GetMetricsProvider(functionName, hubName, storageAccount, this.logger); scaleMonitor = new DurableTaskScaleMonitor( functionId, functionName, hubName, - this.storageAccountProvider.GetStorageAccountDetails(connectionName).ToCloudStorageAccount(), - this.logger); + storageAccount, + this.logger, + metricsProvider); + return true; + } + +#endif +#if FUNCTIONS_V3_OR_GREATER + public override bool TryGetTargetScaler( + string functionId, + string functionName, + string hubName, + string connectionName, + out ITargetScaler targetScaler) + { + // This is only called by the ScaleController, it doesn't run in the Functions Host process. + CloudStorageAccount storageAccount = this.storageAccountProvider.GetStorageAccountDetails(connectionName).ToCloudStorageAccount(); + DurableTaskMetricsProvider metricsProvider = this.GetMetricsProvider(functionName, hubName, storageAccount, this.logger); + targetScaler = new DurableTaskTargetScaler(functionId, metricsProvider, this, this.logger); return true; } #endif diff --git a/src/WebJobs.Extensions.DurableTask/DurabilityProvider.cs b/src/WebJobs.Extensions.DurableTask/DurabilityProvider.cs index 2bc6f22e2..529a7adab 100644 --- a/src/WebJobs.Extensions.DurableTask/DurabilityProvider.cs +++ b/src/WebJobs.Extensions.DurableTask/DurabilityProvider.cs @@ -565,5 +565,27 @@ public virtual bool TryGetScaleMonitor( return false; } #endif + +#if FUNCTIONS_V3_OR_GREATER + /// + /// Tries to obtain a scaler for target based scaling. + /// + /// Function id. + /// Function name. + /// Task hub name. + /// The name of the storage-specific connection settings. + /// The target-based scaler. + /// True if target-based scaling is supported, false otherwise. + public virtual bool TryGetTargetScaler( + string functionId, + string functionName, + string hubName, + string connectionName, + out ITargetScaler targetScaler) + { + targetScaler = null; + return false; + } +#endif } } diff --git a/src/WebJobs.Extensions.DurableTask/DurableTaskExtension.cs b/src/WebJobs.Extensions.DurableTask/DurableTaskExtension.cs index cc1e63976..dd351c1fe 100644 --- a/src/WebJobs.Extensions.DurableTask/DurableTaskExtension.cs +++ b/src/WebJobs.Extensions.DurableTask/DurableTaskExtension.cs @@ -150,13 +150,13 @@ public DurableTaskExtension( this.nameResolver = nameResolver ?? throw new ArgumentNullException(nameof(nameResolver)); this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); this.PlatformInformationService = platformInformationService ?? throw new ArgumentNullException(nameof(platformInformationService)); - this.ResolveAppSettingOptions(); + DurableTaskOptions.ResolveAppSettingOptions(this.Options, this.nameResolver); ILogger logger = loggerFactory.CreateLogger(LoggerCategoryName); this.TraceHelper = new EndToEndTraceHelper(logger, this.Options.Tracing.TraceReplayEvents); this.LifeCycleNotificationHelper = lifeCycleNotificationHelper ?? this.CreateLifeCycleNotificationHelper(); - this.durabilityProviderFactory = this.GetDurabilityProviderFactory(this.Options, logger, orchestrationServiceFactories); + this.durabilityProviderFactory = GetDurabilityProviderFactory(this.Options, logger, orchestrationServiceFactories); this.defaultDurabilityProvider = this.durabilityProviderFactory.GetDurabilityProvider(); this.isOptionsConfigured = true; @@ -249,6 +249,8 @@ public string HubName internal DurableTaskOptions Options { get; } + internal DurabilityProvider DefaultDurabilityProvider => this.defaultDurabilityProvider; + internal HttpApiHandler HttpApiHandler { get; private set; } internal ILifeCycleNotificationHelper LifeCycleNotificationHelper { get; private set; } @@ -296,7 +298,7 @@ private MessagePayloadDataConverter CreateErrorDataConverter(IErrorSerializerSet return new MessagePayloadDataConverter(errorSerializerSettingsFactory.CreateJsonSerializerSettings(), isDefault); } - private IDurabilityProviderFactory GetDurabilityProviderFactory(DurableTaskOptions options, ILogger logger, IEnumerable orchestrationServiceFactories) + internal static IDurabilityProviderFactory GetDurabilityProviderFactory(DurableTaskOptions options, ILogger logger, IEnumerable orchestrationServiceFactories) { bool storageTypeIsConfigured = options.StorageProvider.TryGetValue("type", out object storageType); @@ -578,32 +580,13 @@ private void StopLocalGrpcServer() } #endif - private void ResolveAppSettingOptions() - { - if (this.Options == null) - { - throw new InvalidOperationException($"{nameof(this.Options)} must be set before resolving app settings."); - } - - if (this.nameResolver == null) - { - throw new InvalidOperationException($"{nameof(this.nameResolver)} must be set before resolving app settings."); - } - - if (this.nameResolver.TryResolveWholeString(this.Options.HubName, out string taskHubName)) - { - // use the resolved task hub name - this.Options.HubName = taskHubName; - } - } - private void InitializeForFunctionsV1(ExtensionConfigContext context) { #if FUNCTIONS_V1 context.ApplyConfig(this.Options, "DurableTask"); this.nameResolver = context.Config.NameResolver; this.loggerFactory = context.Config.LoggerFactory; - this.ResolveAppSettingOptions(); + DurableTaskOptions.ResolveAppSettingOptions(this.Options, this.nameResolver); ILogger logger = this.loggerFactory.CreateLogger(LoggerCategoryName); this.TraceHelper = new EndToEndTraceHelper(logger, this.Options.Tracing.TraceReplayEvents); this.connectionInfoResolver = new WebJobsConnectionInfoProvider(); @@ -1573,59 +1556,6 @@ internal static void TagActivityWithOrchestrationStatus(OrchestrationRuntimeStat activity.AddTag("DurableFunctionsRuntimeStatus", statusStr); } } - - internal IScaleMonitor GetScaleMonitor(string functionId, FunctionName functionName, string connectionName) - { - if (this.defaultDurabilityProvider.TryGetScaleMonitor( - functionId, - functionName.Name, - this.Options.HubName, - connectionName, - out IScaleMonitor scaleMonitor)) - { - return scaleMonitor; - } - else - { - // the durability provider does not support runtime scaling. - // Create an empty scale monitor to avoid exceptions (unless runtime scaling is actually turned on). - return new NoOpScaleMonitor($"{functionId}-DurableTaskTrigger-{this.Options.HubName}".ToLower()); - } - } - - /// - /// A placeholder scale monitor, can be used by durability providers that do not support runtime scaling. - /// This is required to allow operation of those providers even if runtime scaling is turned off - /// see discussion https://github.com/Azure/azure-functions-durable-extension/pull/1009/files#r341767018. - /// - private sealed class NoOpScaleMonitor : IScaleMonitor - { - /// - /// Construct a placeholder scale monitor. - /// - /// A descriptive name. - public NoOpScaleMonitor(string name) - { - this.Descriptor = new ScaleMonitorDescriptor(name); - } - - /// - /// A descriptive name. - /// - public ScaleMonitorDescriptor Descriptor { get; private set; } - - /// - Task IScaleMonitor.GetMetricsAsync() - { - throw new InvalidOperationException("The current DurableTask backend configuration does not support runtime scaling"); - } - - /// - ScaleStatus IScaleMonitor.GetScaleStatus(ScaleStatusContext context) - { - throw new InvalidOperationException("The current DurableTask backend configuration does not support runtime scaling"); - } - } #endif } } diff --git a/src/WebJobs.Extensions.DurableTask/DurableTaskJobHostConfigurationExtensions.cs b/src/WebJobs.Extensions.DurableTask/DurableTaskJobHostConfigurationExtensions.cs index ac98d8632..bd793c17a 100644 --- a/src/WebJobs.Extensions.DurableTask/DurableTaskJobHostConfigurationExtensions.cs +++ b/src/WebJobs.Extensions.DurableTask/DurableTaskJobHostConfigurationExtensions.cs @@ -2,16 +2,19 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. using System; -using System.Net.Http; -using System.Threading; +using System.Collections.Generic; +using System.Linq; #if !FUNCTIONS_V1 using Microsoft.Azure.WebJobs.Extensions.DurableTask.Auth; using Microsoft.Azure.WebJobs.Extensions.DurableTask.ContextImplementations; using Microsoft.Azure.WebJobs.Extensions.DurableTask.Correlation; using Microsoft.Azure.WebJobs.Extensions.DurableTask.Options; +using Microsoft.Azure.WebJobs.Extensions.DurableTask.Scale; +using Microsoft.Azure.WebJobs.Host.Scale; using Microsoft.Extensions.Azure; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; #else using Microsoft.Azure.WebJobs.Extensions.DurableTask.ContextImplementations; @@ -109,6 +112,27 @@ public static IWebJobsBuilder AddDurableTask(this IWebJobsBuilder builder) return builder; } +#if FUNCTIONS_V3_OR_GREATER + /// + /// Adds the and providers for the Durable Triggers. + /// + /// The to configure. + /// Returns the provided . + internal static IWebJobsBuilder AddDurableScaleForTrigger(this IWebJobsBuilder builder, TriggerMetadata triggerMetadata) + { + // this segment adheres to the followings pattern: https://github.com/Azure/azure-sdk-for-net/pull/38756 + DurableTaskTriggersScaleProvider provider = null; + builder.Services.AddSingleton(serviceProvider => + { + provider = new DurableTaskTriggersScaleProvider(serviceProvider.GetService>(), serviceProvider.GetService(), serviceProvider.GetService(), serviceProvider.GetService>(), triggerMetadata); + return provider; + }); + builder.Services.AddSingleton(serviceProvider => serviceProvider.GetServices().Single(x => x == provider)); + builder.Services.AddSingleton(serviceProvider => serviceProvider.GetServices().Single(x => x == provider)); + return builder; + } +#endif + /// /// Adds the Durable Task extension to the provided . /// diff --git a/src/WebJobs.Extensions.DurableTask/Listener/DurableTaskListener.cs b/src/WebJobs.Extensions.DurableTask/Listener/DurableTaskListener.cs index e764ae623..4fa87cc07 100644 --- a/src/WebJobs.Extensions.DurableTask/Listener/DurableTaskListener.cs +++ b/src/WebJobs.Extensions.DurableTask/Listener/DurableTaskListener.cs @@ -2,12 +2,9 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. using System; -using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; -using DurableTask.AzureStorage.Monitoring; -using Microsoft.Azure.WebJobs.Host.Executors; +using Microsoft.Azure.WebJobs.Extensions.DurableTask.Scale; using Microsoft.Azure.WebJobs.Host.Listeners; #if !FUNCTIONS_V1 using Microsoft.Azure.WebJobs.Host.Scale; @@ -15,7 +12,9 @@ namespace Microsoft.Azure.WebJobs.Extensions.DurableTask { -#if !FUNCTIONS_V1 +#if FUNCTIONS_V3_OR_GREATER + internal sealed class DurableTaskListener : IListener, IScaleMonitorProvider, ITargetScalerProvider +#elif FUNCTIONS_V2_OR_GREATER internal sealed class DurableTaskListener : IListener, IScaleMonitorProvider #else internal sealed class DurableTaskListener : IListener @@ -26,10 +25,15 @@ internal sealed class DurableTaskListener : IListener private readonly FunctionName functionName; private readonly FunctionType functionType; private readonly string connectionName; + #if !FUNCTIONS_V1 private readonly Lazy scaleMonitor; #endif +#if FUNCTIONS_V3_OR_GREATER + private readonly Lazy targetScaler; +#endif + public DurableTaskListener( DurableTaskExtension config, string functionId, @@ -48,12 +52,25 @@ public DurableTaskListener( this.functionName = functionName; this.functionType = functionType; this.connectionName = connectionName; + #if !FUNCTIONS_V1 this.scaleMonitor = new Lazy(() => - this.config.GetScaleMonitor( + ScaleUtils.GetScaleMonitor( + this.config.DefaultDurabilityProvider, this.functionId, this.functionName, - this.connectionName)); + this.connectionName, + this.config.Options.HubName)); + +#endif +#if FUNCTIONS_V3_OR_GREATER + this.targetScaler = new Lazy(() => + ScaleUtils.GetTargetScaler( + this.config.DefaultDurabilityProvider, + this.functionId, + this.functionName, + this.connectionName, + this.config.Options.HubName)); #endif } @@ -98,5 +115,12 @@ public IScaleMonitor GetMonitor() return this.scaleMonitor.Value; } #endif + +#if FUNCTIONS_V3_OR_GREATER + public ITargetScaler GetTargetScaler() + { + return this.targetScaler.Value; + } +#endif } } diff --git a/src/WebJobs.Extensions.DurableTask/Listener/DurableTaskMetricsProvider.cs b/src/WebJobs.Extensions.DurableTask/Listener/DurableTaskMetricsProvider.cs new file mode 100644 index 000000000..e3565d169 --- /dev/null +++ b/src/WebJobs.Extensions.DurableTask/Listener/DurableTaskMetricsProvider.cs @@ -0,0 +1,79 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +#if !FUNCTIONS_V1 +using System; +using System.Threading.Tasks; +using DurableTask.AzureStorage.Monitoring; +using Microsoft.Extensions.Logging; +using Microsoft.WindowsAzure.Storage; +using Newtonsoft.Json; + +namespace Microsoft.Azure.WebJobs.Extensions.DurableTask +{ + internal class DurableTaskMetricsProvider + { + private readonly string functionName; + private readonly string hubName; + private readonly ILogger logger; + private readonly CloudStorageAccount storageAccount; + + private DisconnectedPerformanceMonitor performanceMonitor; + + public DurableTaskMetricsProvider(string functionName, string hubName, ILogger logger, DisconnectedPerformanceMonitor performanceMonitor, CloudStorageAccount storageAccount) + { + this.functionName = functionName; + this.hubName = hubName; + this.logger = logger; + this.performanceMonitor = performanceMonitor; + this.storageAccount = storageAccount; + } + + public virtual async Task GetMetricsAsync() + { + DurableTaskTriggerMetrics metrics = new DurableTaskTriggerMetrics(); + + // Durable stores its own metrics, so we just collect them here + PerformanceHeartbeat heartbeat = null; + try + { + DisconnectedPerformanceMonitor performanceMonitor = this.GetPerformanceMonitor(); + heartbeat = await performanceMonitor.PulseAsync(); + } + catch (StorageException e) + { + this.logger.LogWarning("{details}. Function: {functionName}. HubName: {hubName}.", e.ToString(), this.functionName, this.hubName); + } + + if (heartbeat != null) + { + metrics.PartitionCount = heartbeat.PartitionCount; + metrics.ControlQueueLengths = JsonConvert.SerializeObject(heartbeat.ControlQueueLengths); + metrics.ControlQueueLatencies = JsonConvert.SerializeObject(heartbeat.ControlQueueLatencies); + metrics.WorkItemQueueLength = heartbeat.WorkItemQueueLength; + if (heartbeat.WorkItemQueueLatency > TimeSpan.Zero) + { + metrics.WorkItemQueueLatency = heartbeat.WorkItemQueueLatency.ToString(); + } + } + + return metrics; + } + + internal DisconnectedPerformanceMonitor GetPerformanceMonitor() + { + if (this.performanceMonitor == null) + { + if (this.storageAccount == null) + { + throw new ArgumentNullException(nameof(this.storageAccount)); + } + + this.performanceMonitor = new DisconnectedPerformanceMonitor(this.storageAccount, this.hubName); + } + + return this.performanceMonitor; + } + } +} +#endif \ No newline at end of file diff --git a/src/WebJobs.Extensions.DurableTask/Listener/DurableTaskScaleMonitor.cs b/src/WebJobs.Extensions.DurableTask/Listener/DurableTaskScaleMonitor.cs index 3bf18598d..4c05b3df0 100644 --- a/src/WebJobs.Extensions.DurableTask/Listener/DurableTaskScaleMonitor.cs +++ b/src/WebJobs.Extensions.DurableTask/Listener/DurableTaskScaleMonitor.cs @@ -22,6 +22,7 @@ internal sealed class DurableTaskScaleMonitor : IScaleMonitor IScaleMonitor.GetMetricsAsync() @@ -72,33 +75,7 @@ async Task IScaleMonitor.GetMetricsAsync() public async Task GetMetricsAsync() { - DurableTaskTriggerMetrics metrics = new DurableTaskTriggerMetrics(); - - // Durable stores its own metrics, so we just collect them here - PerformanceHeartbeat heartbeat = null; - try - { - DisconnectedPerformanceMonitor performanceMonitor = this.GetPerformanceMonitor(); - heartbeat = await performanceMonitor.PulseAsync(); - } - catch (StorageException e) - { - this.logger.LogWarning("{details}. Function: {functionName}. HubName: {hubName}.", e.ToString(), this.functionName, this.hubName); - } - - if (heartbeat != null) - { - metrics.PartitionCount = heartbeat.PartitionCount; - metrics.ControlQueueLengths = JsonConvert.SerializeObject(heartbeat.ControlQueueLengths); - metrics.ControlQueueLatencies = JsonConvert.SerializeObject(heartbeat.ControlQueueLatencies); - metrics.WorkItemQueueLength = heartbeat.WorkItemQueueLength; - if (heartbeat.WorkItemQueueLatency > TimeSpan.Zero) - { - metrics.WorkItemQueueLatency = heartbeat.WorkItemQueueLatency.ToString(); - } - } - - return metrics; + return await this.durableTaskMetricsProvider.GetMetricsAsync(); } ScaleStatus IScaleMonitor.GetScaleStatus(ScaleStatusContext context) @@ -151,7 +128,7 @@ private ScaleStatus GetScaleStatusCore(int workerCount, DurableTaskTriggerMetric } } - DisconnectedPerformanceMonitor performanceMonitor = this.GetPerformanceMonitor(); + DisconnectedPerformanceMonitor performanceMonitor = this.durableTaskMetricsProvider.GetPerformanceMonitor(); var scaleRecommendation = performanceMonitor.MakeScaleRecommendation(workerCount, heartbeats.ToArray()); bool writeToUserLogs = false; diff --git a/src/WebJobs.Extensions.DurableTask/Listener/DurableTaskTargetScaler.cs b/src/WebJobs.Extensions.DurableTask/Listener/DurableTaskTargetScaler.cs new file mode 100644 index 000000000..c1faa94f4 --- /dev/null +++ b/src/WebJobs.Extensions.DurableTask/Listener/DurableTaskTargetScaler.cs @@ -0,0 +1,94 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +#if FUNCTIONS_V3_OR_GREATER +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Azure.WebJobs.Host.Scale; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; + +namespace Microsoft.Azure.WebJobs.Extensions.DurableTask +{ + internal class DurableTaskTargetScaler : ITargetScaler + { + private readonly DurableTaskMetricsProvider metricsProvider; + private readonly TargetScalerResult scaleResult; + private readonly DurabilityProvider durabilityProvider; + private readonly ILogger logger; + private readonly string functionId; + + public DurableTaskTargetScaler(string functionId, DurableTaskMetricsProvider metricsProvider, DurabilityProvider durabilityProvider, ILogger logger) + { + this.functionId = functionId; + this.metricsProvider = metricsProvider; + this.scaleResult = new TargetScalerResult(); + this.TargetScalerDescriptor = new TargetScalerDescriptor(this.functionId); + this.durabilityProvider = durabilityProvider; + this.logger = logger; + } + + public TargetScalerDescriptor TargetScalerDescriptor { get; } + + private int MaxConcurrentActivities => this.durabilityProvider.MaxConcurrentTaskActivityWorkItems; + + private int MaxConcurrentOrchestrators => this.durabilityProvider.MaxConcurrentTaskOrchestrationWorkItems; + + public async Task GetScaleResultAsync(TargetScalerContext context) + { + DurableTaskTriggerMetrics? metrics = null; + try + { + // This method is only invoked by the ScaleController, so it doesn't run in the Functions Host process. + metrics = await this.metricsProvider.GetMetricsAsync(); + + // compute activityWorkers: the number of workers we need to process all activity messages + var workItemQueueLength = metrics.WorkItemQueueLength; + double activityWorkers = Math.Ceiling(workItemQueueLength / (double)this.MaxConcurrentActivities); + + var serializedControlQueueLengths = metrics.ControlQueueLengths; + var controlQueueLengths = JsonConvert.DeserializeObject>(serializedControlQueueLengths); + + var controlQueueMessages = controlQueueLengths.Sum(); + var activeControlQueues = controlQueueLengths.Count(x => x > 0); + + // compute orchestratorWorkers: the number of workers we need to process all orchestrator messages. + // We bound this result to be no larger than the partition count + var upperBoundControlWorkers = Math.Ceiling(controlQueueMessages / (double)this.MaxConcurrentOrchestrators); + var orchestratorWorkers = Math.Min(activeControlQueues, upperBoundControlWorkers); + + int numWorkersToRequest = (int)Math.Max(activityWorkers, orchestratorWorkers); + this.scaleResult.TargetWorkerCount = numWorkersToRequest; + + // When running on ScaleController V3, ILogger logs are forwarded to the ScaleController's Kusto table. + // This works because this code does not execute in the Functions Host process, but in the ScaleController process, + // and the ScaleController is injecting it's own custom ILogger implementation that forwards logs to Kusto. + var metricsLog = $"Metrics: workItemQueueLength={workItemQueueLength}. controlQueueLengths={serializedControlQueueLengths}. " + + $"maxConcurrentOrchestrators={this.MaxConcurrentOrchestrators}. maxConcurrentActivities={this.MaxConcurrentActivities}"; + var scaleControllerLog = $"Target worker count for '{this.functionId}' is '{numWorkersToRequest}'. " + + metricsLog; + + // target worker count should never be negative + if (numWorkersToRequest < 0) + { + throw new InvalidOperationException("Number of workers to request cannot be negative"); + } + + this.logger.LogInformation(scaleControllerLog); + return this.scaleResult; + } + catch (Exception ex) + { + // We want to augment the exception with metrics information for investigation purposes + var metricsLog = $"Metrics: workItemQueueLength={metrics?.WorkItemQueueLength}. controlQueueLengths={metrics?.ControlQueueLengths}. " + + $"maxConcurrentOrchestrators={this.MaxConcurrentOrchestrators}. maxConcurrentActivities={this.MaxConcurrentActivities}"; + var errorLog = $"Error: target worker count for '{this.functionId}' resulted in exception. " + metricsLog; + throw new Exception(errorLog, ex); + } + } + } +} +#endif \ No newline at end of file diff --git a/src/WebJobs.Extensions.DurableTask/Listener/DurableTaskTriggerMetrics.cs b/src/WebJobs.Extensions.DurableTask/Listener/DurableTaskTriggerMetrics.cs index 43008bbd9..68fa8a10c 100644 --- a/src/WebJobs.Extensions.DurableTask/Listener/DurableTaskTriggerMetrics.cs +++ b/src/WebJobs.Extensions.DurableTask/Listener/DurableTaskTriggerMetrics.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. #if !FUNCTIONS_V1 +using System.Collections.Generic; using Microsoft.Azure.WebJobs.Host.Scale; namespace Microsoft.Azure.WebJobs.Extensions.DurableTask @@ -10,13 +11,13 @@ internal class DurableTaskTriggerMetrics : ScaleMetrics /// /// The number of partitions in the task hub. /// - public int PartitionCount { get; set; } + public virtual int PartitionCount { get; set; } /// /// The number of messages across control queues. This will /// be in the form of a serialized array of ints, e.g. "[1,2,3,4]". /// - public string ControlQueueLengths { get; set; } + public virtual string ControlQueueLengths { get; set; } /// /// The latency of messages across control queues. This will @@ -28,7 +29,7 @@ internal class DurableTaskTriggerMetrics : ScaleMetrics /// /// The number of messages in the work-item queue. /// - public int WorkItemQueueLength { get; set; } + public virtual int WorkItemQueueLength { get; set; } /// /// The approximate age of the first work-item queue message. This diff --git a/src/WebJobs.Extensions.DurableTask/Microsoft.Azure.WebJobs.Extensions.DurableTask.xml b/src/WebJobs.Extensions.DurableTask/Microsoft.Azure.WebJobs.Extensions.DurableTask.xml index 518e1305d..f76c0ad61 100644 --- a/src/WebJobs.Extensions.DurableTask/Microsoft.Azure.WebJobs.Extensions.DurableTask.xml +++ b/src/WebJobs.Extensions.DurableTask/Microsoft.Azure.WebJobs.Extensions.DurableTask.xml @@ -2634,30 +2634,6 @@ This metadata will show up in Application Insights, if enabled. - - - A placeholder scale monitor, can be used by durability providers that do not support runtime scaling. - This is required to allow operation of those providers even if runtime scaling is turned off - see discussion https://github.com/Azure/azure-functions-durable-extension/pull/1009/files#r341767018. - - - - - Construct a placeholder scale monitor. - - A descriptive name. - - - - A descriptive name. - - - - - - - - Extension for registering a Durable Functions configuration with JobHostConfiguration. @@ -4850,6 +4826,31 @@ The delegate to handle exception to determine if retries should proceed. + + + A placeholder scale monitor, can be used by durability providers that do not support runtime scaling. + This is required to allow operation of those providers even if runtime scaling is turned off + see discussion https://github.com/Azure/azure-functions-durable-extension/pull/1009/files#r341767018. + + + + + Construct a placeholder scale monitor. + + A descriptive name. + The function ID. + + + + A descriptive name. + + + + + + + + Connection info provider which resolves connection information from a standard application (Non WebJob). diff --git a/src/WebJobs.Extensions.DurableTask/Options/DurableTaskOptions.cs b/src/WebJobs.Extensions.DurableTask/Options/DurableTaskOptions.cs index 47249848a..e5eac789f 100644 --- a/src/WebJobs.Extensions.DurableTask/Options/DurableTaskOptions.cs +++ b/src/WebJobs.Extensions.DurableTask/Options/DurableTaskOptions.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Net.Http; using DurableTask.AzureStorage.Partitioning; +using Microsoft.Azure.WebJobs.Host; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Newtonsoft.Json.Linq; @@ -229,6 +230,25 @@ public string HubName // to mock the value from ExtensionConfigContext. It should not be used in production code paths. internal Func WebhookUriProviderOverride { get; set; } + internal static void ResolveAppSettingOptions(DurableTaskOptions options, INameResolver nameResolver) + { + if (options == null) + { + throw new InvalidOperationException($"{nameof(options)} must be set before resolving app settings."); + } + + if (nameResolver == null) + { + throw new InvalidOperationException($"{nameof(nameResolver)} must be set before resolving app settings."); + } + + if (nameResolver.TryResolveWholeString(options.HubName, out string taskHubName)) + { + // use the resolved task hub name + options.HubName = taskHubName; + } + } + /// /// Sets HubName to a value that is considered a default value. /// diff --git a/src/WebJobs.Extensions.DurableTask/Scale/DurableTaskTriggersScaleProvider.cs b/src/WebJobs.Extensions.DurableTask/Scale/DurableTaskTriggersScaleProvider.cs new file mode 100644 index 000000000..fd50f9e38 --- /dev/null +++ b/src/WebJobs.Extensions.DurableTask/Scale/DurableTaskTriggersScaleProvider.cs @@ -0,0 +1,129 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. +#nullable enable +#if FUNCTIONS_V3_OR_GREATER + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using Microsoft.Azure.WebJobs.Host.Scale; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; + +namespace Microsoft.Azure.WebJobs.Extensions.DurableTask.Scale +{ + internal class DurableTaskTriggersScaleProvider : IScaleMonitorProvider, ITargetScalerProvider + { + private readonly IScaleMonitor monitor; + private readonly ITargetScaler targetScaler; + private readonly DurableTaskOptions options; + private readonly INameResolver nameResolver; + private readonly ILoggerFactory loggerFactory; + private readonly IEnumerable durabilityProviderFactories; + + public DurableTaskTriggersScaleProvider( + IOptions durableTaskOptions, + INameResolver nameResolver, + ILoggerFactory loggerFactory, + IEnumerable durabilityProviderFactories, + TriggerMetadata triggerMetadata) + { + this.options = durableTaskOptions.Value; + this.nameResolver = nameResolver; + this.loggerFactory = loggerFactory; + this.durabilityProviderFactories = durabilityProviderFactories; + + string functionId = triggerMetadata.FunctionName; + FunctionName functionName = new FunctionName(functionId); + + this.GetOptions(triggerMetadata); + + IDurabilityProviderFactory durabilityProviderFactory = this.GetDurabilityProviderFactory(); + DurabilityProvider defaultDurabilityProvider = durabilityProviderFactory.GetDurabilityProvider(); + + string? connectionName = durabilityProviderFactory is AzureStorageDurabilityProviderFactory azureStorageDurabilityProviderFactory + ? azureStorageDurabilityProviderFactory.DefaultConnectionName + : null; + + this.targetScaler = ScaleUtils.GetTargetScaler( + defaultDurabilityProvider, + functionId, + functionName, + connectionName, + this.options.HubName); + + this.monitor = ScaleUtils.GetScaleMonitor( + defaultDurabilityProvider, + functionId, + functionName, + connectionName, + this.options.HubName); + } + + private void GetOptions(TriggerMetadata triggerMetadata) + { + // the metadata is the sync triggers payload + var metadata = triggerMetadata.Metadata.ToObject(); + + // The property `taskHubName` is always expected in the SyncTriggers payload + this.options.HubName = metadata?.TaskHubName ?? throw new InvalidOperationException($"Expected `taskHubName` property in SyncTriggers payload but found none. Payload: {triggerMetadata.Metadata}"); + if (metadata?.MaxConcurrentActivityFunctions != null) + { + this.options.MaxConcurrentActivityFunctions = metadata?.MaxConcurrentActivityFunctions; + } + + if (metadata?.MaxConcurrentOrchestratorFunctions != null) + { + this.options.MaxConcurrentOrchestratorFunctions = metadata?.MaxConcurrentOrchestratorFunctions; + } + + if (metadata?.StorageProvider != null) + { + this.options.StorageProvider = metadata?.StorageProvider; + } + + DurableTaskOptions.ResolveAppSettingOptions(this.options, this.nameResolver); + } + + private IDurabilityProviderFactory GetDurabilityProviderFactory() + { + var logger = this.loggerFactory.CreateLogger(); + var durabilityProviderFactory = DurableTaskExtension.GetDurabilityProviderFactory(this.options, logger, this.durabilityProviderFactories); + return durabilityProviderFactory; + } + + public IScaleMonitor GetMonitor() + { + return this.monitor; + } + + public ITargetScaler GetTargetScaler() + { + return this.targetScaler; + } + + /// + /// Captures the relevant DF SyncTriggers JSON properties for making scaling decisions. + /// + internal class DurableTaskMetadata + { + [JsonProperty] + public string? TaskHubName { get; set; } + + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] + [DefaultValue(null)] + public int? MaxConcurrentOrchestratorFunctions { get; set; } + + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] + [DefaultValue(null)] + public int? MaxConcurrentActivityFunctions { get; set; } + + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] + [DefaultValue(null)] + public IDictionary? StorageProvider { get; set; } + } + } +} +#endif \ No newline at end of file diff --git a/src/WebJobs.Extensions.DurableTask/Scale/ScaleUtils.cs b/src/WebJobs.Extensions.DurableTask/Scale/ScaleUtils.cs new file mode 100644 index 000000000..0d60da23f --- /dev/null +++ b/src/WebJobs.Extensions.DurableTask/Scale/ScaleUtils.cs @@ -0,0 +1,120 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. +#nullable enable + +using System; +using System.Threading.Tasks; + +#if !FUNCTIONS_V1 +using Microsoft.Azure.WebJobs.Host.Scale; +#endif + +namespace Microsoft.Azure.WebJobs.Extensions.DurableTask.Scale +{ + internal static class ScaleUtils + { +#if !FUNCTIONS_V1 + internal static IScaleMonitor GetScaleMonitor(DurabilityProvider durabilityProvider, string functionId, FunctionName functionName, string? connectionName, string hubName) + { + if (durabilityProvider.TryGetScaleMonitor( + functionId, + functionName.Name, + hubName, + connectionName, + out IScaleMonitor scaleMonitor)) + { + return scaleMonitor; + } + else + { + // the durability provider does not support runtime scaling. + // Create an empty scale monitor to avoid exceptions (unless runtime scaling is actually turned on). + return new NoOpScaleMonitor($"{functionId}-DurableTaskTrigger-{hubName}".ToLower(), functionId); + } + } + + /// + /// A placeholder scale monitor, can be used by durability providers that do not support runtime scaling. + /// This is required to allow operation of those providers even if runtime scaling is turned off + /// see discussion https://github.com/Azure/azure-functions-durable-extension/pull/1009/files#r341767018. + /// + internal sealed class NoOpScaleMonitor : IScaleMonitor + { + /// + /// Construct a placeholder scale monitor. + /// + /// A descriptive name. + /// The function ID. + public NoOpScaleMonitor(string name, string functionId) + { +#if FUNCTIONS_V3_OR_GREATER + this.Descriptor = new ScaleMonitorDescriptor(name, functionId); +#else +#pragma warning disable CS0618 // Type or member is obsolete + this.Descriptor = new ScaleMonitorDescriptor(name); +#pragma warning restore CS0618 // Type or member is obsolete +#endif + } + + /// + /// A descriptive name. + /// + public ScaleMonitorDescriptor Descriptor { get; private set; } + + /// + Task IScaleMonitor.GetMetricsAsync() + { + throw new InvalidOperationException("The current DurableTask backend configuration does not support runtime scaling"); + } + + /// + ScaleStatus IScaleMonitor.GetScaleStatus(ScaleStatusContext context) + { + throw new InvalidOperationException("The current DurableTask backend configuration does not support runtime scaling"); + } + } +#endif + +#if FUNCTIONS_V3_OR_GREATER +#pragma warning disable SA1201 // Elements should appear in the correct order + internal static ITargetScaler GetTargetScaler(DurabilityProvider durabilityProvider, string functionId, FunctionName functionName, string? connectionName, string hubName) +#pragma warning restore SA1201 // Elements should appear in the correct order + { + if (durabilityProvider.TryGetTargetScaler( + functionId, + functionName.Name, + hubName, + connectionName, + out ITargetScaler targetScaler)) + { + return targetScaler; + } + else + { + // the durability provider does not support target-based scaling. + // Create an empty target scaler to avoid exceptions (unless target-based scaling is actually turned on). + return new NoOpTargetScaler(functionId); + } + } + + internal sealed class NoOpTargetScaler : ITargetScaler + { + /// + /// Construct a placeholder target scaler. + /// + /// The function ID. + public NoOpTargetScaler(string functionId) + { + this.TargetScalerDescriptor = new TargetScalerDescriptor(functionId); + } + + public TargetScalerDescriptor TargetScalerDescriptor { get; } + + public Task GetScaleResultAsync(TargetScalerContext context) + { + throw new NotSupportedException("The current DurableTask backend configuration does not support target-based scaling"); + } + } +#endif + } +} diff --git a/src/WebJobs.Extensions.DurableTask/WebJobs.Extensions.DurableTask.csproj b/src/WebJobs.Extensions.DurableTask/WebJobs.Extensions.DurableTask.csproj index 8ee4cb106..b057bfdba 100644 --- a/src/WebJobs.Extensions.DurableTask/WebJobs.Extensions.DurableTask.csproj +++ b/src/WebJobs.Extensions.DurableTask/WebJobs.Extensions.DurableTask.csproj @@ -89,7 +89,7 @@ - + diff --git a/test/Common/TestHelpers.cs b/test/Common/TestHelpers.cs index ec1fd043c..dafa10e5d 100644 --- a/test/Common/TestHelpers.cs +++ b/test/Common/TestHelpers.cs @@ -13,9 +13,8 @@ using Microsoft.ApplicationInsights.Channel; #if !FUNCTIONS_V1 using Microsoft.Extensions.Hosting; +using Microsoft.Azure.WebJobs.Host.Scale; #endif -using Microsoft.Azure.WebJobs.Extensions.DurableTask.ContextImplementations; -using Microsoft.Azure.WebJobs.Extensions.DurableTask.Options; using Microsoft.Azure.WebJobs.Host.TestCommon; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -67,6 +66,9 @@ public static ITestHost GetJobHost( int entityMessageReorderWindowInMinutes = 30, string exactTaskHubName = null, bool addDurableClientFactory = false, +#if !FUNCTIONS_V1 + Action configureScaleOptions = null, +#endif Type[] types = null) { switch (storageProviderType) @@ -160,6 +162,7 @@ public static ITestHost GetJobHost( #if !FUNCTIONS_V1 addDurableClientFactory: addDurableClientFactory, types: types, + configureScaleOptions: configureScaleOptions, #endif durabilityProviderFactoryType: durabilityProviderFactoryType); } @@ -175,6 +178,9 @@ public static ITestHost GetJobHostWithOptions( Action onSend = null, Type durabilityProviderFactoryType = null, bool addDurableClientFactory = false, +#if !FUNCTIONS_V1 + Action configureScaleOptions = null, +#endif Type[] types = null) { if (serializerSettings == null) @@ -198,6 +204,7 @@ public static ITestHost GetJobHostWithOptions( durabilityProviderFactoryType: durabilityProviderFactoryType, addDurableClientFactory: addDurableClientFactory, typeLocator: typeLocator, + configureScaleOptions: configureScaleOptions, #endif loggerProvider: loggerProvider, nameResolver: testNameResolver, diff --git a/test/FunctionsV2/DurableTaskScaleMonitorTests.cs b/test/FunctionsV2/DurableTaskScaleMonitorTests.cs index 930ff4a1c..57566da29 100644 --- a/test/FunctionsV2/DurableTaskScaleMonitorTests.cs +++ b/test/FunctionsV2/DurableTaskScaleMonitorTests.cs @@ -40,6 +40,12 @@ public DurableTaskScaleMonitorTests(ITestOutputHelper output) ILogger logger = this.loggerFactory.CreateLogger(LogCategories.CreateTriggerCategory("DurableTask")); this.traceHelper = new EndToEndTraceHelper(logger, false); this.performanceMonitor = new Mock(MockBehavior.Strict, this.storageAccount, this.hubName, (int?)null); + var metricsProvider = new DurableTaskMetricsProvider( + this.functionName.Name, + this.hubName, + logger, + this.performanceMonitor.Object, + this.storageAccount); this.scaleMonitor = new DurableTaskScaleMonitor( this.functionId, @@ -47,6 +53,7 @@ public DurableTaskScaleMonitorTests(ITestOutputHelper output) this.hubName, this.storageAccount, logger, + metricsProvider, this.performanceMonitor.Object); } diff --git a/test/FunctionsV2/DurableTaskTargetScalerTests.cs b/test/FunctionsV2/DurableTaskTargetScalerTests.cs new file mode 100644 index 000000000..cc2272e16 --- /dev/null +++ b/test/FunctionsV2/DurableTaskTargetScalerTests.cs @@ -0,0 +1,179 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System; +using System.Linq; +using System.Threading.Tasks; +using DurableTask.AzureStorage.Monitoring; +using DurableTask.Core; +using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Microsoft.Azure.WebJobs.Extensions.DurableTask.Scale; +using Microsoft.Azure.WebJobs.Extensions.DurableTask.Tests; +using Microsoft.Azure.WebJobs.Host.Scale; +using Microsoft.Azure.WebJobs.Host.TestCommon; +using Microsoft.Azure.WebJobs.Logging; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.WindowsAzure.Storage; +using Moq; +using Xunit; +using Xunit.Abstractions; +using static Microsoft.Azure.WebJobs.Extensions.DurableTask.Scale.ScaleUtils; +using static Microsoft.Azure.WebJobs.Extensions.DurableTask.Tests.PlatformSpecificHelpers; + +namespace WebJobs.Extensions.DurableTask.Tests.V2 +{ + public class DurableTaskTargetScalerTests + { + private readonly DurableTaskTargetScaler targetScaler; + private readonly TargetScalerContext scalerContext; + private readonly Mock metricsProviderMock; + private readonly Mock triggerMetricsMock; + private readonly Mock orchestrationServiceMock; + private readonly Mock durabilityProviderMock; + private readonly TestLoggerProvider loggerProvider; + private readonly ITestOutputHelper output; + + public DurableTaskTargetScalerTests(ITestOutputHelper output) + { + this.scalerContext = new TargetScalerContext(); + this.output = output; + var loggerFactory = new LoggerFactory(); + this.loggerProvider = new TestLoggerProvider(this.output); + loggerFactory.AddProvider(this.loggerProvider); + ILogger logger = loggerFactory.CreateLogger(LogCategories.CreateTriggerCategory("DurableTask")); + + DisconnectedPerformanceMonitor nullPerformanceMonitorMock = null; + CloudStorageAccount nullCloudStorageAccountMock = null; + this.metricsProviderMock = new Mock( + MockBehavior.Strict, + "FunctionName", + "HubName", + logger, + nullPerformanceMonitorMock, + nullCloudStorageAccountMock); + + this.triggerMetricsMock = new Mock(MockBehavior.Strict); + this.orchestrationServiceMock = new Mock(MockBehavior.Strict); + + this.durabilityProviderMock = new Mock( + MockBehavior.Strict, + "storageProviderName", + this.orchestrationServiceMock.Object, + new Mock().Object, + "connectionName"); + + this.targetScaler = new DurableTaskTargetScaler( + "FunctionId", + this.metricsProviderMock.Object, + this.durabilityProviderMock.Object, + logger); + } + + [Theory] + [Trait("Category", PlatformSpecificHelpers.TestCategory)] + [InlineData(1, 10, 10, "[1, 1, 1, 1]", 10)] + [InlineData(1, 10, 0, "[0, 0, 0, 0]", 0)] + [InlineData(1, 10, 0, "[2, 2, 3, 3]", 1)] + [InlineData(1, 10, 0, "[9999, 0, 0, 0]", 1)] + [InlineData(1, 10, 0, "[9999, 0, 0, 1]", 2)] + [InlineData(10, 10, 10, "[2, 2, 3, 3 ]", 1)] + [InlineData(10, 10, 30, "[10, 10, 10, 1]", 4)] + public async Task TestTargetScaler(int maxConcurrentActivities, int maxConcurrentOrchestrators, int workItemQueueLength, string controlQueueLengths, int expectedWorkerCount) + { + this.orchestrationServiceMock.SetupGet(m => m.MaxConcurrentTaskActivityWorkItems).Returns(maxConcurrentActivities); + this.orchestrationServiceMock.SetupGet(m => m.MaxConcurrentTaskOrchestrationWorkItems).Returns(maxConcurrentOrchestrators); + + this.triggerMetricsMock.SetupGet(m => m.WorkItemQueueLength).Returns(workItemQueueLength); + this.triggerMetricsMock.SetupGet(m => m.ControlQueueLengths).Returns(controlQueueLengths); + + this.metricsProviderMock.Setup(m => m.GetMetricsAsync()).ReturnsAsync(this.triggerMetricsMock.Object); + + var scaleResult = await this.targetScaler.GetScaleResultAsync(this.scalerContext); + var targetWorkerCount = scaleResult.TargetWorkerCount; + Assert.Equal(expectedWorkerCount, targetWorkerCount); + } + + [Theory] + [Trait("Category", PlatformSpecificHelpers.TestCategory)] + [InlineData(true)] + [InlineData(false)] + public void TestGetTargetScaler(bool supportsTBS) + { + ITargetScaler targetScaler = new Mock().Object; + this.durabilityProviderMock.Setup(m => m.TryGetTargetScaler(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), out targetScaler)) + .Returns(supportsTBS); + + var scaler = ScaleUtils.GetTargetScaler(this.durabilityProviderMock.Object, "FunctionId", new FunctionName("FunctionName"), "connectionName", "HubName"); + if (!supportsTBS) + { + Assert.IsType(scaler); + Assert.ThrowsAsync(() => scaler.GetScaleResultAsync(context: null)); + } + else + { + Assert.Equal(targetScaler, scaler); + } + } + + [Theory] + [Trait("Category", PlatformSpecificHelpers.TestCategory)] + [InlineData(true)] + [InlineData(false)] + public void TestGetScaleMonitor(bool supportsScaleMonitor) + { + IScaleMonitor scaleMonitor = new Mock().Object; + this.durabilityProviderMock.Setup(m => m.TryGetScaleMonitor(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), out scaleMonitor)) + .Returns(supportsScaleMonitor); + + var monitor = ScaleUtils.GetScaleMonitor(this.durabilityProviderMock.Object, "FunctionId", new FunctionName("FunctionName"), "connectionName", "HubName"); + if (!supportsScaleMonitor) + { + Assert.IsType(monitor); + Assert.Throws(() => monitor.GetScaleStatus(context: null)); + } + else + { + Assert.Equal(scaleMonitor, monitor); + } + } + + [Theory] + [Trait("Category", PlatformSpecificHelpers.TestCategory)] + [InlineData(true)] + [InlineData(false)] + public async void ScaleHostE2ETest(bool isTbsEnabled) + { + Action configureScaleOptions = (scaleOptions) => + { + scaleOptions.IsTargetScalingEnabled = isTbsEnabled; + scaleOptions.MetricsPurgeEnabled = false; + scaleOptions.ScaleMetricsMaxAge = TimeSpan.FromMinutes(4); + scaleOptions.IsRuntimeScalingEnabled = true; + scaleOptions.ScaleMetricsSampleInterval = TimeSpan.FromSeconds(1); + }; + using (FunctionsV2HostWrapper host = (FunctionsV2HostWrapper)TestHelpers.GetJobHost(this.loggerProvider, nameof(this.ScaleHostE2ETest), enableExtendedSessions: false, configureScaleOptions: configureScaleOptions)) + { + await host.StartAsync(); + + IScaleStatusProvider scaleManager = host.InnerHost.Services.GetService(); + var client = await host.StartOrchestratorAsync(nameof(TestOrchestrations.FanOutFanIn), 50, this.output); + var status = await client.WaitForCompletionAsync(this.output, timeout: TimeSpan.FromSeconds(400)); + var scaleStatus = await scaleManager.GetScaleStatusAsync(new ScaleStatusContext()); + await host.StopAsync(); + Assert.Equal(OrchestrationRuntimeStatus.Completed, status?.RuntimeStatus); + + // We inspect the Host's logs for evidence that the Host is correctly sampling our scaling requests. + // the expected logs depend on whether TBS is enabled or not + var expectedSubString = "scale monitors to sample"; + if (isTbsEnabled) + { + expectedSubString = "target scalers to sample"; + } + + bool containsExpectedLog = this.loggerProvider.GetAllLogMessages().Select(p => p.FormattedMessage ?? "").Any(p => p.Contains(expectedSubString)); + Assert.True(containsExpectedLog); + } + } + } +} diff --git a/test/FunctionsV2/PlatformSpecificHelpers.FunctionsV2.cs b/test/FunctionsV2/PlatformSpecificHelpers.FunctionsV2.cs index bfd72124f..4b2d46456 100644 --- a/test/FunctionsV2/PlatformSpecificHelpers.FunctionsV2.cs +++ b/test/FunctionsV2/PlatformSpecificHelpers.FunctionsV2.cs @@ -8,7 +8,7 @@ using System.Threading.Tasks; using Microsoft.ApplicationInsights.Channel; using Microsoft.Azure.WebJobs.Extensions.DurableTask.Correlation; -using Microsoft.Azure.WebJobs.Extensions.DurableTask.Options; +using Microsoft.Azure.WebJobs.Host.Scale; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; @@ -37,7 +37,8 @@ public static ITestHost CreateJobHost( IMessageSerializerSettingsFactory serializerSettingsFactory, Action onSend, bool addDurableClientFactory, - ITypeLocator typeLocator) + ITypeLocator typeLocator, + Action configureScaleOptions = null) { // Unless otherwise specified, use legacy partition management for tests as it makes the task hubs start up faster. // These tests run on a single task hub workers, so they don't test partition management anyways, and that is tested @@ -47,7 +48,7 @@ public static ITestHost CreateJobHost( options.Value.StorageProvider.Add(nameof(AzureStorageOptions.UseLegacyPartitionManagement), true); } - IHost host = new HostBuilder() + var hostBuilder = new HostBuilder() .ConfigureLogging( loggingBuilder => { @@ -97,9 +98,22 @@ public static ITestHost CreateJobHost( return telemetryActivator; }); } - }) - .Build(); + }); + + // if a configureScaleOptions action is provided, then we're probably trying to test the host's scaling logic + // we configure WebJobsScale and set the minimum logging level to `Debug`, as scaling logs are usually at the `Debug` level + if (configureScaleOptions != null) + { + hostBuilder.ConfigureWebJobsScale( + (context, builder) => + { + // ignore + }, + configureScaleOptions) + .ConfigureLogging(builder => builder.SetMinimumLevel(LogLevel.Debug)); + } + var host = hostBuilder.Build(); return new FunctionsV2HostWrapper(host, options, nameResolver); } @@ -216,7 +230,7 @@ private static IWebJobsBuilder AddEmulatorDurableTask(this IWebJobsBuilder build internal class FunctionsV2HostWrapper : ITestHost { - private readonly IHost innerHost; + internal readonly IHost InnerHost; private readonly JobHost innerWebJobsHost; private readonly DurableTaskOptions options; private readonly INameResolver nameResolver; @@ -226,8 +240,8 @@ public FunctionsV2HostWrapper( IOptions options, INameResolver nameResolver) { - this.innerHost = innerHost ?? throw new ArgumentNullException(nameof(innerHost)); - this.innerWebJobsHost = (JobHost)this.innerHost.Services.GetService(); + this.InnerHost = innerHost ?? throw new ArgumentNullException(nameof(innerHost)); + this.innerWebJobsHost = (JobHost)this.InnerHost.Services.GetService(); this.options = options.Value; this.nameResolver = nameResolver; } @@ -236,8 +250,8 @@ internal FunctionsV2HostWrapper( IHost innerHost, IOptions options) { - this.innerHost = innerHost; - this.innerWebJobsHost = (JobHost)this.innerHost.Services.GetService(); + this.InnerHost = innerHost; + this.innerWebJobsHost = (JobHost)this.InnerHost.Services.GetService(); this.options = options.Value; } @@ -249,16 +263,16 @@ public Task CallAsync(MethodInfo method, IDictionary args) public void Dispose() { - this.innerHost.Dispose(); + this.InnerHost.Dispose(); } - public Task StartAsync() => this.innerHost.StartAsync(); + public Task StartAsync() => this.InnerHost.StartAsync(); public async Task StopAsync() { try { - await this.innerHost.StopAsync(); + await this.InnerHost.StopAsync(); } catch (OperationCanceledException) { diff --git a/test/FunctionsV2/WebJobs.Extensions.DurableTask.Tests.V2.csproj b/test/FunctionsV2/WebJobs.Extensions.DurableTask.Tests.V2.csproj index 4840ca88a..ec2d5dd68 100644 --- a/test/FunctionsV2/WebJobs.Extensions.DurableTask.Tests.V2.csproj +++ b/test/FunctionsV2/WebJobs.Extensions.DurableTask.Tests.V2.csproj @@ -16,7 +16,7 @@ - + From 1ffdd3003db9f0dc125c819c4a32b4238614100d Mon Sep 17 00:00:00 2001 From: Naiyuan Tian <110135109+nytian@users.noreply.github.com> Date: Fri, 6 Oct 2023 14:59:36 -0700 Subject: [PATCH 06/11] Update e2e-test.ps1 (#2623) --- test/SmokeTests/e2e-test.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/SmokeTests/e2e-test.ps1 b/test/SmokeTests/e2e-test.ps1 index 842ec7535..44c1279b8 100644 --- a/test/SmokeTests/e2e-test.ps1 +++ b/test/SmokeTests/e2e-test.ps1 @@ -9,7 +9,7 @@ param( [string]$ContainerName="app", [switch]$NoSetup=$false, [switch]$NoValidation=$false, - [string]$AzuriteVersion="3.20.1", + [string]$AzuriteVersion="3.26.0", [int]$Sleep=30 ) @@ -92,4 +92,4 @@ try { throw } -Write-Host "Success!" -ForegroundColor Green \ No newline at end of file +Write-Host "Success!" -ForegroundColor Green From f50aac201fb310c25ffdcdd3f4e608fbcc42d021 Mon Sep 17 00:00:00 2001 From: Jacob Viau Date: Mon, 9 Oct 2023 14:49:55 -0700 Subject: [PATCH 07/11] Use 1.16.0-preview1 Worker SDK for smoke tests (#2626) --- nuget.config | 3 +-- .../DotNetIsolated/DotNetIsolated.csproj | 9 +++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/nuget.config b/nuget.config index 52ed3a3be..652118ea6 100644 --- a/nuget.config +++ b/nuget.config @@ -3,8 +3,7 @@ - - + diff --git a/test/SmokeTests/OOProcSmokeTests/DotNetIsolated/DotNetIsolated.csproj b/test/SmokeTests/OOProcSmokeTests/DotNetIsolated/DotNetIsolated.csproj index 365d9c3e6..7b02222a9 100644 --- a/test/SmokeTests/OOProcSmokeTests/DotNetIsolated/DotNetIsolated.csproj +++ b/test/SmokeTests/OOProcSmokeTests/DotNetIsolated/DotNetIsolated.csproj @@ -11,14 +11,19 @@ - + + + - + PreserveNewest From 08d21809102e4fdd4daf0a1d433f56af2f00abc9 Mon Sep 17 00:00:00 2001 From: Varshitha Bachu Date: Tue, 10 Oct 2023 12:17:20 -0700 Subject: [PATCH 08/11] Update function state change traces to improve orchestration monitoring query (#2302) This PR adds a new RuntimeStatus field to track function state changes. This helps us improve our monitoring query in the portal (Functions -> OrchestratorFunction -> Monitor tab -> App Insights query). --- .../EndToEndTraceHelper.cs | 44 +++++++++---------- test/Common/TestHelpers.cs | 6 +-- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/src/WebJobs.Extensions.DurableTask/EndToEndTraceHelper.cs b/src/WebJobs.Extensions.DurableTask/EndToEndTraceHelper.cs index 2c5085698..e730d6e71 100644 --- a/src/WebJobs.Extensions.DurableTask/EndToEndTraceHelper.cs +++ b/src/WebJobs.Extensions.DurableTask/EndToEndTraceHelper.cs @@ -116,8 +116,8 @@ public void FunctionScheduled( isReplay); this.logger.LogInformation( - "{instanceId}: Function '{functionName} ({functionType})' scheduled. Reason: {reason}. IsReplay: {isReplay}. State: {state}. HubName: {hubName}. AppName: {appName}. SlotName: {slotName}. ExtensionVersion: {extensionVersion}. SequenceNumber: {sequenceNumber}.", - instanceId, functionName, functionType, reason, isReplay, FunctionState.Scheduled, hubName, + "{instanceId}: Function '{functionName} ({functionType})' scheduled. Reason: {reason}. IsReplay: {isReplay}. State: {state}. RuntimeStatus: {runtimeStatus}. HubName: {hubName}. AppName: {appName}. SlotName: {slotName}. ExtensionVersion: {extensionVersion}. SequenceNumber: {sequenceNumber}.", + instanceId, functionName, functionType, reason, isReplay, FunctionState.Scheduled, OrchestrationRuntimeStatus.Pending, hubName, LocalAppName, LocalSlotName, ExtensionVersion, this.sequenceNumber++); } } @@ -146,8 +146,8 @@ public void FunctionStarting( isReplay); this.logger.LogInformation( - "{instanceId}: Function '{functionName} ({functionType})' started. IsReplay: {isReplay}. Input: {input}. State: {state}. HubName: {hubName}. AppName: {appName}. SlotName: {slotName}. ExtensionVersion: {extensionVersion}. SequenceNumber: {sequenceNumber}. TaskEventId: {taskEventId}", - instanceId, functionName, functionType, isReplay, input, FunctionState.Started, hubName, + "{instanceId}: Function '{functionName} ({functionType})' started. IsReplay: {isReplay}. Input: {input}. State: {state}. RuntimeStatus: {runtimeStatus}. HubName: {hubName}. AppName: {appName}. SlotName: {slotName}. ExtensionVersion: {extensionVersion}. SequenceNumber: {sequenceNumber}. TaskEventId: {taskEventId}", + instanceId, functionName, functionType, isReplay, input, FunctionState.Started, OrchestrationRuntimeStatus.Running, hubName, LocalAppName, LocalSlotName, ExtensionVersion, this.sequenceNumber++, taskEventId); } } @@ -233,9 +233,9 @@ public void FunctionCompleted( isReplay); this.logger.LogInformation( - "{instanceId}: Function '{functionName} ({functionType})' completed. ContinuedAsNew: {continuedAsNew}. IsReplay: {isReplay}. Output: {output}. State: {state}. HubName: {hubName}. AppName: {appName}. SlotName: {slotName}. ExtensionVersion: {extensionVersion}. SequenceNumber: {sequenceNumber}. TaskEventId: {taskEventId}", - instanceId, functionName, functionType, continuedAsNew, isReplay, output, FunctionState.Completed, - hubName, LocalAppName, LocalSlotName, ExtensionVersion, this.sequenceNumber++, taskEventId); + "{instanceId}: Function '{functionName} ({functionType})' completed. ContinuedAsNew: {continuedAsNew}. IsReplay: {isReplay}. Output: {output}. State: {state}. RuntimeStatus: {runtimeStatus}. HubName: {hubName}. AppName: {appName}. SlotName: {slotName}. ExtensionVersion: {extensionVersion}. SequenceNumber: {sequenceNumber}. TaskEventId: {taskEventId}", + instanceId, functionName, functionType, continuedAsNew, isReplay, output, FunctionState.Completed, OrchestrationRuntimeStatus.Completed, hubName, + LocalAppName, LocalSlotName, ExtensionVersion, this.sequenceNumber++, taskEventId); } } @@ -279,9 +279,9 @@ public void FunctionTerminated( IsReplay: false); this.logger.LogWarning( - "{instanceId}: Function '{functionName} ({functionType})' was terminated. Reason: {reason}. State: {state}. HubName: {hubName}. AppName: {appName}. SlotName: {slotName}. ExtensionVersion: {extensionVersion}. SequenceNumber: {sequenceNumber}.", - instanceId, functionName, functionType, reason, FunctionState.Terminated, hubName, LocalAppName, - LocalSlotName, ExtensionVersion, this.sequenceNumber++); + "{instanceId}: Function '{functionName} ({functionType})' was terminated. Reason: {reason}. State: {state}. RuntimeStatus: {runtimeStatus}. HubName: {hubName}. AppName: {appName}. SlotName: {slotName}. ExtensionVersion: {extensionVersion}. SequenceNumber: {sequenceNumber}.", + instanceId, functionName, functionType, reason, FunctionState.Terminated, OrchestrationRuntimeStatus.Terminated, hubName, + LocalAppName, LocalSlotName, ExtensionVersion, this.sequenceNumber++); } public void SuspendingOrchestration( @@ -304,9 +304,9 @@ public void SuspendingOrchestration( IsReplay: false); this.logger.LogInformation( - "{instanceId}: Suspending function '{functionName} ({functionType})'. Reason: {reason}. State: {state}. HubName: {hubName}. AppName: {appName}. SlotName: {slotName}. ExtensionVersion: {extensionVersion}. SequenceNumber: {sequenceNumber}.", - instanceId, functionName, functionType, reason, FunctionState.Suspended, hubName, LocalAppName, - LocalSlotName, ExtensionVersion, this.sequenceNumber++); + "{instanceId}: Suspending function '{functionName} ({functionType})'. Reason: {reason}. State: {state}. RuntimeStatus: {runtimeStatus}. HubName: {hubName}. AppName: {appName}. SlotName: {slotName}. ExtensionVersion: {extensionVersion}. SequenceNumber: {sequenceNumber}.", + instanceId, functionName, functionType, reason, FunctionState.Suspended, OrchestrationRuntimeStatus.Suspended, hubName, + LocalAppName, LocalSlotName, ExtensionVersion, this.sequenceNumber++); } public void ResumingOrchestration( @@ -329,9 +329,9 @@ public void ResumingOrchestration( IsReplay: false); this.logger.LogInformation( - "{instanceId}: Resuming function '{functionName} ({functionType})'. Reason: {reason}. State: {state}. HubName: {hubName}. AppName: {appName}. SlotName: {slotName}. ExtensionVersion: {extensionVersion}. SequenceNumber: {sequenceNumber}.", - instanceId, functionName, functionType, reason, FunctionState.Scheduled, hubName, LocalAppName, - LocalSlotName, ExtensionVersion, this.sequenceNumber++); + "{instanceId}: Resuming function '{functionName} ({functionType})'. Reason: {reason}. State: {state}. RuntimeStatus: {runtimeStatus}. HubName: {hubName}. AppName: {appName}. SlotName: {slotName}. ExtensionVersion: {extensionVersion}. SequenceNumber: {sequenceNumber}.", + instanceId, functionName, functionType, reason, FunctionState.Scheduled, OrchestrationRuntimeStatus.Running, hubName, + LocalAppName, LocalSlotName, ExtensionVersion, this.sequenceNumber++); } public void FunctionRewound( @@ -355,8 +355,8 @@ public void FunctionRewound( this.logger.LogWarning( "{instanceId}: Function '{functionName} ({functionType})' was rewound. Reason: {reason}. State: {state}. HubName: {hubName}. AppName: {appName}. SlotName: {slotName}. ExtensionVersion: {extensionVersion}. SequenceNumber: {sequenceNumber}.", - instanceId, functionName, functionType, reason, FunctionState.Rewound, hubName, LocalAppName, - LocalSlotName, ExtensionVersion, this.sequenceNumber++); + instanceId, functionName, functionType, reason, FunctionState.Rewound, hubName, + LocalAppName, LocalSlotName, ExtensionVersion, this.sequenceNumber++); } public void FunctionFailed( @@ -383,8 +383,8 @@ public void FunctionFailed( isReplay); this.logger.LogError( - "{instanceId}: Function '{functionName} ({functionType})' failed with an error. Reason: {reason}. IsReplay: {isReplay}. State: {state}. HubName: {hubName}. AppName: {appName}. SlotName: {slotName}. ExtensionVersion: {extensionVersion}. SequenceNumber: {sequenceNumber}. TaskEventId: {taskEventId}", - instanceId, functionName, functionType, reason, isReplay, FunctionState.Failed, hubName, + "{instanceId}: Function '{functionName} ({functionType})' failed with an error. Reason: {reason}. IsReplay: {isReplay}. State: {state}. RuntimeStatus: {runtimeStatus}. HubName: {hubName}. AppName: {appName}. SlotName: {slotName}. ExtensionVersion: {extensionVersion}. SequenceNumber: {sequenceNumber}. TaskEventId: {taskEventId}", + instanceId, functionName, functionType, reason, isReplay, FunctionState.Failed, OrchestrationRuntimeStatus.Failed, hubName, LocalAppName, LocalSlotName, ExtensionVersion, this.sequenceNumber++, taskEventId); } } @@ -953,8 +953,8 @@ public void TimerExpired( this.logger.LogInformation( "{instanceId}: Function '{functionName} ({functionType})' was resumed by a timer scheduled for '{expirationTime}'. IsReplay: {isReplay}. State: {state}. HubName: {hubName}. AppName: {appName}. SlotName: {slotName}. ExtensionVersion: {extensionVersion}. SequenceNumber: {sequenceNumber}.", - instanceId, functionName, functionType, expirationTimeString, isReplay, FunctionState.TimerExpired, - hubName, LocalAppName, LocalSlotName, ExtensionVersion, this.sequenceNumber++); + instanceId, functionName, functionType, expirationTimeString, isReplay, FunctionState.TimerExpired, hubName, + LocalAppName, LocalSlotName, ExtensionVersion, this.sequenceNumber++); } } diff --git a/test/Common/TestHelpers.cs b/test/Common/TestHelpers.cs index dafa10e5d..83259db08 100644 --- a/test/Common/TestHelpers.cs +++ b/test/Common/TestHelpers.cs @@ -615,9 +615,9 @@ private static List GetLogs_OrchestrationEventGridApiReturnBadStatus(str { var list = new List() { - $"{messageId}: Function '{functionNames[0]} ({FunctionType.Orchestrator})' scheduled. Reason: NewInstance. IsReplay: False. State: Scheduled.", - $"{messageId}: Function '{functionNames[0]} ({FunctionType.Orchestrator})' started. IsReplay: False. Input: \"World\". State: Started.", - $"{messageId}: Function '{functionNames[0]} ({FunctionType.Orchestrator})' completed. ContinuedAsNew: False. IsReplay: False. Output: \"Hello, World!\". State: Completed.", + $"{messageId}: Function '{functionNames[0]} ({FunctionType.Orchestrator})' scheduled. Reason: NewInstance. IsReplay: False. State: Scheduled. RuntimeStatus: Pending.", + $"{messageId}: Function '{functionNames[0]} ({FunctionType.Orchestrator})' started. IsReplay: False. Input: \"World\". State: Started. RuntimeStatus: Running.", + $"{messageId}: Function '{functionNames[0]} ({FunctionType.Orchestrator})' completed. ContinuedAsNew: False. IsReplay: False. Output: \"Hello, World!\". State: Completed. RuntimeStatus: Completed.", $"{messageId}: Function '{functionNames[0]} ({FunctionType.Orchestrator})' failed to send a 'Started' notification event to Azure Event Grid. Status code: 500. Details: {{\"message\":\"Exception has been thrown\"}}. ", $"{messageId}: Function '{functionNames[0]} ({FunctionType.Orchestrator})' failed to send a 'Completed' notification event to Azure Event Grid. Status code: 500. Details: {{\"message\":\"Exception has been thrown\"}}. ", }; From 6d0368ba5e9753e05407834b181c17b005008d0e Mon Sep 17 00:00:00 2001 From: Jacob Viau Date: Wed, 11 Oct 2023 15:33:56 -0700 Subject: [PATCH 09/11] Use IDurableClient with RawInput for LocalGrpcListener (#2634) --- release_notes.md | 2 +- .../LocalGrpcListener.cs | 79 +++++++------------ .../WebJobs.Extensions.DurableTask.csproj | 2 +- 3 files changed, 31 insertions(+), 52 deletions(-) diff --git a/release_notes.md b/release_notes.md index b0f5bd2a9..2d1644667 100644 --- a/release_notes.md +++ b/release_notes.md @@ -4,7 +4,7 @@ ### Bug Fixes -- Address input issues when using .NET isolated (#2581)[https://github.com/Azure/azure-functions-durable-extension/issues/2581] +- Fix support for distributed tracing v2 in dotnet-isolated and Java (https://github.com/Azure/azure-functions-durable-extension/pull/2634) ### Breaking Changes diff --git a/src/WebJobs.Extensions.DurableTask/LocalGrpcListener.cs b/src/WebJobs.Extensions.DurableTask/LocalGrpcListener.cs index 37ff88ddf..caa5f04a5 100644 --- a/src/WebJobs.Extensions.DurableTask/LocalGrpcListener.cs +++ b/src/WebJobs.Extensions.DurableTask/LocalGrpcListener.cs @@ -5,11 +5,13 @@ using System; using System.Collections.Generic; using System.IO; +using System.Reflection.Metadata.Ecma335; using System.Threading; using System.Threading.Tasks; using DurableTask.Core; using DurableTask.Core.History; using DurableTask.Core.Query; +using DurableTask.Core.Serializing.Internal; using Google.Protobuf.WellKnownTypes; using Grpc.Core; using Microsoft.Extensions.Hosting; @@ -142,83 +144,50 @@ public override Task Hello(Empty request, ServerCallContext context) public async override Task StartInstance(P.CreateInstanceRequest request, ServerCallContext context) { - var instance = new OrchestrationInstance - { - InstanceId = request.InstanceId ?? Guid.NewGuid().ToString("N"), - ExecutionId = Guid.NewGuid().ToString(), - }; - try { - await this.GetDurabilityProvider(context).CreateTaskOrchestrationAsync( - new TaskMessage - { - Event = new ExecutionStartedEvent(-1, request.Input) - { - Name = request.Name, - Version = request.Version, - OrchestrationInstance = instance, - ScheduledStartTime = request.ScheduledStartTimestamp?.ToDateTime(), - }, - OrchestrationInstance = instance, - }, - this.GetStatusesNotToOverride()); - + string instanceId = await this.GetClient(context).StartNewAsync( + request.Name, request.InstanceId, Raw(request.Input)); return new P.CreateInstanceResponse { - InstanceId = instance.InstanceId, + InstanceId = instanceId, }; } catch (InvalidOperationException) { - throw new RpcException(new Status(StatusCode.AlreadyExists, $"An Orchestration instance with the ID {instance.InstanceId} already exists.")); + throw new RpcException(new Status(StatusCode.AlreadyExists, $"An Orchestration instance with the ID {request.InstanceId} already exists.")); } } public async override Task RaiseEvent(P.RaiseEventRequest request, ServerCallContext context) { - await this.GetDurabilityProvider(context).SendTaskOrchestrationMessageAsync( - new TaskMessage - { - Event = new EventRaisedEvent(-1, request.Input) - { - Name = request.Name, - }, - OrchestrationInstance = new OrchestrationInstance - { - InstanceId = request.InstanceId, - }, - }); - - // No fields in the response + await this.GetClient(context).RaiseEventAsync(request.InstanceId, request.Name, Raw(request.Input)); return new P.RaiseEventResponse(); } public async override Task TerminateInstance(P.TerminateRequest request, ServerCallContext context) { - await this.GetDurabilityProvider(context).ForceTerminateTaskOrchestrationAsync( - request.InstanceId, - request.Output); - - // No fields in the response + await this.GetClient(context).TerminateAsync(request.InstanceId, request.Output); return new P.TerminateResponse(); } public async override Task SuspendInstance(P.SuspendRequest request, ServerCallContext context) { - await this.GetDurabilityProvider(context).SuspendTaskOrchestrationAsync(request.InstanceId, request.Reason); + await this.GetClient(context).SuspendAsync(request.InstanceId, request.Reason); return new P.SuspendResponse(); } public async override Task ResumeInstance(P.ResumeRequest request, ServerCallContext context) { - await this.GetDurabilityProvider(context).ResumeTaskOrchestrationAsync(request.InstanceId, request.Reason); + await this.GetClient(context).ResumeAsync(request.InstanceId, request.Reason); return new P.ResumeResponse(); } public async override Task RewindInstance(P.RewindInstanceRequest request, ServerCallContext context) { - await this.GetDurabilityProvider(context).RewindAsync(request.InstanceId, request.Reason); +#pragma warning disable CS0618 // Type or member is obsolete + await this.GetClient(context).RewindAsync(request.InstanceId, request.Reason); +#pragma warning restore CS0618 // Type or member is obsolete return new P.RewindInstanceResponse(); } @@ -303,6 +272,13 @@ await this.GetDurabilityProvider(context).ForceTerminateTaskOrchestrationAsync( return CreateGetInstanceResponse(state, request); } +#pragma warning disable CS0618 // Type or member is obsolete -- 'internal' usage. + private static RawInput Raw(string input) + { + return new RawInput(input); + } +#pragma warning restore CS0618 // Type or member is obsolete + private static P.GetInstanceResponse CreateGetInstanceResponse(OrchestrationState state, P.GetInstanceRequest request) { return new P.GetInstanceResponse @@ -338,18 +314,21 @@ private static P.GetInstanceResponse CreateGetInstanceResponse(OrchestrationStat }; } - private DurabilityProvider GetDurabilityProvider(ServerCallContext context) + private DurableClientAttribute GetAttribute(ServerCallContext context) { string? taskHub = context.RequestHeaders.GetValue("Durable-TaskHub"); string? connectionName = context.RequestHeaders.GetValue("Durable-ConnectionName"); - var attribute = new DurableClientAttribute() { TaskHub = taskHub, ConnectionName = connectionName }; - return this.extension.GetDurabilityProvider(attribute); + return new DurableClientAttribute() { TaskHub = taskHub, ConnectionName = connectionName }; + } + + private DurabilityProvider GetDurabilityProvider(ServerCallContext context) + { + return this.extension.GetDurabilityProvider(this.GetAttribute(context)); } - private OrchestrationStatus[] GetStatusesNotToOverride() + private IDurableClient GetClient(ServerCallContext context) { - OverridableStates overridableStates = this.extension.Options.OverridableExistingInstanceStates; - return overridableStates.ToDedupeStatuses(); + return this.extension.GetClient(this.GetAttribute(context)); } } } diff --git a/src/WebJobs.Extensions.DurableTask/WebJobs.Extensions.DurableTask.csproj b/src/WebJobs.Extensions.DurableTask/WebJobs.Extensions.DurableTask.csproj index b057bfdba..380e9eaf3 100644 --- a/src/WebJobs.Extensions.DurableTask/WebJobs.Extensions.DurableTask.csproj +++ b/src/WebJobs.Extensions.DurableTask/WebJobs.Extensions.DurableTask.csproj @@ -104,7 +104,7 @@ - + From 0e4cca1ff3ef4b7c3ff4d379877b5d4b27e9f98e Mon Sep 17 00:00:00 2001 From: David Justo Date: Wed, 11 Oct 2023 15:52:40 -0700 Subject: [PATCH 10/11] rev version to 2.11.4 for release (#2629) --- .../WebJobs.Extensions.DurableTask.csproj | 4 ++-- test/SmokeTests/BackendSmokeTests/Netherite/Dockerfile | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/WebJobs.Extensions.DurableTask/WebJobs.Extensions.DurableTask.csproj b/src/WebJobs.Extensions.DurableTask/WebJobs.Extensions.DurableTask.csproj index 380e9eaf3..83dd33581 100644 --- a/src/WebJobs.Extensions.DurableTask/WebJobs.Extensions.DurableTask.csproj +++ b/src/WebJobs.Extensions.DurableTask/WebJobs.Extensions.DurableTask.csproj @@ -6,7 +6,7 @@ Microsoft.Azure.WebJobs.Extensions.DurableTask 2 11 - 3 + 4 $(MajorVersion).$(MinorVersion).$(PatchVersion) $(MajorVersion).$(MinorVersion).$(PatchVersion) $(MajorVersion).0.0.0 @@ -74,7 +74,7 @@ - + diff --git a/test/SmokeTests/BackendSmokeTests/Netherite/Dockerfile b/test/SmokeTests/BackendSmokeTests/Netherite/Dockerfile index 7daf7ffcb..82adb8e27 100644 --- a/test/SmokeTests/BackendSmokeTests/Netherite/Dockerfile +++ b/test/SmokeTests/BackendSmokeTests/Netherite/Dockerfile @@ -10,7 +10,7 @@ RUN cd /root/test/SmokeTests/BackendSmokeTests/Netherite && \ dotnet publish -c Release --output /home/site/wwwroot # Deploy the app -FROM mcr.microsoft.com/azure-functions/dotnet:4.0 +FROM mcr.microsoft.com/azure-functions/dotnet:4 ENV AzureWebJobsScriptRoot=/home/site/wwwroot \ AzureFunctionsJobHost__Logging__Console__IsEnabled=true COPY --from=build-env ["/home/site/wwwroot", "/home/site/wwwroot"] From be03ba8b5bebfbb9fbfd1b2db3aaf9b38acf4431 Mon Sep 17 00:00:00 2001 From: Jacob Viau Date: Thu, 12 Oct 2023 09:32:38 -0700 Subject: [PATCH 11/11] Remove extra nuget.config (#2636) Unblock build by removing extra nuget.config and pointing ADO yml to the one at repo root. --- .nuget/NuGet.Config | 11 ----------- WebJobs.Extensions.DurableTask.sln | 6 +----- azure-pipelines-analyzer-release.yml | 2 +- azure-pipelines-release.yml | 2 +- azure-pipelines.yml | 6 +++--- .../DotNetIsolated/DotNetIsolated.csproj | 2 +- 6 files changed, 7 insertions(+), 22 deletions(-) delete mode 100644 .nuget/NuGet.Config diff --git a/.nuget/NuGet.Config b/.nuget/NuGet.Config deleted file mode 100644 index fe953401c..000000000 --- a/.nuget/NuGet.Config +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/WebJobs.Extensions.DurableTask.sln b/WebJobs.Extensions.DurableTask.sln index d821ef64a..8efe24eee 100644 --- a/WebJobs.Extensions.DurableTask.sln +++ b/WebJobs.Extensions.DurableTask.sln @@ -9,11 +9,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{7EC858EE-348 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{78BCF152-C22C-408F-9FB1-0F8C99B154B5}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{51D8DCEA-EA91-410E-AA6A-F42473F0C691}" - ProjectSection(SolutionItems) = preProject - .nuget\NuGet.Config = .nuget\NuGet.Config - EndProjectSection -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebJobs.Extensions.DurableTask.Tests.V2", "test\FunctionsV2\WebJobs.Extensions.DurableTask.Tests.V2.csproj", "{F2A5DABB-36D4-4152-AF49-2570149899E1}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebJobs.Extensions.DurableTask.Tests.V1", "test\FunctionsV1\WebJobs.Extensions.DurableTask.Tests.V1.csproj", "{F99CA746-553C-43B4-943B-59A5D190459A}" @@ -23,6 +18,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .editorconfig = .editorconfig azure-pipelines-release-dotnet-isolated.yml = azure-pipelines-release-dotnet-isolated.yml azure-pipelines-release.yml = azure-pipelines-release.yml + nuget.config = nuget.config README.md = README.md release_notes.md = release_notes.md .stylecop\stylecop.json = .stylecop\stylecop.json diff --git a/azure-pipelines-analyzer-release.yml b/azure-pipelines-analyzer-release.yml index 6b0d8974d..56fb72fc6 100644 --- a/azure-pipelines-analyzer-release.yml +++ b/azure-pipelines-analyzer-release.yml @@ -38,7 +38,7 @@ steps: command: restore projects: '**/**/*.csproj' feedsToUse: config - nugetConfigPath: '.nuget/nuget.config' + nugetConfigPath: 'nuget.config' # Build durable-analyzer - task: VSBuild@1 diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index b9ae7f45e..404ce9508 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -38,7 +38,7 @@ steps: command: restore projects: '**/**/*.csproj' feedsToUse: config - nugetConfigPath: '.nuget/nuget.config' + nugetConfigPath: 'nuget.config' # Build durable-extension - task: VSBuild@1 diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 17cf3fdc0..68ef305bf 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -23,7 +23,7 @@ jobs: command: 'restore' projects: 'test/FunctionsV1/*.csproj' feedsToUse: 'config' - nugetConfigPath: '.nuget/nuget.config' + nugetConfigPath: 'nuget.config' - task: DotNetCoreCLI@2 inputs: @@ -72,7 +72,7 @@ jobs: command: 'restore' projects: 'test/FunctionsV2/*.csproj' feedsToUse: 'config' - nugetConfigPath: '.nuget/nuget.config' + nugetConfigPath: 'nuget.config' - task: DotNetCoreCLI@2 inputs: @@ -119,7 +119,7 @@ jobs: command: 'restore' projects: 'test/WebJobs.Extensions.DurableTask.Analyzers.Test/*.csproj' feedsToUse: 'config' - nugetConfigPath: '.nuget/nuget.config' + nugetConfigPath: 'nuget.config' - task: DotNetCoreCLI@2 inputs: diff --git a/test/SmokeTests/OOProcSmokeTests/DotNetIsolated/DotNetIsolated.csproj b/test/SmokeTests/OOProcSmokeTests/DotNetIsolated/DotNetIsolated.csproj index 7b02222a9..9dfbda77f 100644 --- a/test/SmokeTests/OOProcSmokeTests/DotNetIsolated/DotNetIsolated.csproj +++ b/test/SmokeTests/OOProcSmokeTests/DotNetIsolated/DotNetIsolated.csproj @@ -16,7 +16,7 @@ This preview package is to enable better NuGet restore behavior with custom feeds. TODO: upgrade to 1.16.0 when it's released. --> - +