Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -38,25 +38,37 @@ internal class DurableClient : IDurableClient,
private readonly DurableTaskExtension config;
private readonly DurableClientAttribute attribute; // for rehydrating a Client after a webhook
private readonly MessagePayloadDataConverter messageDataConverter;
private readonly DurableTaskOptions durableTaskOptions;

internal DurableClient(
DurabilityProvider serviceClient,
DurableTaskExtension config,
HttpApiHandler httpHandler,
DurableClientAttribute attribute)
DurableClientAttribute attribute,
MessagePayloadDataConverter messageDataConverter,
EndToEndTraceHelper traceHelper,
DurableTaskOptions durableTaskOptions)
{
this.config = config ?? throw new ArgumentNullException(nameof(config));

this.messageDataConverter = config.MessageDataConverter;
this.messageDataConverter = messageDataConverter;

this.client = new TaskHubClient(serviceClient, this.messageDataConverter);
this.durabilityProvider = serviceClient;
this.traceHelper = config.TraceHelper;
this.traceHelper = traceHelper;
this.httpApiHandler = httpHandler;
this.hubName = attribute.TaskHub ?? config.Options.HubName;
this.durableTaskOptions = durableTaskOptions;
this.hubName = attribute.TaskHub ?? this.durableTaskOptions.HubName;
this.attribute = attribute;
}

internal DurableClient(
DurabilityProvider serviceClient,
DurableTaskExtension config,
HttpApiHandler httpHandler,
DurableClientAttribute attribute)
: this(serviceClient, httpHandler, attribute, config.MessageDataConverter, config.TraceHelper, config.Options)
{
this.config = config;
}

public string TaskHubName => this.hubName;

