Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HTTP APIs for .NET Isolated Parity #2653

Merged
merged 10 commits into from
Nov 13, 2023
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/WebJobs.Extensions.DurableTask/OutOfProcMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,12 @@ public async Task CallActivityAsync(DispatchMiddlewareContext dispatchContext, F
throw new InvalidOperationException($"An activity was scheduled but no {nameof(TaskScheduledEvent)} was found!");
}

if (scheduledEvent.Name?.StartsWith("BuiltIn::", StringComparison.OrdinalIgnoreCase) ?? false)
{
await next();
return;
}

FunctionName functionName = new FunctionName(scheduledEvent.Name);

OrchestrationInstance? instance = dispatchContext.GetProperty<OrchestrationInstance>();
Expand Down
2 changes: 2 additions & 0 deletions src/Worker.Extensions.DurableTask/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
84 changes: 84 additions & 0 deletions src/Worker.Extensions.DurableTask/HTTP/DurableHttpRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// 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.Extensions.DurableTask.Http;

/// <summary>
/// Request used to make an HTTP call through Durable Functions.
/// </summary>
public class DurableHttpRequest
{
/// <summary>
/// Initializes a new instance of the <see cref="DurableHttpRequest"/> class.
/// </summary>
public DurableHttpRequest(
bachuv marked this conversation as resolved.
Show resolved Hide resolved
HttpMethod method,
Uri uri,
IDictionary<string, StringValues>? headers = null,
string? content = null,
bool asynchronousPatternEnabled = true,
TimeSpan? timeout = null,
HttpRetryOptions? httpRetryOptions = null)
{
this.Method = method;
this.Uri = uri;
this.Headers = headers;
this.Content = content;
this.AsynchronousPatternEnabled = asynchronousPatternEnabled;
this.Timeout = timeout;
this.HttpRetryOptions = httpRetryOptions;
}

/// <summary>
/// HttpMethod used in the HTTP request made by the Durable Function.
/// </summary>
[JsonPropertyName("method")]
[JsonConverter(typeof(HttpMethodConverter))]
public HttpMethod Method { get; }

/// <summary>
/// Uri used in the HTTP request made by the Durable Function.
/// </summary>
[JsonPropertyName("uri")]
public Uri Uri { get; }

/// <summary>
/// Headers passed with the HTTP request made by the Durable Function.
/// </summary>
[JsonPropertyName("headers")]
[JsonConverter(typeof(HttpHeadersConverter))]
public IDictionary<string, StringValues>? Headers { get; }

/// <summary>
/// Content passed with the HTTP request made by the Durable Function.
/// </summary>
[JsonPropertyName("content")]
public string? Content { get; }
bachuv marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Specifies whether the Durable HTTP APIs should automatically
/// handle the asynchronous HTTP pattern.
/// </summary>
[JsonPropertyName("asynchronousPatternEnabled")]
public bool AsynchronousPatternEnabled { get; }

/// <summary>
/// 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.
/// </summary>
[JsonPropertyName("retryOptions")]
public HttpRetryOptions? HttpRetryOptions { get; }

/// <summary>
/// The total timeout for the original HTTP request and any
/// asynchronous polling.
/// </summary>
[JsonPropertyName("timeout")]
public TimeSpan? Timeout { get; }
}
50 changes: 50 additions & 0 deletions src/Worker.Extensions.DurableTask/HTTP/DurableHttpResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System.Collections.Generic;
using System.Net;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Primitives;

namespace Microsoft.Azure.Functions.Worker.Extensions.DurableTask.Http;

/// <summary>
/// Response received from the HTTP request made by the Durable Function.
/// </summary>
public class DurableHttpResponse
{
/// <summary>
/// Initializes a new instance of the <see cref="DurableHttpResponse"/> class.
/// </summary>
/// <param name="statusCode">HTTP Status code returned from the HTTP call.</param>
/// <param name="headers">Headers returned from the HTTP call.</param>
/// <param name="content">Content returned from the HTTP call.</param>
public DurableHttpResponse(
bachuv marked this conversation as resolved.
Show resolved Hide resolved
HttpStatusCode statusCode,
IDictionary<string, StringValues>? headers = null,
string? content = null)
{
this.StatusCode = statusCode;
this.Headers = headers;
this.Content = content;
}

/// <summary>
/// Status code returned from an HTTP request.
/// </summary>
[JsonPropertyName("statusCode")]
public HttpStatusCode StatusCode { get; }

/// <summary>
/// Headers in the response from an HTTP request.
/// </summary>
[JsonPropertyName("headers")]
[JsonConverter(typeof(HttpHeadersConverter))]
public IDictionary<string, StringValues>? Headers { get; }

/// <summary>
/// Content returned from an HTTP request.
/// </summary>
[JsonPropertyName("content")]
public string? Content { get; }
bachuv marked this conversation as resolved.
Show resolved Hide resolved
}
89 changes: 89 additions & 0 deletions src/Worker.Extensions.DurableTask/HTTP/HttpHeadersConverter.cs
Original file line number Diff line number Diff line change
@@ -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.Extensions.DurableTask.Http;

