Skip to content

Commit

Permalink
Added support for federated stitching. (#2471)
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelstaib committed Oct 23, 2020
1 parent 27c0fac commit 3d6a617
Show file tree
Hide file tree
Showing 64 changed files with 2,030 additions and 522 deletions.
@@ -0,0 +1,139 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;

namespace HotChocolate.Execution.Configuration
{
internal sealed class DefaultRequestExecutorOptionsMonitor
: IRequestExecutorOptionsMonitor
, IDisposable
{
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
private readonly IOptionsMonitor<RequestExecutorFactoryOptions> _optionsMonitor;
private readonly IRequestExecutorOptionsProvider[] _optionsProviders;
private readonly ConcurrentDictionary<NameString, INamedRequestExecutorFactoryOptions>
_options = new ConcurrentDictionary<NameString, INamedRequestExecutorFactoryOptions>();
private readonly List<IDisposable> _disposables = new List<IDisposable>();
private readonly List<Action<RequestExecutorFactoryOptions, string>> _listeners =
new List<Action<RequestExecutorFactoryOptions, string>>();
private bool _initialized;
private bool _disposed;

public DefaultRequestExecutorOptionsMonitor(
IOptionsMonitor<RequestExecutorFactoryOptions> optionsMonitor,
IEnumerable<IRequestExecutorOptionsProvider> optionsProviders)
{
_optionsMonitor = optionsMonitor;
_optionsProviders = optionsProviders.ToArray();
}

public async ValueTask<RequestExecutorFactoryOptions> GetAsync(
NameString schemaName,
CancellationToken cancellationToken = default)
{
await InitializeAsync(cancellationToken).ConfigureAwait(false);

RequestExecutorFactoryOptions options = _optionsMonitor.Get(schemaName);

if (_options.TryGetValue(schemaName, out INamedRequestExecutorFactoryOptions? o))
{
o.Configure(options);
}

return options;
}

private async ValueTask InitializeAsync(CancellationToken cancellationToken)
{
if (!_initialized)
{
await _semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);

if (!_initialized)
{
foreach (IRequestExecutorOptionsProvider provider in _optionsProviders)
{
_disposables.Add(provider.OnChange(OnChange));

IEnumerable<NamedRequestExecutorFactoryOptions> allOptions =
await provider.GetOptionsAsync(cancellationToken)
.ConfigureAwait(false);

foreach (NamedRequestExecutorFactoryOptions options in allOptions)
{
_options[options.SchemaName] = options;
}
}

_initialized = true;
}

_semaphore.Release();
}
}

public IDisposable OnChange(Action<RequestExecutorFactoryOptions, string> listener) =>
new Session(this, listener);

private void OnChange(INamedRequestExecutorFactoryOptions changes)
{
_options[changes.SchemaName] = changes;

RequestExecutorFactoryOptions options = _optionsMonitor.Get(changes.SchemaName);
changes.Configure(options);

lock (_listeners)
{
foreach (Action<RequestExecutorFactoryOptions, string> listener in _listeners)
{
listener.Invoke(options, changes.SchemaName);
}
}
}

public void Dispose()
{
if (!_disposed)
{
_semaphore.Dispose();

foreach (IDisposable disposable in _disposables)
{
disposable.Dispose();
}

_disposed = true;
}
}

private class Session : IDisposable
{
private readonly DefaultRequestExecutorOptionsMonitor _monitor;
private readonly Action<RequestExecutorFactoryOptions, string> _listener;

public Session(
DefaultRequestExecutorOptionsMonitor monitor,
Action<RequestExecutorFactoryOptions, string> listener)
{
lock (monitor._listeners)
{
_monitor = monitor;
_listener = listener;
monitor._listeners.Add(listener);
}
}

public void Dispose()
{
lock (_monitor._listeners)
{
_monitor._listeners.Remove(_listener);
}
}
}
}
}
@@ -0,0 +1,32 @@
using System;
using System.Threading;
using System.Threading.Tasks;

namespace HotChocolate.Execution.Configuration
{
/// <summary>
/// Used for notifications when <see cref="RequestExecutorFactoryOptions"/> instances change.
/// </summary>
public interface IRequestExecutorOptionsMonitor
{
/// <summary>
/// Returns a configured <see cref="RequestExecutorFactoryOptions"/>
/// instance with the given name.
/// </summary>
ValueTask<RequestExecutorFactoryOptions> GetAsync(
NameString schemaName,
CancellationToken cancellationToken);

/// <summary>
/// Registers a listener to be called whenever a named
/// <see cref="RequestExecutorFactoryOptions"/> changes.
/// </summary>
/// <param name="listener">
/// The action to be invoked when <see cref="RequestExecutorFactoryOptions"/> has changed.
/// </param>
/// <returns>
/// An <see cref="IDisposable"/> which should be disposed to stop listening for changes.
/// </returns>
IDisposable OnChange(Action<RequestExecutorFactoryOptions, string> listener);
}
}
@@ -0,0 +1,27 @@
using System;
using System.Threading;
using System.Threading.Tasks;

namespace HotChocolate.Execution.Configuration
{
public readonly struct OnRequestExecutorCreatedAction
{
public OnRequestExecutorCreatedAction(
Action<IRequestExecutor> action)
{
Action = action ?? throw new ArgumentNullException(nameof(action));
AsyncAction = default;
}

public OnRequestExecutorCreatedAction(
Func<IRequestExecutor, CancellationToken, ValueTask> asyncAction)
{
Action = default;
AsyncAction = asyncAction ?? throw new ArgumentNullException(nameof(asyncAction));
}

public Action<IRequestExecutor>? Action { get; }

public Func<IRequestExecutor, CancellationToken, ValueTask>? AsyncAction { get; }
}
}
@@ -0,0 +1,27 @@
using System;
using System.Threading;
using System.Threading.Tasks;