internal DurabilityProvider DurabilityProvider => this.durabilityProvider;
Expand Down Expand Up @@ -110,7 +122,7 @@ async Task<IActionResult> IDurableOrchestrationClient.WaitForCompletionOrCreateC
/// <inheritdoc />
async Task<string> IDurableOrchestrationClient.StartNewAsync<T>(string orchestratorFunctionName, string instanceId, T input)
{
if (this.ClientReferencesCurrentApp(this))
if (!this.attribute.ExternalClient && this.ClientReferencesCurrentApp(this))
{
this.config.ThrowIfFunctionDoesNotExist(orchestratorFunctionName, FunctionType.Orchestrator);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cgillum, is there any reason for us to have this check at all? A DurableClient can be created with a different task hub already, and there is no guarantee that the app that is calling this DurableClient will have the same functions as the app with the TaskHub the client is referencing.

}
Expand Down Expand Up @@ -151,7 +163,7 @@ async Task<string> IDurableOrchestrationClient.StartNewAsync<T>(string orchestra

private OrchestrationStatus[] GetStatusesNotToOverride()
{
var overridableStates = this.config.Options.OverridableExistingInstanceStates;
var overridableStates = this.durableTaskOptions.OverridableExistingInstanceStates;
if (overridableStates == OverridableStates.NonRunningStates)
{
return new OrchestrationStatus[]
Expand Down Expand Up @@ -317,7 +329,7 @@ private bool ClientReferencesCurrentApp(DurableClient client)

private bool TaskHubMatchesCurrentApp(DurableClient client)
{
var taskHubName = this.config.Options.HubName;
var taskHubName = this.durableTaskOptions.HubName;
return client.TaskHubName.Equals(taskHubName);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// 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.Concurrent;
using Microsoft.Azure.WebJobs.Extensions.DurableTask.Options;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;

namespace Microsoft.Azure.WebJobs.Extensions.DurableTask.ContextImplementations
{
/// <summary>
/// Factory class to create Durable Client to start works outside an azure function context.
/// </summary>
public class DurableClientFactory : IDurableClientFactory, IDisposable
{
// Creating client objects is expensive, so we cache them when the attributes match.
// Note that DurableClientAttribute defines a custom equality comparer.
private readonly ConcurrentDictionary<DurableClientAttribute, DurableClient> cachedClients =
new ConcurrentDictionary<DurableClientAttribute, DurableClient>();

private readonly ConcurrentDictionary<DurableClientAttribute, HttpApiHandler> cachedHttpListeners =
new ConcurrentDictionary<DurableClientAttribute, HttpApiHandler>();

private readonly DurableClientOptions defaultDurableClientOptions;
private readonly DurableTaskOptions durableTaskOptions;
private readonly IDurabilityProviderFactory durabilityProviderFactory;
private readonly ILogger logger;

/// <summary>
/// Initializes a new instance of the <see cref="DurableClientFactory"/> class.
/// </summary>
/// <param name="defaultDurableClientOptions">Default Options to Build Durable Clients.</param>
/// <param name="orchestrationServiceFactory">The factory used to create orchestration service based on the configured storage provider.</param>
/// <param name="loggerFactory">The logger factory used for extension-specific logging and orchestration tracking.</param>
/// <param name="durableTaskOptions">The configuration options for this extension.</param>
/// <param name="messageSerializerSettingsFactory">The factory used to create <see cref="JsonSerializerSettings"/> for message settings.</param>
public DurableClientFactory(
IOptions<DurableClientOptions> defaultDurableClientOptions,
IOptions<DurableTaskOptions> durableTaskOptions,
IDurabilityProviderFactory orchestrationServiceFactory,
ILoggerFactory loggerFactory,
IMessageSerializerSettingsFactory messageSerializerSettingsFactory = null)
{
this.logger = loggerFactory.CreateLogger(DurableTaskExtension.LoggerCategoryName);

this.durabilityProviderFactory = orchestrationServiceFactory;
this.defaultDurableClientOptions = defaultDurableClientOptions.Value;
this.durableTaskOptions = durableTaskOptions?.Value ?? new DurableTaskOptions();

this.MessageDataConverter = DurableTaskExtension.CreateMessageDataConverter(messageSerializerSettingsFactory);
this.TraceHelper = new EndToEndTraceHelper(this.logger, this.durableTaskOptions.Tracing.TraceReplayEvents);
}

internal MessagePayloadDataConverter MessageDataConverter { get; private set; }

internal EndToEndTraceHelper TraceHelper { get; private set; }

/// <summary>
/// Gets a <see cref="IDurableClient"/> using configuration from a <see cref="DurableClientOptions"/> instance.
/// </summary>
/// <param name="durableClientOptions">options containing the client configuration parameters.</param>
/// <returns>Returns a <see cref="IDurableClient"/> instance. The returned instance may be a cached instance.</returns>
public IDurableClient CreateClient(DurableClientOptions durableClientOptions)
Copy link
Contributor Author

@davidrevoledo davidrevoledo Dec 26, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It also could be CreateExternalClient and remove the Boolean property and set always true in the attribute to avoid this overhead, I thought it also can be used inside an azure function somewhere not only from external applications.

{
if (durableClientOptions == null)
{
throw new ArgumentException("Please configure 'DurableClientOptions'");
}

if (string.IsNullOrWhiteSpace(durableClientOptions.TaskHub))
{
throw new ArgumentException("Please provide value for 'TaskHub'");
}

DurableClientAttribute attribute = new DurableClientAttribute(durableClientOptions);

HttpApiHandler httpApiHandler = this.cachedHttpListeners.GetOrAdd(
attribute,
attr =>
{
return new HttpApiHandler(null, null, this.durableTaskOptions, this.logger);
});

DurableClient client = this.cachedClients.GetOrAdd(
attribute,
attr =>
{
DurabilityProvider innerClient = this.durabilityProviderFactory.GetDurabilityProvider(attribute);
return new DurableClient(innerClient, httpApiHandler, attribute, this.MessageDataConverter, this.TraceHelper, this.durableTaskOptions);
});

return client;
}

/// <summary>
/// Gets a <see cref="IDurableClient"/> using configuration from a <see cref="DurableClientOptions"/> instance.
/// </summary>
/// <returns>Returns a <see cref="IDurableClient"/> instance. The returned instance may be a cached instance.</returns>
public IDurableClient CreateClient()
{
return this.CreateClient(this.defaultDurableClientOptions);
}

/// <inheritdoc />
public void Dispose()
{
foreach (var cachedHttpListener in this.cachedHttpListeners)
{
cachedHttpListener.Value?.Dispose();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

using Microsoft.Azure.WebJobs.Extensions.DurableTask.Options;

namespace Microsoft.Azure.WebJobs.Extensions.DurableTask.ContextImplementations
{
/// <summary>
/// Factory class to create Durable Client to start works outside an azure function context.
/// </summary>
public interface IDurableClientFactory
{
/// <summary>
/// Gets a <see cref="IDurableClient"/> using configuration from a <see cref="DurableClientOptions"/> instance.
/// </summary>
/// <param name="durableClientOptions">options containing the client configuration parameters.</param>
/// <returns>Returns a <see cref="IDurableClient"/> instance. The returned instance may be a cached instance.</returns>
IDurableClient CreateClient(DurableClientOptions durableClientOptions);

/// <summary>
/// Gets a <see cref="IDurableClient"/> using configuration from a <see cref="DurableClientOptions"/> instance.
/// </summary>
/// <returns>Returns a <see cref="IDurableClient"/> instance. The returned instance may be a cached instance.</returns>
IDurableClient CreateClient();
}
}
22 changes: 22 additions & 0 deletions src/WebJobs.Extensions.DurableTask/DurableClientAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Diagnostics;
using Microsoft.Azure.WebJobs.Description;
using Microsoft.Azure.WebJobs.Extensions.DurableTask.Options;

namespace Microsoft.Azure.WebJobs.Extensions.DurableTask
{
Expand All @@ -15,6 +16,22 @@ namespace Microsoft.Azure.WebJobs.Extensions.DurableTask
[Binding]
public class DurableClientAttribute : Attribute, IEquatable<DurableClientAttribute>
{
/// <summary>
/// Initializes a new instance of the <see cref="DurableClientAttribute"/> class.
/// </summary>
public DurableClientAttribute() { }

/// <summary>
/// Initializes a new instance of the <see cref="DurableClientAttribute"/> class.
/// </summary>
/// <param name="durableClientOptions">durable client options</param>
public DurableClientAttribute(DurableClientOptions durableClientOptions)
{
this.TaskHub = durableClientOptions.TaskHub;
this.ConnectionName = durableClientOptions.ConnectionName;
this.ExternalClient = durableClientOptions.IsExternalClient;
}

/// <summary>
/// Optional. Gets or sets the name of the task hub in which the orchestration data lives.
/// </summary>
Expand All @@ -39,6 +56,11 @@ public class DurableClientAttribute : Attribute, IEquatable<DurableClientAttribu
/// </remarks>
public string ConnectionName { get; set; }

/// <summary>
/// Indicate if the client is External from the azure function where orchestrator functions are hosted.
/// </summary>
public bool ExternalClient { get; set; }

/// <summary>
/// Returns a hash code for this attribute.
/// </summary>
Expand Down
6 changes: 3 additions & 3 deletions src/WebJobs.Extensions.DurableTask/DurableTaskExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public class DurableTaskExtension :
INameVersionObjectManager<TaskOrchestration>,
INameVersionObjectManager<TaskActivity>
{
private static readonly string LoggerCategoryName = LogCategories.CreateTriggerCategory("DurableTask");
internal static readonly string LoggerCategoryName = LogCategories.CreateTriggerCategory("DurableTask");

// Creating client objects is expensive, so we cache them when the attributes match.
// Note that DurableClientAttribute defines a custom equality comparer.
Expand Down Expand Up @@ -132,7 +132,7 @@ public DurableTaskExtension(
DurableHttpClientFactory durableHttpClientFactory = new DurableHttpClientFactory();
this.durableHttpClient = durableHttpClientFactory.GetClient(durableHttpMessageHandlerFactory);

this.MessageDataConverter = this.CreateMessageDataConverter(messageSerializerSettingsFactory);
this.MessageDataConverter = CreateMessageDataConverter(messageSerializerSettingsFactory);
this.ErrorDataConverter = this.CreateErrorDataConverter(errorSerializerSettingsFactory);

this.HttpApiHandler = new HttpApiHandler(this, logger);
Expand Down Expand Up @@ -188,7 +188,7 @@ public string HubName

internal MessagePayloadDataConverter ErrorDataConverter { get; private set; }

private MessagePayloadDataConverter CreateMessageDataConverter(IMessageSerializerSettingsFactory messageSerializerSettingsFactory)
internal static MessagePayloadDataConverter CreateMessageDataConverter(IMessageSerializerSettingsFactory messageSerializerSettingsFactory)
{
bool isDefault;
if (messageSerializerSettingsFactory == null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
using System.Net.Http;
using System.Threading;
#if !FUNCTIONS_V1
using Microsoft.Azure.WebJobs.Extensions.DurableTask.ContextImplementations;
using Microsoft.Azure.WebJobs.Extensions.DurableTask.Options;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
Expand Down Expand Up @@ -43,10 +45,45 @@ public static IWebJobsBuilder AddDurableTask(this IWebJobsBuilder builder)
serviceCollection.TryAddSingleton<IMessageSerializerSettingsFactory, MessageSerializerSettingsFactory>();
serviceCollection.TryAddSingleton<IErrorSerializerSettingsFactory, ErrorSerializerSettingsFactory>();
serviceCollection.TryAddSingleton<IApplicationLifetimeWrapper, HostLifecycleService>();
serviceCollection.TryAddSingleton<IDurableClientFactory, DurableClientFactory>();

return builder;
}

/// <summary>
/// Adds the Durable Task extension to the provided <see cref="IServiceCollection"/>.
/// </summary>
/// <param name="serviceCollection">The <see cref="IServiceCollection"/> to configure.</param>
/// <returns>Returns the provided <see cref="IServiceCollection"/>.</returns>
public static IServiceCollection AddDurableTask(this IServiceCollection serviceCollection)
{
if (serviceCollection == null)
{
throw new ArgumentNullException(nameof(serviceCollection));
}

serviceCollection.TryAddSingleton<INameResolver, DefaultNameResolver>();
serviceCollection.TryAddSingleton<IConnectionStringResolver, StandardConnectionStringProvider>();
serviceCollection.TryAddSingleton<IDurabilityProviderFactory, AzureStorageDurabilityProviderFactory>();
serviceCollection.TryAddSingleton<IDurableClientFactory, DurableClientFactory>();
serviceCollection.TryAddSingleton<IMessageSerializerSettingsFactory, MessageSerializerSettingsFactory>();

return serviceCollection;
}

/// <summary>
/// Adds the Durable Task extension to the provided <see cref="IServiceCollection"/>.
/// </summary>
/// <param name="serviceCollection">The <see cref="IServiceCollection"/> to configure.</param>
/// <param name="optionsBuilder">Populate default configurations of <see cref="DurableClientOptions"/> to create Durable Clients.</param>
/// <returns>Returns the provided <see cref="IServiceCollection"/>.</returns>
public static IServiceCollection AddDurableTask(this IServiceCollection serviceCollection, Action<DurableClientOptions> optionsBuilder)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this method should be AddDurableTaskFactory(), and honestly might be better served in it's own namespace/package.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed elsewhere in this review, we will keep it in this namespace/package.

There are questions to be brought up about other function apps that don't want to use Durable except for the external client and how we support that, as the functions runtime will automatically start up an instance of the extension, but that can be addressed later.

@bachuv, we may still want to rename this before the v2.4.0 release.

{
AddDurableTask(serviceCollection);
serviceCollection.Configure<DurableClientOptions>(optionsBuilder.Invoke);
return serviceCollection;
}

/// <summary>
/// Adds the Durable Task extension to the provided <see cref="IWebJobsBuilder"/>.
/// </summary>
Expand Down
Loading