Jordan Dominion
2024-09-25 20:43:57 -04:00
parent b36ede9a96
commit a979c49ef9
5 changed files with 101 additions and 2 deletions
@@ -56,6 +56,7 @@ using Tgstation.Server.Host.Controllers.Results;
using Tgstation.Server.Host.Database;
using Tgstation.Server.Host.Extensions;
using Tgstation.Server.Host.GraphQL;
using Tgstation.Server.Host.GraphQL.Subscriptions;
using Tgstation.Server.Host.GraphQL.Types;
using Tgstation.Server.Host.GraphQL.Types.Interceptors;
using Tgstation.Server.Host.GraphQL.Types.Scalars;
@@ -295,6 +296,7 @@ namespace Tgstation.Server.Host.Core
// configure graphql
if (postSetupServices.InternalConfiguration.EnableGraphQL)
services
.AddScoped<ITopicEventReceiver, ShutdownAwareTopicEventReceiver>()
.AddGraphQLServer()
.AddAuthorization()
.ModifyOptions(options =>
@@ -38,7 +38,7 @@ namespace Tgstation.Server.Host.GraphQL
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a <see cref="ISourceStream{TMessage}"/> of the <see cref="SessionInvalidationReason"/> for the <paramref name="authenticationContext"/>.</returns>
public ValueTask<ISourceStream<SessionInvalidationReason>> SessionInvalidatedStream(
[Service] ITopicEventReceiver receiver,
[Service] HotChocolate.Subscriptions.ITopicEventReceiver receiver, // Intentionally not using our override here, topic callers are built to explicitly handle cases of server shutdown
[Service] ISessionInvalidationTracker invalidationTracker,
[Service] IAuthenticationContext authenticationContext,
CancellationToken cancellationToken)
@@ -0,0 +1,9 @@
namespace Tgstation.Server.Host.GraphQL.Subscriptions
{
/// <summary>
/// Implementation of <see cref="HotChocolate.Subscriptions.ITopicEventReceiver"/> that works around the <see cref="global::System.Threading.CancellationToken"/> issue described in https://github.com/ChilliCream/graphql-platform/issues/6698.
/// </summary>
public interface ITopicEventReceiver : HotChocolate.Subscriptions.ITopicEventReceiver
{
}
}
@@ -0,0 +1,89 @@
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using HotChocolate.Execution;
using HotChocolate.Subscriptions;
using Microsoft.Extensions.Hosting;
namespace Tgstation.Server.Host.GraphQL.Subscriptions
{
/// <inheritdoc cref="ITopicEventReceiver" />
sealed class ShutdownAwareTopicEventReceiver : ITopicEventReceiver, IAsyncDisposable
{
/// <summary>
/// The <see cref="IHostApplicationLifetime"/> for the <see cref="ShutdownAwareTopicEventReceiver"/>.
/// </summary>
readonly IHostApplicationLifetime hostApplicationLifetime;
/// <summary>
/// The wrapped <see cref="HotChocolate.Subscriptions.ITopicEventReceiver"/>.
/// </summary>
readonly HotChocolate.Subscriptions.ITopicEventReceiver hotChocolateReceiver;
/// <summary>
/// A <see cref="ConcurrentBag{T}"/> of <see cref="CancellationTokenRegistration"/>s that were created for this scope.
/// </summary>
readonly ConcurrentBag<CancellationTokenRegistration> registrations;
/// <summary>
/// A <see cref="ConcurrentBag{T}"/> of <see cref="ValueTask"/>s returned from initiating <see cref="IAsyncDisposable.DisposeAsync"/> calls on <see cref="ISourceStream"/>s.
/// </summary>
readonly ConcurrentBag<Task> disposeTasks;
/// <summary>
/// Initializes a new instance of the <see cref="ShutdownAwareTopicEventReceiver"/> class.
/// </summary>
/// <param name="hostApplicationLifetime">The value of <see cref="hostApplicationLifetime"/>.</param>
/// <param name="hotChocolateReceiver">The value of <see cref="hotChocolateReceiver"/>.</param>
public ShutdownAwareTopicEventReceiver(
IHostApplicationLifetime hostApplicationLifetime,
HotChocolate.Subscriptions.ITopicEventReceiver hotChocolateReceiver)
{
this.hostApplicationLifetime = hostApplicationLifetime ?? throw new ArgumentNullException(nameof(hostApplicationLifetime));
this.hotChocolateReceiver = hotChocolateReceiver ?? throw new ArgumentNullException(nameof(hotChocolateReceiver));
registrations = new ConcurrentBag<CancellationTokenRegistration>();
disposeTasks = new ConcurrentBag<Task>();
}
/// <inheritdoc />
public async ValueTask DisposeAsync()
{
foreach (var registration in registrations)
{
registration.Dispose();
}
await Task.WhenAll(disposeTasks);
}
/// <inheritdoc />
public ValueTask<ISourceStream<TMessage>> SubscribeAsync<TMessage>(string topicName, CancellationToken cancellationToken)
=> WrapWithApplicationLifetimeCancellation(
hotChocolateReceiver.SubscribeAsync<TMessage>(topicName, cancellationToken));
/// <inheritdoc />
public ValueTask<ISourceStream<TMessage>> SubscribeAsync<TMessage>(string topicName, int? bufferCapacity, TopicBufferFullMode? bufferFullMode, CancellationToken cancellationToken)
=> WrapWithApplicationLifetimeCancellation(
hotChocolateReceiver.SubscribeAsync<TMessage>(topicName, bufferCapacity, bufferFullMode, cancellationToken));
/// <summary>
/// Wraps a given <paramref name="sourceStreamTask"/> with <see cref="hostApplicationLifetime"/> cancellation awareness.
/// </summary>
/// <typeparam name="TMessage">The <see cref="Type"/> of message.</typeparam>
/// <param name="sourceStreamTask">The result of a call to the <see cref="hotChocolateReceiver"/>.</param>
/// <returns>The result of <paramref name="sourceStreamTask"/> with lifetime aware cancellation.</returns>
async ValueTask<ISourceStream<TMessage>> WrapWithApplicationLifetimeCancellation<TMessage>(ValueTask<ISourceStream<TMessage>> sourceStreamTask)
{
var sourceStream = await sourceStreamTask;
registrations.Add(
hostApplicationLifetime.ApplicationStopping.Register(
() => disposeTasks.Add(
sourceStream.DisposeAsync().AsTask())));
return sourceStream;
}
}
}
@@ -5,7 +5,6 @@ using System.Threading.Tasks;
using HotChocolate;
using HotChocolate.Execution;
using HotChocolate.Subscriptions;
using HotChocolate.Types;
using HotChocolate.Types.Relay;