From 410dabb7e6cdf3991f2464a0394fb4c5c543d8dd Mon Sep 17 00:00:00 2001 From: Varshitha Bachu Date: Wed, 1 Nov 2023 10:44:47 -0700 Subject: [PATCH 1/9] initial commit --- .../OutOfProcMiddleware.cs | 60 ++++++++++ .../DurableHttpRequest.cs | 70 ++++++++++++ .../DurableHttpResponse.cs | 83 ++++++++++++++ .../HttpHeadersConverter.cs | 103 ++++++++++++++++++ ...askOrchestrationContextExtensionMethods.cs | 29 +++++ 5 files changed, 345 insertions(+) create mode 100644 src/Worker.Extensions.DurableTask/DurableHttpRequest.cs create mode 100644 src/Worker.Extensions.DurableTask/DurableHttpResponse.cs create mode 100644 src/Worker.Extensions.DurableTask/HttpHeadersConverter.cs create mode 100644 src/Worker.Extensions.DurableTask/TaskOrchestrationContextExtensionMethods.cs diff --git a/src/WebJobs.Extensions.DurableTask/OutOfProcMiddleware.cs b/src/WebJobs.Extensions.DurableTask/OutOfProcMiddleware.cs index 1813c131c..c4169fc07 100644 --- a/src/WebJobs.Extensions.DurableTask/OutOfProcMiddleware.cs +++ b/src/WebJobs.Extensions.DurableTask/OutOfProcMiddleware.cs @@ -3,14 +3,18 @@ #nullable enable #if FUNCTIONS_V3_OR_GREATER using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Net.Http; using System.Threading.Tasks; using DurableTask.Core; using DurableTask.Core.Exceptions; using DurableTask.Core.History; using DurableTask.Core.Middleware; using Microsoft.Azure.WebJobs.Host.Executors; +using Microsoft.Extensions.Primitives; +using Newtonsoft.Json; namespace Microsoft.Azure.WebJobs.Extensions.DurableTask { @@ -250,6 +254,45 @@ public async Task CallActivityAsync(DispatchMiddlewareContext dispatchContext, F throw new InvalidOperationException($"An activity was scheduled but no {nameof(TaskScheduledEvent)} was found!"); } + if (!string.IsNullOrEmpty(scheduledEvent.Name) && scheduledEvent.Name.StartsWith("BuiltIn::HttpActivity")) + { + try + { + if (dispatchContext.GetProperty() is TaskHttpActivityShim shim) + { + OrchestrationInstance orchestrationInstance = dispatchContext.GetProperty(); + TaskContext context = new TaskContext(orchestrationInstance); + + // convert the DurableHttpRequest + DurableHttpRequest? req = ConvertDurableHttpRequest(scheduledEvent.Input); + IList list = new List() { req }; + string serializedRequest = JsonConvert.SerializeObject(list); + + string? output = await shim.RunAsync(context, serializedRequest); + dispatchContext.SetProperty(new ActivityExecutionResult + { + ResponseEvent = new TaskCompletedEvent( + eventId: -1, + taskScheduledId: scheduledEvent.EventId, + result: output), + }); + return; + } + } + catch (Exception e) + { + dispatchContext.SetProperty(new ActivityExecutionResult + { + ResponseEvent = new TaskFailedEvent( + eventId: -1, + taskScheduledId: scheduledEvent.EventId, + reason: $"Function failed", + details: e.Message), + }); + return; + } + } + FunctionName functionName = new FunctionName(scheduledEvent.Name); OrchestrationInstance? instance = dispatchContext.GetProperty(); @@ -374,6 +417,23 @@ public async Task CallActivityAsync(DispatchMiddlewareContext dispatchContext, F dispatchContext.SetProperty(activityResult); } + private static DurableHttpRequest? ConvertDurableHttpRequest(string? inputString) + { + IList? input = JsonConvert.DeserializeObject>(inputString); + dynamic? dynamicRequest = input[0]; + + HttpMethod httpMethod = dynamicRequest.method.ToObject(); + Uri uri = dynamicRequest.uri.ToObject(); + string content = dynamicRequest.content.ToString(); + + JsonSerializerSettings settings = new JsonSerializerSettings { Converters = new List { new HttpHeadersConverter() } }; + Dictionary headers = JsonConvert.DeserializeObject>(dynamicRequest.headers.ToString(), settings); + + DurableHttpRequest request = new DurableHttpRequest(httpMethod, uri, headers, content); + + return request; + } + private static FailureDetails GetFailureDetails(Exception e) { if (e.InnerException != null && e.InnerException.Message.StartsWith("Result:")) diff --git a/src/Worker.Extensions.DurableTask/DurableHttpRequest.cs b/src/Worker.Extensions.DurableTask/DurableHttpRequest.cs new file mode 100644 index 000000000..66070e3fa --- /dev/null +++ b/src/Worker.Extensions.DurableTask/DurableHttpRequest.cs @@ -0,0 +1,70 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text.Json.Serialization; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Azure.Functions.Worker; + +/// +/// Request used to make an HTTP call through Durable Functions. +/// +public class DurableHttpRequest +{ + /// + /// Initializes a new instance of the class. + /// + public DurableHttpRequest( + HttpMethod method, + Uri uri, + IDictionary? headers = null, + string? content = null) + { + this.Method = method; + this.Uri = uri; + this.Headers = HttpHeadersConverter.CreateCopy(headers); + this.Content = content; + } + + /// + /// HttpMethod used in the HTTP request made by the Durable Function. + /// + [JsonPropertyName("method")] + public HttpMethod Method { get; } + + /// + /// Uri used in the HTTP request made by the Durable Function. + /// + [JsonPropertyName("uri")] + public Uri Uri { get; } + + /// + /// Headers passed with the HTTP request made by the Durable Function. + /// + [JsonPropertyName("headers")] + [JsonConverter(typeof(HttpHeadersConverter))] + public IDictionary? Headers { get; } + + /// + /// Content passed with the HTTP request made by the Durable Function. + /// + [JsonPropertyName("content")] + public string? Content { get; } + + internal static IDictionary CreateCopy(IDictionary input) + { + var copy = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (input != null) + { + foreach (var pair in input) + { + copy[pair.Key] = pair.Value; + } + } + + return copy; + } +} \ No newline at end of file diff --git a/src/Worker.Extensions.DurableTask/DurableHttpResponse.cs b/src/Worker.Extensions.DurableTask/DurableHttpResponse.cs new file mode 100644 index 000000000..a5ec4613f --- /dev/null +++ b/src/Worker.Extensions.DurableTask/DurableHttpResponse.cs @@ -0,0 +1,83 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Azure.Functions.Worker; + +/// +/// Response received from the HTTP request made by the Durable Function. +/// +public class DurableHttpResponse +{ + /// + /// Initializes a new instance of the class. + /// + /// HTTP Status code returned from the HTTP call. + /// Headers returned from the HTTP call. + /// Content returned from the HTTP call. + public DurableHttpResponse( + HttpStatusCode statusCode, + IDictionary headers = null, + string content = null) + { + this.StatusCode = statusCode; + this.Headers = HttpHeadersConverter.CreateCopy(headers); + this.Content = content; + } + + /// + /// Status code returned from an HTTP request. + /// + [JsonPropertyName("statusCode")] + public HttpStatusCode StatusCode { get; } + + /// + /// Headers in the response from an HTTP request. + /// + [JsonPropertyName("headers")] + [JsonConverter(typeof(HttpHeadersConverter))] + public IDictionary Headers { get; } + + /// + /// Content returned from an HTTP request. + /// + [JsonPropertyName("content")] + public string Content { get; } + + /// + /// Creates a DurableHttpResponse from an HttpResponseMessage. + /// + /// HttpResponseMessage returned from the HTTP call. + /// A representing the result of the asynchronous operation. + public static async Task CreateDurableHttpResponseWithHttpResponseMessage(HttpResponseMessage httpResponseMessage) + { + DurableHttpResponse durableHttpResponse = new DurableHttpResponse( + statusCode: httpResponseMessage.StatusCode, + headers: CreateStringValuesHeaderDictionary(httpResponseMessage.Headers), + content: await httpResponseMessage.Content.ReadAsStringAsync()); + + return durableHttpResponse; + } + + private static IDictionary CreateStringValuesHeaderDictionary(IEnumerable>> headers) + { + IDictionary newHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (headers != null) + { + foreach (var header in headers) + { + newHeaders[header.Key] = new StringValues(header.Value.ToArray()); + } + } + + return newHeaders; + } +} \ No newline at end of file diff --git a/src/Worker.Extensions.DurableTask/HttpHeadersConverter.cs b/src/Worker.Extensions.DurableTask/HttpHeadersConverter.cs new file mode 100644 index 000000000..588274a31 --- /dev/null +++ b/src/Worker.Extensions.DurableTask/HttpHeadersConverter.cs @@ -0,0 +1,103 @@ +// 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.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Azure.Functions.Worker; + +// StringValues does not deserialize as you would expect, so we need a custom mechanism +// for serializing HTTP header collections +internal class HttpHeadersConverter : JsonConverter +{ + public override bool CanConvert(Type objectType) + { + return typeof(IDictionary).IsAssignableFrom(objectType); + } + + public override object Read( + ref Utf8JsonReader reader, + Type objectType, + JsonSerializerOptions options) + { + var headers = new Dictionary(StringComparer.OrdinalIgnoreCase); + + if (reader.TokenType != JsonTokenType.StartObject) + { + return headers; + } + + var valueList = new List(); + while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) + { + string? propertyName = reader.GetString(); + + reader.Read(); + + // Header values can be either individual strings or string arrays + StringValues values = default(StringValues); + if (reader.TokenType == JsonTokenType.String) + { + values = new StringValues(reader.GetString()); + } + else if (reader.TokenType == JsonTokenType.StartArray) + { + while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) + { + valueList.Add(reader.GetString()); + } + + values = new StringValues(valueList.ToArray()); + valueList.Clear(); + } + + headers[propertyName] = values; + } + + return headers; + } + + public override void Write( + Utf8JsonWriter writer, + object value, + JsonSerializerOptions options) + { + writer.WriteStartObject(); + + var headers = (IDictionary)value; + foreach (var pair in headers) + { + if (pair.Value.Count == 1) + { + // serialize as a single string value + writer.WriteString(pair.Key, pair.Value[0]); + } + else + { + // serializes as an array + writer.WriteStartArray(pair.Key); + writer.WriteStringValue(pair.Value); + writer.WriteEndArray(); + } + } + + writer.WriteEndObject(); + } + + internal static IDictionary CreateCopy(IDictionary input) + { + var copy = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (input != null) + { + foreach (var pair in input) + { + copy[pair.Key] = pair.Value; + } + } + + return copy; + } +} \ No newline at end of file diff --git a/src/Worker.Extensions.DurableTask/TaskOrchestrationContextExtensionMethods.cs b/src/Worker.Extensions.DurableTask/TaskOrchestrationContextExtensionMethods.cs new file mode 100644 index 000000000..81a99c7bf --- /dev/null +++ b/src/Worker.Extensions.DurableTask/TaskOrchestrationContextExtensionMethods.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.DurableTask; + +namespace Microsoft.Azure.Functions.Worker; + +/// +/// Extensions for . +/// +public static class TaskOrchestrationContextExtensionMethods +{ + /// + /// Makes an HTTP call using the information in the DurableHttpRequest. + /// + /// The task orchestration context. + /// The DurableHttpRequest used to make the HTTP call. + /// + public static async Task CallHttpAsync(this TaskOrchestrationContext context, DurableHttpRequest req) + { + string responseString = await context.CallActivityAsync("BuiltIn::HttpActivity", req); + + DurableHttpResponse? response = JsonSerializer.Deserialize(responseString); + + return response; + } +} From 1191acb4ab60e24077b37bddae7a0054b8197630 Mon Sep 17 00:00:00 2001 From: Varshitha Bachu Date: Wed, 1 Nov 2023 12:31:23 -0700 Subject: [PATCH 2/9] serialize output --- src/WebJobs.Extensions.DurableTask/OutOfProcMiddleware.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/WebJobs.Extensions.DurableTask/OutOfProcMiddleware.cs b/src/WebJobs.Extensions.DurableTask/OutOfProcMiddleware.cs index c4169fc07..234b27653 100644 --- a/src/WebJobs.Extensions.DurableTask/OutOfProcMiddleware.cs +++ b/src/WebJobs.Extensions.DurableTask/OutOfProcMiddleware.cs @@ -274,7 +274,7 @@ public async Task CallActivityAsync(DispatchMiddlewareContext dispatchContext, F ResponseEvent = new TaskCompletedEvent( eventId: -1, taskScheduledId: scheduledEvent.EventId, - result: output), + result: JsonConvert.SerializeObject(output)), }); return; } From 57e65b6342aa5066e5ae29d2f1b4e042c99d2e04 Mon Sep 17 00:00:00 2001 From: Varshitha Bachu Date: Fri, 3 Nov 2023 16:28:41 -0700 Subject: [PATCH 3/9] added STJ custom converters --- .../OutOfProcMiddleware.cs | 13 +- .../DurableHttpRequest.cs | 40 +++-- .../DurableHttpRequestConverter.cs | 73 +++++++++ .../DurableHttpResponse.cs | 1 + .../DurableHttpResponseConverter.cs | 138 ++++++++++++++++++ .../HttpRetryOptions.cs | 114 +++++++++++++++ .../RetryOptions.cs | 110 ++++++++++++++ 7 files changed, 465 insertions(+), 24 deletions(-) create mode 100644 src/Worker.Extensions.DurableTask/DurableHttpRequestConverter.cs create mode 100644 src/Worker.Extensions.DurableTask/DurableHttpResponseConverter.cs create mode 100644 src/Worker.Extensions.DurableTask/HttpRetryOptions.cs create mode 100644 src/Worker.Extensions.DurableTask/RetryOptions.cs diff --git a/src/WebJobs.Extensions.DurableTask/OutOfProcMiddleware.cs b/src/WebJobs.Extensions.DurableTask/OutOfProcMiddleware.cs index c66bd6613..5bd2e140f 100644 --- a/src/WebJobs.Extensions.DurableTask/OutOfProcMiddleware.cs +++ b/src/WebJobs.Extensions.DurableTask/OutOfProcMiddleware.cs @@ -583,17 +583,8 @@ public async Task CallActivityAsync(DispatchMiddlewareContext dispatchContext, F private static DurableHttpRequest? ConvertDurableHttpRequest(string? inputString) { - IList? input = JsonConvert.DeserializeObject>(inputString); - dynamic? dynamicRequest = input[0]; - - HttpMethod httpMethod = dynamicRequest.method.ToObject(); - Uri uri = dynamicRequest.uri.ToObject(); - string content = dynamicRequest.content.ToString(); - - JsonSerializerSettings settings = new JsonSerializerSettings { Converters = new List { new HttpHeadersConverter() } }; - Dictionary headers = JsonConvert.DeserializeObject>(dynamicRequest.headers.ToString(), settings); - - DurableHttpRequest request = new DurableHttpRequest(httpMethod, uri, headers, content); + IList? input = JsonConvert.DeserializeObject>(inputString); + DurableHttpRequest? request = input?.First(); return request; } diff --git a/src/Worker.Extensions.DurableTask/DurableHttpRequest.cs b/src/Worker.Extensions.DurableTask/DurableHttpRequest.cs index 66070e3fa..9ebc50cee 100644 --- a/src/Worker.Extensions.DurableTask/DurableHttpRequest.cs +++ b/src/Worker.Extensions.DurableTask/DurableHttpRequest.cs @@ -12,6 +12,7 @@ namespace Microsoft.Azure.Functions.Worker; /// /// Request used to make an HTTP call through Durable Functions. /// +[JsonConverter(typeof(DurableHttpRequestConverter))] public class DurableHttpRequest { /// @@ -21,12 +22,18 @@ public DurableHttpRequest( HttpMethod method, Uri uri, IDictionary? headers = null, - string? content = null) + string? content = null, + bool asynchronousPatternEnabled = true, + TimeSpan? timeout = null, + HttpRetryOptions httpRetryOptions = null) { this.Method = method; this.Uri = uri; this.Headers = HttpHeadersConverter.CreateCopy(headers); this.Content = content; + this.AsynchronousPatternEnabled = asynchronousPatternEnabled; + this.Timeout = timeout; + this.HttpRetryOptions = httpRetryOptions; } /// @@ -54,17 +61,24 @@ public DurableHttpRequest( [JsonPropertyName("content")] public string? Content { get; } - internal static IDictionary CreateCopy(IDictionary input) - { - var copy = new Dictionary(StringComparer.OrdinalIgnoreCase); - if (input != null) - { - foreach (var pair in input) - { - copy[pair.Key] = pair.Value; - } - } + /// + /// Specifies whether the Durable HTTP APIs should automatically + /// handle the asynchronous HTTP pattern. + /// + [JsonPropertyName("asynchronousPatternEnabled")] + public bool AsynchronousPatternEnabled { get; } - return copy; - } + /// + /// Defines retry policy for handling of failures in making the HTTP Request. These could be non-successful HTTP status codes + /// in the response, a timeout in making the HTTP call, or an exception raised from the HTTP Client library. + /// + [JsonPropertyName("retryOptions")] + public HttpRetryOptions HttpRetryOptions { get; } + + /// + /// The total timeout for the original HTTP request and any + /// asynchronous polling. + /// + [JsonPropertyName("timeout")] + public TimeSpan? Timeout { get; } } \ No newline at end of file diff --git a/src/Worker.Extensions.DurableTask/DurableHttpRequestConverter.cs b/src/Worker.Extensions.DurableTask/DurableHttpRequestConverter.cs new file mode 100644 index 000000000..fd364e45a --- /dev/null +++ b/src/Worker.Extensions.DurableTask/DurableHttpRequestConverter.cs @@ -0,0 +1,73 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Azure.Functions.Worker; + +internal class DurableHttpRequestConverter : JsonConverter +{ + public override bool CanConvert(Type objectType) + { + return typeof(DurableHttpRequest).IsAssignableFrom(objectType); + } + + public override DurableHttpRequest Read( + ref Utf8JsonReader reader, + Type objectType, + JsonSerializerOptions options) + { + throw new NotImplementedException(); + } + + public override void Write( + Utf8JsonWriter writer, + DurableHttpRequest request, + JsonSerializerOptions options) + { + writer.WriteStartObject(); + + // method + writer.WriteString("method", request.Method?.ToString()); + + // uri + writer.WriteString("uri", request.Uri?.ToString()); + + // headers + writer.WriteStartObject("headers"); + + var headers = request.Headers; + if (headers != null) + { + foreach (var pair in headers) + { + if (pair.Value.Count == 1) + { + // serialize as a single string value + writer.WriteString(pair.Key, pair.Value[0]); + } + else + { + // serializes as an array + writer.WriteStartArray(pair.Key); + writer.WriteStringValue(pair.Value); + writer.WriteEndArray(); + } + } + } + + writer.WriteEndObject(); + + // content + writer.WriteString("content", request.Content); + + // asynchronous pattern enabled + writer.WriteBoolean("asynchronousPatternEnabled", request.AsynchronousPatternEnabled); + + writer.WriteEndObject(); + } +} diff --git a/src/Worker.Extensions.DurableTask/DurableHttpResponse.cs b/src/Worker.Extensions.DurableTask/DurableHttpResponse.cs index a5ec4613f..687675000 100644 --- a/src/Worker.Extensions.DurableTask/DurableHttpResponse.cs +++ b/src/Worker.Extensions.DurableTask/DurableHttpResponse.cs @@ -15,6 +15,7 @@ namespace Microsoft.Azure.Functions.Worker; /// /// Response received from the HTTP request made by the Durable Function. /// +[JsonConverter(typeof(DurableHttpResponseConverter))] public class DurableHttpResponse { /// diff --git a/src/Worker.Extensions.DurableTask/DurableHttpResponseConverter.cs b/src/Worker.Extensions.DurableTask/DurableHttpResponseConverter.cs new file mode 100644 index 000000000..112caa57c --- /dev/null +++ b/src/Worker.Extensions.DurableTask/DurableHttpResponseConverter.cs @@ -0,0 +1,138 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Net; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Azure.Functions.Worker; +internal class DurableHttpResponseConverter : JsonConverter +{ + public override bool CanConvert(Type objectType) + { + return typeof(DurableHttpResponse).IsAssignableFrom(objectType); + } + + public override DurableHttpResponse Read( + ref Utf8JsonReader reader, + Type objectType, + JsonSerializerOptions options) + { + DurableHttpResponse response; + HttpStatusCode statusCode = HttpStatusCode.Moved; + var headers = new Dictionary(StringComparer.OrdinalIgnoreCase); + string content = ""; + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + break; + } + + // Get the key. + if (reader.TokenType != JsonTokenType.PropertyName) + { + throw new JsonException(); + } + + string? propertyName = reader.GetString(); + + // status code + if (string.Equals(propertyName, "statusCode")) + { + reader.Read(); + statusCode = (HttpStatusCode)reader.GetInt64(); + continue; + } + + // content + if (string.Equals(propertyName, "content")) + { + reader.Read(); + content = reader.GetString(); + continue; + } + + // headers + if (string.Equals(propertyName, "headers")) + { + reader.Read(); + + var valueList = new List(); + while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) + { + string? headerName = reader.GetString(); + + reader.Read(); + + // Header values can be either individual strings or string arrays + StringValues values = default(StringValues); + if (reader.TokenType == JsonTokenType.String) + { + values = new StringValues(reader.GetString()); + } + else if (reader.TokenType == JsonTokenType.StartArray) + { + while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) + { + valueList.Add(reader.GetString()); + } + + values = new StringValues(valueList.ToArray()); + valueList.Clear(); + } + + headers[headerName] = values; + } + } + } + + return new DurableHttpResponse(statusCode, headers, content); + } + + public override void Write( + Utf8JsonWriter writer, + DurableHttpResponse response, + JsonSerializerOptions options) + { + writer.WriteStartObject(); + + // status code + writer.WriteString("status code", response.StatusCode.ToString()); + + // content + writer.WriteString("content", response.Content); + + // headers + writer.WriteStartObject(); + + var headers = response.Headers; + if (headers != null) + { + foreach (var pair in headers) + { + if (pair.Value.Count == 1) + { + // serialize as a single string value + writer.WriteString(pair.Key, pair.Value[0]); + } + else + { + // serializes as an array + writer.WriteStartArray(pair.Key); + writer.WriteStringValue(pair.Value); + writer.WriteEndArray(); + } + } + } + + writer.WriteEndObject(); + + writer.WriteEndObject(); + } +} diff --git a/src/Worker.Extensions.DurableTask/HttpRetryOptions.cs b/src/Worker.Extensions.DurableTask/HttpRetryOptions.cs new file mode 100644 index 000000000..6e60a9b54 --- /dev/null +++ b/src/Worker.Extensions.DurableTask/HttpRetryOptions.cs @@ -0,0 +1,114 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Net; +using DurableTask.Core; +using DurableTaskCore = DurableTask.Core; + +namespace Microsoft.Azure.Functions.Worker; + +/// +/// Defines retry policies that can be passed as parameters to various operations. +/// +public class HttpRetryOptions +{ + private readonly DurableTaskCore.RetryOptions coreRetryOptions; + + // Would like to make this durability provider specific, but since this is a developer + // facing type, that is difficult. + private static readonly TimeSpan DefaultMaxRetryinterval = TimeSpan.FromDays(6); + + /// + /// Creates a new instance SerializableRetryOptions with the supplied first retry and max attempts. + /// + /// Timespan to wait for the first retry. + /// Max number of attempts to retry. + /// + /// The value must be greater than . + /// + public HttpRetryOptions(TimeSpan firstRetryInterval, int maxNumberOfAttempts) + { + this.coreRetryOptions = new DurableTaskCore.RetryOptions(firstRetryInterval, maxNumberOfAttempts); + this.MaxRetryInterval = DefaultMaxRetryinterval; + } + + /// + /// Gets or sets the first retry interval. + /// + /// + /// The TimeSpan to wait for the first retries. + /// + public TimeSpan FirstRetryInterval + { + get { return this.coreRetryOptions.FirstRetryInterval; } + set { this.coreRetryOptions.FirstRetryInterval = value; } + } + + /// + /// Gets or sets the max retry interval. + /// + /// + /// The TimeSpan of the max retry interval, defaults to 6 days. + /// + public TimeSpan MaxRetryInterval + { + get { return this.coreRetryOptions.MaxRetryInterval; } + set { this.coreRetryOptions.MaxRetryInterval = value; } + } + + /// + /// Gets or sets the backoff coefficient. + /// + /// + /// The backoff coefficient used to determine rate of increase of backoff. Defaults to 1. + /// + public double BackoffCoefficient + { + get { return this.coreRetryOptions.BackoffCoefficient; } + set { this.coreRetryOptions.BackoffCoefficient = value; } + } + + /// + /// Gets or sets the timeout for retries. + /// + /// + /// The TimeSpan timeout for retries, defaults to . + /// + public TimeSpan RetryTimeout + { + get { return this.coreRetryOptions.RetryTimeout; } + set { this.coreRetryOptions.RetryTimeout = value; } + } + + /// + /// Gets or sets the max number of attempts. + /// + /// + /// The maximum number of retry attempts. + /// + public int MaxNumberOfAttempts + { + get { return this.coreRetryOptions.MaxNumberOfAttempts; } + set { this.coreRetryOptions.MaxNumberOfAttempts = value; } + } + + /// + /// Gets or sets the list of status codes upon which the + /// retry logic specified by this object shall be triggered. + /// If none are provided, all 4xx and 5xx status codes + /// will be retried. + /// + public IList StatusCodesToRetry { get; set; } = new List(); + + internal RetryOptions GetRetryOptions() + { + return new RetryOptions(this.FirstRetryInterval, this.MaxNumberOfAttempts) + { + BackoffCoefficient = this.BackoffCoefficient, + MaxRetryInterval = this.MaxRetryInterval, + RetryTimeout = this.RetryTimeout, + }; + } +} diff --git a/src/Worker.Extensions.DurableTask/RetryOptions.cs b/src/Worker.Extensions.DurableTask/RetryOptions.cs new file mode 100644 index 000000000..4b3273810 --- /dev/null +++ b/src/Worker.Extensions.DurableTask/RetryOptions.cs @@ -0,0 +1,110 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using DurableTaskCore = DurableTask.Core; + +namespace Microsoft.Azure.Functions.Worker; + +/// +/// Defines retry policies that can be passed as parameters to various operations. +/// +public class RetryOptions +{ + private readonly DurableTaskCore.RetryOptions retryOptions; + + // Would like to make this durability provider specific, but since this is a customer + // facing type, that is difficult. + private static readonly TimeSpan DefaultMaxRetryinterval = TimeSpan.FromDays(6); + + /// + /// Creates a new instance RetryOptions with the supplied first retry and max attempts. + /// + /// Timespan to wait for the first retry. + /// Max number of attempts to retry. + /// + /// The value must be greater than . + /// + public RetryOptions(TimeSpan firstRetryInterval, int maxNumberOfAttempts) + { + this.retryOptions = new DurableTaskCore.RetryOptions(firstRetryInterval, maxNumberOfAttempts); + this.MaxRetryInterval = DefaultMaxRetryinterval; + } + + /// + /// Gets or sets the first retry interval. + /// + /// + /// The TimeSpan to wait for the first retries. + /// + public TimeSpan FirstRetryInterval + { + get { return this.retryOptions.FirstRetryInterval; } + set { this.retryOptions.FirstRetryInterval = value; } + } + + /// + /// Gets or sets the max retry interval. + /// + /// + /// The TimeSpan of the max retry interval, defaults to . + /// + public TimeSpan MaxRetryInterval + { + get { return this.retryOptions.MaxRetryInterval; } + set { this.retryOptions.MaxRetryInterval = value; } + } + + /// + /// Gets or sets the backoff coefficient. + /// + /// + /// The backoff coefficient used to determine rate of increase of backoff. Defaults to 1. + /// + public double BackoffCoefficient + { + get { return this.retryOptions.BackoffCoefficient; } + set { this.retryOptions.BackoffCoefficient = value; } + } + + /// + /// Gets or sets the timeout for retries. + /// + /// + /// The TimeSpan timeout for retries, defaults to . + /// + public TimeSpan RetryTimeout + { + get { return this.retryOptions.RetryTimeout; } + set { this.retryOptions.RetryTimeout = value; } + } + + /// + /// Gets or sets the max number of attempts. + /// + /// + /// The maximum number of retry attempts. + /// + public int MaxNumberOfAttempts + { + get { return this.retryOptions.MaxNumberOfAttempts; } + set { this.retryOptions.MaxNumberOfAttempts = value; } + } + + /// + /// Gets or sets a delegate to call on exception to determine if retries should proceed. + /// + /// + /// The delegate to handle exception to determine if retries should proceed. + /// + public Func Handle + { + get { return this.retryOptions.Handle; } + set { this.retryOptions.Handle = value; } + } + + internal DurableTaskCore.RetryOptions GetRetryOptions() + { + return this.retryOptions; + } +} From be87c845402ed7ea73b63d755499259cbd356c3c Mon Sep 17 00:00:00 2001 From: Varshitha Bachu Date: Tue, 7 Nov 2023 09:52:50 -0800 Subject: [PATCH 4/9] added httpretry option support --- .../Constants.cs | 2 + .../DurableHttpRequest.cs | 7 +- .../DurableHttpRequestConverter.cs | 34 ++++-- .../DurableHttpResponse.cs | 43 +------- .../DurableHttpResponseConverter.cs | 2 +- .../HttpHeadersConverter.cs | 103 ------------------ .../HttpHeadersHelper.cs | 25 +++++ .../HttpRetryOptions.cs | 1 - ...askOrchestrationContextExtensionMethods.cs | 7 +- 9 files changed, 66 insertions(+), 158 deletions(-) delete mode 100644 src/Worker.Extensions.DurableTask/HttpHeadersConverter.cs create mode 100644 src/Worker.Extensions.DurableTask/HttpHeadersHelper.cs diff --git a/src/Worker.Extensions.DurableTask/Constants.cs b/src/Worker.Extensions.DurableTask/Constants.cs index 6f3c3cd41..e94200bef 100644 --- a/src/Worker.Extensions.DurableTask/Constants.cs +++ b/src/Worker.Extensions.DurableTask/Constants.cs @@ -9,4 +9,6 @@ internal static class Constants public const string IllegalAwaitErrorMessage = "An invalid asynchronous invocation was detected. This can be caused by awaiting non-durable tasks " + "in an orchestrator function's implementation or by middleware that invokes asynchronous code."; + + public const string HttpTaskActivityReservedName = "BuiltIn::HttpActivity"; } \ No newline at end of file diff --git a/src/Worker.Extensions.DurableTask/DurableHttpRequest.cs b/src/Worker.Extensions.DurableTask/DurableHttpRequest.cs index 9ebc50cee..e38b0a875 100644 --- a/src/Worker.Extensions.DurableTask/DurableHttpRequest.cs +++ b/src/Worker.Extensions.DurableTask/DurableHttpRequest.cs @@ -25,11 +25,11 @@ public DurableHttpRequest( string? content = null, bool asynchronousPatternEnabled = true, TimeSpan? timeout = null, - HttpRetryOptions httpRetryOptions = null) + HttpRetryOptions? httpRetryOptions = null) { this.Method = method; this.Uri = uri; - this.Headers = HttpHeadersConverter.CreateCopy(headers); + this.Headers = HttpHeadersHelper.CreateCopy(headers); this.Content = content; this.AsynchronousPatternEnabled = asynchronousPatternEnabled; this.Timeout = timeout; @@ -52,7 +52,6 @@ public DurableHttpRequest( /// Headers passed with the HTTP request made by the Durable Function. /// [JsonPropertyName("headers")] - [JsonConverter(typeof(HttpHeadersConverter))] public IDictionary? Headers { get; } /// @@ -73,7 +72,7 @@ public DurableHttpRequest( /// in the response, a timeout in making the HTTP call, or an exception raised from the HTTP Client library. /// [JsonPropertyName("retryOptions")] - public HttpRetryOptions HttpRetryOptions { get; } + public HttpRetryOptions? HttpRetryOptions { get; } /// /// The total timeout for the original HTTP request and any diff --git a/src/Worker.Extensions.DurableTask/DurableHttpRequestConverter.cs b/src/Worker.Extensions.DurableTask/DurableHttpRequestConverter.cs index fd364e45a..e3d493afd 100644 --- a/src/Worker.Extensions.DurableTask/DurableHttpRequestConverter.cs +++ b/src/Worker.Extensions.DurableTask/DurableHttpRequestConverter.cs @@ -2,10 +2,9 @@ // Licensed under the MIT License. See License.txt in the project root for license information. using System; -using System.Collections.Generic; +using System.Net; using System.Text.Json; using System.Text.Json.Serialization; -using Microsoft.Extensions.Primitives; namespace Microsoft.Azure.Functions.Worker; @@ -31,13 +30,13 @@ public override void Write( { writer.WriteStartObject(); - // method + // Method writer.WriteString("method", request.Method?.ToString()); - // uri + // URI writer.WriteString("uri", request.Uri?.ToString()); - // headers + // Headers writer.WriteStartObject("headers"); var headers = request.Headers; @@ -62,12 +61,31 @@ public override void Write( writer.WriteEndObject(); - // content + // Content writer.WriteString("content", request.Content); - // asynchronous pattern enabled + // Asynchronous pattern enabled writer.WriteBoolean("asynchronousPatternEnabled", request.AsynchronousPatternEnabled); + // Timeout + writer.WriteString("timeout", request.Timeout.ToString()); + + // HTTP retry options + writer.WriteStartObject("retryOptions"); + writer.WriteString("FirstRetryInterval", request.HttpRetryOptions.FirstRetryInterval.ToString()); + writer.WriteString("MaxRetryInterval", request.HttpRetryOptions.MaxRetryInterval.ToString()); + writer.WriteNumber("BackoffCoefficient", request.HttpRetryOptions.BackoffCoefficient); + writer.WriteString("RetryTimeout", request.HttpRetryOptions.RetryTimeout.ToString()); + writer.WriteNumber("MaxNumberOfAttempts", request.HttpRetryOptions.MaxNumberOfAttempts); + writer.WriteStartArray("StatusCodesToRetry"); + foreach (HttpStatusCode statusCode in request.HttpRetryOptions.StatusCodesToRetry) + { + writer.WriteNumberValue((decimal)statusCode); + } + + writer.WriteEndArray(); + writer.WriteEndObject(); + writer.WriteEndObject(); } -} +} \ No newline at end of file diff --git a/src/Worker.Extensions.DurableTask/DurableHttpResponse.cs b/src/Worker.Extensions.DurableTask/DurableHttpResponse.cs index 687675000..97f3b101d 100644 --- a/src/Worker.Extensions.DurableTask/DurableHttpResponse.cs +++ b/src/Worker.Extensions.DurableTask/DurableHttpResponse.cs @@ -3,11 +3,8 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Net; -using System.Net.Http; using System.Text.Json.Serialization; -using System.Threading.Tasks; using Microsoft.Extensions.Primitives; namespace Microsoft.Azure.Functions.Worker; @@ -26,11 +23,11 @@ public class DurableHttpResponse /// Content returned from the HTTP call. public DurableHttpResponse( HttpStatusCode statusCode, - IDictionary headers = null, - string content = null) + IDictionary? headers = null, + string? content = null) { this.StatusCode = statusCode; - this.Headers = HttpHeadersConverter.CreateCopy(headers); + this.Headers = HttpHeadersHelper.CreateCopy(headers); this.Content = content; } @@ -44,41 +41,11 @@ public DurableHttpResponse( /// Headers in the response from an HTTP request. /// [JsonPropertyName("headers")] - [JsonConverter(typeof(HttpHeadersConverter))] - public IDictionary Headers { get; } + public IDictionary? Headers { get; } /// /// Content returned from an HTTP request. /// [JsonPropertyName("content")] - public string Content { get; } - - /// - /// Creates a DurableHttpResponse from an HttpResponseMessage. - /// - /// HttpResponseMessage returned from the HTTP call. - /// A representing the result of the asynchronous operation. - public static async Task CreateDurableHttpResponseWithHttpResponseMessage(HttpResponseMessage httpResponseMessage) - { - DurableHttpResponse durableHttpResponse = new DurableHttpResponse( - statusCode: httpResponseMessage.StatusCode, - headers: CreateStringValuesHeaderDictionary(httpResponseMessage.Headers), - content: await httpResponseMessage.Content.ReadAsStringAsync()); - - return durableHttpResponse; - } - - private static IDictionary CreateStringValuesHeaderDictionary(IEnumerable>> headers) - { - IDictionary newHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); - if (headers != null) - { - foreach (var header in headers) - { - newHeaders[header.Key] = new StringValues(header.Value.ToArray()); - } - } - - return newHeaders; - } + public string? Content { get; } } \ No newline at end of file diff --git a/src/Worker.Extensions.DurableTask/DurableHttpResponseConverter.cs b/src/Worker.Extensions.DurableTask/DurableHttpResponseConverter.cs index 112caa57c..7503505d3 100644 --- a/src/Worker.Extensions.DurableTask/DurableHttpResponseConverter.cs +++ b/src/Worker.Extensions.DurableTask/DurableHttpResponseConverter.cs @@ -23,7 +23,7 @@ public override DurableHttpResponse Read( JsonSerializerOptions options) { DurableHttpResponse response; - HttpStatusCode statusCode = HttpStatusCode.Moved; + HttpStatusCode statusCode = HttpStatusCode.OK; var headers = new Dictionary(StringComparer.OrdinalIgnoreCase); string content = ""; diff --git a/src/Worker.Extensions.DurableTask/HttpHeadersConverter.cs b/src/Worker.Extensions.DurableTask/HttpHeadersConverter.cs deleted file mode 100644 index 588274a31..000000000 --- a/src/Worker.Extensions.DurableTask/HttpHeadersConverter.cs +++ /dev/null @@ -1,103 +0,0 @@ -// 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.Collections.Generic; -using System.Text.Json; -using System.Text.Json.Serialization; -using Microsoft.Extensions.Primitives; - -namespace Microsoft.Azure.Functions.Worker; - -// StringValues does not deserialize as you would expect, so we need a custom mechanism -// for serializing HTTP header collections -internal class HttpHeadersConverter : JsonConverter -{ - public override bool CanConvert(Type objectType) - { - return typeof(IDictionary).IsAssignableFrom(objectType); - } - - public override object Read( - ref Utf8JsonReader reader, - Type objectType, - JsonSerializerOptions options) - { - var headers = new Dictionary(StringComparer.OrdinalIgnoreCase); - - if (reader.TokenType != JsonTokenType.StartObject) - { - return headers; - } - - var valueList = new List(); - while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) - { - string? propertyName = reader.GetString(); - - reader.Read(); - - // Header values can be either individual strings or string arrays - StringValues values = default(StringValues); - if (reader.TokenType == JsonTokenType.String) - { - values = new StringValues(reader.GetString()); - } - else if (reader.TokenType == JsonTokenType.StartArray) - { - while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) - { - valueList.Add(reader.GetString()); - } - - values = new StringValues(valueList.ToArray()); - valueList.Clear(); - } - - headers[propertyName] = values; - } - - return headers; - } - - public override void Write( - Utf8JsonWriter writer, - object value, - JsonSerializerOptions options) - { - writer.WriteStartObject(); - - var headers = (IDictionary)value; - foreach (var pair in headers) - { - if (pair.Value.Count == 1) - { - // serialize as a single string value - writer.WriteString(pair.Key, pair.Value[0]); - } - else - { - // serializes as an array - writer.WriteStartArray(pair.Key); - writer.WriteStringValue(pair.Value); - writer.WriteEndArray(); - } - } - - writer.WriteEndObject(); - } - - internal static IDictionary CreateCopy(IDictionary input) - { - var copy = new Dictionary(StringComparer.OrdinalIgnoreCase); - if (input != null) - { - foreach (var pair in input) - { - copy[pair.Key] = pair.Value; - } - } - - return copy; - } -} \ No newline at end of file diff --git a/src/Worker.Extensions.DurableTask/HttpHeadersHelper.cs b/src/Worker.Extensions.DurableTask/HttpHeadersHelper.cs new file mode 100644 index 000000000..841946c6e --- /dev/null +++ b/src/Worker.Extensions.DurableTask/HttpHeadersHelper.cs @@ -0,0 +1,25 @@ +// 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.Collections.Generic; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Azure.Functions.Worker; + +internal class HttpHeadersHelper +{ + internal static IDictionary CreateCopy(IDictionary? input) + { + var copy = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (input != null) + { + foreach (var pair in input) + { + copy[pair.Key] = pair.Value; + } + } + + return copy; + } +} \ No newline at end of file diff --git a/src/Worker.Extensions.DurableTask/HttpRetryOptions.cs b/src/Worker.Extensions.DurableTask/HttpRetryOptions.cs index 6e60a9b54..79eec7c3f 100644 --- a/src/Worker.Extensions.DurableTask/HttpRetryOptions.cs +++ b/src/Worker.Extensions.DurableTask/HttpRetryOptions.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Net; -using DurableTask.Core; using DurableTaskCore = DurableTask.Core; namespace Microsoft.Azure.Functions.Worker; diff --git a/src/Worker.Extensions.DurableTask/TaskOrchestrationContextExtensionMethods.cs b/src/Worker.Extensions.DurableTask/TaskOrchestrationContextExtensionMethods.cs index 81a99c7bf..5a32b57f5 100644 --- a/src/Worker.Extensions.DurableTask/TaskOrchestrationContextExtensionMethods.cs +++ b/src/Worker.Extensions.DurableTask/TaskOrchestrationContextExtensionMethods.cs @@ -3,6 +3,7 @@ using System.Text.Json; using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker.Extensions.DurableTask; using Microsoft.DurableTask; namespace Microsoft.Azure.Functions.Worker; @@ -17,11 +18,11 @@ public static class TaskOrchestrationContextExtensionMethods /// /// The task orchestration context. /// The DurableHttpRequest used to make the HTTP call. - /// + /// DurableHttpResponse public static async Task CallHttpAsync(this TaskOrchestrationContext context, DurableHttpRequest req) { - string responseString = await context.CallActivityAsync("BuiltIn::HttpActivity", req); - + string responseString = await context.CallActivityAsync(Constants.HttpTaskActivityReservedName, req); + DurableHttpResponse? response = JsonSerializer.Deserialize(responseString); return response; From 737d09c1ef5dc42c0146cbaecb163ea48a5bd1fc Mon Sep 17 00:00:00 2001 From: Varshitha Bachu Date: Thu, 9 Nov 2023 17:16:15 -0800 Subject: [PATCH 5/9] added converters for HttpMethod and headers --- .../OutOfProcMiddleware.cs | 43 +----- .../DurableHttpRequest.cs | 7 +- .../DurableHttpRequestConverter.cs | 91 ------------ .../DurableHttpResponse.cs | 7 +- .../DurableHttpResponseConverter.cs | 138 ------------------ .../HttpHeadersConverter.cs | 89 +++++++++++ .../HttpHeadersHelper.cs | 25 ---- .../HttpMethodConverter.cs | 44 ++++++ .../HttpRetryOptions.cs | 55 +++---- .../RetryOptions.cs | 110 -------------- ...askOrchestrationContextExtensionMethods.cs | 35 ++++- 11 files changed, 193 insertions(+), 451 deletions(-) delete mode 100644 src/Worker.Extensions.DurableTask/DurableHttpRequestConverter.cs delete mode 100644 src/Worker.Extensions.DurableTask/DurableHttpResponseConverter.cs create mode 100644 src/Worker.Extensions.DurableTask/HttpHeadersConverter.cs delete mode 100644 src/Worker.Extensions.DurableTask/HttpHeadersHelper.cs create mode 100644 src/Worker.Extensions.DurableTask/HttpMethodConverter.cs delete mode 100644 src/Worker.Extensions.DurableTask/RetryOptions.cs diff --git a/src/WebJobs.Extensions.DurableTask/OutOfProcMiddleware.cs b/src/WebJobs.Extensions.DurableTask/OutOfProcMiddleware.cs index 5bd2e140f..a9992cc52 100644 --- a/src/WebJobs.Extensions.DurableTask/OutOfProcMiddleware.cs +++ b/src/WebJobs.Extensions.DurableTask/OutOfProcMiddleware.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Net.Http; using System.Threading.Tasks; using DurableTask.Core; using DurableTask.Core.Entities; @@ -15,7 +14,6 @@ using DurableTask.Core.History; using DurableTask.Core.Middleware; using Microsoft.Azure.WebJobs.Host.Executors; -using Microsoft.Extensions.Primitives; using Newtonsoft.Json; namespace Microsoft.Azure.WebJobs.Extensions.DurableTask @@ -418,45 +416,12 @@ public async Task CallActivityAsync(DispatchMiddlewareContext dispatchContext, F throw new InvalidOperationException($"An activity was scheduled but no {nameof(TaskScheduledEvent)} was found!"); } - if (!string.IsNullOrEmpty(scheduledEvent.Name) && scheduledEvent.Name.StartsWith("BuiltIn::HttpActivity")) + if (scheduledEvent.Name?.StartsWith("BuiltIn::", StringComparison.OrdinalIgnoreCase) ?? false) { - try - { - if (dispatchContext.GetProperty() is TaskHttpActivityShim shim) - { - OrchestrationInstance orchestrationInstance = dispatchContext.GetProperty(); - TaskContext context = new TaskContext(orchestrationInstance); - - // convert the DurableHttpRequest - DurableHttpRequest? req = ConvertDurableHttpRequest(scheduledEvent.Input); - IList list = new List() { req }; - string serializedRequest = JsonConvert.SerializeObject(list); - - string? output = await shim.RunAsync(context, serializedRequest); - dispatchContext.SetProperty(new ActivityExecutionResult - { - ResponseEvent = new TaskCompletedEvent( - eventId: -1, - taskScheduledId: scheduledEvent.EventId, - result: JsonConvert.SerializeObject(output)), - }); - return; - } - } - catch (Exception e) - { - dispatchContext.SetProperty(new ActivityExecutionResult - { - ResponseEvent = new TaskFailedEvent( - eventId: -1, - taskScheduledId: scheduledEvent.EventId, - reason: $"Function failed", - details: e.Message), - }); - return; - } + await next(); + return; } - + FunctionName functionName = new FunctionName(scheduledEvent.Name); OrchestrationInstance? instance = dispatchContext.GetProperty(); diff --git a/src/Worker.Extensions.DurableTask/DurableHttpRequest.cs b/src/Worker.Extensions.DurableTask/DurableHttpRequest.cs index e38b0a875..842495b0f 100644 --- a/src/Worker.Extensions.DurableTask/DurableHttpRequest.cs +++ b/src/Worker.Extensions.DurableTask/DurableHttpRequest.cs @@ -7,12 +7,11 @@ using System.Text.Json.Serialization; using Microsoft.Extensions.Primitives; -namespace Microsoft.Azure.Functions.Worker; +namespace Microsoft.Azure.Functions.Worker.Extensions.DurableTask.Http; /// /// Request used to make an HTTP call through Durable Functions. /// -[JsonConverter(typeof(DurableHttpRequestConverter))] public class DurableHttpRequest { /// @@ -29,7 +28,7 @@ public DurableHttpRequest( { this.Method = method; this.Uri = uri; - this.Headers = HttpHeadersHelper.CreateCopy(headers); + this.Headers = headers; this.Content = content; this.AsynchronousPatternEnabled = asynchronousPatternEnabled; this.Timeout = timeout; @@ -40,6 +39,7 @@ public DurableHttpRequest( /// HttpMethod used in the HTTP request made by the Durable Function. /// [JsonPropertyName("method")] + [JsonConverter(typeof(HttpMethodConverter))] public HttpMethod Method { get; } /// @@ -52,6 +52,7 @@ public DurableHttpRequest( /// Headers passed with the HTTP request made by the Durable Function. /// [JsonPropertyName("headers")] + [JsonConverter(typeof(HttpHeadersConverter))] public IDictionary? Headers { get; } /// diff --git a/src/Worker.Extensions.DurableTask/DurableHttpRequestConverter.cs b/src/Worker.Extensions.DurableTask/DurableHttpRequestConverter.cs deleted file mode 100644 index e3d493afd..000000000 --- a/src/Worker.Extensions.DurableTask/DurableHttpRequestConverter.cs +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. - -using System; -using System.Net; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Microsoft.Azure.Functions.Worker; - -internal class DurableHttpRequestConverter : JsonConverter -{ - public override bool CanConvert(Type objectType) - { - return typeof(DurableHttpRequest).IsAssignableFrom(objectType); - } - - public override DurableHttpRequest Read( - ref Utf8JsonReader reader, - Type objectType, - JsonSerializerOptions options) - { - throw new NotImplementedException(); - } - - public override void Write( - Utf8JsonWriter writer, - DurableHttpRequest request, - JsonSerializerOptions options) - { - writer.WriteStartObject(); - - // Method - writer.WriteString("method", request.Method?.ToString()); - - // URI - writer.WriteString("uri", request.Uri?.ToString()); - - // Headers - writer.WriteStartObject("headers"); - - var headers = request.Headers; - if (headers != null) - { - foreach (var pair in headers) - { - if (pair.Value.Count == 1) - { - // serialize as a single string value - writer.WriteString(pair.Key, pair.Value[0]); - } - else - { - // serializes as an array - writer.WriteStartArray(pair.Key); - writer.WriteStringValue(pair.Value); - writer.WriteEndArray(); - } - } - } - - writer.WriteEndObject(); - - // Content - writer.WriteString("content", request.Content); - - // Asynchronous pattern enabled - writer.WriteBoolean("asynchronousPatternEnabled", request.AsynchronousPatternEnabled); - - // Timeout - writer.WriteString("timeout", request.Timeout.ToString()); - - // HTTP retry options - writer.WriteStartObject("retryOptions"); - writer.WriteString("FirstRetryInterval", request.HttpRetryOptions.FirstRetryInterval.ToString()); - writer.WriteString("MaxRetryInterval", request.HttpRetryOptions.MaxRetryInterval.ToString()); - writer.WriteNumber("BackoffCoefficient", request.HttpRetryOptions.BackoffCoefficient); - writer.WriteString("RetryTimeout", request.HttpRetryOptions.RetryTimeout.ToString()); - writer.WriteNumber("MaxNumberOfAttempts", request.HttpRetryOptions.MaxNumberOfAttempts); - writer.WriteStartArray("StatusCodesToRetry"); - foreach (HttpStatusCode statusCode in request.HttpRetryOptions.StatusCodesToRetry) - { - writer.WriteNumberValue((decimal)statusCode); - } - - writer.WriteEndArray(); - writer.WriteEndObject(); - - writer.WriteEndObject(); - } -} \ No newline at end of file diff --git a/src/Worker.Extensions.DurableTask/DurableHttpResponse.cs b/src/Worker.Extensions.DurableTask/DurableHttpResponse.cs index 97f3b101d..5a1a6b0cd 100644 --- a/src/Worker.Extensions.DurableTask/DurableHttpResponse.cs +++ b/src/Worker.Extensions.DurableTask/DurableHttpResponse.cs @@ -1,18 +1,16 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. -using System; using System.Collections.Generic; using System.Net; using System.Text.Json.Serialization; using Microsoft.Extensions.Primitives; -namespace Microsoft.Azure.Functions.Worker; +namespace Microsoft.Azure.Functions.Worker.Extensions.DurableTask.Http; /// /// Response received from the HTTP request made by the Durable Function. /// -[JsonConverter(typeof(DurableHttpResponseConverter))] public class DurableHttpResponse { /// @@ -27,7 +25,7 @@ public DurableHttpResponse( string? content = null) { this.StatusCode = statusCode; - this.Headers = HttpHeadersHelper.CreateCopy(headers); + this.Headers = headers; this.Content = content; } @@ -41,6 +39,7 @@ public DurableHttpResponse( /// Headers in the response from an HTTP request. /// [JsonPropertyName("headers")] + [JsonConverter(typeof(HttpHeadersConverter))] public IDictionary? Headers { get; } /// diff --git a/src/Worker.Extensions.DurableTask/DurableHttpResponseConverter.cs b/src/Worker.Extensions.DurableTask/DurableHttpResponseConverter.cs deleted file mode 100644 index 7503505d3..000000000 --- a/src/Worker.Extensions.DurableTask/DurableHttpResponseConverter.cs +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Net; -using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; -using Microsoft.Extensions.Primitives; - -namespace Microsoft.Azure.Functions.Worker; -internal class DurableHttpResponseConverter : JsonConverter -{ - public override bool CanConvert(Type objectType) - { - return typeof(DurableHttpResponse).IsAssignableFrom(objectType); - } - - public override DurableHttpResponse Read( - ref Utf8JsonReader reader, - Type objectType, - JsonSerializerOptions options) - { - DurableHttpResponse response; - HttpStatusCode statusCode = HttpStatusCode.OK; - var headers = new Dictionary(StringComparer.OrdinalIgnoreCase); - string content = ""; - - while (reader.Read()) - { - if (reader.TokenType == JsonTokenType.EndObject) - { - break; - } - - // Get the key. - if (reader.TokenType != JsonTokenType.PropertyName) - { - throw new JsonException(); - } - - string? propertyName = reader.GetString(); - - // status code - if (string.Equals(propertyName, "statusCode")) - { - reader.Read(); - statusCode = (HttpStatusCode)reader.GetInt64(); - continue; - } - - // content - if (string.Equals(propertyName, "content")) - { - reader.Read(); - content = reader.GetString(); - continue; - } - - // headers - if (string.Equals(propertyName, "headers")) - { - reader.Read(); - - var valueList = new List(); - while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) - { - string? headerName = reader.GetString(); - - reader.Read(); - - // Header values can be either individual strings or string arrays - StringValues values = default(StringValues); - if (reader.TokenType == JsonTokenType.String) - { - values = new StringValues(reader.GetString()); - } - else if (reader.TokenType == JsonTokenType.StartArray) - { - while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) - { - valueList.Add(reader.GetString()); - } - - values = new StringValues(valueList.ToArray()); - valueList.Clear(); - } - - headers[headerName] = values; - } - } - } - - return new DurableHttpResponse(statusCode, headers, content); - } - - public override void Write( - Utf8JsonWriter writer, - DurableHttpResponse response, - JsonSerializerOptions options) - { - writer.WriteStartObject(); - - // status code - writer.WriteString("status code", response.StatusCode.ToString()); - - // content - writer.WriteString("content", response.Content); - - // headers - writer.WriteStartObject(); - - var headers = response.Headers; - if (headers != null) - { - foreach (var pair in headers) - { - if (pair.Value.Count == 1) - { - // serialize as a single string value - writer.WriteString(pair.Key, pair.Value[0]); - } - else - { - // serializes as an array - writer.WriteStartArray(pair.Key); - writer.WriteStringValue(pair.Value); - writer.WriteEndArray(); - } - } - } - - writer.WriteEndObject(); - - writer.WriteEndObject(); - } -} diff --git a/src/Worker.Extensions.DurableTask/HttpHeadersConverter.cs b/src/Worker.Extensions.DurableTask/HttpHeadersConverter.cs new file mode 100644 index 000000000..af9450e9c --- /dev/null +++ b/src/Worker.Extensions.DurableTask/HttpHeadersConverter.cs @@ -0,0 +1,89 @@ +// 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.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Azure.Functions.Worker; + +// StringValues does not deserialize as you would expect, so we need a custom mechanism +// for serializing HTTP header collections +internal class HttpHeadersConverter : JsonConverter> +{ + public override bool CanConvert(Type objectType) + { + return typeof(IDictionary).IsAssignableFrom(objectType); + } + + public override IDictionary Read( + ref Utf8JsonReader reader, + Type objectType, + JsonSerializerOptions options) + { + var headers = new Dictionary(StringComparer.OrdinalIgnoreCase); + + if (reader.TokenType != JsonTokenType.StartObject) + { + return headers; + } + + var valueList = new List(); + while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) + { + string? propertyName = reader.GetString(); + + reader.Read(); + + // Header values can be either individual strings or string arrays + StringValues values = default(StringValues); + if (reader.TokenType == JsonTokenType.String) + { + values = new StringValues(reader.GetString()); + } + else if (reader.TokenType == JsonTokenType.StartArray) + { + while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) + { + valueList.Add(reader.GetString()); + } + + values = new StringValues(valueList.ToArray()); + valueList.Clear(); + } + + headers[propertyName] = values; + } + + return headers; + } + + public override void Write( + Utf8JsonWriter writer, + IDictionary value, + JsonSerializerOptions options) + { + writer.WriteStartObject(); + + var headers = (IDictionary)value; + foreach (var pair in headers) + { + if (pair.Value.Count == 1) + { + // serialize as a single string value + writer.WriteString(pair.Key, pair.Value[0]); + } + else + { + // serializes as an array + writer.WriteStartArray(pair.Key); + writer.WriteStringValue(pair.Value); + writer.WriteEndArray(); + } + } + + writer.WriteEndObject(); + } +} \ No newline at end of file diff --git a/src/Worker.Extensions.DurableTask/HttpHeadersHelper.cs b/src/Worker.Extensions.DurableTask/HttpHeadersHelper.cs deleted file mode 100644 index 841946c6e..000000000 --- a/src/Worker.Extensions.DurableTask/HttpHeadersHelper.cs +++ /dev/null @@ -1,25 +0,0 @@ -// 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.Collections.Generic; -using Microsoft.Extensions.Primitives; - -namespace Microsoft.Azure.Functions.Worker; - -internal class HttpHeadersHelper -{ - internal static IDictionary CreateCopy(IDictionary? input) - { - var copy = new Dictionary(StringComparer.OrdinalIgnoreCase); - if (input != null) - { - foreach (var pair in input) - { - copy[pair.Key] = pair.Value; - } - } - - return copy; - } -} \ No newline at end of file diff --git a/src/Worker.Extensions.DurableTask/HttpMethodConverter.cs b/src/Worker.Extensions.DurableTask/HttpMethodConverter.cs new file mode 100644 index 000000000..e44c764fe --- /dev/null +++ b/src/Worker.Extensions.DurableTask/HttpMethodConverter.cs @@ -0,0 +1,44 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Net.Http; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.Azure.Functions.Worker.Extensions.DurableTask; + +internal class HttpMethodConverter : JsonConverter +{ + public override bool CanConvert(Type objectType) + { + return typeof(HttpMethod).IsAssignableFrom(objectType); + } + + public override HttpMethod Read( + ref Utf8JsonReader reader, + Type objectType, + JsonSerializerOptions options) + { + HttpMethod method = HttpMethod.Get; + + if (reader.TokenType != JsonTokenType.StartObject) + { + return method; + } + + reader.Read(); + + method = new HttpMethod(reader.GetString()); + + return method; + } + + public override void Write( + Utf8JsonWriter writer, + HttpMethod value, + JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString()); + } +} diff --git a/src/Worker.Extensions.DurableTask/HttpRetryOptions.cs b/src/Worker.Extensions.DurableTask/HttpRetryOptions.cs index 79eec7c3f..f8da05c32 100644 --- a/src/Worker.Extensions.DurableTask/HttpRetryOptions.cs +++ b/src/Worker.Extensions.DurableTask/HttpRetryOptions.cs @@ -4,21 +4,22 @@ using System; using System.Collections.Generic; using System.Net; -using DurableTaskCore = DurableTask.Core; +using System.Text.Json.Serialization; -namespace Microsoft.Azure.Functions.Worker; +namespace Microsoft.Azure.Functions.Worker.Extensions.DurableTask.Http; /// /// Defines retry policies that can be passed as parameters to various operations. /// public class HttpRetryOptions { - private readonly DurableTaskCore.RetryOptions coreRetryOptions; - // Would like to make this durability provider specific, but since this is a developer // facing type, that is difficult. private static readonly TimeSpan DefaultMaxRetryinterval = TimeSpan.FromDays(6); + private TimeSpan firstRetryInterval; + private int maxNumberOfAttempts; + /// /// Creates a new instance SerializableRetryOptions with the supplied first retry and max attempts. /// @@ -29,8 +30,10 @@ public class HttpRetryOptions /// public HttpRetryOptions(TimeSpan firstRetryInterval, int maxNumberOfAttempts) { - this.coreRetryOptions = new DurableTaskCore.RetryOptions(firstRetryInterval, maxNumberOfAttempts); this.MaxRetryInterval = DefaultMaxRetryinterval; + + this.firstRetryInterval = firstRetryInterval; + this.maxNumberOfAttempts = maxNumberOfAttempts; } /// @@ -39,10 +42,11 @@ public HttpRetryOptions(TimeSpan firstRetryInterval, int maxNumberOfAttempts) /// /// The TimeSpan to wait for the first retries. /// + [JsonPropertyName("FirstRetryInterval")] public TimeSpan FirstRetryInterval { - get { return this.coreRetryOptions.FirstRetryInterval; } - set { this.coreRetryOptions.FirstRetryInterval = value; } + get { return this.firstRetryInterval; } + set { this.firstRetryInterval = value; } } /// @@ -51,11 +55,8 @@ public TimeSpan FirstRetryInterval /// /// The TimeSpan of the max retry interval, defaults to 6 days. /// - public TimeSpan MaxRetryInterval - { - get { return this.coreRetryOptions.MaxRetryInterval; } - set { this.coreRetryOptions.MaxRetryInterval = value; } - } + [JsonPropertyName("MaxRetryInterval")] + public TimeSpan MaxRetryInterval { get; set; } /// /// Gets or sets the backoff coefficient. @@ -63,11 +64,8 @@ public TimeSpan MaxRetryInterval /// /// The backoff coefficient used to determine rate of increase of backoff. Defaults to 1. /// - public double BackoffCoefficient - { - get { return this.coreRetryOptions.BackoffCoefficient; } - set { this.coreRetryOptions.BackoffCoefficient = value; } - } + [JsonPropertyName("BackoffCoefficient")] + public double BackoffCoefficient { get; set; } /// /// Gets or sets the timeout for retries. @@ -75,11 +73,8 @@ public double BackoffCoefficient /// /// The TimeSpan timeout for retries, defaults to . /// - public TimeSpan RetryTimeout - { - get { return this.coreRetryOptions.RetryTimeout; } - set { this.coreRetryOptions.RetryTimeout = value; } - } + [JsonPropertyName("RetryTimeout")] + public TimeSpan RetryTimeout { get; set; } /// /// Gets or sets the max number of attempts. @@ -87,10 +82,11 @@ public TimeSpan RetryTimeout /// /// The maximum number of retry attempts. /// + [JsonPropertyName("MaxNumberOfAttempts")] public int MaxNumberOfAttempts { - get { return this.coreRetryOptions.MaxNumberOfAttempts; } - set { this.coreRetryOptions.MaxNumberOfAttempts = value; } + get { return this.maxNumberOfAttempts; } + set { this.maxNumberOfAttempts = value; } } /// @@ -99,15 +95,6 @@ public int MaxNumberOfAttempts /// If none are provided, all 4xx and 5xx status codes /// will be retried. /// + [JsonPropertyName("StatusCodesToRetry")] public IList StatusCodesToRetry { get; set; } = new List(); - - internal RetryOptions GetRetryOptions() - { - return new RetryOptions(this.FirstRetryInterval, this.MaxNumberOfAttempts) - { - BackoffCoefficient = this.BackoffCoefficient, - MaxRetryInterval = this.MaxRetryInterval, - RetryTimeout = this.RetryTimeout, - }; - } } diff --git a/src/Worker.Extensions.DurableTask/RetryOptions.cs b/src/Worker.Extensions.DurableTask/RetryOptions.cs deleted file mode 100644 index 4b3273810..000000000 --- a/src/Worker.Extensions.DurableTask/RetryOptions.cs +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. - -using System; -using DurableTaskCore = DurableTask.Core; - -namespace Microsoft.Azure.Functions.Worker; - -/// -/// Defines retry policies that can be passed as parameters to various operations. -/// -public class RetryOptions -{ - private readonly DurableTaskCore.RetryOptions retryOptions; - - // Would like to make this durability provider specific, but since this is a customer - // facing type, that is difficult. - private static readonly TimeSpan DefaultMaxRetryinterval = TimeSpan.FromDays(6); - - /// - /// Creates a new instance RetryOptions with the supplied first retry and max attempts. - /// - /// Timespan to wait for the first retry. - /// Max number of attempts to retry. - /// - /// The value must be greater than . - /// - public RetryOptions(TimeSpan firstRetryInterval, int maxNumberOfAttempts) - { - this.retryOptions = new DurableTaskCore.RetryOptions(firstRetryInterval, maxNumberOfAttempts); - this.MaxRetryInterval = DefaultMaxRetryinterval; - } - - /// - /// Gets or sets the first retry interval. - /// - /// - /// The TimeSpan to wait for the first retries. - /// - public TimeSpan FirstRetryInterval - { - get { return this.retryOptions.FirstRetryInterval; } - set { this.retryOptions.FirstRetryInterval = value; } - } - - /// - /// Gets or sets the max retry interval. - /// - /// - /// The TimeSpan of the max retry interval, defaults to . - /// - public TimeSpan MaxRetryInterval - { - get { return this.retryOptions.MaxRetryInterval; } - set { this.retryOptions.MaxRetryInterval = value; } - } - - /// - /// Gets or sets the backoff coefficient. - /// - /// - /// The backoff coefficient used to determine rate of increase of backoff. Defaults to 1. - /// - public double BackoffCoefficient - { - get { return this.retryOptions.BackoffCoefficient; } - set { this.retryOptions.BackoffCoefficient = value; } - } - - /// - /// Gets or sets the timeout for retries. - /// - /// - /// The TimeSpan timeout for retries, defaults to . - /// - public TimeSpan RetryTimeout - { - get { return this.retryOptions.RetryTimeout; } - set { this.retryOptions.RetryTimeout = value; } - } - - /// - /// Gets or sets the max number of attempts. - /// - /// - /// The maximum number of retry attempts. - /// - public int MaxNumberOfAttempts - { - get { return this.retryOptions.MaxNumberOfAttempts; } - set { this.retryOptions.MaxNumberOfAttempts = value; } - } - - /// - /// Gets or sets a delegate to call on exception to determine if retries should proceed. - /// - /// - /// The delegate to handle exception to determine if retries should proceed. - /// - public Func Handle - { - get { return this.retryOptions.Handle; } - set { this.retryOptions.Handle = value; } - } - - internal DurableTaskCore.RetryOptions GetRetryOptions() - { - return this.retryOptions; - } -} diff --git a/src/Worker.Extensions.DurableTask/TaskOrchestrationContextExtensionMethods.cs b/src/Worker.Extensions.DurableTask/TaskOrchestrationContextExtensionMethods.cs index 5a32b57f5..f7401fa46 100644 --- a/src/Worker.Extensions.DurableTask/TaskOrchestrationContextExtensionMethods.cs +++ b/src/Worker.Extensions.DurableTask/TaskOrchestrationContextExtensionMethods.cs @@ -1,12 +1,13 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. -using System.Text.Json; +using System; +using System.Net.Http; using System.Threading.Tasks; using Microsoft.Azure.Functions.Worker.Extensions.DurableTask; -using Microsoft.DurableTask; +using Microsoft.Azure.Functions.Worker.Extensions.DurableTask.Http; -namespace Microsoft.Azure.Functions.Worker; +namespace Microsoft.DurableTask; /// /// Extensions for . @@ -17,13 +18,33 @@ public static class TaskOrchestrationContextExtensionMethods /// Makes an HTTP call using the information in the DurableHttpRequest. /// /// The task orchestration context. - /// The DurableHttpRequest used to make the HTTP call. + /// The DurableHttpRequest used to make the HTTP call. /// DurableHttpResponse - public static async Task CallHttpAsync(this TaskOrchestrationContext context, DurableHttpRequest req) + public static async Task CallHttpAsync(this TaskOrchestrationContext context, DurableHttpRequest request) { - string responseString = await context.CallActivityAsync(Constants.HttpTaskActivityReservedName, req); + DurableHttpResponse response = await context.CallActivityAsync(Constants.HttpTaskActivityReservedName, request); - DurableHttpResponse? response = JsonSerializer.Deserialize(responseString); + return response; + } + + /// + /// Makes an HTTP call to the specified uri. + /// + /// The task orchestration context. + /// HttpMethod used for api call. + /// uri used to make the HTTP call. + /// Content passed in the HTTP request. + /// The retry option for the HTTP task. + /// A Result of the HTTP call. + public static async Task CallHttpAsync(this TaskOrchestrationContext context, HttpMethod method, Uri uri, string? content = null, HttpRetryOptions? retryOptions = null) + { + DurableHttpRequest request = new DurableHttpRequest( + method: method, + uri: uri, + content: content, + httpRetryOptions: retryOptions); + + DurableHttpResponse response = await context.CallActivityAsync(Constants.HttpTaskActivityReservedName, request); return response; } From 1ffe1519d138a2ec1adde6da43e4069b0501d666 Mon Sep 17 00:00:00 2001 From: Varshitha Bachu Date: Thu, 9 Nov 2023 17:45:19 -0800 Subject: [PATCH 6/9] updated namespaces and added HTTP folder --- src/WebJobs.Extensions.DurableTask/OutOfProcMiddleware.cs | 2 +- .../{ => HTTP}/DurableHttpRequest.cs | 0 .../{ => HTTP}/DurableHttpResponse.cs | 0 .../{ => HTTP}/HttpHeadersConverter.cs | 2 +- .../{ => HTTP}/HttpMethodConverter.cs | 2 +- .../{ => HTTP}/HttpRetryOptions.cs | 7 +++++-- .../Worker.Extensions.DurableTask.csproj | 3 +++ 7 files changed, 11 insertions(+), 5 deletions(-) rename src/Worker.Extensions.DurableTask/{ => HTTP}/DurableHttpRequest.cs (100%) rename src/Worker.Extensions.DurableTask/{ => HTTP}/DurableHttpResponse.cs (100%) rename src/Worker.Extensions.DurableTask/{ => HTTP}/HttpHeadersConverter.cs (97%) rename src/Worker.Extensions.DurableTask/{ => HTTP}/HttpMethodConverter.cs (99%) rename src/Worker.Extensions.DurableTask/{ => HTTP}/HttpRetryOptions.cs (90%) diff --git a/src/WebJobs.Extensions.DurableTask/OutOfProcMiddleware.cs b/src/WebJobs.Extensions.DurableTask/OutOfProcMiddleware.cs index a9992cc52..2f200c073 100644 --- a/src/WebJobs.Extensions.DurableTask/OutOfProcMiddleware.cs +++ b/src/WebJobs.Extensions.DurableTask/OutOfProcMiddleware.cs @@ -421,7 +421,7 @@ public async Task CallActivityAsync(DispatchMiddlewareContext dispatchContext, F await next(); return; } - + FunctionName functionName = new FunctionName(scheduledEvent.Name); OrchestrationInstance? instance = dispatchContext.GetProperty(); diff --git a/src/Worker.Extensions.DurableTask/DurableHttpRequest.cs b/src/Worker.Extensions.DurableTask/HTTP/DurableHttpRequest.cs similarity index 100% rename from src/Worker.Extensions.DurableTask/DurableHttpRequest.cs rename to src/Worker.Extensions.DurableTask/HTTP/DurableHttpRequest.cs diff --git a/src/Worker.Extensions.DurableTask/DurableHttpResponse.cs b/src/Worker.Extensions.DurableTask/HTTP/DurableHttpResponse.cs similarity index 100% rename from src/Worker.Extensions.DurableTask/DurableHttpResponse.cs rename to src/Worker.Extensions.DurableTask/HTTP/DurableHttpResponse.cs diff --git a/src/Worker.Extensions.DurableTask/HttpHeadersConverter.cs b/src/Worker.Extensions.DurableTask/HTTP/HttpHeadersConverter.cs similarity index 97% rename from src/Worker.Extensions.DurableTask/HttpHeadersConverter.cs rename to src/Worker.Extensions.DurableTask/HTTP/HttpHeadersConverter.cs index af9450e9c..b17d5ac70 100644 --- a/src/Worker.Extensions.DurableTask/HttpHeadersConverter.cs +++ b/src/Worker.Extensions.DurableTask/HTTP/HttpHeadersConverter.cs @@ -7,7 +7,7 @@ using System.Text.Json.Serialization; using Microsoft.Extensions.Primitives; -namespace Microsoft.Azure.Functions.Worker; +namespace Microsoft.Azure.Functions.Worker.Extensions.DurableTask.Http; // StringValues does not deserialize as you would expect, so we need a custom mechanism // for serializing HTTP header collections diff --git a/src/Worker.Extensions.DurableTask/HttpMethodConverter.cs b/src/Worker.Extensions.DurableTask/HTTP/HttpMethodConverter.cs similarity index 99% rename from src/Worker.Extensions.DurableTask/HttpMethodConverter.cs rename to src/Worker.Extensions.DurableTask/HTTP/HttpMethodConverter.cs index e44c764fe..3a75dd806 100644 --- a/src/Worker.Extensions.DurableTask/HttpMethodConverter.cs +++ b/src/Worker.Extensions.DurableTask/HTTP/HttpMethodConverter.cs @@ -6,7 +6,7 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace Microsoft.Azure.Functions.Worker.Extensions.DurableTask; +namespace Microsoft.Azure.Functions.Worker.Extensions.DurableTask.Http; internal class HttpMethodConverter : JsonConverter { diff --git a/src/Worker.Extensions.DurableTask/HttpRetryOptions.cs b/src/Worker.Extensions.DurableTask/HTTP/HttpRetryOptions.cs similarity index 90% rename from src/Worker.Extensions.DurableTask/HttpRetryOptions.cs rename to src/Worker.Extensions.DurableTask/HTTP/HttpRetryOptions.cs index f8da05c32..f4e6570ee 100644 --- a/src/Worker.Extensions.DurableTask/HttpRetryOptions.cs +++ b/src/Worker.Extensions.DurableTask/HTTP/HttpRetryOptions.cs @@ -19,21 +19,24 @@ public class HttpRetryOptions private TimeSpan firstRetryInterval; private int maxNumberOfAttempts; + private IList? statusCodesToRetry; /// /// Creates a new instance SerializableRetryOptions with the supplied first retry and max attempts. /// /// Timespan to wait for the first retry. /// Max number of attempts to retry. + /// List of status codes that specify when to retry. /// /// The value must be greater than . /// - public HttpRetryOptions(TimeSpan firstRetryInterval, int maxNumberOfAttempts) + public HttpRetryOptions(TimeSpan firstRetryInterval, int maxNumberOfAttempts, IList? statusCodesToRetry = null) { this.MaxRetryInterval = DefaultMaxRetryinterval; this.firstRetryInterval = firstRetryInterval; this.maxNumberOfAttempts = maxNumberOfAttempts; + this.statusCodesToRetry = statusCodesToRetry ?? new List(); } /// @@ -96,5 +99,5 @@ public int MaxNumberOfAttempts /// will be retried. /// [JsonPropertyName("StatusCodesToRetry")] - public IList StatusCodesToRetry { get; set; } = new List(); + public IList StatusCodesToRetry { get; } } diff --git a/src/Worker.Extensions.DurableTask/Worker.Extensions.DurableTask.csproj b/src/Worker.Extensions.DurableTask/Worker.Extensions.DurableTask.csproj index 3f63320c5..3bd09964e 100644 --- a/src/Worker.Extensions.DurableTask/Worker.Extensions.DurableTask.csproj +++ b/src/Worker.Extensions.DurableTask/Worker.Extensions.DurableTask.csproj @@ -54,5 +54,8 @@ content/SBOM + + + From de6a60bb16e62c54969d8051cd3cd55005a50808 Mon Sep 17 00:00:00 2001 From: Varshitha Bachu Date: Fri, 10 Nov 2023 09:00:09 -0800 Subject: [PATCH 7/9] removed unused method --- src/WebJobs.Extensions.DurableTask/OutOfProcMiddleware.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/WebJobs.Extensions.DurableTask/OutOfProcMiddleware.cs b/src/WebJobs.Extensions.DurableTask/OutOfProcMiddleware.cs index 2f200c073..169a89858 100644 --- a/src/WebJobs.Extensions.DurableTask/OutOfProcMiddleware.cs +++ b/src/WebJobs.Extensions.DurableTask/OutOfProcMiddleware.cs @@ -546,14 +546,6 @@ public async Task CallActivityAsync(DispatchMiddlewareContext dispatchContext, F dispatchContext.SetProperty(activityResult); } - private static DurableHttpRequest? ConvertDurableHttpRequest(string? inputString) - { - IList? input = JsonConvert.DeserializeObject>(inputString); - DurableHttpRequest? request = input?.First(); - - return request; - } - private static FailureDetails GetFailureDetails(Exception e) { if (e.InnerException != null && e.InnerException.Message.StartsWith("Result:")) From d0e6e997b2fb33cb47b10b4a0b67999d832751c4 Mon Sep 17 00:00:00 2001 From: Varshitha Bachu Date: Fri, 10 Nov 2023 14:20:39 -0800 Subject: [PATCH 8/9] addressed PR feedback --- .../HTTP/DurableHttpRequest.cs | 22 ++++-------- .../HTTP/DurableHttpResponse.cs | 12 ++----- .../HTTP/HttpHeadersConverter.cs | 5 --- .../HTTP/HttpMethodConverter.cs | 13 +------ .../HTTP/HttpRetryOptions.cs | 35 ++++--------------- ...askOrchestrationContextExtensionMethods.cs | 24 ++++++------- .../Worker.Extensions.DurableTask.csproj | 4 --- 7 files changed, 29 insertions(+), 86 deletions(-) diff --git a/src/Worker.Extensions.DurableTask/HTTP/DurableHttpRequest.cs b/src/Worker.Extensions.DurableTask/HTTP/DurableHttpRequest.cs index 842495b0f..14628c185 100644 --- a/src/Worker.Extensions.DurableTask/HTTP/DurableHttpRequest.cs +++ b/src/Worker.Extensions.DurableTask/HTTP/DurableHttpRequest.cs @@ -19,20 +19,10 @@ public class DurableHttpRequest /// public DurableHttpRequest( HttpMethod method, - Uri uri, - IDictionary? headers = null, - string? content = null, - bool asynchronousPatternEnabled = true, - TimeSpan? timeout = null, - HttpRetryOptions? httpRetryOptions = null) + Uri uri) { this.Method = method; this.Uri = uri; - this.Headers = headers; - this.Content = content; - this.AsynchronousPatternEnabled = asynchronousPatternEnabled; - this.Timeout = timeout; - this.HttpRetryOptions = httpRetryOptions; } /// @@ -53,32 +43,32 @@ public DurableHttpRequest( /// [JsonPropertyName("headers")] [JsonConverter(typeof(HttpHeadersConverter))] - public IDictionary? Headers { get; } + public IDictionary? Headers { get; set; } /// /// Content passed with the HTTP request made by the Durable Function. /// [JsonPropertyName("content")] - public string? Content { get; } + public string? Content { get; set; } /// /// Specifies whether the Durable HTTP APIs should automatically /// handle the asynchronous HTTP pattern. /// [JsonPropertyName("asynchronousPatternEnabled")] - public bool AsynchronousPatternEnabled { get; } + public bool AsynchronousPatternEnabled { get; set; } /// /// Defines retry policy for handling of failures in making the HTTP Request. These could be non-successful HTTP status codes /// in the response, a timeout in making the HTTP call, or an exception raised from the HTTP Client library. /// [JsonPropertyName("retryOptions")] - public HttpRetryOptions? HttpRetryOptions { get; } + public HttpRetryOptions? HttpRetryOptions { get; set; } /// /// The total timeout for the original HTTP request and any /// asynchronous polling. /// [JsonPropertyName("timeout")] - public TimeSpan? Timeout { get; } + public TimeSpan? Timeout { get; set; } } \ No newline at end of file diff --git a/src/Worker.Extensions.DurableTask/HTTP/DurableHttpResponse.cs b/src/Worker.Extensions.DurableTask/HTTP/DurableHttpResponse.cs index 5a1a6b0cd..b959cd90a 100644 --- a/src/Worker.Extensions.DurableTask/HTTP/DurableHttpResponse.cs +++ b/src/Worker.Extensions.DurableTask/HTTP/DurableHttpResponse.cs @@ -17,16 +17,10 @@ public class DurableHttpResponse /// Initializes a new instance of the class. /// /// HTTP Status code returned from the HTTP call. - /// Headers returned from the HTTP call. - /// Content returned from the HTTP call. public DurableHttpResponse( - HttpStatusCode statusCode, - IDictionary? headers = null, - string? content = null) + HttpStatusCode statusCode) { this.StatusCode = statusCode; - this.Headers = headers; - this.Content = content; } /// @@ -40,11 +34,11 @@ public DurableHttpResponse( /// [JsonPropertyName("headers")] [JsonConverter(typeof(HttpHeadersConverter))] - public IDictionary? Headers { get; } + public IDictionary? Headers { get; init; } /// /// Content returned from an HTTP request. /// [JsonPropertyName("content")] - public string? Content { get; } + public string? Content { get; init; } } \ No newline at end of file diff --git a/src/Worker.Extensions.DurableTask/HTTP/HttpHeadersConverter.cs b/src/Worker.Extensions.DurableTask/HTTP/HttpHeadersConverter.cs index b17d5ac70..d41acef5e 100644 --- a/src/Worker.Extensions.DurableTask/HTTP/HttpHeadersConverter.cs +++ b/src/Worker.Extensions.DurableTask/HTTP/HttpHeadersConverter.cs @@ -13,11 +13,6 @@ namespace Microsoft.Azure.Functions.Worker.Extensions.DurableTask.Http; // for serializing HTTP header collections internal class HttpHeadersConverter : JsonConverter> { - public override bool CanConvert(Type objectType) - { - return typeof(IDictionary).IsAssignableFrom(objectType); - } - public override IDictionary Read( ref Utf8JsonReader reader, Type objectType, diff --git a/src/Worker.Extensions.DurableTask/HTTP/HttpMethodConverter.cs b/src/Worker.Extensions.DurableTask/HTTP/HttpMethodConverter.cs index 3a75dd806..540f1f981 100644 --- a/src/Worker.Extensions.DurableTask/HTTP/HttpMethodConverter.cs +++ b/src/Worker.Extensions.DurableTask/HTTP/HttpMethodConverter.cs @@ -20,18 +20,7 @@ public override HttpMethod Read( Type objectType, JsonSerializerOptions options) { - HttpMethod method = HttpMethod.Get; - - if (reader.TokenType != JsonTokenType.StartObject) - { - return method; - } - - reader.Read(); - - method = new HttpMethod(reader.GetString()); - - return method; + return new HttpMethod(reader.GetString()); } public override void Write( diff --git a/src/Worker.Extensions.DurableTask/HTTP/HttpRetryOptions.cs b/src/Worker.Extensions.DurableTask/HTTP/HttpRetryOptions.cs index f4e6570ee..77ba82923 100644 --- a/src/Worker.Extensions.DurableTask/HTTP/HttpRetryOptions.cs +++ b/src/Worker.Extensions.DurableTask/HTTP/HttpRetryOptions.cs @@ -17,26 +17,13 @@ public class HttpRetryOptions // facing type, that is difficult. private static readonly TimeSpan DefaultMaxRetryinterval = TimeSpan.FromDays(6); - private TimeSpan firstRetryInterval; - private int maxNumberOfAttempts; - private IList? statusCodesToRetry; - /// /// Creates a new instance SerializableRetryOptions with the supplied first retry and max attempts. /// - /// Timespan to wait for the first retry. - /// Max number of attempts to retry. - /// List of status codes that specify when to retry. - /// - /// The value must be greater than . /// - public HttpRetryOptions(TimeSpan firstRetryInterval, int maxNumberOfAttempts, IList? statusCodesToRetry = null) + public HttpRetryOptions(IList? statusCodesToRetry = null) { - this.MaxRetryInterval = DefaultMaxRetryinterval; - - this.firstRetryInterval = firstRetryInterval; - this.maxNumberOfAttempts = maxNumberOfAttempts; - this.statusCodesToRetry = statusCodesToRetry ?? new List(); + this.StatusCodesToRetry = statusCodesToRetry ?? new List(); } /// @@ -46,11 +33,7 @@ public HttpRetryOptions(TimeSpan firstRetryInterval, int maxNumberOfAttempts, IL /// The TimeSpan to wait for the first retries. /// [JsonPropertyName("FirstRetryInterval")] - public TimeSpan FirstRetryInterval - { - get { return this.firstRetryInterval; } - set { this.firstRetryInterval = value; } - } + public TimeSpan FirstRetryInterval { get; set; } /// /// Gets or sets the max retry interval. @@ -59,7 +42,7 @@ public TimeSpan FirstRetryInterval /// The TimeSpan of the max retry interval, defaults to 6 days. /// [JsonPropertyName("MaxRetryInterval")] - public TimeSpan MaxRetryInterval { get; set; } + public TimeSpan MaxRetryInterval { get; set; } = DefaultMaxRetryinterval; /// /// Gets or sets the backoff coefficient. @@ -68,7 +51,7 @@ public TimeSpan FirstRetryInterval /// The backoff coefficient used to determine rate of increase of backoff. Defaults to 1. /// [JsonPropertyName("BackoffCoefficient")] - public double BackoffCoefficient { get; set; } + public double BackoffCoefficient { get; set; } = 1; /// /// Gets or sets the timeout for retries. @@ -77,7 +60,7 @@ public TimeSpan FirstRetryInterval /// The TimeSpan timeout for retries, defaults to . /// [JsonPropertyName("RetryTimeout")] - public TimeSpan RetryTimeout { get; set; } + public TimeSpan RetryTimeout { get; set; } = TimeSpan.MaxValue; /// /// Gets or sets the max number of attempts. @@ -86,11 +69,7 @@ public TimeSpan FirstRetryInterval /// The maximum number of retry attempts. /// [JsonPropertyName("MaxNumberOfAttempts")] - public int MaxNumberOfAttempts - { - get { return this.maxNumberOfAttempts; } - set { this.maxNumberOfAttempts = value; } - } + public int MaxNumberOfAttempts { get; set; } /// /// Gets or sets the list of status codes upon which the diff --git a/src/Worker.Extensions.DurableTask/TaskOrchestrationContextExtensionMethods.cs b/src/Worker.Extensions.DurableTask/TaskOrchestrationContextExtensionMethods.cs index f7401fa46..aff730038 100644 --- a/src/Worker.Extensions.DurableTask/TaskOrchestrationContextExtensionMethods.cs +++ b/src/Worker.Extensions.DurableTask/TaskOrchestrationContextExtensionMethods.cs @@ -20,11 +20,14 @@ public static class TaskOrchestrationContextExtensionMethods /// The task orchestration context. /// The DurableHttpRequest used to make the HTTP call. /// DurableHttpResponse - public static async Task CallHttpAsync(this TaskOrchestrationContext context, DurableHttpRequest request) + public static Task CallHttpAsync(this TaskOrchestrationContext context, DurableHttpRequest request) { - DurableHttpResponse response = await context.CallActivityAsync(Constants.HttpTaskActivityReservedName, request); - - return response; + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return context.CallActivityAsync(Constants.HttpTaskActivityReservedName, request); } /// @@ -36,16 +39,13 @@ public static async Task CallHttpAsync(this TaskOrchestrati /// Content passed in the HTTP request. /// The retry option for the HTTP task. /// A Result of the HTTP call. - public static async Task CallHttpAsync(this TaskOrchestrationContext context, HttpMethod method, Uri uri, string? content = null, HttpRetryOptions? retryOptions = null) + public static Task CallHttpAsync(this TaskOrchestrationContext context, HttpMethod method, Uri uri, string? content = null, HttpRetryOptions? retryOptions = null) { - DurableHttpRequest request = new DurableHttpRequest( - method: method, - uri: uri, - content: content, - httpRetryOptions: retryOptions); + DurableHttpRequest request = new DurableHttpRequest(method, uri); - DurableHttpResponse response = await context.CallActivityAsync(Constants.HttpTaskActivityReservedName, request); + request.Content = content; + request.HttpRetryOptions = retryOptions; - return response; + return context.CallHttpAsync(request); } } diff --git a/src/Worker.Extensions.DurableTask/Worker.Extensions.DurableTask.csproj b/src/Worker.Extensions.DurableTask/Worker.Extensions.DurableTask.csproj index 3bd09964e..c7b7d1918 100644 --- a/src/Worker.Extensions.DurableTask/Worker.Extensions.DurableTask.csproj +++ b/src/Worker.Extensions.DurableTask/Worker.Extensions.DurableTask.csproj @@ -54,8 +54,4 @@ content/SBOM - - - - From 9870119961aace834075096047a44693df569d48 Mon Sep 17 00:00:00 2001 From: Varshitha Bachu Date: Mon, 13 Nov 2023 09:58:17 -0800 Subject: [PATCH 9/9] minor changes --- .../HTTP/DurableHttpRequest.cs | 4 +--- .../HTTP/DurableHttpResponse.cs | 3 +-- .../HTTP/HttpRetryOptions.cs | 1 - .../TaskOrchestrationContextExtensionMethods.cs | 9 +++++---- 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/Worker.Extensions.DurableTask/HTTP/DurableHttpRequest.cs b/src/Worker.Extensions.DurableTask/HTTP/DurableHttpRequest.cs index 14628c185..333c74cfd 100644 --- a/src/Worker.Extensions.DurableTask/HTTP/DurableHttpRequest.cs +++ b/src/Worker.Extensions.DurableTask/HTTP/DurableHttpRequest.cs @@ -17,9 +17,7 @@ public class DurableHttpRequest /// /// Initializes a new instance of the class. /// - public DurableHttpRequest( - HttpMethod method, - Uri uri) + public DurableHttpRequest(HttpMethod method, Uri uri) { this.Method = method; this.Uri = uri; diff --git a/src/Worker.Extensions.DurableTask/HTTP/DurableHttpResponse.cs b/src/Worker.Extensions.DurableTask/HTTP/DurableHttpResponse.cs index b959cd90a..06875d9e4 100644 --- a/src/Worker.Extensions.DurableTask/HTTP/DurableHttpResponse.cs +++ b/src/Worker.Extensions.DurableTask/HTTP/DurableHttpResponse.cs @@ -17,8 +17,7 @@ public class DurableHttpResponse /// Initializes a new instance of the class. /// /// HTTP Status code returned from the HTTP call. - public DurableHttpResponse( - HttpStatusCode statusCode) + public DurableHttpResponse(HttpStatusCode statusCode) { this.StatusCode = statusCode; } diff --git a/src/Worker.Extensions.DurableTask/HTTP/HttpRetryOptions.cs b/src/Worker.Extensions.DurableTask/HTTP/HttpRetryOptions.cs index 77ba82923..959f7e047 100644 --- a/src/Worker.Extensions.DurableTask/HTTP/HttpRetryOptions.cs +++ b/src/Worker.Extensions.DurableTask/HTTP/HttpRetryOptions.cs @@ -20,7 +20,6 @@ public class HttpRetryOptions /// /// Creates a new instance SerializableRetryOptions with the supplied first retry and max attempts. /// - /// public HttpRetryOptions(IList? statusCodesToRetry = null) { this.StatusCodesToRetry = statusCodesToRetry ?? new List(); diff --git a/src/Worker.Extensions.DurableTask/TaskOrchestrationContextExtensionMethods.cs b/src/Worker.Extensions.DurableTask/TaskOrchestrationContextExtensionMethods.cs index aff730038..c5523d002 100644 --- a/src/Worker.Extensions.DurableTask/TaskOrchestrationContextExtensionMethods.cs +++ b/src/Worker.Extensions.DurableTask/TaskOrchestrationContextExtensionMethods.cs @@ -41,10 +41,11 @@ public static Task CallHttpAsync(this TaskOrchestrationCont /// A Result of the HTTP call. public static Task CallHttpAsync(this TaskOrchestrationContext context, HttpMethod method, Uri uri, string? content = null, HttpRetryOptions? retryOptions = null) { - DurableHttpRequest request = new DurableHttpRequest(method, uri); - - request.Content = content; - request.HttpRetryOptions = retryOptions; + DurableHttpRequest request = new DurableHttpRequest(method, uri) + { + Content = content, + HttpRetryOptions = retryOptions, + }; return context.CallHttpAsync(request); }