namespace HotChocolate.Execution.Configuration
{
public readonly struct OnRequestExecutorEvictedAction
{
public OnRequestExecutorEvictedAction(
Action<IRequestExecutor> action)
{
Action = action ?? throw new ArgumentNullException(nameof(action));
AsyncAction = default;
}

public OnRequestExecutorEvictedAction(
Func<IRequestExecutor, CancellationToken, ValueTask> asyncAction)
{
Action = default;
AsyncAction = asyncAction ?? throw new ArgumentNullException(nameof(asyncAction));
}

public Action<IRequestExecutor>? Action { get; }

public Func<IRequestExecutor, CancellationToken, ValueTask>? AsyncAction { get; }
}
}
@@ -1,7 +1,10 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using HotChocolate.Execution.Options;
using Microsoft.Extensions.Options;

namespace HotChocolate.Execution.Configuration
{
Expand All @@ -24,5 +27,79 @@ public class RequestExecutorFactoryOptions

public IList<Action<IServiceCollection>> SchemaServices { get; } =
new List<Action<IServiceCollection>>();

public IList<OnRequestExecutorCreatedAction> OnRequestExecutorCreated { get; } =
new List<OnRequestExecutorCreatedAction>();

public IList<OnRequestExecutorEvictedAction> OnRequestExecutorEvicted { get; } =
new List<OnRequestExecutorEvictedAction>();
}

/// <summary>
/// Provides dynamic configurations.
/// </summary>
public interface IRequestExecutorOptionsProvider
{
/// <summary>
/// Gets named configuration options.
/// </summary>
/// <param name="cancellationToken">
/// The <see cref="CancellationToken"/>.
/// </param>
/// <returns>
/// Returns the configuration options of this provider.
/// </returns>
ValueTask<IEnumerable<NamedRequestExecutorFactoryOptions>> GetOptionsAsync(
CancellationToken cancellationToken);

/// <summary>
/// Registers a listener to be called whenever a named
/// <see cref="RequestExecutorFactoryOptions"/> changes.
/// </summary>
/// <param name="listener">
/// The action to be invoked when <see cref="RequestExecutorFactoryOptions"/> has changed.
/// </param>
/// <returns>
/// An <see cref="IDisposable"/> which should be disposed to stop listening for changes.
/// </returns>
IDisposable OnChange(Action<INamedRequestExecutorFactoryOptions> listener);
}

/// <summary>
/// Represents something that configures the <see cref="RequestExecutorFactoryOptions"/>.
/// </summary>
public interface INamedRequestExecutorFactoryOptions
: IConfigureOptions<RequestExecutorFactoryOptions>
{
/// <summary>
/// The schema name to which this instance provides configurations to.
/// </summary>
NameString SchemaName { get; }
}

public sealed class NamedRequestExecutorFactoryOptions
: INamedRequestExecutorFactoryOptions
{
private readonly Action<RequestExecutorFactoryOptions> _configure;

public NamedRequestExecutorFactoryOptions(
NameString schemaName,
Action<RequestExecutorFactoryOptions> configure)
{
SchemaName = schemaName.EnsureNotEmpty(nameof(schemaName));
_configure = configure ?? throw new ArgumentNullException(nameof(configure));
}

public NameString SchemaName { get; }

public void Configure(RequestExecutorFactoryOptions options)
{
if (options is null)
{
throw new ArgumentNullException(nameof(options));
}

_configure(options);
}
}
}
Expand Up @@ -6,16 +6,18 @@ namespace HotChocolate.Execution.Configuration
{
public readonly struct SchemaBuilderAction
{
public SchemaBuilderAction(Action<ISchemaBuilder> action)
public SchemaBuilderAction(
Action<ISchemaBuilder> action)
{
Action = action;
Action = action ?? throw new ArgumentNullException(nameof(action));
AsyncAction = default;
}

public SchemaBuilderAction(Func<ISchemaBuilder, CancellationToken, ValueTask> asyncAction)
public SchemaBuilderAction(
Func<ISchemaBuilder, CancellationToken, ValueTask> asyncAction)
{
Action = default;
AsyncAction = asyncAction;
AsyncAction = asyncAction ?? throw new ArgumentNullException(nameof(asyncAction));
}

public Action<ISchemaBuilder>? Action { get; }
Expand Down
@@ -1,3 +1,4 @@
using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.ObjectPool;
using GreenDonut;
Expand All @@ -8,13 +9,25 @@
using HotChocolate.Language;
using HotChocolate.Utilities;
using HotChocolate.DataLoader;
using HotChocolate.Execution.Configuration;
using HotChocolate.Execution.Internal;
using HotChocolate.Types.Relay;
using Microsoft.Extensions.Options;

namespace Microsoft.Extensions.DependencyInjection
{
internal static class InternalServiceCollectionExtensions
{
internal static IServiceCollection TryAddRequestExecutorFactoryOptionsMonitor(
this IServiceCollection services)
{
services.TryAddSingleton<IRequestExecutorOptionsMonitor>(
sp => new DefaultRequestExecutorOptionsMonitor(
sp.GetRequiredService<IOptionsMonitor<RequestExecutorFactoryOptions>>(),
sp.GetRequiredService<IEnumerable<IRequestExecutorOptionsProvider>>()));
return services;
}

internal static IServiceCollection TryAddVariableCoercion(
this IServiceCollection services)
{
Expand Down

0 comments on commit 3d6a617

Please sign in to comment.