From 62afa73705e49f2c5bc55e27691f1ae010024ea4 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Tue, 25 Feb 2020 15:41:26 +0100 Subject: [PATCH 01/16] fix cancellation test --- .../Chat/Schema/ChatQuery.cs | 14 +++-- .../Helpers/ConcurrentTaskWrapper.cs | 54 +++++++++++++++++++ .../GraphQL.Integration.Tests.csproj | 1 + .../WebsocketTests/Base.cs | 48 +++++++++++++---- 4 files changed, 105 insertions(+), 12 deletions(-) create mode 100644 tests/GraphQL.Client.Tests.Common/Helpers/ConcurrentTaskWrapper.cs diff --git a/tests/GraphQL.Client.Tests.Common/Chat/Schema/ChatQuery.cs b/tests/GraphQL.Client.Tests.Common/Chat/Schema/ChatQuery.cs index 641ca262..0816fd8c 100644 --- a/tests/GraphQL.Client.Tests.Common/Chat/Schema/ChatQuery.cs +++ b/tests/GraphQL.Client.Tests.Common/Chat/Schema/ChatQuery.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; +using System.Threading; using GraphQL.Types; namespace GraphQL.Client.Tests.Common.Chat.Schema { @@ -12,6 +12,12 @@ public class ChatQuery : ObjectGraphType { {"another extension", 4711} }; + // properties for unit testing + + public readonly ManualResetEventSlim LongRunningQueryBlocker = new ManualResetEventSlim(); + public readonly ManualResetEventSlim WaitingOnQueryBlocker = new ManualResetEventSlim(); + + public ChatQuery(IChat chat) { Name = "ChatQuery"; @@ -26,8 +32,10 @@ public ChatQuery(IChat chat) { Field() .Name("longRunning") - .ResolveAsync(async context => { - await Task.Delay(TimeSpan.FromSeconds(5)); + .Resolve(context => { + WaitingOnQueryBlocker.Set(); + LongRunningQueryBlocker.Wait(); + WaitingOnQueryBlocker.Reset(); return "finally returned"; }); } diff --git a/tests/GraphQL.Client.Tests.Common/Helpers/ConcurrentTaskWrapper.cs b/tests/GraphQL.Client.Tests.Common/Helpers/ConcurrentTaskWrapper.cs new file mode 100644 index 00000000..64ea0554 --- /dev/null +++ b/tests/GraphQL.Client.Tests.Common/Helpers/ConcurrentTaskWrapper.cs @@ -0,0 +1,54 @@ +using System; +using System.Threading.Tasks; + +namespace GraphQL.Client.Tests.Common.Helpers { + + public class ConcurrentTaskWrapper { + public static ConcurrentTaskWrapper New(Func> createTask) { + return new ConcurrentTaskWrapper(createTask); + } + + private readonly Func createTask; + private Task internalTask = null; + + public ConcurrentTaskWrapper(Func createTask) { + this.createTask = createTask; + } + + public Task Invoke() { + if (internalTask != null) + return internalTask; + + return internalTask = createTask(); + } + } + + public class ConcurrentTaskWrapper { + private readonly Func> createTask; + private Task internalTask = null; + + public ConcurrentTaskWrapper(Func> createTask) { + this.createTask = createTask; + } + + public Task Invoke() { + if (internalTask != null) + return internalTask; + + return internalTask = createTask(); + } + + public void Start() { + if (internalTask == null) + internalTask = createTask(); + } + + public Func> Invoking() { + return Invoke; + } + + public void Clear() { + internalTask = null; + } + } +} diff --git a/tests/GraphQL.Integration.Tests/GraphQL.Integration.Tests.csproj b/tests/GraphQL.Integration.Tests/GraphQL.Integration.Tests.csproj index 3273adae..e2e2d726 100644 --- a/tests/GraphQL.Integration.Tests/GraphQL.Integration.Tests.csproj +++ b/tests/GraphQL.Integration.Tests/GraphQL.Integration.Tests.csproj @@ -10,6 +10,7 @@ + diff --git a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs index 77045a92..0e1cfa6b 100644 --- a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs @@ -2,16 +2,22 @@ using System.Collections.Concurrent; using System.Diagnostics; using System.Net.WebSockets; +using System.Reactive.Concurrency; +using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; using FluentAssertions; +using FluentAssertions.Extensions; using GraphQL.Client.Abstractions; using GraphQL.Client.Abstractions.Websocket; using GraphQL.Client.Tests.Common.Chat; +using GraphQL.Client.Tests.Common.Chat.Schema; using GraphQL.Client.Tests.Common.Helpers; using GraphQL.Integration.Tests.Helpers; using IntegrationTestServer; using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Reactive.Testing; using Xunit; using Xunit.Abstractions; @@ -52,23 +58,47 @@ public async void CanSendRequestViaWebsocket() { } [Fact] - public void WebsocketRequestCanBeCancelled() { + public async void WebsocketRequestCanBeCancelled() { var graphQLRequest = new GraphQLRequest(@" query Long { longRunning }"); using (var setup = WebHostHelpers.SetupTest(true, Serializer)) { - var cancellationTimeout = TimeSpan.FromSeconds(1); - var cts = new CancellationTokenSource(cancellationTimeout); - - Func requestTask = () => setup.Client.SendQueryAsync(graphQLRequest, () => new {longRunning = string.Empty}, cts.Token); - Action timeMeasurement = () => requestTask.Should().Throw(); - - timeMeasurement.ExecutionTime().Should().BeCloseTo(cancellationTimeout, TimeSpan.FromMilliseconds(50)); + await setup.Client.InitializeWebsocketConnection(); + var chatQuery = setup.Server.Services.GetService(); + var cts = new CancellationTokenSource(); + + var request = + ConcurrentTaskWrapper.New(() => setup.Client.SendQueryAsync(graphQLRequest, () => new { longRunning = string.Empty }, cts.Token)); + + // Test regular request + // start request + request.Start(); + // wait until the query has reached the server + chatQuery.WaitingOnQueryBlocker.Wait(500).Should().BeTrue("because the request should have reached the server by then"); + // unblock the query + chatQuery.LongRunningQueryBlocker.Set(); + // check execution time + request.Invoking().ExecutionTime().Should().BeLessThan(100.Milliseconds()); + request.Invoke().Result.Data.longRunning.Should().Be("finally returned"); + + // reset stuff + chatQuery.LongRunningQueryBlocker.Reset(); + request.Clear(); + + // cancellation test + request.Start(); + chatQuery.WaitingOnQueryBlocker.Wait(500).Should().BeTrue("because the request should have reached the server by then"); + cts.Cancel(); + FluentActions.Awaiting(() => request.Invoking().Should().ThrowAsync("because the request was cancelled")) + .ExecutionTime().Should().BeLessThan(100.Milliseconds()); + + // let the server finish its query + chatQuery.LongRunningQueryBlocker.Set(); } } - + [Fact] public async void CanHandleRequestErrorViaWebsocket() { var port = NetworkHelpers.GetFreeTcpPortNumber(); From 53761b1fee8f03e64240373ce277f10d9090e45b Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Tue, 25 Feb 2020 19:03:09 +0100 Subject: [PATCH 02/16] use class fixtures, fix disposal of GraphQLHttpWebSocket --- src/GraphQL.Client/GraphQLHttpClient.cs | 4 +- .../Websocket/GraphQLHttpWebSocket.cs | 30 +- .../Websocket/GraphQLHttpWebsocketHelpers.cs | 10 +- .../Chat/Schema/IChat.cs | 50 +- tests/GraphQL.Client.Tests.Common/Common.cs | 12 +- .../Helpers/IntegrationServerTestFixture.cs | 58 +++ .../Helpers/WebHostHelpers.cs | 44 +- .../QueryAndMutationTests/Base.cs | 158 +++--- .../QueryAndMutationTests/Newtonsoft.cs | 6 +- .../QueryAndMutationTests/SystemTextJson.cs | 6 +- .../WebsocketTests/Base.cs | 448 ++++++++---------- .../WebsocketTests/Newtonsoft.cs | 9 +- .../WebsocketTests/SystemTextJson.cs | 7 +- tests/IntegrationTestServer/Program.cs | 2 +- tests/IntegrationTestServer/Startup.cs | 35 +- tests/IntegrationTestServer/StartupChat.cs | 22 - .../IntegrationTestServer/StartupStarWars.cs | 22 - 17 files changed, 496 insertions(+), 427 deletions(-) create mode 100644 tests/GraphQL.Integration.Tests/Helpers/IntegrationServerTestFixture.cs delete mode 100644 tests/IntegrationTestServer/StartupChat.cs delete mode 100644 tests/IntegrationTestServer/StartupStarWars.cs diff --git a/src/GraphQL.Client/GraphQLHttpClient.cs b/src/GraphQL.Client/GraphQLHttpClient.cs index eb25ebc0..a4da13c6 100644 --- a/src/GraphQL.Client/GraphQLHttpClient.cs +++ b/src/GraphQL.Client/GraphQLHttpClient.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Concurrent; +using System.Diagnostics; using System.Net.Http; using System.Text; using System.Threading; @@ -164,9 +165,10 @@ public void Dispose() { private void _dispose() { disposed = true; + Debug.WriteLine($"disposing GraphQLHttpClient on endpoint {Options.EndPoint}"); + cancellationTokenSource.Cancel(); this.HttpClient.Dispose(); this.graphQlHttpWebSocket.Dispose(); - cancellationTokenSource.Cancel(); cancellationTokenSource.Dispose(); } diff --git a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs index 832fb4cf..1f824c8e 100644 --- a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs +++ b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs @@ -17,6 +17,7 @@ internal class GraphQLHttpWebSocket : IDisposable { private readonly GraphQLHttpClient client; private readonly ArraySegment buffer; private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); + private readonly CancellationToken cancellationToken; private readonly Subject requestSubject = new Subject(); private readonly Subject exceptionSubject = new Subject(); private readonly BehaviorSubject stateSubject = @@ -41,6 +42,7 @@ internal class GraphQLHttpWebSocket : IDisposable { public IObservable ResponseStream { get; } public GraphQLHttpWebSocket(Uri webSocketUri, GraphQLHttpClient client) { + cancellationToken = cancellationTokenSource.Token; this.webSocketUri = webSocketUri; this.client = client; buffer = new ArraySegment(new byte[8192]); @@ -59,7 +61,7 @@ public Task SendWebSocketRequest(GraphQLWebSocketRequest request) { private async Task _sendWebSocketRequest(GraphQLWebSocketRequest request) { try { - if (cancellationTokenSource.Token.IsCancellationRequested) { + if (cancellationToken.IsCancellationRequested) { request.SendCanceled(); return; } @@ -70,7 +72,7 @@ await this.clientWebSocket.SendAsync( new ArraySegment(requestBytes), WebSocketMessageType.Text, true, - cancellationTokenSource.Token).ConfigureAwait(false); + cancellationToken).ConfigureAwait(false); request.SendCompleted(); } catch (Exception e) { @@ -126,7 +128,7 @@ public Task InitializeWebSocket() { clientWebSocket.Options.ClientCertificates = ((HttpClientHandler)Options.HttpMessageHandler).ClientCertificates; clientWebSocket.Options.UseDefaultCredentials = ((HttpClientHandler)Options.HttpMessageHandler).UseDefaultCredentials; #endif - return initializeWebSocketTask = _connectAsync(cancellationTokenSource.Token); + return initializeWebSocketTask = _connectAsync(cancellationToken); } } @@ -172,6 +174,8 @@ private IObservable _createResponseStream() { } private async Task _createResultStream(IObserver observer, CancellationToken token) { + cancellationToken.ThrowIfCancellationRequested(); + if (responseSubject == null || responseSubject.IsDisposed) { // create new response subject responseSubject = new Subject(); @@ -200,7 +204,7 @@ private async Task _createResultStream(IObserver { - Debug.WriteLine("response stream disposed"); + Debug.WriteLine($"response stream {responseSubject.GetHashCode()} disposed"); }) ); } @@ -213,6 +217,7 @@ private async Task _createResultStream(IObserver private Task _getReceiveTask() { lock (receiveTaskLocker) { + cancellationToken.ThrowIfCancellationRequested(); if (receiveAsyncTask == null || receiveAsyncTask.IsFaulted || receiveAsyncTask.IsCompleted) @@ -233,13 +238,13 @@ private async Task _receiveResultAsync() { using (var ms = new MemoryStream()) { WebSocketReceiveResult webSocketReceiveResult = null; do { - cancellationTokenSource.Token.ThrowIfCancellationRequested(); + cancellationToken.ThrowIfCancellationRequested(); webSocketReceiveResult = await clientWebSocket.ReceiveAsync(buffer, CancellationToken.None); ms.Write(buffer.Array, buffer.Offset, webSocketReceiveResult.Count); } while (!webSocketReceiveResult.EndOfMessage); - cancellationTokenSource.Token.ThrowIfCancellationRequested(); + cancellationToken.ThrowIfCancellationRequested(); ms.Seek(0, SeekOrigin.Begin); if (webSocketReceiveResult.MessageType == WebSocketMessageType.Text) { @@ -258,7 +263,7 @@ private async Task _receiveResultAsync() { } } - private async Task _closeAsync(CancellationToken cancellationToken = default) { + private async Task _closeAsync() { if (clientWebSocket == null) return; @@ -271,7 +276,7 @@ private async Task _closeAsync(CancellationToken cancellationToken = default) { } Debug.WriteLine($"closing websocket {clientWebSocket.GetHashCode()}"); - await this.clientWebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", cancellationToken).ConfigureAwait(false); + await this.clientWebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None).ConfigureAwait(false); stateSubject.OnNext(GraphQLWebsocketConnectionState.Disconnected); } @@ -301,6 +306,15 @@ private async Task CompleteAsync() { await _closeAsync().ConfigureAwait(false); requestSubscription?.Dispose(); clientWebSocket?.Dispose(); + + responseSubject?.OnCompleted(); + responseSubject?.Dispose(); + + stateSubject?.OnCompleted(); + stateSubject?.Dispose(); + + exceptionSubject?.OnCompleted(); + exceptionSubject?.Dispose(); cancellationTokenSource.Dispose(); Debug.WriteLine($"websocket {clientWebSocket.GetHashCode()} disposed"); } diff --git a/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs b/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs index 9148a727..f4eeebd5 100644 --- a/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs +++ b/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs @@ -58,8 +58,14 @@ internal static IObservable> CreateSubscriptionStream o.OnCompleted(); } }, - o.OnError, - o.OnCompleted) + e => { + Debug.WriteLine($"response stream for subscription {startRequest.Id} failed: {e}"); + o.OnError(e); + }, + () => { + Debug.WriteLine($"response stream for subscription {startRequest.Id} completed"); + o.OnCompleted(); + }) ); try { diff --git a/tests/GraphQL.Client.Tests.Common/Chat/Schema/IChat.cs b/tests/GraphQL.Client.Tests.Common/Chat/Schema/IChat.cs index 2e31e8b4..6d53e42b 100644 --- a/tests/GraphQL.Client.Tests.Common/Chat/Schema/IChat.cs +++ b/tests/GraphQL.Client.Tests.Common/Chat/Schema/IChat.cs @@ -18,20 +18,25 @@ public interface IChat { } public class Chat : IChat { - private readonly ISubject _messageStream = new ReplaySubject(1); + private readonly RollingReplaySubject _messageStream = new RollingReplaySubject(); private readonly ISubject _userJoined = new Subject(); public Chat() { + Reset(); + } + + public void Reset() { AllMessages = new ConcurrentStack(); Users = new ConcurrentDictionary { ["1"] = "developer", ["2"] = "tester" }; + _messageStream.Clear(); } - public ConcurrentDictionary Users { get; set; } + public ConcurrentDictionary Users { get; private set; } - public ConcurrentStack AllMessages { get; } + public ConcurrentStack AllMessages { get; private set; } public Message AddMessage(ReceivedMessage message) { if (!Users.TryGetValue(message.FromId, out var displayName)) { @@ -90,4 +95,43 @@ public class User { public string Id { get; set; } public string Name { get; set; } } + + public class RollingReplaySubject : ISubject { + private readonly ReplaySubject> _subjects; + private readonly IObservable _concatenatedSubjects; + private ISubject _currentSubject; + + public RollingReplaySubject() { + _subjects = new ReplaySubject>(1); + _concatenatedSubjects = _subjects.Concat(); + _currentSubject = new ReplaySubject(); + _subjects.OnNext(_currentSubject); + } + + public void Clear() { + _currentSubject.OnCompleted(); + _currentSubject = new ReplaySubject(); + _subjects.OnNext(_currentSubject); + } + + public void OnNext(T value) { + _currentSubject.OnNext(value); + } + + public void OnError(Exception error) { + _currentSubject.OnError(error); + } + + public void OnCompleted() { + _currentSubject.OnCompleted(); + _subjects.OnCompleted(); + // a quick way to make the current ReplaySubject unreachable + // except to in-flight observers, and not hold up collection + _currentSubject = new Subject(); + } + + public IDisposable Subscribe(IObserver observer) { + return _concatenatedSubjects.Subscribe(observer); + } + } } diff --git a/tests/GraphQL.Client.Tests.Common/Common.cs b/tests/GraphQL.Client.Tests.Common/Common.cs index e912d907..05673f23 100644 --- a/tests/GraphQL.Client.Tests.Common/Common.cs +++ b/tests/GraphQL.Client.Tests.Common/Common.cs @@ -5,9 +5,11 @@ namespace GraphQL.Client.Tests.Common { - public static class Common - { - public static StarWarsSchema GetStarWarsSchema() { + public static class Common { + public const string StarWarsEndpoint = "/graphql/starwars"; + public const string ChatEndpoint = "/graphql/chat"; + + public static StarWarsSchema GetStarWarsSchema() { var services = new ServiceCollection(); services.AddTransient(provider => new FuncDependencyResolver(provider.GetService)); services.AddStarWarsSchema(); @@ -33,7 +35,9 @@ public static void AddStarWarsSchema(this IServiceCollection services) { } public static void AddChatSchema(this IServiceCollection services) { - services.AddSingleton(); + var chat = new Chat.Schema.Chat(); + services.AddSingleton(chat); + services.AddSingleton(chat); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/tests/GraphQL.Integration.Tests/Helpers/IntegrationServerTestFixture.cs b/tests/GraphQL.Integration.Tests/Helpers/IntegrationServerTestFixture.cs new file mode 100644 index 00000000..3b20ccbe --- /dev/null +++ b/tests/GraphQL.Integration.Tests/Helpers/IntegrationServerTestFixture.cs @@ -0,0 +1,58 @@ +using System; +using System.Threading.Tasks; +using GraphQL.Client.Abstractions.Websocket; +using GraphQL.Client.Http; +using GraphQL.Client.Serializer.Newtonsoft; +using GraphQL.Client.Serializer.SystemTextJson; +using GraphQL.Client.Tests.Common; +using GraphQL.Client.Tests.Common.Helpers; +using Microsoft.AspNetCore.Hosting; + +namespace GraphQL.Integration.Tests.Helpers { + public abstract class IntegrationServerTestFixture { + public int Port { get; private set; } + public IWebHost Server { get; private set; } + public abstract IGraphQLWebsocketJsonSerializer Serializer { get; } + + public IntegrationServerTestFixture() + { + Port = NetworkHelpers.GetFreeTcpPortNumber(); + CreateServer(); + } + + public void CreateServer() { + if(Server != null) + throw new InvalidOperationException("server is already created"); + Server = WebHostHelpers.CreateServer(Port); + } + + public async Task ShutdownServer() { + if (Server == null) + return; + + await Server.StopAsync(); + Server.Dispose(); + Server = null; + } + + public GraphQLHttpClient GetStarWarsClient(bool requestsViaWebsocket = false) + => GetGraphQLClient(Common.StarWarsEndpoint, requestsViaWebsocket); + + public GraphQLHttpClient GetChatClient(bool requestsViaWebsocket = false) + => GetGraphQLClient(Common.ChatEndpoint, requestsViaWebsocket); + + private GraphQLHttpClient GetGraphQLClient(string endpoint, bool requestsViaWebsocket = false) { + if(Serializer == null) + throw new InvalidOperationException("JSON serializer not configured"); + return WebHostHelpers.GetGraphQLClient(Port, endpoint, requestsViaWebsocket, Serializer); + } + } + + public class NewtonsoftIntegrationServerTestFixture: IntegrationServerTestFixture { + public override IGraphQLWebsocketJsonSerializer Serializer { get; } = new NewtonsoftJsonSerializer(); + } + + public class SystemTextJsonIntegrationServerTestFixture : IntegrationServerTestFixture { + public override IGraphQLWebsocketJsonSerializer Serializer { get; } = new SystemTextJsonSerializer(); + } +} diff --git a/tests/GraphQL.Integration.Tests/Helpers/WebHostHelpers.cs b/tests/GraphQL.Integration.Tests/Helpers/WebHostHelpers.cs index 0ee54a08..59084c6f 100644 --- a/tests/GraphQL.Integration.Tests/Helpers/WebHostHelpers.cs +++ b/tests/GraphQL.Integration.Tests/Helpers/WebHostHelpers.cs @@ -1,9 +1,12 @@ using System; +using System.Reactive.Disposables; using GraphQL.Client; using GraphQL.Client.Abstractions.Websocket; using GraphQL.Client.Http; using GraphQL.Client.Serializer.Newtonsoft; +using GraphQL.Client.Tests.Common; using GraphQL.Client.Tests.Common.Helpers; +using IntegrationTestServer; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; @@ -12,7 +15,7 @@ namespace GraphQL.Integration.Tests.Helpers { public static class WebHostHelpers { - public static IWebHost CreateServer(int port) where TStartup : class + public static IWebHost CreateServer(int port) { var configBuilder = new ConfigurationBuilder(); configBuilder.AddInMemoryCollection(); @@ -25,41 +28,46 @@ public static IWebHost CreateServer(int port) where TStartup : class }) .UseConfiguration(config) .UseKestrel() - .UseStartup() + .UseStartup() .Build(); host.Start(); return host; } - - - public static GraphQLHttpClient GetGraphQLClient(int port, bool requestsViaWebsocket = false, IGraphQLWebsocketJsonSerializer serializer = null) + + public static GraphQLHttpClient GetGraphQLClient(int port, string endpoint, bool requestsViaWebsocket = false, IGraphQLWebsocketJsonSerializer serializer = null) => new GraphQLHttpClient(new GraphQLHttpClientOptions { - EndPoint = new Uri($"http://localhost:{port}/graphql"), + EndPoint = new Uri($"http://localhost:{port}{endpoint}"), UseWebSocketForQueriesAndMutations = requestsViaWebsocket, JsonSerializer = serializer ?? new NewtonsoftJsonSerializer() }); - - public static TestServerSetup SetupTest(bool requestsViaWebsocket = false, IGraphQLWebsocketJsonSerializer serializer = null) - where TStartup : class - { - var port = NetworkHelpers.GetFreeTcpPortNumber(); - return new TestServerSetup { - Server = CreateServer(port), - Client = GetGraphQLClient(port, requestsViaWebsocket, serializer) - }; - } } public class TestServerSetup : IDisposable { + public TestServerSetup(IGraphQLWebsocketJsonSerializer serializer) { + this.serializer = serializer; + Port = NetworkHelpers.GetFreeTcpPortNumber(); + } + + public int Port { get; } public IWebHost Server { get; set; } - public GraphQLHttpClient Client { get; set; } + public IGraphQLWebsocketJsonSerializer serializer { get; set; } + + public GraphQLHttpClient GetStarWarsClient(bool requestsViaWebsocket = false) + => GetGraphQLClient(Common.StarWarsEndpoint, requestsViaWebsocket); + + public GraphQLHttpClient GetChatClient(bool requestsViaWebsocket = false) + => GetGraphQLClient(Common.ChatEndpoint, requestsViaWebsocket); + + private GraphQLHttpClient GetGraphQLClient(string endpoint, bool requestsViaWebsocket = false) { + return WebHostHelpers.GetGraphQLClient(Port, endpoint, requestsViaWebsocket); + } + public void Dispose() { Server?.Dispose(); - Client?.Dispose(); } } } diff --git a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs index 304e2982..ea103901 100644 --- a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs @@ -1,41 +1,41 @@ -using System; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using FluentAssertions; +using FluentAssertions.Extensions; using GraphQL.Client.Abstractions; -using GraphQL.Client.Abstractions.Websocket; using GraphQL.Client.Http; +using GraphQL.Client.Tests.Common.Chat.Schema; using GraphQL.Client.Tests.Common.Helpers; using GraphQL.Client.Tests.Common.StarWars; using GraphQL.Integration.Tests.Helpers; -using IntegrationTestServer; +using Microsoft.Extensions.DependencyInjection; using Xunit; namespace GraphQL.Integration.Tests.QueryAndMutationTests { public abstract class Base { - protected IGraphQLWebsocketJsonSerializer serializer; + protected IntegrationServerTestFixture Fixture; + protected GraphQLHttpClient StarWarsClient; + protected GraphQLHttpClient ChatClient; - private TestServerSetup SetupTest(bool requestsViaWebsocket = false) => WebHostHelpers.SetupTest(requestsViaWebsocket, serializer); - - protected Base(IGraphQLWebsocketJsonSerializer serializer) { - this.serializer = serializer; + protected Base(IntegrationServerTestFixture fixture) { + Fixture = fixture; + StarWarsClient = Fixture.GetStarWarsClient(); + ChatClient = Fixture.GetChatClient(); } - + [Theory] [ClassData(typeof(StarWarsHumans))] public async void QueryTheory(int id, string name) { var graphQLRequest = new GraphQLRequest($"{{ human(id: \"{id}\") {{ name }} }}"); - using (var setup = SetupTest()) { - var response = await setup.Client.SendQueryAsync(graphQLRequest, () => new { Human = new { Name = string.Empty }}) - .ConfigureAwait(false); + var response = await StarWarsClient.SendQueryAsync(graphQLRequest, () => new { Human = new { Name = string.Empty }}) + .ConfigureAwait(false); - Assert.Null(response.Errors); - Assert.Equal(name, response.Data.Human.Name); - } + Assert.Null(response.Errors); + Assert.Equal(name, response.Data.Human.Name); } [Theory] @@ -43,13 +43,11 @@ public async void QueryTheory(int id, string name) { public async void QueryWithDynamicReturnTypeTheory(int id, string name) { var graphQLRequest = new GraphQLRequest($"{{ human(id: \"{id}\") {{ name }} }}"); - using (var setup = SetupTest()) { - var response = await setup.Client.SendQueryAsync(graphQLRequest) - .ConfigureAwait(false); + var response = await StarWarsClient.SendQueryAsync(graphQLRequest) + .ConfigureAwait(false); - Assert.Null(response.Errors); - Assert.Equal(name, response.Data.human.name.ToString()); - } + Assert.Null(response.Errors); + Assert.Equal(name, response.Data.human.name.ToString()); } [Theory] @@ -63,13 +61,11 @@ query Human($id: String!){ }", new {id = id.ToString()}); - using (var setup = SetupTest()) { - var response = await setup.Client.SendQueryAsync(graphQLRequest, () => new { Human = new { Name = string.Empty } }) - .ConfigureAwait(false); + var response = await StarWarsClient.SendQueryAsync(graphQLRequest, () => new { Human = new { Name = string.Empty } }) + .ConfigureAwait(false); - Assert.Null(response.Errors); - Assert.Equal(name, response.Data.Human.Name); - } + Assert.Null(response.Errors); + Assert.Equal(name, response.Data.Human.Name); } [Theory] @@ -90,13 +86,11 @@ query Droid($id: String!) { new { id = id.ToString() }, "Human"); - using (var setup = SetupTest()) { - var response = await setup.Client.SendQueryAsync(graphQLRequest, () => new { Human = new { Name = string.Empty } }) - .ConfigureAwait(false); + var response = await StarWarsClient.SendQueryAsync(graphQLRequest, () => new { Human = new { Name = string.Empty } }) + .ConfigureAwait(false); - Assert.Null(response.Errors); - Assert.Equal(name, response.Data.Human.Name); - } + Assert.Null(response.Errors); + Assert.Equal(name, response.Data.Human.Name); } [Fact] @@ -118,27 +112,25 @@ query Human($id: String!){ } }"); - using (var setup = SetupTest()) { - var mutationResponse = await setup.Client.SendMutationAsync(mutationRequest, () => new { - createHuman = new { - Id = "", - Name = "", - HomePlanet = "" - } - }) - .ConfigureAwait(false); - - Assert.Null(mutationResponse.Errors); - Assert.Equal("Han Solo", mutationResponse.Data.createHuman.Name); - Assert.Equal("Corellia", mutationResponse.Data.createHuman.HomePlanet); - - queryRequest.Variables = new {id = mutationResponse.Data.createHuman.Id}; - var queryResponse = await setup.Client.SendQueryAsync(queryRequest, () => new { Human = new { Name = string.Empty } }) - .ConfigureAwait(false); - - Assert.Null(queryResponse.Errors); - Assert.Equal("Han Solo", queryResponse.Data.Human.Name); - } + var mutationResponse = await StarWarsClient.SendMutationAsync(mutationRequest, () => new { + createHuman = new { + Id = "", + Name = "", + HomePlanet = "" + } + }) + .ConfigureAwait(false); + + Assert.Null(mutationResponse.Errors); + Assert.Equal("Han Solo", mutationResponse.Data.createHuman.Name); + Assert.Equal("Corellia", mutationResponse.Data.createHuman.HomePlanet); + + queryRequest.Variables = new {id = mutationResponse.Data.createHuman.Id}; + var queryResponse = await StarWarsClient.SendQueryAsync(queryRequest, () => new { Human = new { Name = string.Empty } }) + .ConfigureAwait(false); + + Assert.Null(queryResponse.Errors); + Assert.Equal("Han Solo", queryResponse.Data.Human.Name); } [Fact] @@ -148,16 +140,14 @@ public async void PreprocessHttpRequestMessageIsCalled() { PreprocessHttpRequestMessage = callbackTester.Invoke }; - using (var setup = SetupTest()) { - var defaultHeaders = setup.Client.HttpClient.DefaultRequestHeaders; - var response = await setup.Client.SendQueryAsync(graphQLRequest, () => new { Human = new { Name = string.Empty } }) - .ConfigureAwait(false); - callbackTester.CallbackShouldHaveBeenInvoked(message => { - Assert.Equal(defaultHeaders, message.Headers); - }); - Assert.Null(response.Errors); - Assert.Equal("Luke", response.Data.Human.Name); - } + var defaultHeaders = StarWarsClient.HttpClient.DefaultRequestHeaders; + var response = await StarWarsClient.SendQueryAsync(graphQLRequest, () => new { Human = new { Name = string.Empty } }) + .ConfigureAwait(false); + callbackTester.CallbackShouldHaveBeenInvoked(message => { + Assert.Equal(defaultHeaders, message.Headers); + }); + Assert.Null(response.Errors); + Assert.Equal("Luke", response.Data.Human.Name); } [Fact] @@ -167,15 +157,37 @@ query Long { longRunning }"); - using (var setup = WebHostHelpers.SetupTest(false, serializer)) { - var cancellationTimeout = TimeSpan.FromSeconds(1); - var cts = new CancellationTokenSource(cancellationTimeout); - - Func requestTask = () => setup.Client.SendQueryAsync(graphQLRequest, () => new {longRunning = string.Empty}, cts.Token); - Action timeMeasurement = () => requestTask.Should().Throw(); - - timeMeasurement.ExecutionTime().Should().BeCloseTo(cancellationTimeout, TimeSpan.FromMilliseconds(50)); - } + var chatQuery = Fixture.Server.Services.GetService(); + var cts = new CancellationTokenSource(); + + var request = + ConcurrentTaskWrapper.New(() => ChatClient.SendQueryAsync(graphQLRequest, () => new { longRunning = string.Empty }, cts.Token)); + + // Test regular request + // start request + request.Start(); + // wait until the query has reached the server + chatQuery.WaitingOnQueryBlocker.Wait(500).Should().BeTrue("because the request should have reached the server by then"); + // unblock the query + chatQuery.LongRunningQueryBlocker.Set(); + // check execution time + request.Invoking().ExecutionTime().Should().BeLessThan(100.Milliseconds()); + request.Invoke().Result.Data.longRunning.Should().Be("finally returned"); + + // reset stuff + chatQuery.LongRunningQueryBlocker.Reset(); + request.Clear(); + + // cancellation test + request.Start(); + chatQuery.WaitingOnQueryBlocker.Wait(500).Should().BeTrue("because the request should have reached the server by then"); + cts.Cancel(); + FluentActions.Awaiting(() => request.Invoking().Should().ThrowAsync("because the request was cancelled")) + .ExecutionTime().Should().BeLessThan(100.Milliseconds()); + + // let the server finish its query + chatQuery.LongRunningQueryBlocker.Set(); } + } } diff --git a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Newtonsoft.cs b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Newtonsoft.cs index 1046ed6d..57f86f64 100644 --- a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Newtonsoft.cs +++ b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Newtonsoft.cs @@ -1,8 +1,10 @@ using GraphQL.Client.Serializer.Newtonsoft; +using GraphQL.Integration.Tests.Helpers; +using Xunit; namespace GraphQL.Integration.Tests.QueryAndMutationTests { - public class Newtonsoft: Base { - public Newtonsoft() : base(new NewtonsoftJsonSerializer()) + public class Newtonsoft: Base, IClassFixture { + public Newtonsoft(NewtonsoftIntegrationServerTestFixture fixture) : base(fixture) { } } diff --git a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/SystemTextJson.cs b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/SystemTextJson.cs index dd725b4d..e74d5012 100644 --- a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/SystemTextJson.cs +++ b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/SystemTextJson.cs @@ -1,8 +1,10 @@ using GraphQL.Client.Serializer.SystemTextJson; +using GraphQL.Integration.Tests.Helpers; +using Xunit; namespace GraphQL.Integration.Tests.QueryAndMutationTests { - public class SystemTextJson: Base { - public SystemTextJson() : base(new SystemTextJsonSerializer()) + public class SystemTextJson: Base, IClassFixture { + public SystemTextJson(SystemTextJsonIntegrationServerTestFixture fixture) : base(fixture) { } } diff --git a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs index 0e1cfa6b..e4bd5010 100644 --- a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs @@ -2,59 +2,49 @@ using System.Collections.Concurrent; using System.Diagnostics; using System.Net.WebSockets; -using System.Reactive.Concurrency; -using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; using FluentAssertions; using FluentAssertions.Extensions; using GraphQL.Client.Abstractions; using GraphQL.Client.Abstractions.Websocket; +using GraphQL.Client.Http; using GraphQL.Client.Tests.Common.Chat; using GraphQL.Client.Tests.Common.Chat.Schema; using GraphQL.Client.Tests.Common.Helpers; using GraphQL.Integration.Tests.Helpers; -using IntegrationTestServer; -using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Reactive.Testing; using Xunit; using Xunit.Abstractions; namespace GraphQL.Integration.Tests.WebsocketTests { - public abstract class Base { + public abstract class Base: IAsyncLifetime { protected readonly ITestOutputHelper Output; - protected readonly IGraphQLWebsocketJsonSerializer Serializer; - protected IWebHost CreateServer(int port) => WebHostHelpers.CreateServer(port); - - protected Base(ITestOutputHelper output, IGraphQLWebsocketJsonSerializer serializer) { + protected readonly IntegrationServerTestFixture Fixture; + protected GraphQLHttpClient ChatClient; + + protected Base(ITestOutputHelper output, IntegrationServerTestFixture fixture) { this.Output = output; - this.Serializer = serializer; + this.Fixture = fixture; } - - [Fact] - public async void AssertTestingHarness() { - var port = NetworkHelpers.GetFreeTcpPortNumber(); - using (CreateServer(port)) { - var client = WebHostHelpers.GetGraphQLClient(port, serializer: Serializer); - const string message = "some random testing message"; - var response = await client.AddMessageAsync(message).ConfigureAwait(false); + public async Task InitializeAsync() { + Fixture.Server.Services.GetService().Reset(); + ChatClient = Fixture.GetChatClient(true); + Output.WriteLine($"ChatClient: {ChatClient.GetHashCode()}"); + } - Assert.Equal(message, response.Data.AddMessage.Content); - } + public Task DisposeAsync() { + ChatClient?.Dispose(); + return Task.CompletedTask; } [Fact] public async void CanSendRequestViaWebsocket() { - var port = NetworkHelpers.GetFreeTcpPortNumber(); - using (CreateServer(port)) { - var client = WebHostHelpers.GetGraphQLClient(port, true, Serializer); - const string message = "some random testing message"; - var response = await client.AddMessageAsync(message).ConfigureAwait(false); - - Assert.Equal(message, response.Data.AddMessage.Content); - } + await ChatClient.InitializeWebsocketConnection(); + const string message = "some random testing message"; + var response = await ChatClient.AddMessageAsync(message).ConfigureAwait(false); + response.Data.AddMessage.Content.Should().Be(message); } [Fact] @@ -64,50 +54,44 @@ query Long { longRunning }"); - using (var setup = WebHostHelpers.SetupTest(true, Serializer)) { - await setup.Client.InitializeWebsocketConnection(); - var chatQuery = setup.Server.Services.GetService(); - var cts = new CancellationTokenSource(); - - var request = - ConcurrentTaskWrapper.New(() => setup.Client.SendQueryAsync(graphQLRequest, () => new { longRunning = string.Empty }, cts.Token)); - - // Test regular request - // start request - request.Start(); - // wait until the query has reached the server - chatQuery.WaitingOnQueryBlocker.Wait(500).Should().BeTrue("because the request should have reached the server by then"); - // unblock the query - chatQuery.LongRunningQueryBlocker.Set(); - // check execution time - request.Invoking().ExecutionTime().Should().BeLessThan(100.Milliseconds()); - request.Invoke().Result.Data.longRunning.Should().Be("finally returned"); - - // reset stuff - chatQuery.LongRunningQueryBlocker.Reset(); - request.Clear(); - - // cancellation test - request.Start(); - chatQuery.WaitingOnQueryBlocker.Wait(500).Should().BeTrue("because the request should have reached the server by then"); - cts.Cancel(); - FluentActions.Awaiting(() => request.Invoking().Should().ThrowAsync("because the request was cancelled")) - .ExecutionTime().Should().BeLessThan(100.Milliseconds()); - - // let the server finish its query - chatQuery.LongRunningQueryBlocker.Set(); - } + var chatQuery = Fixture.Server.Services.GetService(); + var cts = new CancellationTokenSource(); + + await ChatClient.InitializeWebsocketConnection(); + var request = + ConcurrentTaskWrapper.New(() => ChatClient.SendQueryAsync(graphQLRequest, () => new { longRunning = string.Empty }, cts.Token)); + + // Test regular request + // start request + request.Start(); + // wait until the query has reached the server + chatQuery.WaitingOnQueryBlocker.Wait(500).Should().BeTrue("because the request should have reached the server by then"); + // unblock the query + chatQuery.LongRunningQueryBlocker.Set(); + // check execution time + request.Invoking().ExecutionTime().Should().BeLessThan(100.Milliseconds()); + request.Invoke().Result.Data.longRunning.Should().Be("finally returned"); + + // reset stuff + chatQuery.LongRunningQueryBlocker.Reset(); + request.Clear(); + + // cancellation test + request.Start(); + chatQuery.WaitingOnQueryBlocker.Wait(500).Should().BeTrue("because the request should have reached the server by then"); + cts.Cancel(); + FluentActions.Awaiting(() => request.Invoking().Should().ThrowAsync("because the request was cancelled")) + .ExecutionTime().Should().BeLessThan(100.Milliseconds()); + + // let the server finish its query + chatQuery.LongRunningQueryBlocker.Set(); } [Fact] public async void CanHandleRequestErrorViaWebsocket() { - var port = NetworkHelpers.GetFreeTcpPortNumber(); - using (CreateServer(port)) { - var client = WebHostHelpers.GetGraphQLClient(port, true, Serializer); - var response = await client.SendQueryAsync("this query is formatted quite badly").ConfigureAwait(false); - - Assert.Single(response.Errors); - } + await ChatClient.InitializeWebsocketConnection(); + var response = await ChatClient.SendQueryAsync("this query is formatted quite badly").ConfigureAwait(false); + response.Errors.Should().ContainSingle("because the query is invalid"); } private const string SubscriptionQuery = @" @@ -122,36 +106,31 @@ public async void CanHandleRequestErrorViaWebsocket() { [Fact] public async void CanCreateObservableSubscription() { - var port = NetworkHelpers.GetFreeTcpPortNumber(); - using (CreateServer(port)){ - var client = WebHostHelpers.GetGraphQLClient(port, serializer: Serializer); - var callbackMonitor = client.ConfigureMonitorForOnWebsocketConnected(); - await client.InitializeWebsocketConnection(); - callbackMonitor.Should().HaveBeenInvokedWithPayload(); - - Debug.WriteLine("creating subscription stream"); - IObservable> observable = client.CreateSubscriptionStream(SubscriptionRequest); - - Debug.WriteLine("subscribing..."); - using (var tester = observable.Monitor()) { - const string message1 = "Hello World"; - - var response = await client.AddMessageAsync(message1).ConfigureAwait(false); - response.Data.AddMessage.Content.Should().Be(message1); - tester.Should().HaveReceivedPayload(TimeSpan.FromSeconds(3)) - .Which.Data.MessageAdded.Content.Should().Be(message1); - - const string message2 = "lorem ipsum dolor si amet"; - response = await client.AddMessageAsync(message2).ConfigureAwait(false); - response.Data.AddMessage.Content.Should().Be(message2); - tester.Should().HaveReceivedPayload() - .Which.Data.MessageAdded.Content.Should().Be(message2); - - // disposing the client should throw a TaskCanceledException on the subscription - client.Dispose(); - tester.Should().HaveCompleted(); - } - } + var callbackMonitor = ChatClient.ConfigureMonitorForOnWebsocketConnected(); + await ChatClient.InitializeWebsocketConnection(); + callbackMonitor.Should().HaveBeenInvokedWithPayload(); + + Debug.WriteLine("creating subscription stream"); + var observable = ChatClient.CreateSubscriptionStream(SubscriptionRequest); + + Debug.WriteLine("subscribing..."); + using var tester = observable.Monitor(); + const string message1 = "Hello World"; + + var response = await ChatClient.AddMessageAsync(message1).ConfigureAwait(false); + response.Data.AddMessage.Content.Should().Be(message1); + tester.Should().HaveReceivedPayload(TimeSpan.FromSeconds(3)) + .Which.Data.MessageAdded.Content.Should().Be(message1); + + const string message2 = "lorem ipsum dolor si amet"; + response = await ChatClient.AddMessageAsync(message2).ConfigureAwait(false); + response.Data.AddMessage.Content.Should().Be(message2); + tester.Should().HaveReceivedPayload() + .Which.Data.MessageAdded.Content.Should().Be(message2); + + // disposing the client should throw a TaskCanceledException on the subscription + ChatClient.Dispose(); + tester.Should().HaveCompleted(); } public class MessageAddedSubscriptionResult { @@ -165,48 +144,45 @@ public class MessageAddedContent { [Fact] public async void CanReconnectWithSameObservable() { - var port = NetworkHelpers.GetFreeTcpPortNumber(); - using (CreateServer(port)) { - var client = WebHostHelpers.GetGraphQLClient(port, serializer: Serializer); - var callbackMonitor = client.ConfigureMonitorForOnWebsocketConnected(); - - Debug.WriteLine("creating subscription stream"); - var observable = client.CreateSubscriptionStream(SubscriptionRequest); - - Debug.WriteLine("subscribing..."); - var tester = observable.Monitor(); - callbackMonitor.Should().HaveBeenInvokedWithPayload(); - - const string message1 = "Hello World"; - var response = await client.AddMessageAsync(message1).ConfigureAwait(false); - response.Data.AddMessage.Content.Should().Be(message1); - tester.Should().HaveReceivedPayload() - .Which.Data.MessageAdded.Content.Should().Be(message1); - - const string message2 = "How are you?"; - response = await client.AddMessageAsync(message2).ConfigureAwait(false); - response.Data.AddMessage.Content.Should().Be(message2); - tester.Should().HaveReceivedPayload() - .Which.Data.MessageAdded.Content.Should().Be(message2); - - Debug.WriteLine("disposing subscription..."); - tester.Dispose(); // does not close the websocket connection - - Debug.WriteLine("creating new subscription..."); - tester = observable.Monitor(); - tester.Should().HaveReceivedPayload(TimeSpan.FromSeconds(10)) - .Which.Data.MessageAdded.Content.Should().Be(message2); - - const string message3 = "lorem ipsum dolor si amet"; - response = await client.AddMessageAsync(message3).ConfigureAwait(false); - response.Data.AddMessage.Content.Should().Be(message3); - tester.Should().HaveReceivedPayload() - .Which.Data.MessageAdded.Content.Should().Be(message3); - - // disposing the client should complete the subscription - client.Dispose(); - tester.Should().HaveCompleted(); - } + var callbackMonitor = ChatClient.ConfigureMonitorForOnWebsocketConnected(); + + Debug.WriteLine("creating subscription stream"); + var observable = ChatClient.CreateSubscriptionStream(SubscriptionRequest); + + Debug.WriteLine("subscribing..."); + var tester = observable.Monitor(); + callbackMonitor.Should().HaveBeenInvokedWithPayload(); + + const string message1 = "Hello World"; + var response = await ChatClient.AddMessageAsync(message1).ConfigureAwait(false); + response.Data.AddMessage.Content.Should().Be(message1); + tester.Should().HaveReceivedPayload(3.Seconds()) + .Which.Data.MessageAdded.Content.Should().Be(message1); + + const string message2 = "How are you?"; + response = await ChatClient.AddMessageAsync(message2).ConfigureAwait(false); + response.Data.AddMessage.Content.Should().Be(message2); + tester.Should().HaveReceivedPayload() + .Which.Data.MessageAdded.Content.Should().Be(message2); + + Debug.WriteLine("disposing subscription..."); + tester.Dispose(); // does not close the websocket connection + + Debug.WriteLine("creating new subscription..."); + var tester2 = observable.Monitor(); + tester2.Should().HaveReceivedPayload(TimeSpan.FromSeconds(10)) + .Which.Data.MessageAdded.Content.Should().Be(message2); + + const string message3 = "lorem ipsum dolor si amet"; + response = await ChatClient.AddMessageAsync(message3).ConfigureAwait(false); + response.Data.AddMessage.Content.Should().Be(message3); + tester2.Should().HaveReceivedPayload() + .Which.Data.MessageAdded.Content.Should().Be(message3); + + // disposing the client should complete the subscription + ChatClient.Dispose(); + tester2.Should().HaveCompleted(); + tester2.Dispose(); } private const string SubscriptionQuery2 = @" @@ -235,75 +211,66 @@ public async void CanConnectTwoSubscriptionsSimultaneously() { var port = NetworkHelpers.GetFreeTcpPortNumber(); var callbackTester = new CallbackMonitor(); var callbackTester2 = new CallbackMonitor(); - using (CreateServer(port)) { - var client = WebHostHelpers.GetGraphQLClient(port, serializer: Serializer); - var callbackMonitor = client.ConfigureMonitorForOnWebsocketConnected(); - await client.InitializeWebsocketConnection(); - callbackMonitor.Should().HaveBeenInvokedWithPayload(); - - Debug.WriteLine("creating subscription stream"); - IObservable> observable1 = - client.CreateSubscriptionStream(SubscriptionRequest, callbackTester.Invoke); - IObservable> observable2 = - client.CreateSubscriptionStream(SubscriptionRequest2, callbackTester2.Invoke); - - Debug.WriteLine("subscribing..."); - var tester = observable1.Monitor(); - var tester2 = observable2.Monitor(); - - const string message1 = "Hello World"; - var response = await client.AddMessageAsync(message1).ConfigureAwait(false); - response.Data.AddMessage.Content.Should().Be(message1); - tester.Should().HaveReceivedPayload() - .Which.Data.MessageAdded.Content.Should().Be(message1); - - var joinResponse = await client.JoinDeveloperUser().ConfigureAwait(false); - joinResponse.Data.Join.DisplayName.Should().Be("developer", "because that's the display name of user \"1\""); - - var payload = tester2.Should().HaveReceivedPayload().Subject; - payload.Data.UserJoined.Id.Should().Be("1", "because that's the id we sent with our mutation request"); - payload.Data.UserJoined.DisplayName.Should().Be("developer", "because that's the display name of user \"1\""); - Debug.WriteLine("disposing subscription..."); - tester2.Dispose(); - - const string message3 = "lorem ipsum dolor si amet"; - response = await client.AddMessageAsync(message3).ConfigureAwait(false); - response.Data.AddMessage.Content.Should().Be(message3); - tester.Should().HaveReceivedPayload() - .Which.Data.MessageAdded.Content.Should().Be(message3); - - // disposing the client should complete the subscription - client.Dispose(); - tester.Should().HaveCompleted(); - } + var callbackMonitor = ChatClient.ConfigureMonitorForOnWebsocketConnected(); + await ChatClient.InitializeWebsocketConnection(); + callbackMonitor.Should().HaveBeenInvokedWithPayload(); + + Debug.WriteLine("creating subscription stream"); + var observable1 = ChatClient.CreateSubscriptionStream(SubscriptionRequest, callbackTester.Invoke); + var observable2 = ChatClient.CreateSubscriptionStream(SubscriptionRequest2, callbackTester2.Invoke); + + Debug.WriteLine("subscribing..."); + var tester = observable1.Monitor(); + var tester2 = observable2.Monitor(); + + const string message1 = "Hello World"; + var response = await ChatClient.AddMessageAsync(message1).ConfigureAwait(false); + response.Data.AddMessage.Content.Should().Be(message1); + tester.Should().HaveReceivedPayload() + .Which.Data.MessageAdded.Content.Should().Be(message1); + + var joinResponse = await ChatClient.JoinDeveloperUser().ConfigureAwait(false); + joinResponse.Data.Join.DisplayName.Should().Be("developer", "because that's the display name of user \"1\""); + + var payload = tester2.Should().HaveReceivedPayload().Subject; + payload.Data.UserJoined.Id.Should().Be("1", "because that's the id we sent with our mutation request"); + payload.Data.UserJoined.DisplayName.Should().Be("developer", "because that's the display name of user \"1\""); + + Debug.WriteLine("disposing subscription..."); + tester2.Dispose(); + + const string message3 = "lorem ipsum dolor si amet"; + response = await ChatClient.AddMessageAsync(message3).ConfigureAwait(false); + response.Data.AddMessage.Content.Should().Be(message3); + tester.Should().HaveReceivedPayload() + .Which.Data.MessageAdded.Content.Should().Be(message3); + + // disposing the client should complete the subscription + ChatClient.Dispose(); + tester.Should().HaveCompleted(); } [Fact] public async void CanHandleConnectionTimeout() { - var port = NetworkHelpers.GetFreeTcpPortNumber(); - var server = CreateServer(port); var errorMonitor = new CallbackMonitor(); var reconnectBlocker = new ManualResetEventSlim(false); - var client = WebHostHelpers.GetGraphQLClient(port, serializer: Serializer); - var callbackMonitor = client.ConfigureMonitorForOnWebsocketConnected(); + var callbackMonitor = ChatClient.ConfigureMonitorForOnWebsocketConnected(); // configure back-off strategy to allow it to be controlled from within the unit test - client.Options.BackOffStrategy = i => { + ChatClient.Options.BackOffStrategy = i => { reconnectBlocker.Wait(); return TimeSpan.Zero; }; var websocketStates = new ConcurrentQueue(); - using (client.WebsocketConnectionState.Subscribe(websocketStates.Enqueue)) { + using (ChatClient.WebsocketConnectionState.Subscribe(websocketStates.Enqueue)) { websocketStates.Should().ContainSingle(state => state == GraphQLWebsocketConnectionState.Disconnected); Debug.WriteLine("creating subscription stream"); - IObservable> observable = - client.CreateSubscriptionStream(SubscriptionRequest, - errorMonitor.Invoke); + var observable = ChatClient.CreateSubscriptionStream(SubscriptionRequest, errorMonitor.Invoke); Debug.WriteLine("subscribing..."); var tester = observable.Monitor(); @@ -317,21 +284,20 @@ public async void CanHandleConnectionTimeout() { websocketStates.Clear(); const string message1 = "Hello World"; - var response = await client.AddMessageAsync(message1).ConfigureAwait(false); + var response = await ChatClient.AddMessageAsync(message1).ConfigureAwait(false); response.Data.AddMessage.Content.Should().Be(message1); tester.Should().HaveReceivedPayload() .Which.Data.MessageAdded.Content.Should().Be(message1); Debug.WriteLine("stopping web host..."); - await server.StopAsync(CancellationToken.None).ConfigureAwait(false); - server.Dispose(); + await Fixture.ShutdownServer(); Debug.WriteLine("web host stopped..."); errorMonitor.Should().HaveBeenInvokedWithPayload(TimeSpan.FromSeconds(10)) .Which.Should().BeOfType(); websocketStates.Should().Contain(GraphQLWebsocketConnectionState.Disconnected); - server = CreateServer(port); + Fixture.CreateServer(); reconnectBlocker.Set(); callbackMonitor.Should().HaveBeenInvokedWithPayload(); websocketStates.Should().ContainInOrder( @@ -340,71 +306,63 @@ public async void CanHandleConnectionTimeout() { GraphQLWebsocketConnectionState.Connected); // disposing the client should complete the subscription - client.Dispose(); + ChatClient.Dispose(); tester.Should().HaveCompleted(TimeSpan.FromSeconds(5)); - server.Dispose(); } } [Fact] public async void CanHandleSubscriptionError() { - var port = NetworkHelpers.GetFreeTcpPortNumber(); - using (CreateServer(port)) { - var client = WebHostHelpers.GetGraphQLClient(port, serializer: Serializer); - var callbackMonitor = client.ConfigureMonitorForOnWebsocketConnected(); - await client.InitializeWebsocketConnection(); - callbackMonitor.Should().HaveBeenInvokedWithPayload(); - Debug.WriteLine("creating subscription stream"); - IObservable> observable = client.CreateSubscriptionStream( - new GraphQLRequest(@" - subscription { - failImmediately { - content - } - }") - ); + var callbackMonitor = ChatClient.ConfigureMonitorForOnWebsocketConnected(); + await ChatClient.InitializeWebsocketConnection(); + callbackMonitor.Should().HaveBeenInvokedWithPayload(); + Debug.WriteLine("creating subscription stream"); + IObservable> observable = ChatClient.CreateSubscriptionStream( + new GraphQLRequest(@" + subscription { + failImmediately { + content + } + }") + ); - Debug.WriteLine("subscribing..."); - using (var tester = observable.Monitor()) { - tester.Should().HaveReceivedPayload(TimeSpan.FromSeconds(3)) - .Which.Errors.Should().ContainSingle(); - tester.Should().HaveCompleted(); - client.Dispose(); - } + Debug.WriteLine("subscribing..."); + using (var tester = observable.Monitor()) { + tester.Should().HaveReceivedPayload(TimeSpan.FromSeconds(3)) + .Which.Errors.Should().ContainSingle(); + tester.Should().HaveCompleted(); + ChatClient.Dispose(); } + } [Fact] public async void CanHandleQueryErrorInSubscription() { - var port = NetworkHelpers.GetFreeTcpPortNumber(); - using (CreateServer(port)) { - - var test = new GraphQLRequest("tset", new { test = "blaa" }); - - var client = WebHostHelpers.GetGraphQLClient(port, serializer: Serializer); - var callbackMonitor = client.ConfigureMonitorForOnWebsocketConnected(); - await client.InitializeWebsocketConnection(); - callbackMonitor.Should().HaveBeenInvokedWithPayload(); - Debug.WriteLine("creating subscription stream"); - IObservable> observable = client.CreateSubscriptionStream( - new GraphQLRequest(@" - subscription { - fieldDoesNotExist { - content - } - }") - ); - - Debug.WriteLine("subscribing..."); - using (var tester = observable.Monitor()) { - tester.Should().HaveReceivedPayload() - .Which.Errors.Should().ContainSingle(); - tester.Should().HaveCompleted(); - client.Dispose(); - } + var test = new GraphQLRequest("tset", new { test = "blaa" }); + + var callbackMonitor = ChatClient.ConfigureMonitorForOnWebsocketConnected(); + await ChatClient.InitializeWebsocketConnection(); + callbackMonitor.Should().HaveBeenInvokedWithPayload(); + Debug.WriteLine("creating subscription stream"); + IObservable> observable = ChatClient.CreateSubscriptionStream( + new GraphQLRequest(@" + subscription { + fieldDoesNotExist { + content + } + }") + ); + + Debug.WriteLine("subscribing..."); + using (var tester = observable.Monitor()) { + tester.Should().HaveReceivedPayload() + .Which.Errors.Should().ContainSingle(); + tester.Should().HaveCompleted(); + ChatClient.Dispose(); } } + } } diff --git a/tests/GraphQL.Integration.Tests/WebsocketTests/Newtonsoft.cs b/tests/GraphQL.Integration.Tests/WebsocketTests/Newtonsoft.cs index 02a0b030..76169663 100644 --- a/tests/GraphQL.Integration.Tests/WebsocketTests/Newtonsoft.cs +++ b/tests/GraphQL.Integration.Tests/WebsocketTests/Newtonsoft.cs @@ -1,10 +1,11 @@ using GraphQL.Client.Serializer.Newtonsoft; +using GraphQL.Integration.Tests.Helpers; +using Xunit; using Xunit.Abstractions; namespace GraphQL.Integration.Tests.WebsocketTests { - public class Newtonsoft: Base { - public Newtonsoft(ITestOutputHelper output) : base(output, new NewtonsoftJsonSerializer()) - { - } + public class Newtonsoft: Base, IClassFixture { + public Newtonsoft(ITestOutputHelper output, NewtonsoftIntegrationServerTestFixture fixture) : base(output, fixture) + { } } } diff --git a/tests/GraphQL.Integration.Tests/WebsocketTests/SystemTextJson.cs b/tests/GraphQL.Integration.Tests/WebsocketTests/SystemTextJson.cs index 3a7882e4..fc7698ed 100644 --- a/tests/GraphQL.Integration.Tests/WebsocketTests/SystemTextJson.cs +++ b/tests/GraphQL.Integration.Tests/WebsocketTests/SystemTextJson.cs @@ -1,9 +1,10 @@ -using GraphQL.Client.Serializer.SystemTextJson; +using GraphQL.Integration.Tests.Helpers; +using Xunit; using Xunit.Abstractions; namespace GraphQL.Integration.Tests.WebsocketTests { - public class SystemTextJson: Base { - public SystemTextJson(ITestOutputHelper output) : base(output, new SystemTextJsonSerializer()) + public class SystemTextJson: Base, IClassFixture { + public SystemTextJson(ITestOutputHelper output, SystemTextJsonIntegrationServerTestFixture fixture) : base(output, fixture) { } } diff --git a/tests/IntegrationTestServer/Program.cs b/tests/IntegrationTestServer/Program.cs index 67cbd96b..ec3530e1 100644 --- a/tests/IntegrationTestServer/Program.cs +++ b/tests/IntegrationTestServer/Program.cs @@ -10,7 +10,7 @@ public static void Main(string[] args) { public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) - .UseStartup() + .UseStartup() .ConfigureLogging((ctx, logging) => logging.SetMinimumLevel(LogLevel.Debug)); } } diff --git a/tests/IntegrationTestServer/Startup.cs b/tests/IntegrationTestServer/Startup.cs index 0ae7280e..2f25f733 100644 --- a/tests/IntegrationTestServer/Startup.cs +++ b/tests/IntegrationTestServer/Startup.cs @@ -1,8 +1,11 @@ using GraphQL; +using GraphQL.Client.Tests.Common; +using GraphQL.Client.Tests.Common.Chat.Schema; using GraphQL.Server; using GraphQL.Server.Ui.GraphiQL; -using GraphQL.Server.Ui.Voyager; using GraphQL.Server.Ui.Playground; +using GraphQL.StarWars; +using GraphQL.Types; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Server.Kestrel.Core; @@ -11,8 +14,8 @@ using Microsoft.Extensions.Hosting; namespace IntegrationTestServer { - public abstract class Startup { - protected Startup(IConfiguration configuration, IWebHostEnvironment environment) { + public class Startup { + public Startup(IConfiguration configuration, IWebHostEnvironment environment) { Configuration = configuration; Environment = environment; } @@ -29,9 +32,8 @@ public void ConfigureServices(IServiceCollection services) { }); services.AddTransient(provider => new FuncDependencyResolver(provider.GetService)); - - ConfigureGraphQLSchemaServices(services); - + services.AddChatSchema(); + services.AddStarWarsSchema(); services.AddGraphQL(options => { options.EnableMetrics = true; options.ExposeExceptions = Environment.IsDevelopment(); @@ -39,9 +41,6 @@ public void ConfigureServices(IServiceCollection services) { .AddWebSockets(); } - public abstract void ConfigureGraphQLSchemaServices(IServiceCollection services); - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { @@ -50,21 +49,23 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseWebSockets(); - ConfigureGraphQLSchema(app); + ConfigureGraphQLSchema(app, Common.ChatEndpoint); + ConfigureGraphQLSchema(app, Common.StarWarsEndpoint); app.UseGraphiQLServer(new GraphiQLOptions { GraphiQLPath = "/ui/graphiql", - GraphQLEndPoint = "/graphql" - }); - app.UseGraphQLVoyager(new GraphQLVoyagerOptions() { - GraphQLEndPoint = "/graphql", - Path = "/ui/voyager" + GraphQLEndPoint = Common.StarWarsEndpoint }); app.UseGraphQLPlayground(new GraphQLPlaygroundOptions { - Path = "/ui/playground" + Path = "/ui/playground", + GraphQLEndPoint = Common.ChatEndpoint }); } - public abstract void ConfigureGraphQLSchema(IApplicationBuilder app); + private void ConfigureGraphQLSchema(IApplicationBuilder app, string endpoint) where TSchema: Schema + { + app.UseGraphQLWebSockets(endpoint); + app.UseGraphQL(endpoint); + } } } diff --git a/tests/IntegrationTestServer/StartupChat.cs b/tests/IntegrationTestServer/StartupChat.cs deleted file mode 100644 index 2a782991..00000000 --- a/tests/IntegrationTestServer/StartupChat.cs +++ /dev/null @@ -1,22 +0,0 @@ -using GraphQL.Client.Tests.Common; -using GraphQL.Client.Tests.Common.Chat.Schema; -using GraphQL.Server; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; - -namespace IntegrationTestServer { - public class StartupChat: Startup { - public StartupChat(IConfiguration configuration, IWebHostEnvironment environment): base(configuration, environment) { } - - public override void ConfigureGraphQLSchemaServices(IServiceCollection services) { - services.AddChatSchema(); - } - - public override void ConfigureGraphQLSchema(IApplicationBuilder app) { - app.UseGraphQLWebSockets("/graphql"); - app.UseGraphQL("/graphql"); - } - } -} diff --git a/tests/IntegrationTestServer/StartupStarWars.cs b/tests/IntegrationTestServer/StartupStarWars.cs deleted file mode 100644 index 15609515..00000000 --- a/tests/IntegrationTestServer/StartupStarWars.cs +++ /dev/null @@ -1,22 +0,0 @@ -using GraphQL.Client.Tests.Common; -using GraphQL.Server; -using GraphQL.StarWars; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; - -namespace IntegrationTestServer { - public class StartupStarWars: Startup { - public StartupStarWars(IConfiguration configuration, IWebHostEnvironment environment): base(configuration, environment) { } - - public override void ConfigureGraphQLSchemaServices(IServiceCollection services) { - services.AddStarWarsSchema(); - } - - public override void ConfigureGraphQLSchema(IApplicationBuilder app) { - app.UseGraphQLWebSockets("/graphql"); - app.UseGraphQL("/graphql"); - } - } -} From da094ae55a0f0546282f3919336d32a83bf44814 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Wed, 26 Feb 2020 14:19:22 +0100 Subject: [PATCH 03/16] remove configure await --- .gitignore | 1 + .../Helpers/IntegrationServerTestFixture.cs | 8 ++-- .../Helpers/WebHostHelpers.cs | 12 ++++-- .../QueryAndMutationTests/Base.cs | 36 +++++++++-------- .../WebsocketTests/Base.cs | 39 +++++++++++-------- 5 files changed, 53 insertions(+), 43 deletions(-) diff --git a/.gitignore b/.gitignore index f173174b..be040ad5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .idea/ .vs/ +.vscode/ bin/ obj/ *.user diff --git a/tests/GraphQL.Integration.Tests/Helpers/IntegrationServerTestFixture.cs b/tests/GraphQL.Integration.Tests/Helpers/IntegrationServerTestFixture.cs index 3b20ccbe..e8344a13 100644 --- a/tests/GraphQL.Integration.Tests/Helpers/IntegrationServerTestFixture.cs +++ b/tests/GraphQL.Integration.Tests/Helpers/IntegrationServerTestFixture.cs @@ -17,13 +17,11 @@ public abstract class IntegrationServerTestFixture { public IntegrationServerTestFixture() { Port = NetworkHelpers.GetFreeTcpPortNumber(); - CreateServer(); } - public void CreateServer() { - if(Server != null) - throw new InvalidOperationException("server is already created"); - Server = WebHostHelpers.CreateServer(Port); + public async Task CreateServer() { + if (Server != null) return; + Server = await WebHostHelpers.CreateServer(Port); } public async Task ShutdownServer() { diff --git a/tests/GraphQL.Integration.Tests/Helpers/WebHostHelpers.cs b/tests/GraphQL.Integration.Tests/Helpers/WebHostHelpers.cs index 59084c6f..923c11d1 100644 --- a/tests/GraphQL.Integration.Tests/Helpers/WebHostHelpers.cs +++ b/tests/GraphQL.Integration.Tests/Helpers/WebHostHelpers.cs @@ -1,6 +1,6 @@ using System; using System.Reactive.Disposables; -using GraphQL.Client; +using System.Threading.Tasks; using GraphQL.Client.Abstractions.Websocket; using GraphQL.Client.Http; using GraphQL.Client.Serializer.Newtonsoft; @@ -9,13 +9,15 @@ using IntegrationTestServer; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace GraphQL.Integration.Tests.Helpers { public static class WebHostHelpers { - public static IWebHost CreateServer(int port) + public static async Task CreateServer(int port) { var configBuilder = new ConfigurationBuilder(); configBuilder.AddInMemoryCollection(); @@ -31,8 +33,10 @@ public static IWebHost CreateServer(int port) .UseStartup() .Build(); - host.Start(); - + var tcs = new TaskCompletionSource(); + host.Services.GetService().ApplicationStarted.Register(() => tcs.TrySetResult(true)); + await host.StartAsync(); + await tcs.Task; return host; } diff --git a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs index ea103901..92e61f8d 100644 --- a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs @@ -14,7 +14,7 @@ namespace GraphQL.Integration.Tests.QueryAndMutationTests { - public abstract class Base { + public abstract class Base: IAsyncLifetime { protected IntegrationServerTestFixture Fixture; protected GraphQLHttpClient StarWarsClient; @@ -22,17 +22,25 @@ public abstract class Base { protected Base(IntegrationServerTestFixture fixture) { Fixture = fixture; + } + + public async Task InitializeAsync() { + await Fixture.CreateServer(); StarWarsClient = Fixture.GetStarWarsClient(); ChatClient = Fixture.GetChatClient(); } - + + public Task DisposeAsync() { + ChatClient?.Dispose(); + StarWarsClient?.Dispose(); + return Task.CompletedTask; + } + [Theory] [ClassData(typeof(StarWarsHumans))] public async void QueryTheory(int id, string name) { var graphQLRequest = new GraphQLRequest($"{{ human(id: \"{id}\") {{ name }} }}"); - - var response = await StarWarsClient.SendQueryAsync(graphQLRequest, () => new { Human = new { Name = string.Empty }}) - .ConfigureAwait(false); + var response = await StarWarsClient.SendQueryAsync(graphQLRequest, () => new { Human = new { Name = string.Empty }}); Assert.Null(response.Errors); Assert.Equal(name, response.Data.Human.Name); @@ -43,8 +51,7 @@ public async void QueryTheory(int id, string name) { public async void QueryWithDynamicReturnTypeTheory(int id, string name) { var graphQLRequest = new GraphQLRequest($"{{ human(id: \"{id}\") {{ name }} }}"); - var response = await StarWarsClient.SendQueryAsync(graphQLRequest) - .ConfigureAwait(false); + var response = await StarWarsClient.SendQueryAsync(graphQLRequest); Assert.Null(response.Errors); Assert.Equal(name, response.Data.human.name.ToString()); @@ -61,8 +68,7 @@ query Human($id: String!){ }", new {id = id.ToString()}); - var response = await StarWarsClient.SendQueryAsync(graphQLRequest, () => new { Human = new { Name = string.Empty } }) - .ConfigureAwait(false); + var response = await StarWarsClient.SendQueryAsync(graphQLRequest, () => new { Human = new { Name = string.Empty } }); Assert.Null(response.Errors); Assert.Equal(name, response.Data.Human.Name); @@ -86,8 +92,7 @@ query Droid($id: String!) { new { id = id.ToString() }, "Human"); - var response = await StarWarsClient.SendQueryAsync(graphQLRequest, () => new { Human = new { Name = string.Empty } }) - .ConfigureAwait(false); + var response = await StarWarsClient.SendQueryAsync(graphQLRequest, () => new { Human = new { Name = string.Empty } }); Assert.Null(response.Errors); Assert.Equal(name, response.Data.Human.Name); @@ -118,16 +123,14 @@ query Human($id: String!){ Name = "", HomePlanet = "" } - }) - .ConfigureAwait(false); + }); Assert.Null(mutationResponse.Errors); Assert.Equal("Han Solo", mutationResponse.Data.createHuman.Name); Assert.Equal("Corellia", mutationResponse.Data.createHuman.HomePlanet); queryRequest.Variables = new {id = mutationResponse.Data.createHuman.Id}; - var queryResponse = await StarWarsClient.SendQueryAsync(queryRequest, () => new { Human = new { Name = string.Empty } }) - .ConfigureAwait(false); + var queryResponse = await StarWarsClient.SendQueryAsync(queryRequest, () => new { Human = new { Name = string.Empty } }); Assert.Null(queryResponse.Errors); Assert.Equal("Han Solo", queryResponse.Data.Human.Name); @@ -141,8 +144,7 @@ public async void PreprocessHttpRequestMessageIsCalled() { }; var defaultHeaders = StarWarsClient.HttpClient.DefaultRequestHeaders; - var response = await StarWarsClient.SendQueryAsync(graphQLRequest, () => new { Human = new { Name = string.Empty } }) - .ConfigureAwait(false); + var response = await StarWarsClient.SendQueryAsync(graphQLRequest, () => new { Human = new { Name = string.Empty } }); callbackTester.CallbackShouldHaveBeenInvoked(message => { Assert.Equal(defaultHeaders, message.Headers); }); diff --git a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs index e4bd5010..d4c6ade2 100644 --- a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.Diagnostics; using System.Net.WebSockets; +using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; using FluentAssertions; @@ -14,6 +15,7 @@ using GraphQL.Client.Tests.Common.Helpers; using GraphQL.Integration.Tests.Helpers; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Xunit; using Xunit.Abstractions; @@ -29,9 +31,12 @@ protected Base(ITestOutputHelper output, IntegrationServerTestFixture fixture) { } public async Task InitializeAsync() { + await Fixture.CreateServer(); Fixture.Server.Services.GetService().Reset(); - ChatClient = Fixture.GetChatClient(true); - Output.WriteLine($"ChatClient: {ChatClient.GetHashCode()}"); + if (ChatClient == null) { + ChatClient = Fixture.GetChatClient(true); + Output.WriteLine($"ChatClient: {ChatClient.GetHashCode()}"); + } } public Task DisposeAsync() { @@ -43,7 +48,7 @@ public Task DisposeAsync() { public async void CanSendRequestViaWebsocket() { await ChatClient.InitializeWebsocketConnection(); const string message = "some random testing message"; - var response = await ChatClient.AddMessageAsync(message).ConfigureAwait(false); + var response = await ChatClient.AddMessageAsync(message); response.Data.AddMessage.Content.Should().Be(message); } @@ -90,7 +95,7 @@ query Long { [Fact] public async void CanHandleRequestErrorViaWebsocket() { await ChatClient.InitializeWebsocketConnection(); - var response = await ChatClient.SendQueryAsync("this query is formatted quite badly").ConfigureAwait(false); + var response = await ChatClient.SendQueryAsync("this query is formatted quite badly"); response.Errors.Should().ContainSingle("because the query is invalid"); } @@ -117,13 +122,13 @@ public async void CanCreateObservableSubscription() { using var tester = observable.Monitor(); const string message1 = "Hello World"; - var response = await ChatClient.AddMessageAsync(message1).ConfigureAwait(false); + var response = await ChatClient.AddMessageAsync(message1); response.Data.AddMessage.Content.Should().Be(message1); tester.Should().HaveReceivedPayload(TimeSpan.FromSeconds(3)) .Which.Data.MessageAdded.Content.Should().Be(message1); const string message2 = "lorem ipsum dolor si amet"; - response = await ChatClient.AddMessageAsync(message2).ConfigureAwait(false); + response = await ChatClient.AddMessageAsync(message2); response.Data.AddMessage.Content.Should().Be(message2); tester.Should().HaveReceivedPayload() .Which.Data.MessageAdded.Content.Should().Be(message2); @@ -154,13 +159,13 @@ public async void CanReconnectWithSameObservable() { callbackMonitor.Should().HaveBeenInvokedWithPayload(); const string message1 = "Hello World"; - var response = await ChatClient.AddMessageAsync(message1).ConfigureAwait(false); + var response = await ChatClient.AddMessageAsync(message1); response.Data.AddMessage.Content.Should().Be(message1); - tester.Should().HaveReceivedPayload(3.Seconds()) + tester.Should().HaveReceivedPayload(10.Seconds()) .Which.Data.MessageAdded.Content.Should().Be(message1); const string message2 = "How are you?"; - response = await ChatClient.AddMessageAsync(message2).ConfigureAwait(false); + response = await ChatClient.AddMessageAsync(message2); response.Data.AddMessage.Content.Should().Be(message2); tester.Should().HaveReceivedPayload() .Which.Data.MessageAdded.Content.Should().Be(message2); @@ -174,7 +179,7 @@ public async void CanReconnectWithSameObservable() { .Which.Data.MessageAdded.Content.Should().Be(message2); const string message3 = "lorem ipsum dolor si amet"; - response = await ChatClient.AddMessageAsync(message3).ConfigureAwait(false); + response = await ChatClient.AddMessageAsync(message3); response.Data.AddMessage.Content.Should().Be(message3); tester2.Should().HaveReceivedPayload() .Which.Data.MessageAdded.Content.Should().Be(message3); @@ -225,12 +230,12 @@ public async void CanConnectTwoSubscriptionsSimultaneously() { var tester2 = observable2.Monitor(); const string message1 = "Hello World"; - var response = await ChatClient.AddMessageAsync(message1).ConfigureAwait(false); + var response = await ChatClient.AddMessageAsync(message1); response.Data.AddMessage.Content.Should().Be(message1); tester.Should().HaveReceivedPayload() .Which.Data.MessageAdded.Content.Should().Be(message1); - var joinResponse = await ChatClient.JoinDeveloperUser().ConfigureAwait(false); + var joinResponse = await ChatClient.JoinDeveloperUser(); joinResponse.Data.Join.DisplayName.Should().Be("developer", "because that's the display name of user \"1\""); var payload = tester2.Should().HaveReceivedPayload().Subject; @@ -241,7 +246,7 @@ public async void CanConnectTwoSubscriptionsSimultaneously() { tester2.Dispose(); const string message3 = "lorem ipsum dolor si amet"; - response = await ChatClient.AddMessageAsync(message3).ConfigureAwait(false); + response = await ChatClient.AddMessageAsync(message3); response.Data.AddMessage.Content.Should().Be(message3); tester.Should().HaveReceivedPayload() .Which.Data.MessageAdded.Content.Should().Be(message3); @@ -284,9 +289,9 @@ public async void CanHandleConnectionTimeout() { websocketStates.Clear(); const string message1 = "Hello World"; - var response = await ChatClient.AddMessageAsync(message1).ConfigureAwait(false); + var response = await ChatClient.AddMessageAsync(message1); response.Data.AddMessage.Content.Should().Be(message1); - tester.Should().HaveReceivedPayload() + tester.Should().HaveReceivedPayload(TimeSpan.FromSeconds(10)) .Which.Data.MessageAdded.Content.Should().Be(message1); Debug.WriteLine("stopping web host..."); @@ -297,9 +302,9 @@ public async void CanHandleConnectionTimeout() { .Which.Should().BeOfType(); websocketStates.Should().Contain(GraphQLWebsocketConnectionState.Disconnected); - Fixture.CreateServer(); + await InitializeAsync(); reconnectBlocker.Set(); - callbackMonitor.Should().HaveBeenInvokedWithPayload(); + callbackMonitor.Should().HaveBeenInvokedWithPayload(TimeSpan.FromSeconds(10)); websocketStates.Should().ContainInOrder( GraphQLWebsocketConnectionState.Disconnected, GraphQLWebsocketConnectionState.Connecting, From b218496d3854b47e588c0316cf1128dc3ec3be46 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Mon, 2 Mar 2020 15:51:32 +0100 Subject: [PATCH 04/16] fix reconnect test --- .../Chat/Schema/IChat.cs | 60 +++--------------- .../Helpers/ObservableTester.cs | 8 ++- .../WebsocketTests/Base.cs | 61 ++++++++++++------- 3 files changed, 53 insertions(+), 76 deletions(-) diff --git a/tests/GraphQL.Client.Tests.Common/Chat/Schema/IChat.cs b/tests/GraphQL.Client.Tests.Common/Chat/Schema/IChat.cs index 6d53e42b..9ebfbd1a 100644 --- a/tests/GraphQL.Client.Tests.Common/Chat/Schema/IChat.cs +++ b/tests/GraphQL.Client.Tests.Common/Chat/Schema/IChat.cs @@ -16,22 +16,17 @@ public interface IChat { Message AddMessage(ReceivedMessage message); } - + public class Chat : IChat { - private readonly RollingReplaySubject _messageStream = new RollingReplaySubject(); - private readonly ISubject _userJoined = new Subject(); + private readonly ISubject messageStream = new ReplaySubject(1); + private readonly ISubject userJoined = new Subject(); public Chat() { - Reset(); - } - - public void Reset() { AllMessages = new ConcurrentStack(); Users = new ConcurrentDictionary { ["1"] = "developer", ["2"] = "tester" }; - _messageStream.Clear(); } public ConcurrentDictionary Users { get; private set; } @@ -55,7 +50,7 @@ public Message AddMessage(ReceivedMessage message) { public Message AddMessage(Message message) { AllMessages.Push(message); - _messageStream.OnNext(message); + messageStream.OnNext(message); return message; } @@ -69,12 +64,12 @@ public MessageFrom Join(string userId) { DisplayName = displayName }; - _userJoined.OnNext(joinedUser); + userJoined.OnNext(joinedUser); return joinedUser; } public IObservable Messages(string user) { - return _messageStream + return messageStream .Select(message => { message.Sub = user; return message; @@ -83,11 +78,11 @@ public IObservable Messages(string user) { } public void AddError(Exception exception) { - _messageStream.OnError(exception); + messageStream.OnError(exception); } public IObservable UserJoined() { - return _userJoined.AsObservable(); + return userJoined.AsObservable(); } } @@ -95,43 +90,4 @@ public class User { public string Id { get; set; } public string Name { get; set; } } - - public class RollingReplaySubject : ISubject { - private readonly ReplaySubject> _subjects; - private readonly IObservable _concatenatedSubjects; - private ISubject _currentSubject; - - public RollingReplaySubject() { - _subjects = new ReplaySubject>(1); - _concatenatedSubjects = _subjects.Concat(); - _currentSubject = new ReplaySubject(); - _subjects.OnNext(_currentSubject); - } - - public void Clear() { - _currentSubject.OnCompleted(); - _currentSubject = new ReplaySubject(); - _subjects.OnNext(_currentSubject); - } - - public void OnNext(T value) { - _currentSubject.OnNext(value); - } - - public void OnError(Exception error) { - _currentSubject.OnError(error); - } - - public void OnCompleted() { - _currentSubject.OnCompleted(); - _subjects.OnCompleted(); - // a quick way to make the current ReplaySubject unreachable - // except to in-flight observers, and not hold up collection - _currentSubject = new Subject(); - } - - public IDisposable Subscribe(IObserver observer) { - return _concatenatedSubjects.Subscribe(observer); - } - } } diff --git a/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs b/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs index 43bcbe18..ec850d38 100644 --- a/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs +++ b/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Reactive.Concurrency; using System.Reactive.Linq; using System.Threading; @@ -9,6 +10,7 @@ namespace GraphQL.Client.Tests.Common.Helpers { public class ObservableTester : IDisposable { private readonly IDisposable subscription; + private readonly EventLoopScheduler scheduler; private readonly ManualResetEventSlim updateReceived = new ManualResetEventSlim(); private readonly ManualResetEventSlim completed = new ManualResetEventSlim(); private readonly ManualResetEventSlim error = new ManualResetEventSlim(); @@ -34,12 +36,15 @@ public class ObservableTester : IDisposable { /// /// the under test public ObservableTester(IObservable observable) { - subscription = observable.ObserveOn(TaskPoolScheduler.Default).Subscribe( + scheduler = new EventLoopScheduler(); + subscription = observable.SubscribeOn(Scheduler.CurrentThread).ObserveOn(scheduler).Subscribe( obj => { + Debug.WriteLine($"observable tester {GetHashCode()}: payload received"); LastPayload = obj; updateReceived.Set(); }, ex => { + Debug.WriteLine($"observable tester {GetHashCode()} error received: {ex}"); Error = ex; error.Set(); }, @@ -57,6 +62,7 @@ private void Reset() { /// public void Dispose() { subscription?.Dispose(); + scheduler?.Dispose(); } public SubscriptionAssertions Should() { diff --git a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs index d4c6ade2..bcfedb98 100644 --- a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs @@ -15,7 +15,6 @@ using GraphQL.Client.Tests.Common.Helpers; using GraphQL.Integration.Tests.Helpers; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Xunit; using Xunit.Abstractions; @@ -30,12 +29,20 @@ protected Base(ITestOutputHelper output, IntegrationServerTestFixture fixture) { this.Fixture = fixture; } + protected static ReceivedMessage InitialMessage = new ReceivedMessage { + Content = "initial message", + SentAt = DateTime.Now, + FromId = "1" + }; + public async Task InitializeAsync() { await Fixture.CreateServer(); - Fixture.Server.Services.GetService().Reset(); + // make sure the buffer always contains the same message + Fixture.Server.Services.GetService().AddMessage(InitialMessage); + if (ChatClient == null) { + // then create the chat client ChatClient = Fixture.GetChatClient(true); - Output.WriteLine($"ChatClient: {ChatClient.GetHashCode()}"); } } @@ -120,18 +127,17 @@ public async void CanCreateObservableSubscription() { Debug.WriteLine("subscribing..."); using var tester = observable.Monitor(); - const string message1 = "Hello World"; + tester.Should().HaveReceivedPayload().Which.Data.MessageAdded.Content.Should().Be(InitialMessage.Content); + const string message1 = "Hello World"; var response = await ChatClient.AddMessageAsync(message1); response.Data.AddMessage.Content.Should().Be(message1); - tester.Should().HaveReceivedPayload(TimeSpan.FromSeconds(3)) - .Which.Data.MessageAdded.Content.Should().Be(message1); + tester.Should().HaveReceivedPayload().Which.Data.MessageAdded.Content.Should().Be(message1); const string message2 = "lorem ipsum dolor si amet"; response = await ChatClient.AddMessageAsync(message2); response.Data.AddMessage.Content.Should().Be(message2); - tester.Should().HaveReceivedPayload() - .Which.Data.MessageAdded.Content.Should().Be(message2); + tester.Should().HaveReceivedPayload().Which.Data.MessageAdded.Content.Should().Be(message2); // disposing the client should throw a TaskCanceledException on the subscription ChatClient.Dispose(); @@ -157,32 +163,32 @@ public async void CanReconnectWithSameObservable() { Debug.WriteLine("subscribing..."); var tester = observable.Monitor(); callbackMonitor.Should().HaveBeenInvokedWithPayload(); + await ChatClient.InitializeWebsocketConnection(); + Debug.WriteLine("websocket connection initialized"); + tester.Should().HaveReceivedPayload().Which.Data.MessageAdded.Content.Should().Be(InitialMessage.Content); const string message1 = "Hello World"; + Debug.WriteLine($"adding message {message1}"); var response = await ChatClient.AddMessageAsync(message1); response.Data.AddMessage.Content.Should().Be(message1); - tester.Should().HaveReceivedPayload(10.Seconds()) - .Which.Data.MessageAdded.Content.Should().Be(message1); + tester.Should().HaveReceivedPayload().Which.Data.MessageAdded.Content.Should().Be(message1); const string message2 = "How are you?"; response = await ChatClient.AddMessageAsync(message2); response.Data.AddMessage.Content.Should().Be(message2); - tester.Should().HaveReceivedPayload() - .Which.Data.MessageAdded.Content.Should().Be(message2); + tester.Should().HaveReceivedPayload().Which.Data.MessageAdded.Content.Should().Be(message2); Debug.WriteLine("disposing subscription..."); tester.Dispose(); // does not close the websocket connection Debug.WriteLine("creating new subscription..."); var tester2 = observable.Monitor(); - tester2.Should().HaveReceivedPayload(TimeSpan.FromSeconds(10)) - .Which.Data.MessageAdded.Content.Should().Be(message2); + tester2.Should().HaveReceivedPayload().Which.Data.MessageAdded.Content.Should().Be(message2); const string message3 = "lorem ipsum dolor si amet"; response = await ChatClient.AddMessageAsync(message3); response.Data.AddMessage.Content.Should().Be(message3); - tester2.Should().HaveReceivedPayload() - .Which.Data.MessageAdded.Content.Should().Be(message3); + tester2.Should().HaveReceivedPayload().Which.Data.MessageAdded.Content.Should().Be(message3); // disposing the client should complete the subscription ChatClient.Dispose(); @@ -229,6 +235,8 @@ public async void CanConnectTwoSubscriptionsSimultaneously() { var tester = observable1.Monitor(); var tester2 = observable2.Monitor(); + tester.Should().HaveReceivedPayload().Which.Data.MessageAdded.Content.Should().Be(InitialMessage.Content); + const string message1 = "Hello World"; var response = await ChatClient.AddMessageAsync(message1); response.Data.AddMessage.Content.Should().Be(message1); @@ -265,7 +273,9 @@ public async void CanHandleConnectionTimeout() { var callbackMonitor = ChatClient.ConfigureMonitorForOnWebsocketConnected(); // configure back-off strategy to allow it to be controlled from within the unit test ChatClient.Options.BackOffStrategy = i => { + Debug.WriteLine("back-off strategy: waiting on reconnect blocker"); reconnectBlocker.Wait(); + Debug.WriteLine("back-off strategy: reconnecting..."); return TimeSpan.Zero; }; @@ -288,23 +298,28 @@ public async void CanHandleConnectionTimeout() { // clear the collection so the next tests on the collection work as expected websocketStates.Clear(); + tester.Should().HaveReceivedPayload().Which.Data.MessageAdded.Content.Should().Be(InitialMessage.Content); + const string message1 = "Hello World"; var response = await ChatClient.AddMessageAsync(message1); response.Data.AddMessage.Content.Should().Be(message1); - tester.Should().HaveReceivedPayload(TimeSpan.FromSeconds(10)) - .Which.Data.MessageAdded.Content.Should().Be(message1); + tester.Should().HaveReceivedPayload().Which.Data.MessageAdded.Content.Should().Be(message1); Debug.WriteLine("stopping web host..."); await Fixture.ShutdownServer(); - Debug.WriteLine("web host stopped..."); + Debug.WriteLine("web host stopped"); - errorMonitor.Should().HaveBeenInvokedWithPayload(TimeSpan.FromSeconds(10)) + errorMonitor.Should().HaveBeenInvokedWithPayload(10.Seconds()) .Which.Should().BeOfType(); websocketStates.Should().Contain(GraphQLWebsocketConnectionState.Disconnected); - + + Debug.WriteLine("restarting web host..."); await InitializeAsync(); + Debug.WriteLine("web host started"); reconnectBlocker.Set(); - callbackMonitor.Should().HaveBeenInvokedWithPayload(TimeSpan.FromSeconds(10)); + callbackMonitor.Should().HaveBeenInvokedWithPayload(3.Seconds()); + tester.Should().HaveReceivedPayload().Which.Data.MessageAdded.Content.Should().Be(InitialMessage.Content); + websocketStates.Should().ContainInOrder( GraphQLWebsocketConnectionState.Disconnected, GraphQLWebsocketConnectionState.Connecting, @@ -312,7 +327,7 @@ public async void CanHandleConnectionTimeout() { // disposing the client should complete the subscription ChatClient.Dispose(); - tester.Should().HaveCompleted(TimeSpan.FromSeconds(5)); + tester.Should().HaveCompleted(5.Seconds()); } } From 5e83a24551d5e231de8f5f78c8031964421125d3 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Tue, 3 Mar 2020 12:15:04 +0100 Subject: [PATCH 05/16] add debug output --- .../Websocket/GraphQLHttpWebSocket.cs | 1 + .../Websocket/GraphQLHttpWebsocketHelpers.cs | 7 ++--- .../Helpers/ObservableTester.cs | 26 +++++++++++++------ .../WebsocketTests/Base.cs | 8 +++--- 4 files changed, 27 insertions(+), 15 deletions(-) diff --git a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs index 1f824c8e..ce1d2ff5 100644 --- a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs +++ b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs @@ -179,6 +179,7 @@ private async Task _createResultStream(IObserver(); + Debug.WriteLine($"creating new response stream {responseSubject.GetHashCode()}"); // initialize and connect websocket await InitializeWebSocket().ConfigureAwait(false); diff --git a/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs b/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs index f4eeebd5..b74f373b 100644 --- a/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs +++ b/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs @@ -133,9 +133,10 @@ internal static IObservable> CreateSubscriptionStream } // throw exception on the observable to be caught by Retry() or complete sequence if cancellation was requested - return cancellationToken.IsCancellationRequested - ? Observable.Empty, Exception>>() - : Observable.Throw, Exception>>(e); + if (cancellationToken.IsCancellationRequested) + return Observable.Empty, Exception>>(); + else + return Observable.Throw, Exception>>(e); } catch (Exception exception) { // wrap all other exceptions to be propagated behind retry diff --git a/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs b/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs index ec850d38..00580f14 100644 --- a/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs +++ b/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs @@ -1,7 +1,5 @@ using System; using System.Diagnostics; -using System.Reactive.Concurrency; -using System.Reactive.Linq; using System.Threading; using FluentAssertions; using FluentAssertions.Execution; @@ -10,7 +8,6 @@ namespace GraphQL.Client.Tests.Common.Helpers { public class ObservableTester : IDisposable { private readonly IDisposable subscription; - private readonly EventLoopScheduler scheduler; private readonly ManualResetEventSlim updateReceived = new ManualResetEventSlim(); private readonly ManualResetEventSlim completed = new ManualResetEventSlim(); private readonly ManualResetEventSlim error = new ManualResetEventSlim(); @@ -36,8 +33,7 @@ public class ObservableTester : IDisposable { /// /// the under test public ObservableTester(IObservable observable) { - scheduler = new EventLoopScheduler(); - subscription = observable.SubscribeOn(Scheduler.CurrentThread).ObserveOn(scheduler).Subscribe( + subscription = observable.Subscribe( obj => { Debug.WriteLine($"observable tester {GetHashCode()}: payload received"); LastPayload = obj; @@ -48,8 +44,10 @@ public ObservableTester(IObservable observable) { Error = ex; error.Set(); }, - () => completed.Set() - ); + () => { + Debug.WriteLine($"observable tester {GetHashCode()}: completed"); + completed.Set(); + }); } /// @@ -62,7 +60,6 @@ private void Reset() { /// public void Dispose() { subscription?.Dispose(); - scheduler?.Dispose(); } public SubscriptionAssertions Should() { @@ -130,6 +127,19 @@ public AndConstraint> HaveCompleted(TimeSpan ti } public AndConstraint> HaveCompleted(string because = "", params object[] becauseArgs) => HaveCompleted(Subject.Timeout, because, becauseArgs); + + public AndConstraint> NotHaveCompleted(TimeSpan timeout, + string because = "", params object[] becauseArgs) { + Execute.Assertion + .BecauseOf(because, becauseArgs) + .Given(() => Subject.completed.Wait(timeout)) + .ForCondition(isSet => !isSet) + .FailWith("Expected {context:Subscription} not to complete within {0}{reason}, but it did!", timeout); + + return new AndConstraint>(this); + } + public AndConstraint> NotHaveCompleted(string because = "", params object[] becauseArgs) + => NotHaveCompleted(Subject.Timeout, because, becauseArgs); } } diff --git a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs index bcfedb98..062a3071 100644 --- a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs @@ -81,7 +81,7 @@ query Long { // unblock the query chatQuery.LongRunningQueryBlocker.Set(); // check execution time - request.Invoking().ExecutionTime().Should().BeLessThan(100.Milliseconds()); + request.Invoking().ExecutionTime().Should().BeLessThan(500.Milliseconds()); request.Invoke().Result.Data.longRunning.Should().Be("finally returned"); // reset stuff @@ -93,7 +93,7 @@ query Long { chatQuery.WaitingOnQueryBlocker.Wait(500).Should().BeTrue("because the request should have reached the server by then"); cts.Cancel(); FluentActions.Awaiting(() => request.Invoking().Should().ThrowAsync("because the request was cancelled")) - .ExecutionTime().Should().BeLessThan(100.Milliseconds()); + .ExecutionTime().Should().BeLessThan(500.Milliseconds()); // let the server finish its query chatQuery.LongRunningQueryBlocker.Set(); @@ -312,12 +312,12 @@ public async void CanHandleConnectionTimeout() { errorMonitor.Should().HaveBeenInvokedWithPayload(10.Seconds()) .Which.Should().BeOfType(); websocketStates.Should().Contain(GraphQLWebsocketConnectionState.Disconnected); - + Debug.WriteLine("restarting web host..."); await InitializeAsync(); Debug.WriteLine("web host started"); reconnectBlocker.Set(); - callbackMonitor.Should().HaveBeenInvokedWithPayload(3.Seconds()); + callbackMonitor.Should().HaveBeenInvokedWithPayload(10.Seconds()); tester.Should().HaveReceivedPayload().Which.Data.MessageAdded.Content.Should().Be(InitialMessage.Content); websocketStates.Should().ContainInOrder( From ce40801cb3c8a77651e09d3641093d8c637ff628 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Wed, 4 Mar 2020 14:52:26 +0100 Subject: [PATCH 06/16] consolidate naming, optimize threading in websocket --- .../IGraphQLWebsocketJsonSerializer.cs | 2 +- ...eWrapper.cs => WebsocketMessageWrapper.cs} | 2 +- .../NewtonsoftJsonSerializer.cs | 4 +- .../SystemTextJsonSerializer.cs | 4 +- .../Websocket/GraphQLHttpWebSocket.cs | 91 +++++++++++-------- .../Websocket/GraphQLHttpWebsocketHelpers.cs | 11 ++- .../Helpers/ObservableTester.cs | 4 +- .../WebsocketTests/Base.cs | 2 +- 8 files changed, 68 insertions(+), 52 deletions(-) rename src/GraphQL.Client.Abstractions.Websocket/{WebsocketResponseWrapper.cs => WebsocketMessageWrapper.cs} (69%) diff --git a/src/GraphQL.Client.Abstractions.Websocket/IGraphQLWebsocketJsonSerializer.cs b/src/GraphQL.Client.Abstractions.Websocket/IGraphQLWebsocketJsonSerializer.cs index e2f445f5..376f45b6 100644 --- a/src/GraphQL.Client.Abstractions.Websocket/IGraphQLWebsocketJsonSerializer.cs +++ b/src/GraphQL.Client.Abstractions.Websocket/IGraphQLWebsocketJsonSerializer.cs @@ -10,7 +10,7 @@ namespace GraphQL.Client.Abstractions.Websocket public interface IGraphQLWebsocketJsonSerializer: IGraphQLJsonSerializer { byte[] SerializeToBytes(GraphQLWebSocketRequest request); - Task DeserializeToWebsocketResponseWrapperAsync(Stream stream); + Task DeserializeToWebsocketResponseWrapperAsync(Stream stream); GraphQLWebSocketResponse> DeserializeToWebsocketResponse(byte[] bytes); } diff --git a/src/GraphQL.Client.Abstractions.Websocket/WebsocketResponseWrapper.cs b/src/GraphQL.Client.Abstractions.Websocket/WebsocketMessageWrapper.cs similarity index 69% rename from src/GraphQL.Client.Abstractions.Websocket/WebsocketResponseWrapper.cs rename to src/GraphQL.Client.Abstractions.Websocket/WebsocketMessageWrapper.cs index 5e90a38f..2a27677e 100644 --- a/src/GraphQL.Client.Abstractions.Websocket/WebsocketResponseWrapper.cs +++ b/src/GraphQL.Client.Abstractions.Websocket/WebsocketMessageWrapper.cs @@ -1,7 +1,7 @@ using System.Runtime.Serialization; namespace GraphQL.Client.Abstractions.Websocket { - public class WebsocketResponseWrapper : GraphQLWebSocketResponse { + public class WebsocketMessageWrapper : GraphQLWebSocketResponse { [IgnoreDataMember] public byte[] MessageBytes { get; set; } diff --git a/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs b/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs index a40ad456..7194a547 100644 --- a/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs +++ b/src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs @@ -42,8 +42,8 @@ public byte[] SerializeToBytes(Abstractions.Websocket.GraphQLWebSocketRequest re return Encoding.UTF8.GetBytes(json); } - public Task DeserializeToWebsocketResponseWrapperAsync(Stream stream) { - return DeserializeFromUtf8Stream(stream); + public Task DeserializeToWebsocketResponseWrapperAsync(Stream stream) { + return DeserializeFromUtf8Stream(stream); } public GraphQLWebSocketResponse> DeserializeToWebsocketResponse(byte[] bytes) { diff --git a/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs b/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs index 420a8522..62be2e37 100644 --- a/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs +++ b/src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs @@ -45,8 +45,8 @@ public byte[] SerializeToBytes(Abstractions.Websocket.GraphQLWebSocketRequest re return JsonSerializer.SerializeToUtf8Bytes(new GraphQLWebSocketRequest(request), Options); } - public Task DeserializeToWebsocketResponseWrapperAsync(Stream stream) { - return JsonSerializer.DeserializeAsync(stream, Options).AsTask(); + public Task DeserializeToWebsocketResponseWrapperAsync(Stream stream) { + return JsonSerializer.DeserializeAsync(stream, Options).AsTask(); } public GraphQLWebSocketResponse> DeserializeToWebsocketResponse(byte[] bytes) { diff --git a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs index ce1d2ff5..fec8b75d 100644 --- a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs +++ b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs @@ -3,6 +3,7 @@ using System.IO; using System.Net.Http; using System.Net.WebSockets; +using System.Reactive.Concurrency; using System.Reactive.Disposables; using System.Reactive.Linq; using System.Reactive.Subjects; @@ -23,9 +24,11 @@ internal class GraphQLHttpWebSocket : IDisposable { private readonly BehaviorSubject stateSubject = new BehaviorSubject(GraphQLWebsocketConnectionState.Disconnected); private readonly IDisposable requestSubscription; + private readonly EventLoopScheduler receiveLoopScheduler = new EventLoopScheduler(); + private readonly EventLoopScheduler sendLoopScheduler = new EventLoopScheduler(); private int connectionAttempt = 0; - private Subject responseSubject; + private Subject incomingMessagesSubject; private GraphQLHttpClientOptions Options => client.Options; #if NETFRAMEWORK @@ -39,27 +42,28 @@ internal class GraphQLHttpWebSocket : IDisposable { public IObservable ReceiveErrors => exceptionSubject.AsObservable(); public IObservable ConnectionState => stateSubject.DistinctUntilChanged(); - public IObservable ResponseStream { get; } + public IObservable ResponseStream { get; } public GraphQLHttpWebSocket(Uri webSocketUri, GraphQLHttpClient client) { cancellationToken = cancellationTokenSource.Token; this.webSocketUri = webSocketUri; this.client = client; buffer = new ArraySegment(new byte[8192]); - ResponseStream = _createResponseStream(); + ResponseStream = GetMessageStream(); - requestSubscription = requestSubject.Select(request => Observable.FromAsync(() => _sendWebSocketRequest(request))).Concat().Subscribe(); + requestSubscription = requestSubject + .ObserveOn(sendLoopScheduler) + .Subscribe(async request => await SendWebSocketRequest(request)); } - - + #region Send requests - public Task SendWebSocketRequest(GraphQLWebSocketRequest request) { + public Task QueueWebSocketRequest(GraphQLWebSocketRequest request) { requestSubject.OnNext(request); return request.SendTask(); } - private async Task _sendWebSocketRequest(GraphQLWebSocketRequest request) { + private async Task SendWebSocketRequest(GraphQLWebSocketRequest request) { try { if (cancellationToken.IsCancellationRequested) { request.SendCanceled(); @@ -128,13 +132,13 @@ public Task InitializeWebSocket() { clientWebSocket.Options.ClientCertificates = ((HttpClientHandler)Options.HttpMessageHandler).ClientCertificates; clientWebSocket.Options.UseDefaultCredentials = ((HttpClientHandler)Options.HttpMessageHandler).UseDefaultCredentials; #endif - return initializeWebSocketTask = _connectAsync(cancellationToken); + return initializeWebSocketTask = ConnectAsync(cancellationToken); } } - private async Task _connectAsync(CancellationToken token) { + private async Task ConnectAsync(CancellationToken token) { try { - await _backOff().ConfigureAwait(false); + await BackOff().ConfigureAwait(false); stateSubject.OnNext(GraphQLWebsocketConnectionState.Connecting); Debug.WriteLine($"opening websocket {clientWebSocket.GetHashCode()}"); await clientWebSocket.ConnectAsync(webSocketUri, token).ConfigureAwait(false); @@ -155,74 +159,79 @@ private async Task _connectAsync(CancellationToken token) { /// delay the next connection attempt using /// /// - private Task _backOff() { + private Task BackOff() { connectionAttempt++; if (connectionAttempt == 1) return Task.CompletedTask; var delay = Options.BackOffStrategy?.Invoke(connectionAttempt - 1) ?? TimeSpan.FromSeconds(5); Debug.WriteLine($"connection attempt #{connectionAttempt}, backing off for {delay.TotalSeconds} s"); - return Task.Delay(delay); + return Task.Delay(delay, cancellationToken); } - private IObservable _createResponseStream() { - return Observable.Create(_createResultStream) + private IObservable GetMessageStream() { + return Observable.Create(CreateMessageStream) // complete sequence on OperationCanceledException, this is triggered by the cancellation token on disposal - .Catch(exception => - Observable.Empty()); + .Catch(exception => + Observable.Empty()); } - private async Task _createResultStream(IObserver observer, CancellationToken token) { - cancellationToken.ThrowIfCancellationRequested(); + private async Task CreateMessageStream(IObserver observer, CancellationToken token) { + var cts = CancellationTokenSource.CreateLinkedTokenSource(token, cancellationToken); + cts.Token.ThrowIfCancellationRequested(); - if (responseSubject == null || responseSubject.IsDisposed) { + if (incomingMessagesSubject == null || incomingMessagesSubject.IsDisposed) { // create new response subject - responseSubject = new Subject(); - Debug.WriteLine($"creating new response stream {responseSubject.GetHashCode()}"); + incomingMessagesSubject = new Subject(); + Debug.WriteLine($"creating new incoming message stream {incomingMessagesSubject.GetHashCode()}"); // initialize and connect websocket await InitializeWebSocket().ConfigureAwait(false); // loop the receive task and subscribe the created subject to the results - Observable.Defer(() => _getReceiveTask().ToObservable()).Repeat().Subscribe(responseSubject); + Observable + .Defer(() => GetReceiveTask().ToObservable()) + .Repeat() + .SubscribeOn(receiveLoopScheduler) + .Subscribe(incomingMessagesSubject); // dispose the subject on any error or completion (will be recreated) - responseSubject.Subscribe(_ => { }, ex => { + incomingMessagesSubject.Subscribe(_ => { }, ex => { exceptionSubject.OnNext(ex); - responseSubject?.Dispose(); - responseSubject = null; + incomingMessagesSubject?.Dispose(); + incomingMessagesSubject = null; stateSubject.OnNext(GraphQLWebsocketConnectionState.Disconnected); }, () => { - responseSubject?.Dispose(); - responseSubject = null; + incomingMessagesSubject?.Dispose(); + incomingMessagesSubject = null; stateSubject.OnNext(GraphQLWebsocketConnectionState.Disconnected); }); } return new CompositeDisposable ( - responseSubject.Subscribe(observer), + incomingMessagesSubject.Subscribe(observer), Disposable.Create(() => { - Debug.WriteLine($"response stream {responseSubject.GetHashCode()} disposed"); + Debug.WriteLine($"incoming message stream {incomingMessagesSubject.GetHashCode()} disposed"); }) ); } - private Task receiveAsyncTask = null; + private Task receiveAsyncTask = null; private readonly object receiveTaskLocker = new object(); /// /// wrapper method to pick up the existing request task if already running /// /// - private Task _getReceiveTask() { + private Task GetReceiveTask() { lock (receiveTaskLocker) { cancellationToken.ThrowIfCancellationRequested(); if (receiveAsyncTask == null || receiveAsyncTask.IsFaulted || receiveAsyncTask.IsCompleted) - receiveAsyncTask = _receiveResultAsync(); + receiveAsyncTask = ReceiveWebsocketMessagesAsync(); } return receiveAsyncTask; @@ -232,9 +241,9 @@ private Task _getReceiveTask() { /// read a single message from the websocket /// /// - private async Task _receiveResultAsync() { + private async Task ReceiveWebsocketMessagesAsync() { try { - Debug.WriteLine($"receiving data on websocket {clientWebSocket.GetHashCode()} ..."); + Debug.WriteLine($"waiting for data on websocket {clientWebSocket.GetHashCode()} ..."); using (var ms = new MemoryStream()) { WebSocketReceiveResult webSocketReceiveResult = null; @@ -251,6 +260,7 @@ private async Task _receiveResultAsync() { if (webSocketReceiveResult.MessageType == WebSocketMessageType.Text) { var response = await Options.JsonSerializer.DeserializeToWebsocketResponseWrapperAsync(ms); response.MessageBytes = ms.ToArray(); + Debug.WriteLine($"{response.MessageBytes.Length} bytes received on websocket {clientWebSocket.GetHashCode()} ..."); return response; } else { @@ -264,7 +274,7 @@ private async Task _receiveResultAsync() { } } - private async Task _closeAsync() { + private async Task CloseAsync() { if (clientWebSocket == null) return; @@ -304,12 +314,15 @@ private async Task CompleteAsync() { Debug.WriteLine($"disposing websocket {clientWebSocket.GetHashCode()}..."); if (!cancellationTokenSource.IsCancellationRequested) cancellationTokenSource.Cancel(); - await _closeAsync().ConfigureAwait(false); + await CloseAsync().ConfigureAwait(false); requestSubscription?.Dispose(); clientWebSocket?.Dispose(); - responseSubject?.OnCompleted(); - responseSubject?.Dispose(); + incomingMessagesSubject?.OnCompleted(); + incomingMessagesSubject?.Dispose(); + + sendLoopScheduler?.Dispose(); + receiveLoopScheduler?.Dispose(); stateSubject?.OnCompleted(); stateSubject?.Dispose(); diff --git a/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs b/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs index b74f373b..11f29bfc 100644 --- a/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs +++ b/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs @@ -4,6 +4,7 @@ using System.Reactive.Disposables; using System.Reactive.Linq; using System.Reactive.Threading.Tasks; +using System.Text; using System.Threading; using System.Threading.Tasks; using GraphQL.Client.Abstractions.Websocket; @@ -85,7 +86,7 @@ internal static IObservable> CreateSubscriptionStream try { Debug.WriteLine($"sending close message on subscription {startRequest.Id}"); - await graphQlHttpWebSocket.SendWebSocketRequest(closeRequest).ConfigureAwait(false); + await graphQlHttpWebSocket.QueueWebSocketRequest(closeRequest).ConfigureAwait(false); } // do not break on disposing catch (OperationCanceledException) { } @@ -95,7 +96,7 @@ internal static IObservable> CreateSubscriptionStream // send connection init Debug.WriteLine($"sending connection init on subscription {startRequest.Id}"); try { - await graphQlHttpWebSocket.SendWebSocketRequest(initRequest).ConfigureAwait(false); + await graphQlHttpWebSocket.QueueWebSocketRequest(initRequest).ConfigureAwait(false); } catch (Exception e) { Console.WriteLine(e); @@ -105,7 +106,7 @@ internal static IObservable> CreateSubscriptionStream Debug.WriteLine($"sending initial message on subscription {startRequest.Id}"); // send subscription request try { - await graphQlHttpWebSocket.SendWebSocketRequest(startRequest).ConfigureAwait(false); + await graphQlHttpWebSocket.QueueWebSocketRequest(startRequest).ConfigureAwait(false); } catch (Exception e) { Console.WriteLine(e); @@ -198,7 +199,7 @@ internal static Task> SendRequest( Debug.WriteLine($"submitting request {websocketRequest.Id}"); // send request try { - await graphQlHttpWebSocket.SendWebSocketRequest(websocketRequest).ConfigureAwait(false); + await graphQlHttpWebSocket.QueueWebSocketRequest(websocketRequest).ConfigureAwait(false); } catch (Exception e) { Console.WriteLine(e); @@ -210,7 +211,7 @@ internal static Task> SendRequest( // complete sequence on OperationCanceledException, this is triggered by the cancellation token .Catch, OperationCanceledException>(exception => Observable.Empty>()) - .FirstOrDefaultAsync() + .FirstAsync() .ToTask(cancellationToken); } } diff --git a/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs b/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs index 00580f14..4d070e71 100644 --- a/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs +++ b/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs @@ -1,5 +1,7 @@ using System; using System.Diagnostics; +using System.Reactive.Concurrency; +using System.Reactive.Linq; using System.Threading; using FluentAssertions; using FluentAssertions.Execution; @@ -33,7 +35,7 @@ public class ObservableTester : IDisposable { /// /// the under test public ObservableTester(IObservable observable) { - subscription = observable.Subscribe( + subscription = observable.SubscribeOn(Scheduler.Default).Subscribe( obj => { Debug.WriteLine($"observable tester {GetHashCode()}: payload received"); LastPayload = obj; diff --git a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs index 062a3071..662ca85b 100644 --- a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs @@ -317,7 +317,7 @@ public async void CanHandleConnectionTimeout() { await InitializeAsync(); Debug.WriteLine("web host started"); reconnectBlocker.Set(); - callbackMonitor.Should().HaveBeenInvokedWithPayload(10.Seconds()); + callbackMonitor.Should().HaveBeenInvokedWithPayload(3.Seconds()); tester.Should().HaveReceivedPayload().Which.Data.MessageAdded.Content.Should().Be(InitialMessage.Content); websocketStates.Should().ContainInOrder( From a6dbfb9732515653bd3d65e3ed5522f555ff0108 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Wed, 4 Mar 2020 15:02:47 +0100 Subject: [PATCH 07/16] allow 500ms for cancellation --- tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs index 92e61f8d..773e4feb 100644 --- a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs @@ -173,7 +173,7 @@ query Long { // unblock the query chatQuery.LongRunningQueryBlocker.Set(); // check execution time - request.Invoking().ExecutionTime().Should().BeLessThan(100.Milliseconds()); + request.Invoking().ExecutionTime().Should().BeLessThan(500.Milliseconds()); request.Invoke().Result.Data.longRunning.Should().Be("finally returned"); // reset stuff @@ -185,7 +185,7 @@ query Long { chatQuery.WaitingOnQueryBlocker.Wait(500).Should().BeTrue("because the request should have reached the server by then"); cts.Cancel(); FluentActions.Awaiting(() => request.Invoking().Should().ThrowAsync("because the request was cancelled")) - .ExecutionTime().Should().BeLessThan(100.Milliseconds()); + .ExecutionTime().Should().BeLessThan(500.Milliseconds()); // let the server finish its query chatQuery.LongRunningQueryBlocker.Set(); From b501c04e51471cbfd339464a284bf72e38ca107a Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Wed, 4 Mar 2020 15:09:01 +0100 Subject: [PATCH 08/16] extend default timeout --- tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs | 2 +- tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs b/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs index 4d070e71..1d468deb 100644 --- a/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs +++ b/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs @@ -17,7 +17,7 @@ public class ObservableTester : IDisposable { /// /// The timeout for . Defaults to 1 s /// - public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(1); + public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(3); /// /// Indicates that an update has been received since the last diff --git a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs index 773e4feb..415c7f32 100644 --- a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs @@ -169,7 +169,7 @@ query Long { // start request request.Start(); // wait until the query has reached the server - chatQuery.WaitingOnQueryBlocker.Wait(500).Should().BeTrue("because the request should have reached the server by then"); + chatQuery.WaitingOnQueryBlocker.Wait(1000).Should().BeTrue("because the request should have reached the server by then"); // unblock the query chatQuery.LongRunningQueryBlocker.Set(); // check execution time @@ -182,7 +182,7 @@ query Long { // cancellation test request.Start(); - chatQuery.WaitingOnQueryBlocker.Wait(500).Should().BeTrue("because the request should have reached the server by then"); + chatQuery.WaitingOnQueryBlocker.Wait(1000).Should().BeTrue("because the request should have reached the server by then"); cts.Cancel(); FluentActions.Awaiting(() => request.Invoking().Should().ThrowAsync("because the request was cancelled")) .ExecutionTime().Should().BeLessThan(500.Milliseconds()); From 7837eddd97a8a11fcc14573c6aeb5efb83cf6cb8 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Mon, 9 Mar 2020 09:28:13 +0100 Subject: [PATCH 09/16] remove ConfigureAwait --- .../GraphQLLocalExecutionClient.cs | 8 +-- .../Websocket/GraphQLHttpWebSocket.cs | 66 +++++++++++-------- .../Websocket/GraphQLHttpWebsocketHelpers.cs | 21 +++--- .../BaseSerializerTest.cs | 6 +- .../Helpers/CallbackMonitor.cs | 43 +++--------- .../Helpers/ObservableTester.cs | 12 +++- .../QueryAndMutationTests/Base.cs | 4 +- .../WebsocketTests/Base.cs | 1 + 8 files changed, 81 insertions(+), 80 deletions(-) diff --git a/src/GraphQL.Client.LocalExecution/GraphQLLocalExecutionClient.cs b/src/GraphQL.Client.LocalExecution/GraphQLLocalExecutionClient.cs index d73e2cc6..6624020c 100644 --- a/src/GraphQL.Client.LocalExecution/GraphQLLocalExecutionClient.cs +++ b/src/GraphQL.Client.LocalExecution/GraphQLLocalExecutionClient.cs @@ -76,11 +76,11 @@ public IObservable> CreateSubscriptionStream> ExecuteQueryAsync(GraphQLRequest request, CancellationToken cancellationToken) { - var executionResult = await ExecuteAsync(request, cancellationToken).ConfigureAwait(false); - return await ExecutionResultToGraphQLResponse(executionResult, cancellationToken).ConfigureAwait(false); + var executionResult = await ExecuteAsync(request, cancellationToken); + return await ExecutionResultToGraphQLResponse(executionResult, cancellationToken); } private async Task>> ExecuteSubscriptionAsync(GraphQLRequest request, CancellationToken cancellationToken = default) { - var result = await ExecuteAsync(request, cancellationToken).ConfigureAwait(false); + var result = await ExecuteAsync(request, cancellationToken); return ((SubscriptionExecutionResult)result).Streams?.Values.SingleOrDefault()? .SelectMany(executionResult => Observable.FromAsync(token => ExecutionResultToGraphQLResponse(executionResult, token))); } @@ -100,7 +100,7 @@ private async Task ExecuteAsync(GraphQLRequest request, Cancell options.Query = request.Query; options.Inputs = inputs; options.CancellationToken = cancellationToken; - }).ConfigureAwait(false); + }); return result; } diff --git a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs index fec8b75d..fccf7894 100644 --- a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs +++ b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs @@ -14,6 +14,9 @@ namespace GraphQL.Client.Http.Websocket { internal class GraphQLHttpWebSocket : IDisposable { + + #region Private fields + private readonly Uri webSocketUri; private readonly GraphQLHttpClient client; private readonly ArraySegment buffer; @@ -29,6 +32,7 @@ internal class GraphQLHttpWebSocket : IDisposable { private int connectionAttempt = 0; private Subject incomingMessagesSubject; + private IDisposable incomingMessagesDisposable; private GraphQLHttpClientOptions Options => client.Options; #if NETFRAMEWORK @@ -37,11 +41,11 @@ internal class GraphQLHttpWebSocket : IDisposable { private ClientWebSocket clientWebSocket = null; #endif - + #endregion + public WebSocketState WebSocketState => clientWebSocket?.State ?? WebSocketState.None; public IObservable ReceiveErrors => exceptionSubject.AsObservable(); public IObservable ConnectionState => stateSubject.DistinctUntilChanged(); - public IObservable ResponseStream { get; } public GraphQLHttpWebSocket(Uri webSocketUri, GraphQLHttpClient client) { @@ -50,6 +54,8 @@ public GraphQLHttpWebSocket(Uri webSocketUri, GraphQLHttpClient client) { this.client = client; buffer = new ArraySegment(new byte[8192]); ResponseStream = GetMessageStream(); + receiveLoopScheduler.Schedule(() => + Debug.WriteLine($"receive loop scheduler thread id: {Thread.CurrentThread.ManagedThreadId}")); requestSubscription = requestSubject .ObserveOn(sendLoopScheduler) @@ -70,13 +76,13 @@ private async Task SendWebSocketRequest(GraphQLWebSocketRequest request) { return; } - await InitializeWebSocket().ConfigureAwait(false); + await InitializeWebSocket(); var requestBytes = Options.JsonSerializer.SerializeToBytes(request); await this.clientWebSocket.SendAsync( new ArraySegment(requestBytes), WebSocketMessageType.Text, true, - cancellationToken).ConfigureAwait(false); + cancellationToken); request.SendCompleted(); } catch (Exception e) { @@ -138,10 +144,10 @@ public Task InitializeWebSocket() { private async Task ConnectAsync(CancellationToken token) { try { - await BackOff().ConfigureAwait(false); + await BackOff(); stateSubject.OnNext(GraphQLWebsocketConnectionState.Connecting); Debug.WriteLine($"opening websocket {clientWebSocket.GetHashCode()}"); - await clientWebSocket.ConnectAsync(webSocketUri, token).ConfigureAwait(false); + await clientWebSocket.ConnectAsync(webSocketUri, token); stateSubject.OnNext(GraphQLWebsocketConnectionState.Connected); Debug.WriteLine($"connection established on websocket {clientWebSocket.GetHashCode()}, invoking Options.OnWebsocketConnected()"); await (Options.OnWebsocketConnected?.Invoke(client) ?? Task.CompletedTask); @@ -181,42 +187,49 @@ private async Task CreateMessageStream(IObserver(); Debug.WriteLine($"creating new incoming message stream {incomingMessagesSubject.GetHashCode()}"); // initialize and connect websocket - await InitializeWebSocket().ConfigureAwait(false); + await InitializeWebSocket(); // loop the receive task and subscribe the created subject to the results - Observable + var receiveLoopSubscription = Observable .Defer(() => GetReceiveTask().ToObservable()) .Repeat() - .SubscribeOn(receiveLoopScheduler) .Subscribe(incomingMessagesSubject); + + incomingMessagesDisposable = new CompositeDisposable( + incomingMessagesSubject, + receiveLoopSubscription, + Disposable.Create(() => { + Debug.WriteLine($"incoming message stream {incomingMessagesSubject.GetHashCode()} disposed"); + })); // dispose the subject on any error or completion (will be recreated) incomingMessagesSubject.Subscribe(_ => { }, ex => { exceptionSubject.OnNext(ex); - incomingMessagesSubject?.Dispose(); + incomingMessagesDisposable?.Dispose(); incomingMessagesSubject = null; stateSubject.OnNext(GraphQLWebsocketConnectionState.Disconnected); }, () => { - incomingMessagesSubject?.Dispose(); + incomingMessagesDisposable?.Dispose(); incomingMessagesSubject = null; stateSubject.OnNext(GraphQLWebsocketConnectionState.Disconnected); }); } - return new CompositeDisposable - ( - incomingMessagesSubject.Subscribe(observer), - Disposable.Create(() => { - Debug.WriteLine($"incoming message stream {incomingMessagesSubject.GetHashCode()} disposed"); - }) - ); + var subscription = new CompositeDisposable(incomingMessagesSubject.Subscribe(observer)); + var hashCode = subscription.GetHashCode(); + subscription.Add(Disposable.Create(() => { + Debug.WriteLine($"incoming message subscription {hashCode} disposed"); + })); + Debug.WriteLine($"new incoming message subscription {hashCode} created"); + return subscription; } private Task receiveAsyncTask = null; @@ -243,7 +256,7 @@ private Task GetReceiveTask() { /// private async Task ReceiveWebsocketMessagesAsync() { try { - Debug.WriteLine($"waiting for data on websocket {clientWebSocket.GetHashCode()} ..."); + Debug.WriteLine($"waiting for data on websocket {clientWebSocket.GetHashCode()} (thread {Thread.CurrentThread.ManagedThreadId})..."); using (var ms = new MemoryStream()) { WebSocketReceiveResult webSocketReceiveResult = null; @@ -260,7 +273,7 @@ private async Task ReceiveWebsocketMessagesAsync() { if (webSocketReceiveResult.MessageType == WebSocketMessageType.Text) { var response = await Options.JsonSerializer.DeserializeToWebsocketResponseWrapperAsync(ms); response.MessageBytes = ms.ToArray(); - Debug.WriteLine($"{response.MessageBytes.Length} bytes received on websocket {clientWebSocket.GetHashCode()} ..."); + Debug.WriteLine($"{response.MessageBytes.Length} bytes received on websocket {clientWebSocket.GetHashCode()} (thread {Thread.CurrentThread.ManagedThreadId})..."); return response; } else { @@ -287,7 +300,7 @@ private async Task CloseAsync() { } Debug.WriteLine($"closing websocket {clientWebSocket.GetHashCode()}"); - await this.clientWebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None).ConfigureAwait(false); + await this.clientWebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None); stateSubject.OnNext(GraphQLWebsocketConnectionState.Disconnected); } @@ -314,15 +327,12 @@ private async Task CompleteAsync() { Debug.WriteLine($"disposing websocket {clientWebSocket.GetHashCode()}..."); if (!cancellationTokenSource.IsCancellationRequested) cancellationTokenSource.Cancel(); - await CloseAsync().ConfigureAwait(false); + await CloseAsync(); requestSubscription?.Dispose(); clientWebSocket?.Dispose(); incomingMessagesSubject?.OnCompleted(); - incomingMessagesSubject?.Dispose(); - - sendLoopScheduler?.Dispose(); - receiveLoopScheduler?.Dispose(); + incomingMessagesDisposable?.Dispose(); stateSubject?.OnCompleted(); stateSubject?.Dispose(); @@ -330,6 +340,10 @@ private async Task CompleteAsync() { exceptionSubject?.OnCompleted(); exceptionSubject?.Dispose(); cancellationTokenSource.Dispose(); + + sendLoopScheduler?.Dispose(); + receiveLoopScheduler?.Dispose(); + Debug.WriteLine($"websocket {clientWebSocket.GetHashCode()} disposed"); } #endregion diff --git a/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs b/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs index 11f29bfc..82fc6f2d 100644 --- a/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs +++ b/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics; using System.Net.WebSockets; +using System.Reactive.Concurrency; using System.Reactive.Disposables; using System.Reactive.Linq; using System.Reactive.Threading.Tasks; @@ -19,6 +20,7 @@ internal static IObservable> CreateSubscriptionStream CancellationToken cancellationToken = default) { return Observable.Defer(() => Observable.Create>(async observer => { + Debug.WriteLine($"Create observable thread id: {Thread.CurrentThread.ManagedThreadId}"); await client.Options.PreprocessRequest(request, client); var startRequest = new GraphQLWebSocketRequest { Id = Guid.NewGuid().ToString("N"), @@ -47,7 +49,7 @@ internal static IObservable> CreateSubscriptionStream } // post the GraphQLResponse to the stream (even if a GraphQL error occurred) - Debug.WriteLine($"received payload on subscription {startRequest.Id}"); + Debug.WriteLine($"received payload on subscription {startRequest.Id} (thread {Thread.CurrentThread.ManagedThreadId})"); var typedResponse = client.Options.JsonSerializer.DeserializeToWebsocketResponse( response.MessageBytes); @@ -71,7 +73,7 @@ internal static IObservable> CreateSubscriptionStream try { // initialize websocket (completes immediately if socket is already open) - await graphQlHttpWebSocket.InitializeWebSocket().ConfigureAwait(false); + await graphQlHttpWebSocket.InitializeWebSocket(); } catch (Exception e) { // subscribe observer to failed observable @@ -86,7 +88,7 @@ internal static IObservable> CreateSubscriptionStream try { Debug.WriteLine($"sending close message on subscription {startRequest.Id}"); - await graphQlHttpWebSocket.QueueWebSocketRequest(closeRequest).ConfigureAwait(false); + await graphQlHttpWebSocket.QueueWebSocketRequest(closeRequest); } // do not break on disposing catch (OperationCanceledException) { } @@ -96,7 +98,7 @@ internal static IObservable> CreateSubscriptionStream // send connection init Debug.WriteLine($"sending connection init on subscription {startRequest.Id}"); try { - await graphQlHttpWebSocket.QueueWebSocketRequest(initRequest).ConfigureAwait(false); + await graphQlHttpWebSocket.QueueWebSocketRequest(initRequest); } catch (Exception e) { Console.WriteLine(e); @@ -106,7 +108,7 @@ internal static IObservable> CreateSubscriptionStream Debug.WriteLine($"sending initial message on subscription {startRequest.Id}"); // send subscription request try { - await graphQlHttpWebSocket.QueueWebSocketRequest(startRequest).ConfigureAwait(false); + await graphQlHttpWebSocket.QueueWebSocketRequest(startRequest); } catch (Exception e) { Console.WriteLine(e); @@ -136,8 +138,10 @@ internal static IObservable> CreateSubscriptionStream // throw exception on the observable to be caught by Retry() or complete sequence if cancellation was requested if (cancellationToken.IsCancellationRequested) return Observable.Empty, Exception>>(); - else + else { + Debug.WriteLine($"Catch handler thread id: {Thread.CurrentThread.ManagedThreadId}"); return Observable.Throw, Exception>>(e); + } } catch (Exception exception) { // wrap all other exceptions to be propagated behind retry @@ -148,6 +152,7 @@ internal static IObservable> CreateSubscriptionStream .Retry() // unwrap and push results or throw wrapped exceptions .SelectMany(t => { + Debug.WriteLine($"unwrap exception thread id: {Thread.CurrentThread.ManagedThreadId}"); // if the result contains an exception, throw it on the observable if (t.Item2 != null) return Observable.Throw>(t.Item2); @@ -185,7 +190,7 @@ internal static Task> SendRequest( try { // intialize websocket (completes immediately if socket is already open) - await graphQlHttpWebSocket.InitializeWebSocket().ConfigureAwait(false); + await graphQlHttpWebSocket.InitializeWebSocket(); } catch (Exception e) { // subscribe observer to failed observable @@ -199,7 +204,7 @@ internal static Task> SendRequest( Debug.WriteLine($"submitting request {websocketRequest.Id}"); // send request try { - await graphQlHttpWebSocket.QueueWebSocketRequest(websocketRequest).ConfigureAwait(false); + await graphQlHttpWebSocket.QueueWebSocketRequest(websocketRequest); } catch (Exception e) { Console.WriteLine(e); diff --git a/tests/GraphQL.Client.Serializer.Tests/BaseSerializerTest.cs b/tests/GraphQL.Client.Serializer.Tests/BaseSerializerTest.cs index a07f38f1..579e756e 100644 --- a/tests/GraphQL.Client.Serializer.Tests/BaseSerializerTest.cs +++ b/tests/GraphQL.Client.Serializer.Tests/BaseSerializerTest.cs @@ -46,7 +46,7 @@ public async void CanDeserializeExtensions() { var response = await ChatClient.SendQueryAsync(new GraphQLRequest("query { extensionsTest }"), () => new { extensionsTest = "" }) - .ConfigureAwait(false); + ; response.Errors.Should().NotBeNull(); response.Errors.Should().ContainSingle(); @@ -76,7 +76,7 @@ query Droid($id: String!) { "Human"); var response = await StarWarsClient.SendQueryAsync(graphQLRequest, () => new { Human = new { Name = string.Empty } }) - .ConfigureAwait(false); + ; Assert.Null(response.Errors); Assert.Equal(name, response.Data.Human.Name); @@ -85,7 +85,7 @@ query Droid($id: String!) { [Fact] public async void CanDoSerializationWithPredefinedTypes() { const string message = "some random testing message"; - var response = await ChatClient.AddMessageAsync(message).ConfigureAwait(false); + var response = await ChatClient.AddMessageAsync(message); Assert.Equal(message, response.Data.AddMessage.Content); } diff --git a/tests/GraphQL.Client.Tests.Common/Helpers/CallbackMonitor.cs b/tests/GraphQL.Client.Tests.Common/Helpers/CallbackMonitor.cs index 4b623ac6..2a27a259 100644 --- a/tests/GraphQL.Client.Tests.Common/Helpers/CallbackMonitor.cs +++ b/tests/GraphQL.Client.Tests.Common/Helpers/CallbackMonitor.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Threading; using FluentAssertions; using FluentAssertions.Execution; @@ -24,40 +25,10 @@ public class CallbackMonitor { public void Invoke(T param) { LastPayload = param; + Debug.WriteLine($"CallbackMonitor invoke handler thread id: {Thread.CurrentThread.ManagedThreadId}"); callbackInvoked.Set(); } - - /// - /// Asserts that a new update has been pushed to the within the configured since the last . - /// If supplied, the action is executed on the submitted payload. - /// - /// action to assert the contents of the payload - public void CallbackShouldHaveBeenInvoked(Action assertPayload = null, TimeSpan? timeout = null) { - try { - callbackInvoked.Wait(timeout ?? Timeout).Should().BeTrue("because the callback method should have been invoked (timeout: {0} s)", - (timeout ?? Timeout).TotalSeconds); - - assertPayload?.Invoke(LastPayload); - } - finally { - Reset(); - } - } - - /// - /// Asserts that no new update has been pushed within the given since the last - /// - /// the time in ms in which no new update must be pushed to the . defaults to 100 - public void CallbackShouldNotHaveBeenInvoked(TimeSpan? timeout = null) { - if (!timeout.HasValue) timeout = TimeSpan.FromMilliseconds(100); - try { - callbackInvoked.Wait(timeout.Value).Should().BeFalse("because the callback method should not have been invoked"); - } - finally { - Reset(); - } - } - + /// /// Resets the tester class. Should be called before triggering the potential update /// @@ -65,8 +36,7 @@ public void Reset() { LastPayload = default(T); callbackInvoked.Reset(); } - - + public CallbackAssertions Should() { return new CallbackAssertions(this); } @@ -82,7 +52,10 @@ public AndWhichConstraint, TPayload> HaveBeenInvoke string because = "", params object[] becauseArgs) { Execute.Assertion .BecauseOf(because, becauseArgs) - .Given(() => Subject.callbackInvoked.Wait(timeout)) + .Given(() => { + Debug.WriteLine($"HaveBeenInvokedWithPayload thread id: {Thread.CurrentThread.ManagedThreadId}"); + return Subject.callbackInvoked.Wait(timeout); + }) .ForCondition(isSet => isSet) .FailWith("Expected {context:callback} to be invoked{reason}, but did not receive a call within {0}", timeout); diff --git a/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs b/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs index 1d468deb..86ec3a1f 100644 --- a/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs +++ b/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs @@ -13,6 +13,8 @@ public class ObservableTester : IDisposable { private readonly ManualResetEventSlim updateReceived = new ManualResetEventSlim(); private readonly ManualResetEventSlim completed = new ManualResetEventSlim(); private readonly ManualResetEventSlim error = new ManualResetEventSlim(); + private readonly EventLoopScheduler subscriptionScheduler = new EventLoopScheduler(); + private readonly EventLoopScheduler observeScheduler = new EventLoopScheduler(); /// /// The timeout for . Defaults to 1 s @@ -35,7 +37,13 @@ public class ObservableTester : IDisposable { /// /// the under test public ObservableTester(IObservable observable) { - subscription = observable.SubscribeOn(Scheduler.Default).Subscribe( + subscriptionScheduler.Schedule(() => + Debug.WriteLine($"Subscription scheduler thread id: {Thread.CurrentThread.ManagedThreadId}")); + + observeScheduler.Schedule(() => + Debug.WriteLine($"Observe scheduler thread id: {Thread.CurrentThread.ManagedThreadId}")); + + subscription = observable.SubscribeOn(subscriptionScheduler).ObserveOn(observeScheduler).Subscribe( obj => { Debug.WriteLine($"observable tester {GetHashCode()}: payload received"); LastPayload = obj; @@ -62,6 +70,8 @@ private void Reset() { /// public void Dispose() { subscription?.Dispose(); + subscriptionScheduler.Dispose(); + observeScheduler.Dispose(); } public SubscriptionAssertions Should() { diff --git a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs index 415c7f32..c25117ca 100644 --- a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs @@ -145,9 +145,7 @@ public async void PreprocessHttpRequestMessageIsCalled() { var defaultHeaders = StarWarsClient.HttpClient.DefaultRequestHeaders; var response = await StarWarsClient.SendQueryAsync(graphQLRequest, () => new { Human = new { Name = string.Empty } }); - callbackTester.CallbackShouldHaveBeenInvoked(message => { - Assert.Equal(defaultHeaders, message.Headers); - }); + callbackTester.Should().HaveBeenInvokedWithPayload().Which.Headers.Should().BeEquivalentTo(defaultHeaders); Assert.Null(response.Errors); Assert.Equal("Luke", response.Data.Human.Name); } diff --git a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs index 662ca85b..47d2e5e8 100644 --- a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs @@ -284,6 +284,7 @@ public async void CanHandleConnectionTimeout() { using (ChatClient.WebsocketConnectionState.Subscribe(websocketStates.Enqueue)) { websocketStates.Should().ContainSingle(state => state == GraphQLWebsocketConnectionState.Disconnected); + Debug.WriteLine($"Test method thread id: {Thread.CurrentThread.ManagedThreadId}"); Debug.WriteLine("creating subscription stream"); var observable = ChatClient.CreateSubscriptionStream(SubscriptionRequest, errorMonitor.Invoke); From 2c4aec72b13d29ada7c6e13a2abb0aa9f3422cc0 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Mon, 9 Mar 2020 10:06:56 +0100 Subject: [PATCH 10/16] eliminate helpers class --- src/GraphQL.Client/GraphQLHttpClient.cs | 6 +- .../Websocket/GraphQLHttpWebSocket.cs | 275 ++++++++++++++++-- .../Websocket/GraphQLHttpWebsocketHelpers.cs | 223 -------------- 3 files changed, 258 insertions(+), 246 deletions(-) delete mode 100644 src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs diff --git a/src/GraphQL.Client/GraphQLHttpClient.cs b/src/GraphQL.Client/GraphQLHttpClient.cs index a4da13c6..30e85fd0 100644 --- a/src/GraphQL.Client/GraphQLHttpClient.cs +++ b/src/GraphQL.Client/GraphQLHttpClient.cs @@ -66,7 +66,7 @@ public GraphQLHttpClient(GraphQLHttpClientOptions options, HttpClient httpClient /// public Task> SendQueryAsync(GraphQLRequest request, CancellationToken cancellationToken = default) { return Options.UseWebSocketForQueriesAndMutations - ? this.graphQlHttpWebSocket.SendRequest(request, this, cancellationToken) + ? this.graphQlHttpWebSocket.SendRequest(request, cancellationToken) : this.SendHttpPostRequestAsync(request, cancellationToken); } @@ -85,7 +85,7 @@ public IObservable> CreateSubscriptionStream>)subscriptionStreams[key]; - var observable = graphQlHttpWebSocket.CreateSubscriptionStream(request, this, cancellationToken: cancellationTokenSource.Token); + var observable = graphQlHttpWebSocket.CreateSubscriptionStream(request); subscriptionStreams.TryAdd(key, observable); return observable; @@ -101,7 +101,7 @@ public IObservable> CreateSubscriptionStream>)subscriptionStreams[key]; - var observable = graphQlHttpWebSocket.CreateSubscriptionStream(request, this, exceptionHandler, cancellationTokenSource.Token); + var observable = graphQlHttpWebSocket.CreateSubscriptionStream(request, exceptionHandler); subscriptionStreams.TryAdd(key, observable); return observable; } diff --git a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs index fccf7894..61462a10 100644 --- a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs +++ b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs @@ -20,8 +20,8 @@ internal class GraphQLHttpWebSocket : IDisposable { private readonly Uri webSocketUri; private readonly GraphQLHttpClient client; private readonly ArraySegment buffer; - private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); - private readonly CancellationToken cancellationToken; + private readonly CancellationTokenSource internalCancellationTokenSource = new CancellationTokenSource(); + private readonly CancellationToken internalCancellationToken; private readonly Subject requestSubject = new Subject(); private readonly Subject exceptionSubject = new Subject(); private readonly BehaviorSubject stateSubject = @@ -34,6 +34,9 @@ internal class GraphQLHttpWebSocket : IDisposable { private Subject incomingMessagesSubject; private IDisposable incomingMessagesDisposable; private GraphQLHttpClientOptions Options => client.Options; + + private Task initializeWebSocketTask = Task.CompletedTask; + private readonly object initializeLock = new object(); #if NETFRAMEWORK private WebSocket clientWebSocket = null; @@ -42,18 +45,39 @@ internal class GraphQLHttpWebSocket : IDisposable { #endif #endregion + + #region Public properties + + /// + /// The current websocket state + /// public WebSocketState WebSocketState => clientWebSocket?.State ?? WebSocketState.None; + + /// + /// Publishes all errors which occur within the receive pipeline + /// public IObservable ReceiveErrors => exceptionSubject.AsObservable(); + + /// + /// Publishes the connection state of the + /// public IObservable ConnectionState => stateSubject.DistinctUntilChanged(); - public IObservable ResponseStream { get; } + /// + /// Publishes all messages which are received on the websocket + /// + public IObservable IncomingMessageStream { get; } + + #endregion + + public GraphQLHttpWebSocket(Uri webSocketUri, GraphQLHttpClient client) { - cancellationToken = cancellationTokenSource.Token; + internalCancellationToken = internalCancellationTokenSource.Token; this.webSocketUri = webSocketUri; this.client = client; buffer = new ArraySegment(new byte[8192]); - ResponseStream = GetMessageStream(); + IncomingMessageStream = GetMessageStream(); receiveLoopScheduler.Schedule(() => Debug.WriteLine($"receive loop scheduler thread id: {Thread.CurrentThread.ManagedThreadId}")); @@ -61,9 +85,223 @@ public GraphQLHttpWebSocket(Uri webSocketUri, GraphQLHttpClient client) { .ObserveOn(sendLoopScheduler) .Subscribe(async request => await SendWebSocketRequest(request)); } - + + #region Send requests + /// + /// Create a new subscription stream + /// + /// the response type + /// the to start the subscription + /// Optional: exception handler for handling exceptions within the receive pipeline + /// a which represents the subscription + public IObservable> CreateSubscriptionStream(GraphQLRequest request, Action exceptionHandler = null) { + return Observable.Defer(() => + Observable.Create>(async observer => { + Debug.WriteLine($"Create observable thread id: {Thread.CurrentThread.ManagedThreadId}"); + await client.Options.PreprocessRequest(request, client); + var startRequest = new GraphQLWebSocketRequest { + Id = Guid.NewGuid().ToString("N"), + Type = GraphQLWebSocketMessageType.GQL_START, + Payload = request + }; + var closeRequest = new GraphQLWebSocketRequest { + Id = startRequest.Id, + Type = GraphQLWebSocketMessageType.GQL_STOP + }; + var initRequest = new GraphQLWebSocketRequest { + Id = startRequest.Id, + Type = GraphQLWebSocketMessageType.GQL_CONNECTION_INIT, + }; + + var observable = Observable.Create>(o => + IncomingMessageStream + // ignore null values and messages for other requests + .Where(response => response != null && response.Id == startRequest.Id) + .Subscribe(response => { + // terminate the sequence when a 'complete' message is received + if (response.Type == GraphQLWebSocketMessageType.GQL_COMPLETE) { + Debug.WriteLine($"received 'complete' message on subscription {startRequest.Id}"); + o.OnCompleted(); + return; + } + + // post the GraphQLResponse to the stream (even if a GraphQL error occurred) + Debug.WriteLine($"received payload on subscription {startRequest.Id} (thread {Thread.CurrentThread.ManagedThreadId})"); + var typedResponse = + client.Options.JsonSerializer.DeserializeToWebsocketResponse( + response.MessageBytes); + o.OnNext(typedResponse.Payload); + + // in case of a GraphQL error, terminate the sequence after the response has been posted + if (response.Type == GraphQLWebSocketMessageType.GQL_ERROR) { + Debug.WriteLine($"terminating subscription {startRequest.Id} because of a GraphQL error"); + o.OnCompleted(); + } + }, + e => { + Debug.WriteLine($"response stream for subscription {startRequest.Id} failed: {e}"); + o.OnError(e); + }, + () => { + Debug.WriteLine($"response stream for subscription {startRequest.Id} completed"); + o.OnCompleted(); + }) + ); + + try { + // initialize websocket (completes immediately if socket is already open) + await InitializeWebSocket(); + } + catch (Exception e) { + // subscribe observer to failed observable + return Observable.Throw>(e).Subscribe(observer); + } + + var disposable = new CompositeDisposable( + observable.Subscribe(observer), + Disposable.Create(async () => { + // only try to send close request on open websocket + if (WebSocketState != WebSocketState.Open) return; + + try { + Debug.WriteLine($"sending close message on subscription {startRequest.Id}"); + await QueueWebSocketRequest(closeRequest); + } + // do not break on disposing + catch (OperationCanceledException) { } + }) + ); + + // send connection init + Debug.WriteLine($"sending connection init on subscription {startRequest.Id}"); + try { + await QueueWebSocketRequest(initRequest); + } + catch (Exception e) { + Console.WriteLine(e); + throw; + } + + Debug.WriteLine($"sending initial message on subscription {startRequest.Id}"); + // send subscription request + try { + await QueueWebSocketRequest(startRequest); + } + catch (Exception e) { + Console.WriteLine(e); + throw; + } + + return disposable; + })) + // complete sequence on OperationCanceledException, this is triggered by the cancellation token + .Catch, OperationCanceledException>(exception => + Observable.Empty>()) + // wrap results + .Select(response => new Tuple, Exception>(response, null)) + // do exception handling + .Catch, Exception>, Exception>(e => { + try { + if (exceptionHandler == null) { + // if the external handler is not set, propagate all exceptions except WebSocketExceptions + // this will ensure that the client tries to re-establish subscriptions on connection loss + if (!(e is WebSocketException)) throw e; + } + else { + // exceptions thrown by the handler will propagate to OnError() + exceptionHandler?.Invoke(e); + } + + // throw exception on the observable to be caught by Retry() or complete sequence if cancellation was requested + if (internalCancellationToken.IsCancellationRequested) + return Observable.Empty, Exception>>(); + else { + Debug.WriteLine($"Catch handler thread id: {Thread.CurrentThread.ManagedThreadId}"); + return Observable.Throw, Exception>>(e); + } + } + catch (Exception exception) { + // wrap all other exceptions to be propagated behind retry + return Observable.Return(new Tuple, Exception>(null, exception)); + } + }) + // attempt to recreate the websocket for rethrown exceptions + .Retry() + // unwrap and push results or throw wrapped exceptions + .SelectMany(t => { + Debug.WriteLine($"unwrap exception thread id: {Thread.CurrentThread.ManagedThreadId}"); + // if the result contains an exception, throw it on the observable + if (t.Item2 != null) + return Observable.Throw>(t.Item2); + + return t.Item1 == null + ? Observable.Empty>() + : Observable.Return(t.Item1); + }) + // transform to hot observable and auto-connect + .Publish().RefCount(); + } + + /// + /// Send a regular GraphQL request (query, mutation) via websocket + /// + /// the response type + /// the to send + /// the token to cancel the request + /// + public Task> SendRequest(GraphQLRequest request, CancellationToken cancellationToken = default) { + return Observable.Create>(async observer => { + await client.Options.PreprocessRequest(request, client); + var websocketRequest = new GraphQLWebSocketRequest { + Id = Guid.NewGuid().ToString("N"), + Type = GraphQLWebSocketMessageType.GQL_START, + Payload = request + }; + var observable = IncomingMessageStream + .Where(response => response != null && response.Id == websocketRequest.Id) + .TakeUntil(response => response.Type == GraphQLWebSocketMessageType.GQL_COMPLETE) + .Select(response => { + Debug.WriteLine($"received response for request {websocketRequest.Id}"); + var typedResponse = + client.Options.JsonSerializer.DeserializeToWebsocketResponse( + response.MessageBytes); + return typedResponse.Payload; + }); + + try { + // initialize websocket (completes immediately if socket is already open) + await InitializeWebSocket(); + } + catch (Exception e) { + // subscribe observer to failed observable + return Observable.Throw>(e).Subscribe(observer); + } + + var disposable = new CompositeDisposable( + observable.Subscribe(observer) + ); + + Debug.WriteLine($"submitting request {websocketRequest.Id}"); + // send request + try { + await QueueWebSocketRequest(websocketRequest); + } + catch (Exception e) { + Console.WriteLine(e); + throw; + } + + return disposable; + }) + // complete sequence on OperationCanceledException, this is triggered by the cancellation token + .Catch, OperationCanceledException>(exception => + Observable.Empty>()) + .FirstAsync() + .ToTask(cancellationToken); + } + public Task QueueWebSocketRequest(GraphQLWebSocketRequest request) { requestSubject.OnNext(request); return request.SendTask(); @@ -71,7 +309,7 @@ public Task QueueWebSocketRequest(GraphQLWebSocketRequest request) { private async Task SendWebSocketRequest(GraphQLWebSocketRequest request) { try { - if (cancellationToken.IsCancellationRequested) { + if (internalCancellationToken.IsCancellationRequested) { request.SendCanceled(); return; } @@ -82,7 +320,7 @@ await this.clientWebSocket.SendAsync( new ArraySegment(requestBytes), WebSocketMessageType.Text, true, - cancellationToken); + internalCancellationToken); request.SendCompleted(); } catch (Exception e) { @@ -91,9 +329,6 @@ await this.clientWebSocket.SendAsync( } #endregion - - private Task initializeWebSocketTask = Task.CompletedTask; - private readonly object initializeLock = new object(); public Task InitializeWebSocket() { // do not attempt to initialize if cancellation is requested @@ -138,7 +373,7 @@ public Task InitializeWebSocket() { clientWebSocket.Options.ClientCertificates = ((HttpClientHandler)Options.HttpMessageHandler).ClientCertificates; clientWebSocket.Options.UseDefaultCredentials = ((HttpClientHandler)Options.HttpMessageHandler).UseDefaultCredentials; #endif - return initializeWebSocketTask = ConnectAsync(cancellationToken); + return initializeWebSocketTask = ConnectAsync(internalCancellationToken); } } @@ -172,7 +407,7 @@ private Task BackOff() { var delay = Options.BackOffStrategy?.Invoke(connectionAttempt - 1) ?? TimeSpan.FromSeconds(5); Debug.WriteLine($"connection attempt #{connectionAttempt}, backing off for {delay.TotalSeconds} s"); - return Task.Delay(delay, cancellationToken); + return Task.Delay(delay, internalCancellationToken); } @@ -184,7 +419,7 @@ private IObservable GetMessageStream() { } private async Task CreateMessageStream(IObserver observer, CancellationToken token) { - var cts = CancellationTokenSource.CreateLinkedTokenSource(token, cancellationToken); + var cts = CancellationTokenSource.CreateLinkedTokenSource(token, internalCancellationToken); cts.Token.ThrowIfCancellationRequested(); @@ -240,7 +475,7 @@ private async Task CreateMessageStream(IObserver private Task GetReceiveTask() { lock (receiveTaskLocker) { - cancellationToken.ThrowIfCancellationRequested(); + internalCancellationToken.ThrowIfCancellationRequested(); if (receiveAsyncTask == null || receiveAsyncTask.IsFaulted || receiveAsyncTask.IsCompleted) @@ -261,13 +496,13 @@ private async Task ReceiveWebsocketMessagesAsync() { using (var ms = new MemoryStream()) { WebSocketReceiveResult webSocketReceiveResult = null; do { - cancellationToken.ThrowIfCancellationRequested(); + internalCancellationToken.ThrowIfCancellationRequested(); webSocketReceiveResult = await clientWebSocket.ReceiveAsync(buffer, CancellationToken.None); ms.Write(buffer.Array, buffer.Offset, webSocketReceiveResult.Count); } while (!webSocketReceiveResult.EndOfMessage); - cancellationToken.ThrowIfCancellationRequested(); + internalCancellationToken.ThrowIfCancellationRequested(); ms.Seek(0, SeekOrigin.Begin); if (webSocketReceiveResult.MessageType == WebSocketMessageType.Text) { @@ -325,8 +560,8 @@ public void Complete() { private readonly object completedLocker = new object(); private async Task CompleteAsync() { Debug.WriteLine($"disposing websocket {clientWebSocket.GetHashCode()}..."); - if (!cancellationTokenSource.IsCancellationRequested) - cancellationTokenSource.Cancel(); + if (!internalCancellationTokenSource.IsCancellationRequested) + internalCancellationTokenSource.Cancel(); await CloseAsync(); requestSubscription?.Dispose(); clientWebSocket?.Dispose(); @@ -339,7 +574,7 @@ private async Task CompleteAsync() { exceptionSubject?.OnCompleted(); exceptionSubject?.Dispose(); - cancellationTokenSource.Dispose(); + internalCancellationTokenSource.Dispose(); sendLoopScheduler?.Dispose(); receiveLoopScheduler?.Dispose(); diff --git a/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs b/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs deleted file mode 100644 index 82fc6f2d..00000000 --- a/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs +++ /dev/null @@ -1,223 +0,0 @@ -using System; -using System.Diagnostics; -using System.Net.WebSockets; -using System.Reactive.Concurrency; -using System.Reactive.Disposables; -using System.Reactive.Linq; -using System.Reactive.Threading.Tasks; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using GraphQL.Client.Abstractions.Websocket; - -namespace GraphQL.Client.Http.Websocket { - public static class GraphQLHttpWebsocketHelpers { - internal static IObservable> CreateSubscriptionStream( - this GraphQLHttpWebSocket graphQlHttpWebSocket, - GraphQLRequest request, - GraphQLHttpClient client, - Action exceptionHandler = null, - CancellationToken cancellationToken = default) { - return Observable.Defer(() => - Observable.Create>(async observer => { - Debug.WriteLine($"Create observable thread id: {Thread.CurrentThread.ManagedThreadId}"); - await client.Options.PreprocessRequest(request, client); - var startRequest = new GraphQLWebSocketRequest { - Id = Guid.NewGuid().ToString("N"), - Type = GraphQLWebSocketMessageType.GQL_START, - Payload = request - }; - var closeRequest = new GraphQLWebSocketRequest { - Id = startRequest.Id, - Type = GraphQLWebSocketMessageType.GQL_STOP - }; - var initRequest = new GraphQLWebSocketRequest { - Id = startRequest.Id, - Type = GraphQLWebSocketMessageType.GQL_CONNECTION_INIT, - }; - - var observable = Observable.Create>(o => - graphQlHttpWebSocket.ResponseStream - // ignore null values and messages for other requests - .Where(response => response != null && response.Id == startRequest.Id) - .Subscribe(response => { - // terminate the sequence when a 'complete' message is received - if (response.Type == GraphQLWebSocketMessageType.GQL_COMPLETE) { - Debug.WriteLine($"received 'complete' message on subscription {startRequest.Id}"); - o.OnCompleted(); - return; - } - - // post the GraphQLResponse to the stream (even if a GraphQL error occurred) - Debug.WriteLine($"received payload on subscription {startRequest.Id} (thread {Thread.CurrentThread.ManagedThreadId})"); - var typedResponse = - client.Options.JsonSerializer.DeserializeToWebsocketResponse( - response.MessageBytes); - o.OnNext(typedResponse.Payload); - - // in case of a GraphQL error, terminate the sequence after the response has been posted - if (response.Type == GraphQLWebSocketMessageType.GQL_ERROR) { - Debug.WriteLine($"terminating subscription {startRequest.Id} because of a GraphQL error"); - o.OnCompleted(); - } - }, - e => { - Debug.WriteLine($"response stream for subscription {startRequest.Id} failed: {e}"); - o.OnError(e); - }, - () => { - Debug.WriteLine($"response stream for subscription {startRequest.Id} completed"); - o.OnCompleted(); - }) - ); - - try { - // initialize websocket (completes immediately if socket is already open) - await graphQlHttpWebSocket.InitializeWebSocket(); - } - catch (Exception e) { - // subscribe observer to failed observable - return Observable.Throw>(e).Subscribe(observer); - } - - var disposable = new CompositeDisposable( - observable.Subscribe(observer), - Disposable.Create(async () => { - // only try to send close request on open websocket - if (graphQlHttpWebSocket.WebSocketState != WebSocketState.Open) return; - - try { - Debug.WriteLine($"sending close message on subscription {startRequest.Id}"); - await graphQlHttpWebSocket.QueueWebSocketRequest(closeRequest); - } - // do not break on disposing - catch (OperationCanceledException) { } - }) - ); - - // send connection init - Debug.WriteLine($"sending connection init on subscription {startRequest.Id}"); - try { - await graphQlHttpWebSocket.QueueWebSocketRequest(initRequest); - } - catch (Exception e) { - Console.WriteLine(e); - throw; - } - - Debug.WriteLine($"sending initial message on subscription {startRequest.Id}"); - // send subscription request - try { - await graphQlHttpWebSocket.QueueWebSocketRequest(startRequest); - } - catch (Exception e) { - Console.WriteLine(e); - throw; - } - - return disposable; - })) - // complete sequence on OperationCanceledException, this is triggered by the cancellation token - .Catch, OperationCanceledException>(exception => - Observable.Empty>()) - // wrap results - .Select(response => new Tuple, Exception>(response, null)) - // do exception handling - .Catch, Exception>, Exception>(e => { - try { - if (exceptionHandler == null) { - // if the external handler is not set, propagate all exceptions except WebSocketExceptions - // this will ensure that the client tries to re-establish subscriptions on connection loss - if (!(e is WebSocketException)) throw e; - } - else { - // exceptions thrown by the handler will propagate to OnError() - exceptionHandler?.Invoke(e); - } - - // throw exception on the observable to be caught by Retry() or complete sequence if cancellation was requested - if (cancellationToken.IsCancellationRequested) - return Observable.Empty, Exception>>(); - else { - Debug.WriteLine($"Catch handler thread id: {Thread.CurrentThread.ManagedThreadId}"); - return Observable.Throw, Exception>>(e); - } - } - catch (Exception exception) { - // wrap all other exceptions to be propagated behind retry - return Observable.Return(new Tuple, Exception>(null, exception)); - } - }) - // attempt to recreate the websocket for rethrown exceptions - .Retry() - // unwrap and push results or throw wrapped exceptions - .SelectMany(t => { - Debug.WriteLine($"unwrap exception thread id: {Thread.CurrentThread.ManagedThreadId}"); - // if the result contains an exception, throw it on the observable - if (t.Item2 != null) - return Observable.Throw>(t.Item2); - - return t.Item1 == null - ? Observable.Empty>() - : Observable.Return(t.Item1); - }) - // transform to hot observable and auto-connect - .Publish().RefCount(); - } - - internal static Task> SendRequest( - this GraphQLHttpWebSocket graphQlHttpWebSocket, - GraphQLRequest request, - GraphQLHttpClient client, - CancellationToken cancellationToken = default) { - return Observable.Create>(async observer => { - await client.Options.PreprocessRequest(request, client); - var websocketRequest = new GraphQLWebSocketRequest { - Id = Guid.NewGuid().ToString("N"), - Type = GraphQLWebSocketMessageType.GQL_START, - Payload = request - }; - var observable = graphQlHttpWebSocket.ResponseStream - .Where(response => response != null && response.Id == websocketRequest.Id) - .TakeUntil(response => response.Type == GraphQLWebSocketMessageType.GQL_COMPLETE) - .Select(response => { - Debug.WriteLine($"received response for request {websocketRequest.Id}"); - var typedResponse = - client.Options.JsonSerializer.DeserializeToWebsocketResponse( - response.MessageBytes); - return typedResponse.Payload; - }); - - try { - // intialize websocket (completes immediately if socket is already open) - await graphQlHttpWebSocket.InitializeWebSocket(); - } - catch (Exception e) { - // subscribe observer to failed observable - return Observable.Throw>(e).Subscribe(observer); - } - - var disposable = new CompositeDisposable( - observable.Subscribe(observer) - ); - - Debug.WriteLine($"submitting request {websocketRequest.Id}"); - // send request - try { - await graphQlHttpWebSocket.QueueWebSocketRequest(websocketRequest); - } - catch (Exception e) { - Console.WriteLine(e); - throw; - } - - return disposable; - }) - // complete sequence on OperationCanceledException, this is triggered by the cancellation token - .Catch, OperationCanceledException>(exception => - Observable.Empty>()) - .FirstAsync() - .ToTask(cancellationToken); - } - } -} From b1888b4931c0c964d5129e771a8b4c74ae0f7617 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Mon, 9 Mar 2020 13:00:00 +0100 Subject: [PATCH 11/16] eliminate incomingMessagesSubject --- .../Websocket/GraphQLHttpWebSocket.cs | 151 +++++++++--------- .../Helpers/ObservableTester.cs | 7 +- .../WebsocketTests/Base.cs | 9 +- 3 files changed, 83 insertions(+), 84 deletions(-) diff --git a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs index 61462a10..a8efeaeb 100644 --- a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs +++ b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs @@ -31,8 +31,8 @@ internal class GraphQLHttpWebSocket : IDisposable { private readonly EventLoopScheduler sendLoopScheduler = new EventLoopScheduler(); private int connectionAttempt = 0; - private Subject incomingMessagesSubject; - private IDisposable incomingMessagesDisposable; + private IConnectableObservable incomingMessages; + private IDisposable incomingMessagesConnection; private GraphQLHttpClientOptions Options => client.Options; private Task initializeWebSocketTask = Task.CompletedTask; @@ -302,7 +302,7 @@ public Task> SendRequest(GraphQLRequest re .ToTask(cancellationToken); } - public Task QueueWebSocketRequest(GraphQLWebSocketRequest request) { + private Task QueueWebSocketRequest(GraphQLWebSocketRequest request) { requestSubject.OnNext(request); return request.SendTask(); } @@ -381,13 +381,41 @@ private async Task ConnectAsync(CancellationToken token) { try { await BackOff(); stateSubject.OnNext(GraphQLWebsocketConnectionState.Connecting); - Debug.WriteLine($"opening websocket {clientWebSocket.GetHashCode()}"); + Debug.WriteLine($"opening websocket {clientWebSocket.GetHashCode()} (thread {Thread.CurrentThread.ManagedThreadId})"); await clientWebSocket.ConnectAsync(webSocketUri, token); stateSubject.OnNext(GraphQLWebsocketConnectionState.Connected); Debug.WriteLine($"connection established on websocket {clientWebSocket.GetHashCode()}, invoking Options.OnWebsocketConnected()"); await (Options.OnWebsocketConnected?.Invoke(client) ?? Task.CompletedTask); Debug.WriteLine($"invoking Options.OnWebsocketConnected() on websocket {clientWebSocket.GetHashCode()}"); connectionAttempt = 1; + + // create receiving observable + incomingMessages = Observable + .Defer(() => GetReceiveTask().ToObservable().ObserveOn(receiveLoopScheduler)) + .Repeat() + // complete sequence on OperationCanceledException, this is triggered by the cancellation token on disposal + .Catch(exception => Observable.Empty()) + .Publish(); + + // subscribe maintenance + var maintenanceSubscription = incomingMessages.Subscribe(_ => { }, ex => { + Debug.WriteLine($"incoming message stream {incomingMessages.GetHashCode()} received an error: {ex}"); + exceptionSubject.OnNext(ex); + incomingMessagesConnection?.Dispose(); + stateSubject.OnNext(GraphQLWebsocketConnectionState.Disconnected); + }, + () => { + Debug.WriteLine($"incoming message stream {incomingMessages.GetHashCode()} completed"); + incomingMessagesConnection?.Dispose(); + stateSubject.OnNext(GraphQLWebsocketConnectionState.Disconnected); + }); + + + // connect observable + var connection = incomingMessages.Connect(); + Debug.WriteLine($"new incoming message stream {incomingMessages.GetHashCode()} created"); + + incomingMessagesConnection = new CompositeDisposable(maintenanceSubscription, connection); } catch (Exception e) { stateSubject.OnNext(GraphQLWebsocketConnectionState.Disconnected); @@ -410,61 +438,25 @@ private Task BackOff() { return Task.Delay(delay, internalCancellationToken); } - private IObservable GetMessageStream() { - return Observable.Create(CreateMessageStream) - // complete sequence on OperationCanceledException, this is triggered by the cancellation token on disposal - .Catch(exception => - Observable.Empty()); - } - - private async Task CreateMessageStream(IObserver observer, CancellationToken token) { - var cts = CancellationTokenSource.CreateLinkedTokenSource(token, internalCancellationToken); - cts.Token.ThrowIfCancellationRequested(); - - - if (incomingMessagesSubject == null || incomingMessagesSubject.IsDisposed) { - // create new response subject - incomingMessagesSubject = new Subject(); - Debug.WriteLine($"creating new incoming message stream {incomingMessagesSubject.GetHashCode()}"); - - // initialize and connect websocket - await InitializeWebSocket(); - - // loop the receive task and subscribe the created subject to the results - var receiveLoopSubscription = Observable - .Defer(() => GetReceiveTask().ToObservable()) - .Repeat() - .Subscribe(incomingMessagesSubject); - - incomingMessagesDisposable = new CompositeDisposable( - incomingMessagesSubject, - receiveLoopSubscription, - Disposable.Create(() => { - Debug.WriteLine($"incoming message stream {incomingMessagesSubject.GetHashCode()} disposed"); + return Observable.Using(() => new EventLoopScheduler(), scheduler => + Observable.Create(async observer => { + // make sure the websocket ist connected + await InitializeWebSocket(); + // subscribe observer to message stream + var subscription = new CompositeDisposable(incomingMessages.ObserveOn(scheduler).Subscribe(observer)); + // register the observer's OnCompleted method with the cancellation token to complete the sequence on disposal + subscription.Add(internalCancellationTokenSource.Token.Register(observer.OnCompleted)); + + // add some debug output + var hashCode = subscription.GetHashCode(); + subscription.Add(Disposable.Create(() => { + Debug.WriteLine($"incoming message subscription {hashCode} disposed"); })); + Debug.WriteLine($"new incoming message subscription {hashCode} created"); - // dispose the subject on any error or completion (will be recreated) - incomingMessagesSubject.Subscribe(_ => { }, ex => { - exceptionSubject.OnNext(ex); - incomingMessagesDisposable?.Dispose(); - incomingMessagesSubject = null; - stateSubject.OnNext(GraphQLWebsocketConnectionState.Disconnected); - }, - () => { - incomingMessagesDisposable?.Dispose(); - incomingMessagesSubject = null; - stateSubject.OnNext(GraphQLWebsocketConnectionState.Disconnected); - }); - } - - var subscription = new CompositeDisposable(incomingMessagesSubject.Subscribe(observer)); - var hashCode = subscription.GetHashCode(); - subscription.Add(Disposable.Create(() => { - Debug.WriteLine($"incoming message subscription {hashCode} disposed"); - })); - Debug.WriteLine($"new incoming message subscription {hashCode} created"); - return subscription; + return subscription; + })); } private Task receiveAsyncTask = null; @@ -490,30 +482,31 @@ private Task GetReceiveTask() { /// /// private async Task ReceiveWebsocketMessagesAsync() { + internalCancellationToken.ThrowIfCancellationRequested(); + try { Debug.WriteLine($"waiting for data on websocket {clientWebSocket.GetHashCode()} (thread {Thread.CurrentThread.ManagedThreadId})..."); - using (var ms = new MemoryStream()) { - WebSocketReceiveResult webSocketReceiveResult = null; - do { - internalCancellationToken.ThrowIfCancellationRequested(); - webSocketReceiveResult = await clientWebSocket.ReceiveAsync(buffer, CancellationToken.None); - ms.Write(buffer.Array, buffer.Offset, webSocketReceiveResult.Count); - } - while (!webSocketReceiveResult.EndOfMessage); + using var ms = new MemoryStream(); + WebSocketReceiveResult webSocketReceiveResult = null; + do { + // cancellation is done implicitly via the close method + webSocketReceiveResult = await clientWebSocket.ReceiveAsync(buffer, CancellationToken.None); + ms.Write(buffer.Array, buffer.Offset, webSocketReceiveResult.Count); + } + while (!webSocketReceiveResult.EndOfMessage && !internalCancellationToken.IsCancellationRequested); - internalCancellationToken.ThrowIfCancellationRequested(); - ms.Seek(0, SeekOrigin.Begin); + internalCancellationToken.ThrowIfCancellationRequested(); + ms.Seek(0, SeekOrigin.Begin); - if (webSocketReceiveResult.MessageType == WebSocketMessageType.Text) { - var response = await Options.JsonSerializer.DeserializeToWebsocketResponseWrapperAsync(ms); - response.MessageBytes = ms.ToArray(); - Debug.WriteLine($"{response.MessageBytes.Length} bytes received on websocket {clientWebSocket.GetHashCode()} (thread {Thread.CurrentThread.ManagedThreadId})..."); - return response; - } - else { - throw new NotSupportedException("binary websocket messages are not supported"); - } + if (webSocketReceiveResult.MessageType == WebSocketMessageType.Text) { + var response = await Options.JsonSerializer.DeserializeToWebsocketResponseWrapperAsync(ms); + response.MessageBytes = ms.ToArray(); + Debug.WriteLine($"{response.MessageBytes.Length} bytes received on websocket {clientWebSocket.GetHashCode()} (thread {Thread.CurrentThread.ManagedThreadId})..."); + return response; + } + else { + throw new NotSupportedException("binary websocket messages are not supported"); } } catch (Exception e) { @@ -560,15 +553,15 @@ public void Complete() { private readonly object completedLocker = new object(); private async Task CompleteAsync() { Debug.WriteLine($"disposing websocket {clientWebSocket.GetHashCode()}..."); + incomingMessagesConnection?.Dispose(); + if (!internalCancellationTokenSource.IsCancellationRequested) internalCancellationTokenSource.Cancel(); + await CloseAsync(); requestSubscription?.Dispose(); clientWebSocket?.Dispose(); - - incomingMessagesSubject?.OnCompleted(); - incomingMessagesDisposable?.Dispose(); - + stateSubject?.OnCompleted(); stateSubject?.Dispose(); diff --git a/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs b/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs index 86ec3a1f..bd1ac46b 100644 --- a/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs +++ b/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs @@ -89,7 +89,12 @@ public AndWhichConstraint, TPayload> HaveReceiv string because = "", params object[] becauseArgs) { Execute.Assertion .BecauseOf(because, becauseArgs) - .Given(() => Subject.updateReceived.Wait(timeout)) + .Given(() => { + var isSet = Subject.updateReceived.Wait(timeout); + if(!isSet) + Debug.WriteLine($"waiting for payload on thread {Thread.CurrentThread.ManagedThreadId} timed out!"); + return isSet; + }) .ForCondition(isSet => isSet) .FailWith("Expected {context:Subscription} to receive new payload{reason}, but did not receive an update within {0}", timeout); diff --git a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs index 47d2e5e8..e689dd06 100644 --- a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs @@ -169,24 +169,25 @@ public async void CanReconnectWithSameObservable() { const string message1 = "Hello World"; Debug.WriteLine($"adding message {message1}"); - var response = await ChatClient.AddMessageAsync(message1); + var response = await ChatClient.AddMessageAsync(message1).ConfigureAwait(true); response.Data.AddMessage.Content.Should().Be(message1); tester.Should().HaveReceivedPayload().Which.Data.MessageAdded.Content.Should().Be(message1); const string message2 = "How are you?"; - response = await ChatClient.AddMessageAsync(message2); + response = await ChatClient.AddMessageAsync(message2).ConfigureAwait(true); response.Data.AddMessage.Content.Should().Be(message2); tester.Should().HaveReceivedPayload().Which.Data.MessageAdded.Content.Should().Be(message2); Debug.WriteLine("disposing subscription..."); tester.Dispose(); // does not close the websocket connection - Debug.WriteLine("creating new subscription..."); + Debug.WriteLine($"creating new subscription from thread {Thread.CurrentThread.ManagedThreadId} ..."); var tester2 = observable.Monitor(); + Debug.WriteLine($"waiting for payload on {Thread.CurrentThread.ManagedThreadId} ..."); tester2.Should().HaveReceivedPayload().Which.Data.MessageAdded.Content.Should().Be(message2); const string message3 = "lorem ipsum dolor si amet"; - response = await ChatClient.AddMessageAsync(message3); + response = await ChatClient.AddMessageAsync(message3).ConfigureAwait(true); response.Data.AddMessage.Content.Should().Be(message3); tester2.Should().HaveReceivedPayload().Which.Data.MessageAdded.Content.Should().Be(message3); From c9acd489f0a565378c8a1f77d97c2178df1e0fd5 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Mon, 9 Mar 2020 13:33:46 +0100 Subject: [PATCH 12/16] fix tests locally --- .../Websocket/GraphQLHttpWebSocket.cs | 2 +- .../Helpers/ObservableTester.cs | 6 +----- .../WebsocketTests/Base.cs | 18 ++++++++++-------- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs index a8efeaeb..ec3d5a18 100644 --- a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs +++ b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs @@ -502,7 +502,7 @@ private async Task ReceiveWebsocketMessagesAsync() { if (webSocketReceiveResult.MessageType == WebSocketMessageType.Text) { var response = await Options.JsonSerializer.DeserializeToWebsocketResponseWrapperAsync(ms); response.MessageBytes = ms.ToArray(); - Debug.WriteLine($"{response.MessageBytes.Length} bytes received on websocket {clientWebSocket.GetHashCode()} (thread {Thread.CurrentThread.ManagedThreadId})..."); + Debug.WriteLine($"{response.MessageBytes.Length} bytes received for id {response.Id} on websocket {clientWebSocket.GetHashCode()} (thread {Thread.CurrentThread.ManagedThreadId})..."); return response; } else { diff --git a/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs b/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs index bd1ac46b..05ed7e38 100644 --- a/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs +++ b/tests/GraphQL.Client.Tests.Common/Helpers/ObservableTester.cs @@ -13,7 +13,6 @@ public class ObservableTester : IDisposable { private readonly ManualResetEventSlim updateReceived = new ManualResetEventSlim(); private readonly ManualResetEventSlim completed = new ManualResetEventSlim(); private readonly ManualResetEventSlim error = new ManualResetEventSlim(); - private readonly EventLoopScheduler subscriptionScheduler = new EventLoopScheduler(); private readonly EventLoopScheduler observeScheduler = new EventLoopScheduler(); /// @@ -37,13 +36,11 @@ public class ObservableTester : IDisposable { /// /// the under test public ObservableTester(IObservable observable) { - subscriptionScheduler.Schedule(() => - Debug.WriteLine($"Subscription scheduler thread id: {Thread.CurrentThread.ManagedThreadId}")); observeScheduler.Schedule(() => Debug.WriteLine($"Observe scheduler thread id: {Thread.CurrentThread.ManagedThreadId}")); - subscription = observable.SubscribeOn(subscriptionScheduler).ObserveOn(observeScheduler).Subscribe( + subscription = observable.ObserveOn(observeScheduler).Subscribe( obj => { Debug.WriteLine($"observable tester {GetHashCode()}: payload received"); LastPayload = obj; @@ -70,7 +67,6 @@ private void Reset() { /// public void Dispose() { subscription?.Dispose(); - subscriptionScheduler.Dispose(); observeScheduler.Dispose(); } diff --git a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs index e689dd06..3c2d63f1 100644 --- a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs @@ -233,36 +233,38 @@ public async void CanConnectTwoSubscriptionsSimultaneously() { var observable2 = ChatClient.CreateSubscriptionStream(SubscriptionRequest2, callbackTester2.Invoke); Debug.WriteLine("subscribing..."); - var tester = observable1.Monitor(); - var tester2 = observable2.Monitor(); + var messagesMonitor = observable1.Monitor(); + var joinedMonitor = observable2.Monitor(); - tester.Should().HaveReceivedPayload().Which.Data.MessageAdded.Content.Should().Be(InitialMessage.Content); + messagesMonitor.Should().HaveReceivedPayload().Which.Data.MessageAdded.Content.Should().Be(InitialMessage.Content); const string message1 = "Hello World"; var response = await ChatClient.AddMessageAsync(message1); response.Data.AddMessage.Content.Should().Be(message1); - tester.Should().HaveReceivedPayload() + messagesMonitor.Should().HaveReceivedPayload() .Which.Data.MessageAdded.Content.Should().Be(message1); + joinedMonitor.Should().NotHaveReceivedPayload(); var joinResponse = await ChatClient.JoinDeveloperUser(); joinResponse.Data.Join.DisplayName.Should().Be("developer", "because that's the display name of user \"1\""); - var payload = tester2.Should().HaveReceivedPayload().Subject; + var payload = joinedMonitor.Should().HaveReceivedPayload().Subject; payload.Data.UserJoined.Id.Should().Be("1", "because that's the id we sent with our mutation request"); payload.Data.UserJoined.DisplayName.Should().Be("developer", "because that's the display name of user \"1\""); + messagesMonitor.Should().NotHaveReceivedPayload(); Debug.WriteLine("disposing subscription..."); - tester2.Dispose(); + joinedMonitor.Dispose(); const string message3 = "lorem ipsum dolor si amet"; response = await ChatClient.AddMessageAsync(message3); response.Data.AddMessage.Content.Should().Be(message3); - tester.Should().HaveReceivedPayload() + messagesMonitor.Should().HaveReceivedPayload() .Which.Data.MessageAdded.Content.Should().Be(message3); // disposing the client should complete the subscription ChatClient.Dispose(); - tester.Should().HaveCompleted(); + messagesMonitor.Should().HaveCompleted(); } From 3a9df0ca08a1257a51e87b18677f6b1072380df5 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Mon, 9 Mar 2020 13:58:50 +0100 Subject: [PATCH 13/16] extend execution time in cancellation tests --- .../QueryAndMutationTests/Base.cs | 4 ++-- tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs index c25117ca..7b2dc79e 100644 --- a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs @@ -171,7 +171,7 @@ query Long { // unblock the query chatQuery.LongRunningQueryBlocker.Set(); // check execution time - request.Invoking().ExecutionTime().Should().BeLessThan(500.Milliseconds()); + request.Invoking().ExecutionTime().Should().BeLessThan(1000.Milliseconds()); request.Invoke().Result.Data.longRunning.Should().Be("finally returned"); // reset stuff @@ -183,7 +183,7 @@ query Long { chatQuery.WaitingOnQueryBlocker.Wait(1000).Should().BeTrue("because the request should have reached the server by then"); cts.Cancel(); FluentActions.Awaiting(() => request.Invoking().Should().ThrowAsync("because the request was cancelled")) - .ExecutionTime().Should().BeLessThan(500.Milliseconds()); + .ExecutionTime().Should().BeLessThan(1000.Milliseconds()); // let the server finish its query chatQuery.LongRunningQueryBlocker.Set(); diff --git a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs index 3c2d63f1..016f0dab 100644 --- a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs @@ -2,7 +2,6 @@ using System.Collections.Concurrent; using System.Diagnostics; using System.Net.WebSockets; -using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; using FluentAssertions; @@ -77,11 +76,11 @@ query Long { // start request request.Start(); // wait until the query has reached the server - chatQuery.WaitingOnQueryBlocker.Wait(500).Should().BeTrue("because the request should have reached the server by then"); + chatQuery.WaitingOnQueryBlocker.Wait(1000).Should().BeTrue("because the request should have reached the server by then"); // unblock the query chatQuery.LongRunningQueryBlocker.Set(); // check execution time - request.Invoking().ExecutionTime().Should().BeLessThan(500.Milliseconds()); + request.Invoking().ExecutionTime().Should().BeLessThan(1000.Milliseconds()); request.Invoke().Result.Data.longRunning.Should().Be("finally returned"); // reset stuff @@ -90,10 +89,10 @@ query Long { // cancellation test request.Start(); - chatQuery.WaitingOnQueryBlocker.Wait(500).Should().BeTrue("because the request should have reached the server by then"); + chatQuery.WaitingOnQueryBlocker.Wait(1000).Should().BeTrue("because the request should have reached the server by then"); cts.Cancel(); FluentActions.Awaiting(() => request.Invoking().Should().ThrowAsync("because the request was cancelled")) - .ExecutionTime().Should().BeLessThan(500.Milliseconds()); + .ExecutionTime().Should().BeLessThan(1000.Milliseconds()); // let the server finish its query chatQuery.LongRunningQueryBlocker.Set(); From 6834eef0a4b23d53f283da1fb4012a4c6e48e8af Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Mon, 9 Mar 2020 14:27:26 +0100 Subject: [PATCH 14/16] don't measure execution time in tests --- tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs | 4 +--- tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs index 7b2dc79e..e60c26f0 100644 --- a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs @@ -171,7 +171,6 @@ query Long { // unblock the query chatQuery.LongRunningQueryBlocker.Set(); // check execution time - request.Invoking().ExecutionTime().Should().BeLessThan(1000.Milliseconds()); request.Invoke().Result.Data.longRunning.Should().Be("finally returned"); // reset stuff @@ -182,8 +181,7 @@ query Long { request.Start(); chatQuery.WaitingOnQueryBlocker.Wait(1000).Should().BeTrue("because the request should have reached the server by then"); cts.Cancel(); - FluentActions.Awaiting(() => request.Invoking().Should().ThrowAsync("because the request was cancelled")) - .ExecutionTime().Should().BeLessThan(1000.Milliseconds()); + request.Invoking().Should().Throw("because the request was cancelled"); // let the server finish its query chatQuery.LongRunningQueryBlocker.Set(); diff --git a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs index 016f0dab..59f57ceb 100644 --- a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs @@ -80,7 +80,6 @@ query Long { // unblock the query chatQuery.LongRunningQueryBlocker.Set(); // check execution time - request.Invoking().ExecutionTime().Should().BeLessThan(1000.Milliseconds()); request.Invoke().Result.Data.longRunning.Should().Be("finally returned"); // reset stuff @@ -91,8 +90,7 @@ query Long { request.Start(); chatQuery.WaitingOnQueryBlocker.Wait(1000).Should().BeTrue("because the request should have reached the server by then"); cts.Cancel(); - FluentActions.Awaiting(() => request.Invoking().Should().ThrowAsync("because the request was cancelled")) - .ExecutionTime().Should().BeLessThan(1000.Milliseconds()); + request.Invoking().Should().Throw("because the request was cancelled"); // let the server finish its query chatQuery.LongRunningQueryBlocker.Set(); From 80ec16795fe588e101f58e2e8764fee084e65854 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Mon, 9 Mar 2020 14:35:48 +0100 Subject: [PATCH 15/16] enable windows workflow for comparison --- .github/workflows/branches-windows.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/branches-windows.yml b/.github/workflows/branches-windows.yml index 430b2121..a896d523 100644 --- a/.github/workflows/branches-windows.yml +++ b/.github/workflows/branches-windows.yml @@ -1,8 +1,10 @@ name: Branch workflow -on: +on: push: branches-ignore: - - '**' + - develop + - 'release/**' + - 'releases/**' jobs: generateVersionInfo: name: GenerateVersionInfo From 0a7f91d03ffeecd162e3a3177d1dacb45faf96a7 Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Mon, 9 Mar 2020 15:09:38 +0100 Subject: [PATCH 16/16] disable windows job again --- .github/workflows/branches-windows.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/branches-windows.yml b/.github/workflows/branches-windows.yml index a896d523..fdf2b81c 100644 --- a/.github/workflows/branches-windows.yml +++ b/.github/workflows/branches-windows.yml @@ -2,9 +2,7 @@ name: Branch workflow on: push: branches-ignore: - - develop - - 'release/**' - - 'releases/**' + - '**' jobs: generateVersionInfo: name: GenerateVersionInfo