// StringValues does not deserialize as you would expect, so we need a custom mechanism
// for serializing HTTP header collections
internal class HttpHeadersConverter : JsonConverter<IDictionary<string, StringValues>>
{
public override bool CanConvert(Type objectType)
bachuv marked this conversation as resolved.
Show resolved Hide resolved
{
return typeof(IDictionary<string, StringValues>).IsAssignableFrom(objectType);
}

public override IDictionary<string, StringValues> Read(
ref Utf8JsonReader reader,
Type objectType,
JsonSerializerOptions options)
{
var headers = new Dictionary<string, StringValues>(StringComparer.OrdinalIgnoreCase);

if (reader.TokenType != JsonTokenType.StartObject)
{
return headers;
}

var valueList = new List<string>();
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<string, StringValues> value,
JsonSerializerOptions options)
{
writer.WriteStartObject();

var headers = (IDictionary<string, StringValues>)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();
}
}
44 changes: 44 additions & 0 deletions src/Worker.Extensions.DurableTask/HTTP/HttpMethodConverter.cs
Original file line number Diff line number Diff line change
@@ -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.Http;

internal class HttpMethodConverter : JsonConverter<HttpMethod>
{
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)
bachuv marked this conversation as resolved.
Show resolved Hide resolved
{
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());
}
}
103 changes: 103 additions & 0 deletions src/Worker.Extensions.DurableTask/HTTP/HttpRetryOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// 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;

namespace Microsoft.Azure.Functions.Worker.Extensions.DurableTask.Http;

/// <summary>
/// Defines retry policies that can be passed as parameters to various operations.
/// </summary>
public class HttpRetryOptions
{
// 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;
private IList<HttpStatusCode>? statusCodesToRetry;

/// <summary>
/// Creates a new instance SerializableRetryOptions with the supplied first retry and max attempts.
/// </summary>
/// <param name="firstRetryInterval">Timespan to wait for the first retry.</param>
/// <param name="maxNumberOfAttempts">Max number of attempts to retry.</param>
/// <param name="statusCodesToRetry">List of status codes that specify when to retry.</param>
/// <exception cref="ArgumentException">
/// The <paramref name="firstRetryInterval"/> value must be greater than <see cref="TimeSpan.Zero"/>.
/// </exception>
public HttpRetryOptions(TimeSpan firstRetryInterval, int maxNumberOfAttempts, IList<HttpStatusCode>? statusCodesToRetry = null)
{
this.MaxRetryInterval = DefaultMaxRetryinterval;

this.firstRetryInterval = firstRetryInterval;
this.maxNumberOfAttempts = maxNumberOfAttempts;
this.statusCodesToRetry = statusCodesToRetry ?? new List<HttpStatusCode>();
bachuv marked this conversation as resolved.
Show resolved Hide resolved
}

/// <summary>
/// Gets or sets the first retry interval.
/// </summary>
/// <value>
/// The TimeSpan to wait for the first retries.
/// </value>
[JsonPropertyName("FirstRetryInterval")]
public TimeSpan FirstRetryInterval
{
get { return this.firstRetryInterval; }
bachuv marked this conversation as resolved.
Show resolved Hide resolved
set { this.firstRetryInterval = value; }
}

/// <summary>
/// Gets or sets the max retry interval.
/// </summary>
/// <value>
/// The TimeSpan of the max retry interval, defaults to 6 days.
/// </value>
[JsonPropertyName("MaxRetryInterval")]
public TimeSpan MaxRetryInterval { get; set; }
bachuv marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Gets or sets the backoff coefficient.
/// </summary>
/// <value>
/// The backoff coefficient used to determine rate of increase of backoff. Defaults to 1.
/// </value>
[JsonPropertyName("BackoffCoefficient")]
public double BackoffCoefficient { get; set; }
bachuv marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Gets or sets the timeout for retries.
/// </summary>
/// <value>
/// The TimeSpan timeout for retries, defaults to <see cref="TimeSpan.MaxValue"/>.
/// </value>
[JsonPropertyName("RetryTimeout")]
public TimeSpan RetryTimeout { get; set; }
bachuv marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Gets or sets the max number of attempts.
/// </summary>
/// <value>
/// The maximum number of retry attempts.
/// </value>
[JsonPropertyName("MaxNumberOfAttempts")]
public int MaxNumberOfAttempts
bachuv marked this conversation as resolved.
Show resolved Hide resolved
{
get { return this.maxNumberOfAttempts; }
set { this.maxNumberOfAttempts = value; }
}

/// <summary>
/// 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.
/// </summary>
[JsonPropertyName("StatusCodesToRetry")]
public IList<HttpStatusCode> StatusCodesToRetry { get; }
}
Loading
Loading