From 3738f84a3b32650c3bc5161eb68bb1005779d146 Mon Sep 17 00:00:00 2001 From: thefringeninja <495495+thefringeninja@users.noreply.github.com> Date: Mon, 4 Jan 2021 11:10:51 +0100 Subject: [PATCH 01/21] dual target netcoreapp3.1 and net5.0 --- .github/workflows/ci.yml | 38 +++++++++++-------- Directory.Build.props | 4 ++ src/Directory.Build.props | 8 ++-- ...ntStoreProjectionManagementClient.State.cs | 4 +- ...reProjectionManagementClient.Statistics.cs | 2 +- .../EventStoreClient.Read.cs | 2 +- .../StreamAclJsonConverter.cs | 4 +- .../StreamMetadataJsonConverter.cs | 2 +- .../StreamMetadataResult.cs | 10 +---- src/EventStore.Client/ClusterMessage.cs | 4 +- src/EventStore.Client/EndPointExtensions.cs | 9 +++-- .../EventStore.Client.csproj | 15 +++++--- ...entStoreClientSettings.ConnectionString.cs | 10 ++--- .../EventStoreClientSettings.cs | 4 +- .../Interceptors/TypedExceptionInterceptor.cs | 1 - .../SingleNodeHttpHandler.cs | 2 +- src/EventStore.Client/UserCredentials.cs | 4 ++ .../TestEventExtensions.cs | 2 +- .../ConnectionStringTests.cs | 2 +- .../EventStore.Client.Tests.csproj | 3 -- 20 files changed, 69 insertions(+), 61 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6edca43b2..84cb3efb7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,13 +8,11 @@ on: tags: - v* -env: - DOTNET_SDK_VERSION: 3.1.302 jobs: vulnerability-scan: runs-on: ubuntu-latest name: ci/github/scan-vulnerabilities - container: mcr.microsoft.com/dotnet/core/sdk:3.1-bionic + container: mcr.microsoft.com/dotnet/sdk:5.0-bionic steps: - name: Checkout uses: actions/checkout@v2 @@ -23,16 +21,17 @@ jobs: dotnet tool restore dotnet restore dotnet tool run dotnet-retire - build: + build-dotnet: strategy: fail-fast: false matrix: + framework: [netcoreapp3.1, net5.0] os: [ubuntu-18.04] test: ["", .Streams, .PersistentSubscriptions, .Operations, .UserManagement, .ProjectionManagement] configuration: [release] docker-tag: ['ci'] runs-on: ${{ matrix.os }} - name: ci/github/build-${{ matrix.os }}/EventStore.Client${{ matrix.test }} + name: build-${{ matrix.os }}/${{ matrix.framework }}/EventStore.Client${{ matrix.test }} steps: - name: Checkout uses: actions/checkout@v2 @@ -49,14 +48,18 @@ jobs: shell: bash run: | docker pull docker.pkg.github.com/eventstore/eventstore/eventstore:${{ matrix.docker-tag }} - - name: Install Dotnet + - name: Install netcoreapp3.1 uses: actions/setup-dotnet@v1 with: - dotnet-version: ${{ env.DOTNET_SDK_VERSION }} + dotnet-version: 3.1.x + - name: Install net5.0 + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 5.0.x - name: Compile shell: bash run: | - dotnet build --configuration ${{ matrix.configuration }} src/EventStore.Client${{ matrix.test }} + dotnet build --configuration ${{ matrix.configuration }} --framework ${{ matrix.framework }} src/EventStore.Client${{ matrix.test }} - name: Run Tests shell: bash env: @@ -64,9 +67,10 @@ jobs: run: | ./gencert.sh dotnet test --configuration ${{ matrix.configuration }} --blame \ - --logger:html --logger:trx --logger:"console;verbosity=normal" \ - --results-directory=$(pwd)/test-results/test/EventStore.Client${{ matrix.test }}.Tests \ - test/EventStore.Client${{ matrix.test }}.Tests + --logger:html --logger:trx --logger:"console;verbosity=normal" \ + --results-directory=$(pwd)/test-results/test/EventStore.Client${{ matrix.test }}.Tests \ + --framework ${{ matrix.framework }} \ + test/EventStore.Client${{ matrix.test }}.Tests - name: Collect Test Results shell: bash if: always() @@ -85,9 +89,9 @@ jobs: name: test-results-${{ matrix.configuration }}-EventStore.Client${{ matrix.test }} path: test-results publish: - needs: [vulnerability-scan, build] + needs: [vulnerability-scan, build-dotnet] runs-on: ubuntu-latest - name: ci/github/publish + name: publish steps: - name: Checkout uses: actions/checkout@v2 @@ -102,10 +106,14 @@ jobs: - shell: bash run: | git fetch --prune --unshallow - - name: Setup Dotnet + - name: Install netcoreapp3.1 + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 3.1.x + - name: Install net5.0 uses: actions/setup-dotnet@v1 with: - dotnet-version: ${{ env.DOTNET_SDK_VERSION }} + dotnet-version: 5.0.x - name: Dotnet Pack shell: bash run: | diff --git a/Directory.Build.props b/Directory.Build.props index 85cbad82e..c197af600 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,6 @@ + netcoreapp3.1;net5.0 x64 true disable @@ -13,6 +14,9 @@ EventStore.Client true + + $(DefineConstants);GRPC_CORE + diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 41aa5f448..5992e66b0 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,8 +1,5 @@ - - netcoreapp3.1;netstandard2.1 - $(MSBuildProjectName.Remove(0,18)) $(ESPackageIdSuffix.ToLower()).proto @@ -28,6 +25,7 @@ Copyright 2012-2020 Event Store Ltd v true + 2.34.0 @@ -38,11 +36,11 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers - + diff --git a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.State.cs b/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.State.cs index cc382dd10..cff716c74 100644 --- a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.State.cs +++ b/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.State.cs @@ -55,7 +55,7 @@ public async Task GetResultAsync(string name, string? partition = null, await writer.FlushAsync(cancellationToken).ConfigureAwait(false); stream.Position = 0; - return JsonSerializer.Deserialize(stream.ToArray(), serializerOptions); + return JsonSerializer.Deserialize(stream.ToArray(), serializerOptions)!; } private async ValueTask GetResultInternalAsync(string name, string? partition, @@ -118,7 +118,7 @@ public async Task GetStateAsync(string name, string? partition = null, await writer.FlushAsync(cancellationToken).ConfigureAwait(false); stream.Position = 0; - return JsonSerializer.Deserialize(stream.ToArray(), serializerOptions); + return JsonSerializer.Deserialize(stream.ToArray(), serializerOptions)!; } private async ValueTask GetStateInternalAsync(string name, string? partition, diff --git a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Statistics.cs b/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Statistics.cs index d20ba5d3c..83498129c 100644 --- a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Statistics.cs +++ b/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Statistics.cs @@ -44,7 +44,7 @@ public Task GetStatusAsync(string name, UserCredentials? user CancellationToken cancellationToken = default) => ListInternalAsync(new StatisticsReq.Types.Options { Name = name - }, userCredentials, cancellationToken).FirstOrDefaultAsync(cancellationToken).AsTask(); + }, userCredentials, cancellationToken).FirstOrDefaultAsync(cancellationToken).AsTask()!; private async IAsyncEnumerable ListInternalAsync(StatisticsReq.Types.Options options, UserCredentials? userCredentials, diff --git a/src/EventStore.Client.Streams/EventStoreClient.Read.cs b/src/EventStore.Client.Streams/EventStoreClient.Read.cs index 675bceaf9..036f37ae1 100644 --- a/src/EventStore.Client.Streams/EventStoreClient.Read.cs +++ b/src/EventStore.Client.Streams/EventStoreClient.Read.cs @@ -275,7 +275,7 @@ private static ResolvedEvent ConvertToResolvedEvent(ReadResp.Types.ReadEvent rea _ => throw new InvalidOperationException() }); - private static EventRecord? ConvertToEventRecord(ReadResp.Types.ReadEvent.Types.RecordedEvent e) => + private static EventRecord? ConvertToEventRecord(ReadResp.Types.ReadEvent.Types.RecordedEvent? e) => e == null ? null : new EventRecord( diff --git a/src/EventStore.Client.Streams/StreamAclJsonConverter.cs b/src/EventStore.Client.Streams/StreamAclJsonConverter.cs index 36d41dcff..3063a9214 100644 --- a/src/EventStore.Client.Streams/StreamAclJsonConverter.cs +++ b/src/EventStore.Client.Streams/StreamAclJsonConverter.cs @@ -60,7 +60,7 @@ public override StreamAcl Read(ref Utf8JsonReader reader, Type typeToConvert, } if (reader.TokenType == JsonTokenType.String) { - return new[] {reader.GetString()}; + return new[] {reader.GetString()!}; } if (reader.TokenType != JsonTokenType.StartArray) { @@ -78,7 +78,7 @@ public override StreamAcl Read(ref Utf8JsonReader reader, Type typeToConvert, throw new InvalidOperationException(); } - roles.Add(reader.GetString()); + roles.Add(reader.GetString()!); } return roles.ToArray(); diff --git a/src/EventStore.Client.Streams/StreamMetadataJsonConverter.cs b/src/EventStore.Client.Streams/StreamMetadataJsonConverter.cs index 88c4faf57..877b8ed7a 100644 --- a/src/EventStore.Client.Streams/StreamMetadataJsonConverter.cs +++ b/src/EventStore.Client.Streams/StreamMetadataJsonConverter.cs @@ -73,7 +73,7 @@ public override StreamMetadata Read(ref Utf8JsonReader reader, Type typeToConver acl = StreamAclJsonConverter.Instance.Read(ref reader, typeof(StreamAcl), options); break; default: - customMetadataWriter.WritePropertyName(reader.GetString()); + customMetadataWriter.WritePropertyName(reader.GetString()!); reader.Read(); switch (reader.TokenType) { case JsonTokenType.Comment: diff --git a/src/EventStore.Client.Streams/StreamMetadataResult.cs b/src/EventStore.Client.Streams/StreamMetadataResult.cs index 7d5572e14..f3a56359d 100644 --- a/src/EventStore.Client.Streams/StreamMetadataResult.cs +++ b/src/EventStore.Client.Streams/StreamMetadataResult.cs @@ -71,18 +71,10 @@ public bool Equals(StreamMetadataResult other) => /// /// public static StreamMetadataResult Create(string streamName, StreamPosition revision, - StreamMetadata metadata) { - if (metadata == null) throw new ArgumentNullException(nameof(metadata)); - - return new StreamMetadataResult(streamName, revision, metadata); - } + StreamMetadata metadata) => new StreamMetadataResult(streamName, revision, metadata); private StreamMetadataResult(string streamName, StreamPosition? metastreamRevision = null, StreamMetadata metadata = default, bool streamDeleted = false) { - if (streamName == null) { - throw new ArgumentNullException(nameof(streamName)); - } - StreamName = streamName; StreamDeleted = streamDeleted; Metadata = metadata; diff --git a/src/EventStore.Client/ClusterMessage.cs b/src/EventStore.Client/ClusterMessage.cs index bf8d28bc7..bc180d31a 100644 --- a/src/EventStore.Client/ClusterMessage.cs +++ b/src/EventStore.Client/ClusterMessage.cs @@ -1,4 +1,5 @@ using System; +using System.Net; #nullable enable namespace EventStore.Client { @@ -12,8 +13,7 @@ public class MemberInfo { public VNodeState State { get; set; } public bool IsAlive { get; set; } - public string? HttpEndPointAddress { get; set; } - public int HttpEndPointPort { get; set; } + public DnsEndPoint EndPoint { get; set; } = null!; } public enum VNodeState { diff --git a/src/EventStore.Client/EndPointExtensions.cs b/src/EventStore.Client/EndPointExtensions.cs index b74a13ea8..39e101b81 100644 --- a/src/EventStore.Client/EndPointExtensions.cs +++ b/src/EventStore.Client/EndPointExtensions.cs @@ -3,9 +3,6 @@ namespace EventStore.Client { internal static class EndPointExtensions { - public static string HTTP_SCHEMA => Uri.UriSchemeHttp; - public static string HTTPS_SCHEMA => Uri.UriSchemeHttps; - public static string GetHost(this EndPoint endpoint) => endpoint switch { IPEndPoint ip => ip.Address.ToString(), @@ -22,6 +19,12 @@ public static int GetPort(this EndPoint endpoint) => "An invalid endpoint has been provided") }; + public static Uri ToUri(this EndPoint endPoint, bool https) => new UriBuilder { + Scheme = https ? Uri.UriSchemeHttps : Uri.UriSchemeHttp, + Host = endPoint.GetHost(), + Port = endPoint.GetPort() + }.Uri; + public static string ToHttpUrl(this EndPoint endPoint, string schema, string rawUrl = null) => endPoint switch { IPEndPoint ipEndPoint => CreateHttpUrl(schema, ipEndPoint.Address.ToString(), ipEndPoint.Port, diff --git a/src/EventStore.Client/EventStore.Client.csproj b/src/EventStore.Client/EventStore.Client.csproj index df8b250b1..7b754b03d 100644 --- a/src/EventStore.Client/EventStore.Client.csproj +++ b/src/EventStore.Client/EventStore.Client.csproj @@ -6,12 +6,17 @@ EventStore.Client.Grpc - - - + + - - + + + + + + + + diff --git a/src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs b/src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs index ade2e457b..d815f7f5a 100644 --- a/src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs +++ b/src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs @@ -2,7 +2,9 @@ using System.Collections.Generic; using System.Linq; using System.Net; +#if !GRPC_CORE using System.Net.Http; +#endif namespace EventStore.Client { public partial class EventStoreClientSettings { @@ -159,16 +161,12 @@ private static EventStoreClientSettings CreateSettings(string scheme, (string us if (typedOptions.TryGetValue(TlsVerifyCert, out object tlsVerifyCert)) { if (!(bool)tlsVerifyCert) { -#if NETCOREAPP3_1 +#if !GRPC_CORE settings.CreateHttpMessageHandler = () => new SocketsHttpHandler { SslOptions = { RemoteCertificateValidationCallback = delegate { return true; } } }; -#elif NETSTANDARD2_1 - settings.CreateHttpMessageHandler = () => new HttpClientHandler { - ServerCertificateCustomValidationCallback = delegate { return true; } - }; #endif } } @@ -180,7 +178,7 @@ private static EventStoreClientSettings CreateSettings(string scheme, (string us settings.OperationOptions.ThrowOnAppendFailure = (bool)throwOnAppendFailure; if (hosts.Length == 1 && scheme != UriSchemeDiscover) { - connSettings.Address = new Uri(hosts[0].ToHttpUrl(useTls ? Uri.UriSchemeHttps : Uri.UriSchemeHttp)); + connSettings.Address = hosts[0].ToUri(useTls); } else { if (hosts.Any(x => x is DnsEndPoint)) connSettings.DnsGossipSeeds = diff --git a/src/EventStore.Client/EventStoreClientSettings.cs b/src/EventStore.Client/EventStoreClientSettings.cs index 9e294aa1b..08016b52d 100644 --- a/src/EventStore.Client/EventStoreClientSettings.cs +++ b/src/EventStore.Client/EventStoreClientSettings.cs @@ -3,7 +3,7 @@ using System.Net.Http; using Grpc.Core; using Grpc.Core.Interceptors; -using Grpc.Net.Client; + using Microsoft.Extensions.Logging; #nullable enable @@ -33,7 +33,7 @@ public partial class EventStoreClientSettings { public ILoggerFactory? LoggerFactory { get; set; } /// - /// The optional to use when creating the . + /// The optional to use when creating the . /// public ChannelCredentials? ChannelCredentials { get; set; } diff --git a/src/EventStore.Client/Interceptors/TypedExceptionInterceptor.cs b/src/EventStore.Client/Interceptors/TypedExceptionInterceptor.cs index d642f8486..f4bc0dc2b 100644 --- a/src/EventStore.Client/Interceptors/TypedExceptionInterceptor.cs +++ b/src/EventStore.Client/Interceptors/TypedExceptionInterceptor.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Net; using System.Threading; using System.Threading.Tasks; using Grpc.Core; diff --git a/src/EventStore.Client/SingleNodeHttpHandler.cs b/src/EventStore.Client/SingleNodeHttpHandler.cs index 09af2b1ca..45315f313 100644 --- a/src/EventStore.Client/SingleNodeHttpHandler.cs +++ b/src/EventStore.Client/SingleNodeHttpHandler.cs @@ -14,7 +14,7 @@ public SingleNodeHttpHandler(EventStoreClientSettings settings) { protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { - request.RequestUri = new UriBuilder(request.RequestUri) { + request.RequestUri = new UriBuilder(request.RequestUri!) { Scheme = _settings.ConnectivitySettings.Address.Scheme }.Uri; return base.SendAsync(request, cancellationToken); diff --git a/src/EventStore.Client/UserCredentials.cs b/src/EventStore.Client/UserCredentials.cs index 7e49b0bb2..027b3b78b 100644 --- a/src/EventStore.Client/UserCredentials.cs +++ b/src/EventStore.Client/UserCredentials.cs @@ -47,6 +47,10 @@ private bool TryGetBasicAuth(int index, out string? value) { return false; } + if (_authorization.Parameter == null) { + return false; + } + var parts = Encoding.ASCII.GetString(Convert.FromBase64String(_authorization.Parameter)).Split(':'); if (parts.Length <= index) { return false; diff --git a/test/EventStore.Client.Tests.Common/TestEventExtensions.cs b/test/EventStore.Client.Tests.Common/TestEventExtensions.cs index 36a161a5a..4ee554532 100644 --- a/test/EventStore.Client.Tests.Common/TestEventExtensions.cs +++ b/test/EventStore.Client.Tests.Common/TestEventExtensions.cs @@ -6,7 +6,7 @@ namespace EventStore.Client { internal static class TestEventExtensions { public static IEnumerable AsResolvedTestEvents(this IEnumerable events) { if (events == null) throw new ArgumentNullException(nameof(events)); - return events.Where(x => x.Event.EventType == EventStoreClientFixture.TestEventType).Select(x => x.Event); + return events.Where(x => x.Event.EventType == EventStoreClientFixtureBase.TestEventType).Select(x => x.Event); } } } diff --git a/test/EventStore.Client.Tests/ConnectionStringTests.cs b/test/EventStore.Client.Tests/ConnectionStringTests.cs index 32d973866..5b3c9c6f1 100644 --- a/test/EventStore.Client.Tests/ConnectionStringTests.cs +++ b/test/EventStore.Client.Tests/ConnectionStringTests.cs @@ -269,7 +269,7 @@ public void with_different_tls_verify_cert_settings() { Assert.NotNull(settings.CreateHttpMessageHandler); } - private static IEnumerable DiscoverSchemeCases() { + public static IEnumerable DiscoverSchemeCases() { yield return new object[] { "esdb+discover://hostname:4321", new EndPoint[] { new DnsEndPoint("hostname", 4321) diff --git a/test/EventStore.Client.Tests/EventStore.Client.Tests.csproj b/test/EventStore.Client.Tests/EventStore.Client.Tests.csproj index 9f1e6901d..ecd708b6f 100644 --- a/test/EventStore.Client.Tests/EventStore.Client.Tests.csproj +++ b/test/EventStore.Client.Tests/EventStore.Client.Tests.csproj @@ -3,9 +3,6 @@ - - - From 27ba7fc382b2a415db390fe93ef1ff89feeb23a1 Mon Sep 17 00:00:00 2001 From: thefringeninja <495495+thefringeninja@users.noreply.github.com> Date: Tue, 5 Jan 2021 08:41:49 +0100 Subject: [PATCH 02/21] add gossip support to grpc.core implementation --- .../AsyncStreamReaderExtensions.cs | 16 + src/EventStore.Client/ArrayExtensions.cs | 17 + src/EventStore.Client/ChannelFactory.cs | 45 +++ .../ClusterAwareHttpHandler.cs | 71 ++-- src/EventStore.Client/EventStoreClientBase.cs | 66 +--- .../Exceptions/DiscoveryException.cs | 16 + .../GossipBasedEndpointDiscoverer.cs | 258 +++++------- src/EventStore.Client/GrpcGossipClient.cs | 54 +++ src/EventStore.Client/IEndpointDiscoverer.cs | 3 +- src/EventStore.Client/IGossipClient.cs | 10 + .../Interceptors/HostSelectorInterceptor.cs | 104 +++++ .../SingleNodeEndpointDiscoverer.cs | 16 + test/Directory.Build.props | 15 +- .../EventStore.Client.IntegrationTests.csproj | 11 + .../EventStoreClientFixture.cs | 154 ++++++++ .../ClusterAwareHttpHandlerTests.cs | 66 ++-- .../GossipBasedEndpointDiscovererTests.cs | 367 +++++++----------- .../HostSelectorInterceptorTests.cs | 177 +++++++++ 18 files changed, 942 insertions(+), 524 deletions(-) create mode 100644 src/EventStore.Client.Common/AsyncStreamReaderExtensions.cs create mode 100644 src/EventStore.Client/ArrayExtensions.cs create mode 100644 src/EventStore.Client/ChannelFactory.cs create mode 100644 src/EventStore.Client/GrpcGossipClient.cs create mode 100644 src/EventStore.Client/IGossipClient.cs create mode 100644 src/EventStore.Client/Interceptors/HostSelectorInterceptor.cs create mode 100644 src/EventStore.Client/SingleNodeEndpointDiscoverer.cs create mode 100644 test/EventStore.Client.IntegrationTests/EventStore.Client.IntegrationTests.csproj create mode 100644 test/EventStore.Client.IntegrationTests/EventStoreClientFixture.cs create mode 100644 test/EventStore.Client.Tests/Interceptors/HostSelectorInterceptorTests.cs diff --git a/src/EventStore.Client.Common/AsyncStreamReaderExtensions.cs b/src/EventStore.Client.Common/AsyncStreamReaderExtensions.cs new file mode 100644 index 000000000..18a39edb8 --- /dev/null +++ b/src/EventStore.Client.Common/AsyncStreamReaderExtensions.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; + +#if GRPC_CORE +namespace Grpc.Core { + internal static class AsyncStreamReaderExtensions { + public static async IAsyncEnumerable ReadAllAsync(this IAsyncStreamReader reader, + [EnumeratorCancellation] CancellationToken cancellationToken = default) { + while (await reader.MoveNext(cancellationToken).ConfigureAwait(false)) { + yield return reader.Current; + } + } + } +} +#endif diff --git a/src/EventStore.Client/ArrayExtensions.cs b/src/EventStore.Client/ArrayExtensions.cs new file mode 100644 index 000000000..05be5b980 --- /dev/null +++ b/src/EventStore.Client/ArrayExtensions.cs @@ -0,0 +1,17 @@ +using System; + +namespace EventStore.Client { + internal static class ArrayExtensions { + public static void RandomShuffle(this T[] arr, int i, int j) { + if (i >= j) + return; + var rnd = new Random(Guid.NewGuid().GetHashCode()); + for (int k = i; k <= j; ++k) { + var index = rnd.Next(k, j + 1); + var tmp = arr[index]; + arr[index] = arr[k]; + arr[k] = tmp; + } + } + } +} diff --git a/src/EventStore.Client/ChannelFactory.cs b/src/EventStore.Client/ChannelFactory.cs new file mode 100644 index 000000000..66a8e4c34 --- /dev/null +++ b/src/EventStore.Client/ChannelFactory.cs @@ -0,0 +1,45 @@ +using System; +using System.Net; +using Grpc.Core; +#if !GRPC_CORE +using System.Net.Http; +using System.Threading; +using Grpc.Net.Client; +#endif +#nullable enable +namespace EventStore.Client { + internal static class ChannelFactory { + public static ChannelBase CreateChannel(EventStoreClientSettings settings, EndPoint endPoint) => + CreateChannel(settings, endPoint.ToUri(settings.ConnectivitySettings.GossipOverHttps)); + + public static ChannelBase CreateChannel(EventStoreClientSettings settings, Uri? address) { + address ??= settings.ConnectivitySettings.Address; + +#if !GRPC_CORE + if (address.Scheme == Uri.UriSchemeHttp || + !settings.ConnectivitySettings.GossipOverHttps) { + //this must be switched on before creation of the HttpMessageHandler + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + } + return GrpcChannel.ForAddress(address, new GrpcChannelOptions { + HttpClient = new HttpClient(new ClusterAwareHttpHandler(settings.ConnectivitySettings.GossipOverHttps, + settings.ConnectivitySettings.NodePreference == NodePreference.Leader, + settings.ConnectivitySettings.IsSingleNode + ? (IEndpointDiscoverer)new SingleNodeEndpointDiscoverer(settings.ConnectivitySettings.Address) + : new GossipBasedEndpointDiscoverer(settings.ConnectivitySettings, + new GrpcGossipClient(settings))) { + InnerHandler = settings.CreateHttpMessageHandler?.Invoke() ?? new SocketsHttpHandler() + }, true) { + Timeout = Timeout.InfiniteTimeSpan, + DefaultRequestVersion = new Version(2, 0), + }, + LoggerFactory = settings.LoggerFactory, + Credentials = settings.ChannelCredentials, + DisposeHttpClient = true + }); +#else + return new Channel(address.Host, address.Port, settings.ChannelCredentials ?? ChannelCredentials.Insecure); +#endif + } + } +} diff --git a/src/EventStore.Client/ClusterAwareHttpHandler.cs b/src/EventStore.Client/ClusterAwareHttpHandler.cs index b3e2eb32c..d563cd872 100644 --- a/src/EventStore.Client/ClusterAwareHttpHandler.cs +++ b/src/EventStore.Client/ClusterAwareHttpHandler.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Net; using System.Net.Http; using System.Threading; @@ -7,45 +8,17 @@ #nullable enable namespace EventStore.Client { /// - public class ClusterAwareHttpHandler : DelegatingHandler { + internal class ClusterAwareHttpHandler : DelegatingHandler { private readonly bool _useHttps; private readonly bool _requiresLeader; private readonly IEndpointDiscoverer _endpointDiscoverer; private Lazy> _endpoint; - /// - /// Factory method to create a . - /// - /// - /// - /// - public static ClusterAwareHttpHandler Create(EventStoreClientSettings settings, - HttpMessageHandler? httpMessageHandler = null) => new ClusterAwareHttpHandler( - settings.ConnectivitySettings.GossipOverHttps, - settings.ConnectivitySettings.NodePreference == NodePreference.Leader, - new ClusterEndpointDiscoverer( - settings.ConnectivitySettings.MaxDiscoverAttempts, - settings.ConnectivitySettings.GossipSeeds, - settings.ConnectivitySettings.GossipTimeout, - settings.ConnectivitySettings.GossipOverHttps, - settings.ConnectivitySettings.DiscoveryInterval, - settings.ConnectivitySettings.NodePreference, - httpMessageHandler)) { - InnerHandler = httpMessageHandler - }; - - - /// - /// Constructs a new . - /// - /// - /// - /// public ClusterAwareHttpHandler(bool useHttps, bool requiresLeader, IEndpointDiscoverer endpointDiscoverer) { _useHttps = useHttps; _requiresLeader = requiresLeader; _endpointDiscoverer = endpointDiscoverer; - _endpoint = new Lazy>(endpointDiscoverer.DiscoverAsync, + _endpoint = new Lazy>(() => endpointDiscoverer.DiscoverAsync(), LazyThreadSafetyMode.ExecutionAndPublication); } @@ -56,30 +29,42 @@ protected override async Task SendAsync(HttpRequestMessage try { var endpoint = await endpointResolver.Value.ConfigureAwait(false); - request.RequestUri = new UriBuilder(request.RequestUri) { + request.RequestUri = new UriBuilder(request.RequestUri!) { Host = endpoint.GetHost(), Port = endpoint.GetPort(), Scheme = _useHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp }.Uri; request.Headers.Add("requires-leader", _requiresLeader.ToString()); - return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + + if (!response.TrailingHeaders.TryGetValues(Constants.Exceptions.ExceptionKey, out var key) || + !key.Contains(Constants.Exceptions.NotLeader) || + !response.TrailingHeaders.TryGetValues(Constants.Exceptions.LeaderEndpointHost, out var hosts) || + !response.TrailingHeaders.TryGetValues(Constants.Exceptions.LeaderEndpointPort, out var ports)) { + return response; + } + + foreach (var host in hosts) { + foreach (var port in ports) { + if (!int.TryParse(port, out var p)) { + continue; + } + + Interlocked.Exchange(ref _endpoint, + new Lazy>(Task.FromResult(new DnsEndPoint(host, p)))); + + return response; + } + } + + return response; } catch (Exception) { Interlocked.CompareExchange(ref _endpoint, - new Lazy>(_endpointDiscoverer.DiscoverAsync, + new Lazy>(() => _endpointDiscoverer.DiscoverAsync(cancellationToken), LazyThreadSafetyMode.ExecutionAndPublication), endpointResolver); throw; } } - - /// - /// Notifies the that an exception occurred, to allow it to select another . - /// - /// - public void ExceptionOccurred(Exception exception) { - if (exception is NotLeaderException ex) { - _endpoint = new Lazy>(Task.FromResult(ex.LeaderEndpoint)); - } - } } } diff --git a/src/EventStore.Client/EventStoreClientBase.cs b/src/EventStore.Client/EventStoreClientBase.cs index f3276fbd0..9a3607581 100644 --- a/src/EventStore.Client/EventStoreClientBase.cs +++ b/src/EventStore.Client/EventStoreClientBase.cs @@ -1,12 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Net.Http; -using System.Threading; using EventStore.Client.Interceptors; using Grpc.Core; using Grpc.Core.Interceptors; -using Grpc.Net.Client; #nullable enable namespace EventStore.Client { @@ -14,9 +11,7 @@ namespace EventStore.Client { /// The base class used by clients used to communicate with the EventStoreDB. /// public abstract class EventStoreClientBase : IDisposable { - private readonly GrpcChannel _channel; - private readonly HttpMessageHandler _httpHandler; - private readonly HttpMessageHandler _innerHttpHandler; + private readonly ChannelBase _channel; /// /// The . @@ -39,56 +34,31 @@ protected EventStoreClientBase(EventStoreClientSettings? settings, var connectionName = Settings.ConnectionName ?? $"ES-{Guid.NewGuid()}"; - if (Settings.ConnectivitySettings.Address.Scheme == Uri.UriSchemeHttp || - !Settings.ConnectivitySettings.GossipOverHttps) { - //this must be switched on before creation of the HttpMessageHandler - AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); - } - -#if NETCOREAPP3_1 - _innerHttpHandler = Settings.CreateHttpMessageHandler?.Invoke() ?? new SocketsHttpHandler(); -#elif NETSTANDARD2_1 - _innerHttpHandler = Settings.CreateHttpMessageHandler?.Invoke() ?? new HttpClientHandler(); -#endif - - _httpHandler = Settings.ConnectivitySettings.IsSingleNode - ? (HttpMessageHandler)new SingleNodeHttpHandler(Settings) { - InnerHandler = _innerHttpHandler - } - : ClusterAwareHttpHandler.Create(Settings, _innerHttpHandler); - -#if NETSTANDARD2_1 - _httpHandler = new DefaultRequestVersionHandler(_httpHandler); -#endif - - _channel = GrpcChannel.ForAddress(Settings.ConnectivitySettings.Address, new GrpcChannelOptions { - HttpClient = new HttpClient(_httpHandler) { - Timeout = Timeout.InfiniteTimeSpan, -#if NETCOREAPP3_1 - DefaultRequestVersion = new Version(2, 0), -#endif - }, - LoggerFactory = Settings.LoggerFactory, - Credentials = Settings.ChannelCredentials - }); - - Action? exceptionNotificationHook = - _httpHandler is ClusterAwareHttpHandler h - ? h.ExceptionOccurred - : new Action(ex => { }); + _channel = ChannelFactory.CreateChannel(Settings, Settings.ConnectivitySettings.Address); CallInvoker = (Settings.Interceptors ?? Array.Empty()).Aggregate( _channel.CreateCallInvoker() - .Intercept(new TypedExceptionInterceptor(exceptionMap, exceptionNotificationHook)) - .Intercept(new ConnectionNameInterceptor(connectionName)), + .Intercept(new TypedExceptionInterceptor(exceptionMap, ex => { })) + .Intercept(new ConnectionNameInterceptor(connectionName)) +#if GRPC_CORE + .Intercept(new HostSelectorInterceptor(Settings.ConnectivitySettings.IsSingleNode + ? (IEndpointDiscoverer)new SingleNodeEndpointDiscoverer( + Settings.ConnectivitySettings.Address) + : new GossipBasedEndpointDiscoverer(Settings.ConnectivitySettings, + new GrpcGossipClient(Settings)), + Settings.ConnectivitySettings.NodePreference)) +#endif + , (invoker, interceptor) => invoker.Intercept(interceptor)); } /// public void Dispose() { - _channel?.Dispose(); - _innerHttpHandler?.Dispose(); - _httpHandler?.Dispose(); + // ReSharper disable SuspiciousTypeConversion.Global + if (_channel is IDisposable disposable) { + // ReSharper restore SuspiciousTypeConversion.Global + disposable.Dispose(); + } } } } diff --git a/src/EventStore.Client/Exceptions/DiscoveryException.cs b/src/EventStore.Client/Exceptions/DiscoveryException.cs index 8cae6ab7e..485da6ba3 100644 --- a/src/EventStore.Client/Exceptions/DiscoveryException.cs +++ b/src/EventStore.Client/Exceptions/DiscoveryException.cs @@ -6,13 +6,29 @@ namespace EventStore.Client { /// The exception that is thrown when discovery fails. /// public class DiscoveryException : Exception { + /// + /// The configured number of discovery attempts. + /// + public int MaxDiscoverAttempts { get; } + /// /// Constructs a new . /// /// /// + [Obsolete] public DiscoveryException(string message, Exception? innerException = null) : base(message, innerException) { + MaxDiscoverAttempts = 0; + } + + /// + /// Constructs a new . + /// + /// The configured number of discovery attempts. + public DiscoveryException(int maxDiscoverAttempts) : base( + $"Failed to discover candidate in {maxDiscoverAttempts} attempts.") { + MaxDiscoverAttempts = maxDiscoverAttempts; } } } diff --git a/src/EventStore.Client/GossipBasedEndpointDiscoverer.cs b/src/EventStore.Client/GossipBasedEndpointDiscoverer.cs index d57b56d69..af6d5a5ab 100644 --- a/src/EventStore.Client/GossipBasedEndpointDiscoverer.cs +++ b/src/EventStore.Client/GossipBasedEndpointDiscoverer.cs @@ -1,210 +1,140 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Net; -using System.Net.Http; using System.Threading; using System.Threading.Tasks; -using Grpc.Net.Client; using EndPoint = System.Net.EndPoint; -using GossipClient = EventStore.Client.Gossip.Gossip.GossipClient; #nullable enable namespace EventStore.Client { - internal class ClusterEndpointDiscoverer : IEndpointDiscoverer { - private readonly int _maxDiscoverAttempts; - private readonly EndPoint[] _gossipSeeds; - private readonly TimeSpan _discoveryInterval; - private ClusterMessages.MemberInfo[]? _oldGossip; - private readonly NodePreference _nodePreference; - private readonly Dictionary _gossipClients; - private readonly Func _gossipClientFactory; - - public ClusterEndpointDiscoverer( - int maxDiscoverAttempts, - EndPoint[] gossipSeeds, - TimeSpan gossipTimeout, - bool gossipOverHttps, - TimeSpan discoveryInterval, - NodePreference nodePreference, - HttpMessageHandler? httpMessageHandler = null) { - _maxDiscoverAttempts = maxDiscoverAttempts; - _gossipSeeds = gossipSeeds; - _discoveryInterval = discoveryInterval; - _nodePreference = nodePreference; - _gossipClients = new Dictionary(); - _gossipClientFactory = gossipSeedEndPoint => { - string url = gossipSeedEndPoint.ToHttpUrl(gossipOverHttps?EndPointExtensions.HTTPS_SCHEMA:EndPointExtensions.HTTP_SCHEMA); - var httpHandler = httpMessageHandler ?? new HttpClientHandler(); -#if NETSTANDARD2_1 - httpHandler = new DefaultRequestVersionHandler(httpHandler); -#endif - - var channel = GrpcChannel.ForAddress(url, new GrpcChannelOptions { - HttpClient = new HttpClient(httpHandler) { - Timeout = gossipTimeout, -#if NETCOREAPP3_1 - DefaultRequestVersion = new Version(2, 0), -#endif - } - }); - var callInvoker = channel.CreateCallInvoker(); - return new GossipClient(callInvoker); - }; + internal class GossipBasedEndpointDiscoverer : IEndpointDiscoverer { + private static readonly ClusterMessages.VNodeState[] _notAllowedStates = { + ClusterMessages.VNodeState.Manager, + ClusterMessages.VNodeState.ShuttingDown, + ClusterMessages.VNodeState.Manager, + ClusterMessages.VNodeState.Shutdown, + ClusterMessages.VNodeState.Unknown, + ClusterMessages.VNodeState.Initializing, + ClusterMessages.VNodeState.CatchingUp, + ClusterMessages.VNodeState.ResigningLeader, + ClusterMessages.VNodeState.ShuttingDown, + ClusterMessages.VNodeState.PreLeader, + ClusterMessages.VNodeState.PreReplica, + ClusterMessages.VNodeState.PreReadOnlyReplica, + ClusterMessages.VNodeState.Clone, + ClusterMessages.VNodeState.DiscoverLeader + }; + + private readonly EventStoreClientConnectivitySettings _settings; + private readonly IGossipClient _gossipClient; + + private ClusterMessages.ClusterInfo? _oldGossip; + + public GossipBasedEndpointDiscoverer(EventStoreClientConnectivitySettings settings, + IGossipClient gossipClient) { + _gossipClient = gossipClient; + _settings = settings; } - public async Task DiscoverAsync() { - for (int attempt = 1; attempt <= _maxDiscoverAttempts; ++attempt) { + public async Task DiscoverAsync(CancellationToken cancellationToken = default) { + for (int attempt = 1; attempt <= _settings.MaxDiscoverAttempts; ++attempt) { try { - var endpoint = await DiscoverEndpointAsync().ConfigureAwait(false); + var endpoint = await DiscoverEndpointAsync(cancellationToken).ConfigureAwait(false); if (endpoint != null) { return endpoint; } - } catch (Exception) { + } catch { } - await Task.Delay(_discoveryInterval).ConfigureAwait(false); + await Task.Delay(_settings.DiscoveryInterval, cancellationToken).ConfigureAwait(false); } - throw new DiscoveryException($"Failed to discover candidate in {_maxDiscoverAttempts} attempts."); + throw new DiscoveryException(_settings.MaxDiscoverAttempts); } - private async Task DiscoverEndpointAsync() { + private async Task DiscoverEndpointAsync(CancellationToken cancellationToken) { var oldGossip = Interlocked.Exchange(ref _oldGossip, null); - var gossipCandidates = oldGossip != null - ? ArrangeGossipCandidates(oldGossip.ToArray()) - : GetGossipCandidates(); + var gossipCandidates = oldGossip?.Members != null + ? ArrangeGossipCandidates(oldGossip.Members) + : GetGossipCandidates(_settings.GossipSeeds); foreach (var candidate in gossipCandidates) { - var gossip = await TryGetGossipFrom(candidate).ConfigureAwait(false); + var gossip = await _gossipClient.GetAsync(candidate, cancellationToken).ConfigureAwait(false); + if (gossip?.Members == null || gossip.Members.Length == 0) continue; - var bestNode = TryDetermineBestNode(gossip.Members, _nodePreference); - if (bestNode == null) continue; - _oldGossip = gossip.Members; + if (!TryDetermineBestNode(out var bestNode)) { + continue; + } + + _oldGossip = gossip; + return bestNode; + + bool TryDetermineBestNode(out EndPoint? endPoint) { + var nodes = gossip.Members.Where(x => x.IsAlive) + .Where(x => !_notAllowedStates.Contains(x.State)) + .OrderByDescending(x => x.State) + .ToArray(); + + switch (_settings.NodePreference) { + case NodePreference.Random: + nodes.RandomShuffle(0, nodes.Length - 1); + break; + case NodePreference.Leader: + nodes = nodes.OrderBy(nodeEntry => nodeEntry.State != ClusterMessages.VNodeState.Leader) + .ToArray(); + nodes.RandomShuffle(0, + nodes.Count(nodeEntry => nodeEntry.State == ClusterMessages.VNodeState.Leader) - 1); + break; + case NodePreference.Follower: + nodes = nodes.OrderBy(nodeEntry => nodeEntry.State != ClusterMessages.VNodeState.Follower) + .ToArray(); + nodes.RandomShuffle(0, + nodes.Count(nodeEntry => nodeEntry.State == ClusterMessages.VNodeState.Follower) - 1); + break; + case NodePreference.ReadOnlyReplica: + nodes = nodes.OrderBy(IsNotReadOnlyReplica).ToArray(); + nodes.RandomShuffle(0, + nodes.Count(nodeEntry => !IsNotReadOnlyReplica(nodeEntry)) - 1); + break; + } + + endPoint = nodes.FirstOrDefault()?.EndPoint; + + return endPoint != null; + } } return null; } - private EndPoint[] GetGossipCandidates() { - EndPoint[] endpoints = _gossipSeeds; - RandomShuffle(endpoints, 0, endpoints.Length - 1); + private static EndPoint[] GetGossipCandidates(EndPoint[] gossipSeeds) { + var endpoints = new EndPoint[gossipSeeds.Length]; + Array.Copy(gossipSeeds, endpoints, gossipSeeds.Length); + endpoints.RandomShuffle(0, endpoints.Length - 1); return endpoints; } - private EndPoint[] ArrangeGossipCandidates(ClusterMessages.MemberInfo[] members) { - var result = new EndPoint[members.Length]; + private static EndPoint[] ArrangeGossipCandidates(IReadOnlyList members) { + var result = new EndPoint[members.Count]; int i = -1; - int j = members.Length; - for (int k = 0; k < members.Length; ++k) { + int j = members.Count; + for (int k = 0; k < members.Count; ++k) { if (members[k].State == ClusterMessages.VNodeState.Manager) - result[--j] = new DnsEndPoint(members[k].HttpEndPointAddress, - members[k].HttpEndPointPort); + result[--j] = members[k].EndPoint; else - result[++i] = new DnsEndPoint(members[k].HttpEndPointAddress, - members[k].HttpEndPointPort); + result[++i] = members[k].EndPoint; } - RandomShuffle(result, 0, i); - RandomShuffle(result, j, members.Length - 1); + result.RandomShuffle(0, i); + result.RandomShuffle(j, members.Count - 1); return result; } - private void RandomShuffle(T[] arr, int i, int j) { - if (i >= j) - return; - var rnd = new Random(Guid.NewGuid().GetHashCode()); - for (int k = i; k <= j; ++k) { - var index = rnd.Next(k, j + 1); - var tmp = arr[index]; - arr[index] = arr[k]; - arr[k] = tmp; - } - } - - private async Task TryGetGossipFrom(EndPoint gossipSeed) { - if (!_gossipClients.TryGetValue(gossipSeed, out var client)) { - client = _gossipClientFactory(gossipSeed); - _gossipClients[gossipSeed] = client; - } - - var clusterInfoDto = await client.ReadAsync(new Empty()); - return ConvertGrpcClusterInfo(clusterInfoDto); - } - - private static ClusterMessages.ClusterInfo ConvertGrpcClusterInfo(Gossip.ClusterInfo clusterInfo) { - var receivedMembers = Array.ConvertAll(clusterInfo.Members.ToArray(), x => - new ClusterMessages.MemberInfo { - InstanceId = Uuid.FromDto(x.InstanceId).ToGuid(), - State = (ClusterMessages.VNodeState) x.State, - IsAlive = x.IsAlive, - HttpEndPointAddress = x.HttpEndPoint.Address, - HttpEndPointPort = (int)x.HttpEndPoint.Port - }); - return new ClusterMessages.ClusterInfo { Members = receivedMembers }; - } - - private EndPoint? TryDetermineBestNode(IEnumerable members, - NodePreference nodePreference) { - var notAllowedStates = new[] { - ClusterMessages.VNodeState.Manager, - ClusterMessages.VNodeState.ShuttingDown, - ClusterMessages.VNodeState.Manager, - ClusterMessages.VNodeState.Shutdown, - ClusterMessages.VNodeState.Unknown, - ClusterMessages.VNodeState.Initializing, - ClusterMessages.VNodeState.CatchingUp, - ClusterMessages.VNodeState.ResigningLeader, - ClusterMessages.VNodeState.ShuttingDown, - ClusterMessages.VNodeState.PreLeader, - ClusterMessages.VNodeState.PreReplica, - ClusterMessages.VNodeState.PreReadOnlyReplica, - ClusterMessages.VNodeState.Clone, - ClusterMessages.VNodeState.DiscoverLeader - }; - - var nodes = members.Where(x => x.IsAlive) - .Where(x => !notAllowedStates.Contains(x.State)) - .OrderByDescending(x => x.State) - .ToArray(); - - switch (nodePreference) { - case NodePreference.Random: - RandomShuffle(nodes, 0, nodes.Length - 1); - break; - case NodePreference.Leader: - nodes = nodes.OrderBy(nodeEntry => nodeEntry.State != ClusterMessages.VNodeState.Leader) - .ToArray(); - RandomShuffle(nodes, 0, - nodes.Count(nodeEntry => nodeEntry.State == ClusterMessages.VNodeState.Leader) - 1); - break; - case NodePreference.Follower: - nodes = nodes.OrderBy(nodeEntry => nodeEntry.State != ClusterMessages.VNodeState.Follower) - .ToArray(); - RandomShuffle(nodes, 0, - nodes.Count(nodeEntry => nodeEntry.State == ClusterMessages.VNodeState.Follower) - 1); - break; - case NodePreference.ReadOnlyReplica: - nodes = nodes.OrderBy(nodeEntry => !IsReadOnlyReplicaState(nodeEntry.State)) - .ToArray(); - RandomShuffle(nodes, 0, - nodes.Count(nodeEntry => IsReadOnlyReplicaState(nodeEntry.State)) - 1); - break; - } - - var node = nodes.FirstOrDefault(); - - return node == default(ClusterMessages.MemberInfo) - ? null - : new DnsEndPoint(node.HttpEndPointAddress, node.HttpEndPointPort); - } - - private bool IsReadOnlyReplicaState(ClusterMessages.VNodeState state) { - return state == ClusterMessages.VNodeState.ReadOnlyLeaderless - || state == ClusterMessages.VNodeState.ReadOnlyReplica; - } + private static bool IsNotReadOnlyReplica(ClusterMessages.MemberInfo node) => + !(node.State == ClusterMessages.VNodeState.ReadOnlyLeaderless || + node.State == ClusterMessages.VNodeState.ReadOnlyReplica); } } diff --git a/src/EventStore.Client/GrpcGossipClient.cs b/src/EventStore.Client/GrpcGossipClient.cs new file mode 100644 index 000000000..a1aa041b8 --- /dev/null +++ b/src/EventStore.Client/GrpcGossipClient.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; + +namespace EventStore.Client { + internal class GrpcGossipClient : IGossipClient, IDisposable { + private readonly EventStoreClientSettings _settings; + private readonly ConcurrentDictionary _clients; + private int _disposed; + + public GrpcGossipClient(EventStoreClientSettings settings) { + _settings = settings; + _clients = new ConcurrentDictionary(); + } + + public async ValueTask GetAsync(EndPoint endPoint, + CancellationToken cancellationToken = default) { + if (Interlocked.CompareExchange(ref _disposed, 0, 0) != 0) { + throw new ObjectDisposedException(GetType().ToString()); + } + + var client = _clients.GetOrAdd(endPoint, + endpoint => new Gossip.Gossip.GossipClient(ChannelFactory.CreateChannel(_settings, endpoint))); + + var result = await client.ReadAsync(new Empty(), cancellationToken: cancellationToken); + + return new ClusterMessages.ClusterInfo { + Members = Array.ConvertAll(result.Members.ToArray(), x => + new ClusterMessages.MemberInfo { + InstanceId = Uuid.FromDto(x.InstanceId).ToGuid(), + State = (ClusterMessages.VNodeState)x.State, + IsAlive = x.IsAlive, + EndPoint = new DnsEndPoint(x.HttpEndPoint.Address, (int)x.HttpEndPoint.Port) + }) + }; + } + + public void Dispose() { + if (Interlocked.Exchange(ref _disposed, 1) == 1) { + return; + } + + foreach (var client in _clients.Values) { + // ReSharper disable once SuspiciousTypeConversion.Global + if (client is IDisposable disposable) { + disposable.Dispose(); + } + } + } + } +} diff --git a/src/EventStore.Client/IEndpointDiscoverer.cs b/src/EventStore.Client/IEndpointDiscoverer.cs index d68665fcd..1f2f190d8 100644 --- a/src/EventStore.Client/IEndpointDiscoverer.cs +++ b/src/EventStore.Client/IEndpointDiscoverer.cs @@ -1,4 +1,5 @@ using System.Net; +using System.Threading; using System.Threading.Tasks; namespace EventStore.Client { @@ -10,6 +11,6 @@ public interface IEndpointDiscoverer { /// Discovers the used to communicate with EventStoreDB. /// /// - Task DiscoverAsync(); + Task DiscoverAsync(CancellationToken cancellationToken = default); } } diff --git a/src/EventStore.Client/IGossipClient.cs b/src/EventStore.Client/IGossipClient.cs new file mode 100644 index 000000000..9e47459ec --- /dev/null +++ b/src/EventStore.Client/IGossipClient.cs @@ -0,0 +1,10 @@ +using System.Net; +using System.Threading; +using System.Threading.Tasks; + +namespace EventStore.Client { + internal interface IGossipClient { + public ValueTask GetAsync(EndPoint endPoint, + CancellationToken cancellationToken = default); + } +} diff --git a/src/EventStore.Client/Interceptors/HostSelectorInterceptor.cs b/src/EventStore.Client/Interceptors/HostSelectorInterceptor.cs new file mode 100644 index 000000000..5dc0021eb --- /dev/null +++ b/src/EventStore.Client/Interceptors/HostSelectorInterceptor.cs @@ -0,0 +1,104 @@ +using System; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Grpc.Core; +using Grpc.Core.Interceptors; + +#nullable enable +namespace EventStore.Client.Interceptors { + internal class HostSelectorInterceptor : Interceptor { + private readonly string _requiresLeaderHeaderValue; + private readonly IEndpointDiscoverer _endpointDiscoverer; + private Lazy> _selectedEndpoint; + + public HostSelectorInterceptor(IEndpointDiscoverer endpointDiscoverer, NodePreference nodePreference) { + _endpointDiscoverer = endpointDiscoverer; + _requiresLeaderHeaderValue = nodePreference == NodePreference.Leader ? bool.TrueString : bool.FalseString; + _selectedEndpoint = DeferEndpointSelection(); + } + + private Lazy> DeferEndpointSelection(CancellationToken cancellationToken = default) => + new Lazy>(() => _endpointDiscoverer.DiscoverAsync(cancellationToken), + LazyThreadSafetyMode.ExecutionAndPublication); + + private void ScheduleEndpointSelection(Task _) => + Interlocked.Exchange(ref _selectedEndpoint, DeferEndpointSelection()); + + public override AsyncUnaryCall AsyncUnaryCall(TRequest request, + ClientInterceptorContext context, + AsyncUnaryCallContinuation continuation) { + var call = continuation(request, CreateClientInterceptorContext(context)); + + call.ResponseAsync.ContinueWith(ScheduleEndpointSelection, + TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously); + + return new AsyncUnaryCall(call.ResponseAsync, call.ResponseHeadersAsync, call.GetStatus, + call.GetTrailers, + call.Dispose); + } + + public override AsyncClientStreamingCall AsyncClientStreamingCall( + ClientInterceptorContext context, + AsyncClientStreamingCallContinuation continuation) { + var call = continuation(CreateClientInterceptorContext(context)); + + call.ResponseAsync.ContinueWith(ScheduleEndpointSelection, + TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously); + + return new AsyncClientStreamingCall(call.RequestStream, call.ResponseAsync, + call.ResponseHeadersAsync, call.GetStatus, call.GetTrailers, call.Dispose); + } + + public override AsyncServerStreamingCall AsyncServerStreamingCall( + TRequest request, ClientInterceptorContext context, + AsyncServerStreamingCallContinuation continuation) { + var call = continuation(request, CreateClientInterceptorContext(context)); + + return new AsyncServerStreamingCall( + new StreamReader(call.ResponseStream, ScheduleEndpointSelection), call.ResponseHeadersAsync, + call.GetStatus, call.GetTrailers, call.Dispose); + } + + public override AsyncDuplexStreamingCall AsyncDuplexStreamingCall( + ClientInterceptorContext context, + AsyncDuplexStreamingCallContinuation continuation) { + var call = continuation(CreateClientInterceptorContext(context)); + + return new AsyncDuplexStreamingCall(call.RequestStream, + new StreamReader(call.ResponseStream, ScheduleEndpointSelection), call.ResponseHeadersAsync, + call.GetStatus, call.GetTrailers, call.Dispose); + } + + private ClientInterceptorContext CreateClientInterceptorContext( + ClientInterceptorContext context) where TRequest : class where TResponse : class { + context.Options.Headers.Add("requires-leader", _requiresLeaderHeaderValue); + + return new ClientInterceptorContext(context.Method, SelectHost(), context.Options); + } + + private string SelectHost() => + $"{_selectedEndpoint.Value.Result.GetHost()}:{_selectedEndpoint.Value.Result.GetPort()}"; + + private class StreamReader : IAsyncStreamReader { + private readonly IAsyncStreamReader _inner; + private readonly Action> _callback; + + public StreamReader(IAsyncStreamReader inner, Action> callback) { + _inner = inner; + _callback = callback; + } + + public Task MoveNext(CancellationToken cancellationToken) { + var task = _inner.MoveNext(cancellationToken); + + task.ContinueWith(_callback, + TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously); + + return task; + } + + public T Current { get; } = default!; + } + } +} diff --git a/src/EventStore.Client/SingleNodeEndpointDiscoverer.cs b/src/EventStore.Client/SingleNodeEndpointDiscoverer.cs new file mode 100644 index 000000000..231ce3835 --- /dev/null +++ b/src/EventStore.Client/SingleNodeEndpointDiscoverer.cs @@ -0,0 +1,16 @@ +using System; +using System.Net; +using System.Threading; +using System.Threading.Tasks; + +namespace EventStore.Client { + internal class SingleNodeEndpointDiscoverer : IEndpointDiscoverer { + private readonly Task _endPoint; + + public SingleNodeEndpointDiscoverer(Uri address) { + _endPoint = Task.FromResult(new DnsEndPoint(address.Host, address.Port)); + } + + public Task DiscoverAsync(CancellationToken cancellationToken = default) => _endPoint; + } +} diff --git a/test/Directory.Build.props b/test/Directory.Build.props index 817c744db..fd1a068d5 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -1,24 +1,21 @@ - - netcoreapp3.0;netcoreapp3.1 - - + - - + + - + - + all runtime; build; native; contentfiles; analyzers - + diff --git a/test/EventStore.Client.IntegrationTests/EventStore.Client.IntegrationTests.csproj b/test/EventStore.Client.IntegrationTests/EventStore.Client.IntegrationTests.csproj new file mode 100644 index 000000000..7223670c0 --- /dev/null +++ b/test/EventStore.Client.IntegrationTests/EventStore.Client.IntegrationTests.csproj @@ -0,0 +1,11 @@ + + + + + + + + Always + + + diff --git a/test/EventStore.Client.IntegrationTests/EventStoreClientFixture.cs b/test/EventStore.Client.IntegrationTests/EventStoreClientFixture.cs new file mode 100644 index 000000000..549efa8b4 --- /dev/null +++ b/test/EventStore.Client.IntegrationTests/EventStoreClientFixture.cs @@ -0,0 +1,154 @@ +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Ductus.FluentDocker.Builders; +using Ductus.FluentDocker.Services; +using EventStore.Client.Gossip; +using Grpc.Core; +#if !GRPC_CORE +using Grpc.Net.Client; +#endif +using Polly; +using Xunit; +using EndPoint = System.Net.EndPoint; + +#nullable enable +namespace EventStore.Client { + public class Somerting { + [Fact] + public async Task YouSuck() { + await using var fixture = new EventStoreClientInsecureClusterFixture(); + await fixture.Start(); + using var client = fixture.CreateEventStoreClient( + Array.ConvertAll(fixture.Ports, port => new IPEndPoint(IPAddress.Loopback, port))); + + var leader = await fixture.GetLeaderNode(); + + leader!.ToString(); + } + } + + public class EventStoreClientInsecureClusterFixture : EventStoreClientClusterFixture { + public EventStoreClientInsecureClusterFixture() : base(new UriBuilder { + Scheme = Uri.UriSchemeHttp, + Port = 2113, + Query = "?tls=false" + }.Uri, "insecure") { + } + } + + public abstract class EventStoreClientClusterFixture : IAsyncDisposable { + private readonly Uri _address; + private readonly EventStoreTestServer _server; + public Gossip.Gossip.GossipClient GossipClient => _server.GossipClient; + + public int[] Ports => _server.Ports.ToArray(); + + protected EventStoreClientClusterFixture(Uri address, string composeFileName) { + _address = address; + _server = new EventStoreTestServer(address, composeFileName); + } + + public EventStoreClient CreateEventStoreClient(DnsEndPoint[] dnsGossipSeeds) { + var settings = EventStoreClientSettings.Create(_address.ToString()); + settings.ConnectivitySettings.DnsGossipSeeds = dnsGossipSeeds; + return new EventStoreClient(settings); + } + + public EventStoreClient CreateEventStoreClient(IPEndPoint[] ipGossipSeeds) { + var settings = EventStoreClientSettings.Create(new UriBuilder(_address) { + Scheme = "esdb+discover" + }.Uri.ToString()); + settings.ConnectivitySettings.IpGossipSeeds = ipGossipSeeds; + return new EventStoreClient(settings); + } + + public async ValueTask GetLeaderNode() { + var clusterInfo = await GossipClient.ReadAsync(new Empty()); + return clusterInfo.Members.Where(x => x.State == MemberInfo.Types.VNodeState.Leader) + .Select(x => new DnsEndPoint(x.HttpEndPoint.Address, (int)x.HttpEndPoint.Port)) + .FirstOrDefault(); + } + + public ValueTask Start() => _server.Start(); + public ValueTask DisposeAsync() => _server.DisposeAsync(); + + private class EventStoreTestServer : IAsyncDisposable { + private readonly ICompositeService _eventStore; + private readonly HttpClient _httpClient; + private readonly ChannelBase _channel; + public Gossip.Gossip.GossipClient GossipClient { get; } + + public int[] Ports => + (from container in _eventStore.Containers + where container.Name.Contains("esdb-node") + from pair in container.GetConfiguration().NetworkSettings.Ports + where pair.Key == "2113/tcp" + select pair.Value.First().Port) + .ToArray(); + + public EventStoreTestServer(Uri address, string mode) { + _httpClient = new HttpClient(new SocketsHttpHandler { + SslOptions = { + RemoteCertificateValidationCallback = delegate { return true; } + } + }) { + BaseAddress = address + }; + + _eventStore = new Builder() + .UseContainer() + .UseCompose() + .FromFile($"docker-compose.{mode}.yml") + .Build(); + _channel = CreateChannel(address); + GossipClient = new Gossip.Gossip.GossipClient(_channel); + } + + private ChannelBase CreateChannel(Uri address) { +#if !GRPC_CORE + return GrpcChannel.ForAddress(address); +#else + return new Channel(address.Host, address.Port, + address.Scheme == Uri.UriSchemeHttps + ? new SslCredentials() + : ChannelCredentials.Insecure); +#endif + } + + public ValueTask DisposeAsync() { + if (_channel is IDisposable disposable) { + disposable.Dispose(); + } + + try { + _eventStore.Dispose(); + } + catch {} + + return new ValueTask(); + } + + public async ValueTask Start(CancellationToken cancellationToken = default) { + _eventStore.Start(); + try { + await Policy.Handle() + .WaitAndRetryAsync(5, retryCount => TimeSpan.FromSeconds(retryCount * retryCount)) + .ExecuteAsync(async () => { + using var response = await _httpClient.GetAsync("/health/live", cancellationToken); + if (response.StatusCode >= HttpStatusCode.BadRequest) { + throw new Exception($"Health check failed with status code: {response.StatusCode}."); + } + }); + } catch (Exception) { + _httpClient.Dispose(); + _eventStore.Dispose(); + throw; + } + } + } + } +} diff --git a/test/EventStore.Client.Tests/ClusterAwareHttpHandlerTests.cs b/test/EventStore.Client.Tests/ClusterAwareHttpHandlerTests.cs index 3b0204017..95591754d 100644 --- a/test/EventStore.Client.Tests/ClusterAwareHttpHandlerTests.cs +++ b/test/EventStore.Client.Tests/ClusterAwareHttpHandlerTests.cs @@ -11,14 +11,15 @@ namespace EventStore.Client { public class ClusterAwareHttpHandlerTests { [Theory, - InlineData(true,true), - InlineData(true,false), - InlineData(false,true), - InlineData(false,false) + InlineData(true, true), + InlineData(true, false), + InlineData(false, true), + InlineData(false, false) ] - public async Task should_set_requires_leader_header(bool useHttps,bool requiresLeader) { + public async Task should_set_requires_leader_header(bool useHttps, bool requiresLeader) { var sut = new ClusterAwareHttpHandler( - useHttps, requiresLeader, new FakeEndpointDiscoverer(() => new IPEndPoint(IPAddress.Parse("0.0.0.0"), 2113))) { + useHttps, requiresLeader, + new FakeEndpointDiscoverer(() => new IPEndPoint(IPAddress.Parse("0.0.0.0"), 2113))) { InnerHandler = new TestMessageHandler() }; @@ -77,29 +78,29 @@ await Assert.ThrowsAsync(() => [Theory, ClassData(typeof(EndPoints))] public async Task should_set_endpoint_to_leader_endpoint_on_exception(bool useHttps, EndPoint endpoint) { var sut = new ClusterAwareHttpHandler( - useHttps, true, new FakeEndpointDiscoverer(() => new IPEndPoint(IPAddress.Parse("0.0.0.0"), 2113))) { - InnerHandler = new TestMessageHandler() + useHttps, true, new FakeEndpointDiscoverer(() => new IPEndPoint(IPAddress.Loopback, 2113))) { + InnerHandler = new TestNotLeaderMessageHandler(endpoint) }; - var client = new HttpClient(sut); + using var client = new HttpClient(sut); - var request = new HttpRequestMessage(HttpMethod.Get, new UriBuilder().Uri); + using var _ = await client.GetAsync(new UriBuilder().Uri); // first request results in leadernotfound + + using var request = new HttpRequestMessage(HttpMethod.Get, new UriBuilder().Uri); + + using var __ = await client.SendAsync(request); - sut.ExceptionOccurred(new NotLeaderException(endpoint.GetHost(), endpoint.GetPort())); - await client.SendAsync(request); - Assert.Equal(endpoint.GetHost(), request.RequestUri.Host); Assert.Equal(endpoint.GetPort(), request.RequestUri.Port); } } - + public class EndPoints : IEnumerable { public IEnumerator GetEnumerator() { - yield return new object[] {true, new IPEndPoint(IPAddress.Parse("0.0.0.0"), 2113)}; - yield return new object[] {true, new DnsEndPoint("nodea.eventstore.dev", 2113), }; - yield return new object[] {false, new IPEndPoint(IPAddress.Parse("0.0.0.0"), 2113)}; - yield return new object[] {false, new DnsEndPoint("nodea.eventstore.dev", 2113), }; - + yield return new object[] {true, new IPEndPoint(IPAddress.Any, 2113)}; + yield return new object[] {true, new DnsEndPoint("nodea.eventstore.dev", 2113),}; + yield return new object[] {false, new IPEndPoint(IPAddress.Any, 2113)}; + yield return new object[] {false, new DnsEndPoint("nodea.eventstore.dev", 2113),}; } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); @@ -112,15 +113,32 @@ public FakeEndpointDiscoverer(Func function) { _function = function; } - public Task DiscoverAsync() { - return Task.FromResult(_function()); - } + public Task DiscoverAsync(CancellationToken cancellationToken = default) => + Task.FromResult(_function()); } internal class TestMessageHandler : HttpMessageHandler { protected override Task SendAsync(HttpRequestMessage request, - CancellationToken cancellationToken) { - return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)); + CancellationToken cancellationToken) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)); + } + + internal class TestNotLeaderMessageHandler : TestMessageHandler { + private readonly EndPoint _endpoint; + private int _messageCount = -1; + + public TestNotLeaderMessageHandler(EndPoint endpoint) { + _endpoint = endpoint; } + + protected override Task SendAsync(HttpRequestMessage request, + CancellationToken cancellationToken) => Interlocked.Increment(ref _messageCount) == 0 + ? Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { + TrailingHeaders = { + {Constants.Exceptions.ExceptionKey, Constants.Exceptions.NotLeader}, + {Constants.Exceptions.LeaderEndpointHost, _endpoint.GetHost()}, + {Constants.Exceptions.LeaderEndpointPort, _endpoint.GetPort().ToString()} + } + }) + : base.SendAsync(request, cancellationToken); } } diff --git a/test/EventStore.Client.Tests/GossipBasedEndpointDiscovererTests.cs b/test/EventStore.Client.Tests/GossipBasedEndpointDiscovererTests.cs index 60505c4cf..cbf843f12 100644 --- a/test/EventStore.Client.Tests/GossipBasedEndpointDiscovererTests.cs +++ b/test/EventStore.Client.Tests/GossipBasedEndpointDiscovererTests.cs @@ -1,76 +1,60 @@ using System; using System.Linq; using System.Net; -using System.Net.Http; -using System.Net.Sockets; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; -using System.Text; using System.Threading; using System.Threading.Tasks; -using EventStore.Client.Gossip; -using Grpc.Core; using Xunit; +using EndPoint = System.Net.EndPoint; #nullable enable namespace EventStore.Client { - public class GossipBasedEndpointDiscovererTests: IAsyncLifetime { - private readonly Fixture _fixture; + public class GossipBasedEndpointDiscovererTests { + private readonly DnsEndPoint _gossipSeed; public GossipBasedEndpointDiscovererTests() { - _fixture = new Fixture(); + _gossipSeed = new DnsEndPoint("localhost", 443); } [Theory, InlineData(true), InlineData(false)] public async Task should_issue_gossip_to_gossip_seed(bool useHttps) { - HttpRequestMessage? request = null; + var endPoint = new DnsEndPoint(IPAddress.Any.ToString(), 4444); var gossip = new ClusterMessages.ClusterInfo { Members = new[] { new ClusterMessages.MemberInfo { State = ClusterMessages.VNodeState.Leader, InstanceId = Guid.NewGuid(), - HttpEndPointAddress = IPAddress.Any.ToString(), - HttpEndPointPort = 4444, + EndPoint = endPoint, IsAlive = true, }, } }; - var handler = new CustomMessageHandler(req => { - request = req; - _fixture.CurrentClusterInfo.Members = gossip.Members; - }); + var sut = new GossipBasedEndpointDiscoverer( + new EventStoreClientConnectivitySettings { + MaxDiscoverAttempts = 1, + GossipTimeout = Timeout.InfiniteTimeSpan, + GossipOverHttps = useHttps, + DiscoveryInterval = TimeSpan.Zero, + NodePreference = NodePreference.Leader, + DnsGossipSeeds = new[] {_gossipSeed} + }, new CallbackTestGossipClient(gossip)); - var gossipSeed = new DnsEndPoint(_fixture.Host, _fixture.GetPort(useHttps)); - - var sut = new ClusterEndpointDiscoverer(1, new[] { - gossipSeed, - }, Timeout.InfiniteTimeSpan, useHttps, TimeSpan.Zero, NodePreference.Leader, handler); - - await sut.DiscoverAsync(); - - Assert.Equal(useHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp, request?.RequestUri.Scheme); - Assert.Equal(gossipSeed.Host, request?.RequestUri.Host); - Assert.Equal(gossipSeed.Port, request?.RequestUri.Port); + var result = await sut.DiscoverAsync(); + Assert.Equal(result, endPoint); } - + [Theory, InlineData(true), InlineData(false)] public async Task should_be_able_to_discover_twice(bool useHttps) { - bool isFirstGossip = true; var firstGossip = new ClusterMessages.ClusterInfo { Members = new[] { new ClusterMessages.MemberInfo { State = ClusterMessages.VNodeState.Leader, - InstanceId = Guid.NewGuid(), - HttpEndPointAddress = IPAddress.Any.ToString(), - HttpEndPointPort = 1111, + InstanceId = Guid.NewGuid(), EndPoint = new DnsEndPoint(IPAddress.Any.ToString(), 1111), IsAlive = true, }, new ClusterMessages.MemberInfo { State = ClusterMessages.VNodeState.Follower, - InstanceId = Guid.NewGuid(), - HttpEndPointAddress = IPAddress.Any.ToString(), - HttpEndPointPort = 2222, + InstanceId = Guid.NewGuid(), EndPoint = new DnsEndPoint(IPAddress.Any.ToString(), 2222), IsAlive = true, }, } @@ -79,113 +63,107 @@ public async Task should_be_able_to_discover_twice(bool useHttps) { Members = new[] { new ClusterMessages.MemberInfo { State = ClusterMessages.VNodeState.Leader, - InstanceId = Guid.NewGuid(), - HttpEndPointAddress = IPAddress.Any.ToString(), - HttpEndPointPort = 1111, + InstanceId = Guid.NewGuid(), EndPoint = new DnsEndPoint(IPAddress.Any.ToString(), 1111), IsAlive = false, }, new ClusterMessages.MemberInfo { State = ClusterMessages.VNodeState.Leader, - InstanceId = Guid.NewGuid(), - HttpEndPointAddress = IPAddress.Any.ToString(), - HttpEndPointPort = 2222, + InstanceId = Guid.NewGuid(), EndPoint = new DnsEndPoint(IPAddress.Any.ToString(), 2222), IsAlive = true, }, } }; - var handler = new CustomMessageHandler(req => { - if (isFirstGossip) { - isFirstGossip = false; - _fixture.CurrentClusterInfo.Members = firstGossip.Members; - } else { - _fixture.CurrentClusterInfo.Members = secondGossip.Members; - } - }); - - var gossipSeed = new DnsEndPoint(_fixture.Host, _fixture.GetPort(useHttps)); - - var sut = new ClusterEndpointDiscoverer(5, new[] { - gossipSeed, - }, Timeout.InfiniteTimeSpan, useHttps, TimeSpan.Zero, NodePreference.Leader, handler); + var sut = new GossipBasedEndpointDiscoverer( + new EventStoreClientConnectivitySettings { + MaxDiscoverAttempts = 5, + GossipTimeout = Timeout.InfiniteTimeSpan, + GossipOverHttps = useHttps, + DiscoveryInterval = TimeSpan.Zero, + NodePreference = NodePreference.Leader, + DnsGossipSeeds = new[] {_gossipSeed} + }, new MultiGossipCallback(firstGossip, secondGossip)); var result = await sut.DiscoverAsync(); - var expected = firstGossip.Members.First(x => x.HttpEndPointPort == 1111); + var expected = firstGossip.Members.First(x => x.EndPoint.Port == 1111); + + Assert.Equal(expected.EndPoint.Host, result.GetHost()); + Assert.Equal(expected.EndPoint.Port, result.GetPort()); - Assert.Equal(expected.HttpEndPointAddress, result.GetHost()); - Assert.Equal(expected.HttpEndPointPort, result.GetPort()); - result = await sut.DiscoverAsync(); - expected = secondGossip.Members.First(x => x.HttpEndPointPort == 2222); + expected = secondGossip.Members.First(x => x.EndPoint.Port == 2222); - Assert.Equal(expected.HttpEndPointAddress, result.GetHost()); - Assert.Equal(expected.HttpEndPointPort, result.GetPort()); + Assert.Equal(expected.EndPoint.Host, result.GetHost()); + Assert.Equal(expected.EndPoint.Port, result.GetPort()); } [Theory, InlineData(true), InlineData(false)] public async Task should_not_exceed_max_discovery_attempts(bool useHttps) { int maxDiscoveryAttempts = 5; - int discoveryAttempts = 0; - var handler = new CustomMessageHandler(request => { - discoveryAttempts++; - throw new Exception(); - }); + var sut = new GossipBasedEndpointDiscoverer( + new EventStoreClientConnectivitySettings { + MaxDiscoverAttempts = 5, + GossipTimeout = Timeout.InfiniteTimeSpan, + GossipOverHttps = useHttps, + DiscoveryInterval = TimeSpan.Zero, + NodePreference = NodePreference.Leader, + DnsGossipSeeds = new[] {_gossipSeed} + }, new CallbackTestGossipClient(new ClusterMessages.ClusterInfo(), () => throw new Exception())); - var sut = new ClusterEndpointDiscoverer(maxDiscoveryAttempts, new[] { - new DnsEndPoint(_fixture.Host, _fixture.GetPort(useHttps)), - }, Timeout.InfiniteTimeSpan, useHttps, TimeSpan.Zero, NodePreference.Leader, handler); + var result = await Assert.ThrowsAsync(async () => await sut.DiscoverAsync()); - await Assert.ThrowsAsync(() => sut.DiscoverAsync()); - - Assert.Equal(maxDiscoveryAttempts, discoveryAttempts); + Assert.Equal(maxDiscoveryAttempts, result.MaxDiscoverAttempts); } [Theory, - InlineData(true,ClusterMessages.VNodeState.Manager), - InlineData(true,ClusterMessages.VNodeState.Shutdown), - InlineData(true,ClusterMessages.VNodeState.Unknown), - InlineData(true,ClusterMessages.VNodeState.Initializing), - InlineData(true,ClusterMessages.VNodeState.CatchingUp), - InlineData(true,ClusterMessages.VNodeState.ResigningLeader), - InlineData(true,ClusterMessages.VNodeState.ShuttingDown), - InlineData(true,ClusterMessages.VNodeState.PreLeader), - InlineData(true,ClusterMessages.VNodeState.PreReplica), - InlineData(true,ClusterMessages.VNodeState.PreReadOnlyReplica), - InlineData(false,ClusterMessages.VNodeState.Manager), - InlineData(false,ClusterMessages.VNodeState.Shutdown), - InlineData(false,ClusterMessages.VNodeState.Unknown), - InlineData(false,ClusterMessages.VNodeState.Initializing), - InlineData(false,ClusterMessages.VNodeState.CatchingUp), - InlineData(false,ClusterMessages.VNodeState.ResigningLeader), - InlineData(false,ClusterMessages.VNodeState.ShuttingDown), - InlineData(false,ClusterMessages.VNodeState.PreLeader), - InlineData(false,ClusterMessages.VNodeState.PreReplica), - InlineData(false,ClusterMessages.VNodeState.PreReadOnlyReplica) + InlineData(true, ClusterMessages.VNodeState.Manager), + InlineData(true, ClusterMessages.VNodeState.Shutdown), + InlineData(true, ClusterMessages.VNodeState.Unknown), + InlineData(true, ClusterMessages.VNodeState.Initializing), + InlineData(true, ClusterMessages.VNodeState.CatchingUp), + InlineData(true, ClusterMessages.VNodeState.ResigningLeader), + InlineData(true, ClusterMessages.VNodeState.ShuttingDown), + InlineData(true, ClusterMessages.VNodeState.PreLeader), + InlineData(true, ClusterMessages.VNodeState.PreReplica), + InlineData(true, ClusterMessages.VNodeState.PreReadOnlyReplica), + InlineData(false, ClusterMessages.VNodeState.Manager), + InlineData(false, ClusterMessages.VNodeState.Shutdown), + InlineData(false, ClusterMessages.VNodeState.Unknown), + InlineData(false, ClusterMessages.VNodeState.Initializing), + InlineData(false, ClusterMessages.VNodeState.CatchingUp), + InlineData(false, ClusterMessages.VNodeState.ResigningLeader), + InlineData(false, ClusterMessages.VNodeState.ShuttingDown), + InlineData(false, ClusterMessages.VNodeState.PreLeader), + InlineData(false, ClusterMessages.VNodeState.PreReplica), + InlineData(false, ClusterMessages.VNodeState.PreReadOnlyReplica) ] - internal async Task should_not_be_able_to_pick_invalid_node(bool useHttps, ClusterMessages.VNodeState invalidState) { + internal async Task should_not_be_able_to_pick_invalid_node(bool useHttps, + ClusterMessages.VNodeState invalidState) { var gossip = new ClusterMessages.ClusterInfo { Members = new[] { new ClusterMessages.MemberInfo { State = invalidState, - InstanceId = Guid.NewGuid(), - HttpEndPointAddress = IPAddress.Any.ToString(), - HttpEndPointPort = 4444, + InstanceId = Guid.NewGuid(), EndPoint = new DnsEndPoint(IPAddress.Any.ToString(), 4444), IsAlive = true, }, } }; - var handler = new CustomMessageHandler(req => { - _fixture.CurrentClusterInfo.Members = gossip.Members; - }); - - var sut = new ClusterEndpointDiscoverer(1, new[] { new DnsEndPoint(_fixture.Host, _fixture.GetPort(useHttps)), - }, Timeout.InfiniteTimeSpan, useHttps, TimeSpan.Zero, NodePreference.Leader, handler); - - await Assert.ThrowsAsync(() => sut.DiscoverAsync()); + var sut = new GossipBasedEndpointDiscoverer( + new EventStoreClientConnectivitySettings { + MaxDiscoverAttempts = 1, + GossipTimeout = Timeout.InfiniteTimeSpan, + GossipOverHttps = useHttps, + DiscoveryInterval = TimeSpan.Zero, + NodePreference = NodePreference.Leader, + DnsGossipSeeds = new[] {_gossipSeed} + }, new CallbackTestGossipClient(gossip)); + + var ex = await Assert.ThrowsAsync(async () => await sut.DiscoverAsync()); + Assert.Equal(1, ex.MaxDiscoverAttempts); } [Theory, @@ -197,54 +175,49 @@ internal async Task should_not_be_able_to_pick_invalid_node(bool useHttps, Clust InlineData(false, NodePreference.Follower, ClusterMessages.VNodeState.Follower), InlineData(false, NodePreference.ReadOnlyReplica, ClusterMessages.VNodeState.ReadOnlyReplica), InlineData(false, NodePreference.ReadOnlyReplica, ClusterMessages.VNodeState.ReadOnlyLeaderless) - ] + ] internal async Task should_pick_node_based_on_preference(bool useHttps, NodePreference preference, ClusterMessages.VNodeState expectedState) { var gossip = new ClusterMessages.ClusterInfo { Members = new[] { new ClusterMessages.MemberInfo { State = ClusterMessages.VNodeState.Leader, - InstanceId = Guid.NewGuid(), - HttpEndPointAddress = IPAddress.Any.ToString(), - HttpEndPointPort = 1111, + InstanceId = Guid.NewGuid(), EndPoint = new DnsEndPoint(IPAddress.Any.ToString(), 1111), IsAlive = true, }, new ClusterMessages.MemberInfo { State = ClusterMessages.VNodeState.Follower, - InstanceId = Guid.NewGuid(), - HttpEndPointAddress = IPAddress.Any.ToString(), - HttpEndPointPort = 2222, + InstanceId = Guid.NewGuid(), EndPoint = new DnsEndPoint(IPAddress.Any.ToString(), 2222), IsAlive = true, }, new ClusterMessages.MemberInfo { State = expectedState == ClusterMessages.VNodeState.ReadOnlyLeaderless ? expectedState : ClusterMessages.VNodeState.ReadOnlyReplica, - InstanceId = Guid.NewGuid(), - HttpEndPointAddress = IPAddress.Any.ToString(), - HttpEndPointPort = 3333, + InstanceId = Guid.NewGuid(), EndPoint = new DnsEndPoint(IPAddress.Any.ToString(), 3333), IsAlive = true, }, new ClusterMessages.MemberInfo { State = ClusterMessages.VNodeState.Manager, - InstanceId = Guid.NewGuid(), - HttpEndPointAddress = IPAddress.Any.ToString(), - HttpEndPointPort = 4444, + InstanceId = Guid.NewGuid(), EndPoint = new DnsEndPoint(IPAddress.Any.ToString(), 4444), IsAlive = true, }, } }; - var handler = new CustomMessageHandler(req => { - _fixture.CurrentClusterInfo.Members = gossip.Members; - }); - var sut = new ClusterEndpointDiscoverer(1, new[] { - new DnsEndPoint(_fixture.Host, _fixture.GetPort(useHttps)) - }, Timeout.InfiniteTimeSpan, useHttps, TimeSpan.Zero, preference, handler); + var sut = new GossipBasedEndpointDiscoverer( + new EventStoreClientConnectivitySettings { + MaxDiscoverAttempts = 1, + GossipTimeout = Timeout.InfiniteTimeSpan, + GossipOverHttps = useHttps, + DiscoveryInterval = TimeSpan.Zero, + NodePreference = preference, + DnsGossipSeeds = new[] {_gossipSeed} + }, new CallbackTestGossipClient(gossip)); var result = await sut.DiscoverAsync(); Assert.Equal(result.GetPort(), - gossip.Members.Last(x => x.State == expectedState).HttpEndPointPort); + gossip.Members.Last(x => x.State == expectedState).EndPoint.Port); } [Theory, InlineData(true), InlineData(false)] @@ -254,139 +227,63 @@ public async Task falls_back_to_first_alive_node_if_a_preferred_node_is_not_foun new ClusterMessages.MemberInfo { State = ClusterMessages.VNodeState.Leader, InstanceId = Guid.NewGuid(), - HttpEndPointAddress = IPAddress.Any.ToString(), - HttpEndPointPort = 1111, + EndPoint = new DnsEndPoint(IPAddress.Any.ToString(), 1111), IsAlive = false, }, new ClusterMessages.MemberInfo { State = ClusterMessages.VNodeState.Follower, InstanceId = Guid.NewGuid(), - HttpEndPointAddress = IPAddress.Any.ToString(), - HttpEndPointPort = 2222, + EndPoint = new DnsEndPoint(IPAddress.Any.ToString(), 2222), IsAlive = true, }, } }; - var handler = new CustomMessageHandler(req => { - _fixture.CurrentClusterInfo.Members = gossip.Members; - }); - var sut = new ClusterEndpointDiscoverer(1, new[] { - new DnsEndPoint(_fixture.Host, _fixture.GetPort(useHttps)) - }, Timeout.InfiniteTimeSpan, useHttps, TimeSpan.Zero, NodePreference.Leader, handler); + var sut = new GossipBasedEndpointDiscoverer( + new EventStoreClientConnectivitySettings { + MaxDiscoverAttempts = 1, + GossipTimeout = Timeout.InfiniteTimeSpan, + GossipOverHttps = useHttps, + DiscoveryInterval = TimeSpan.Zero, + NodePreference = NodePreference.Leader, + DnsGossipSeeds = new[] {_gossipSeed} + }, new CallbackTestGossipClient(gossip)); var result = await sut.DiscoverAsync(); Assert.Equal(result.GetPort(), - gossip.Members.Last(x => x.State == ClusterMessages.VNodeState.Follower).HttpEndPointPort); - } - - private class CustomMessageHandler : HttpClientHandler { - private readonly Action _handle; - - public CustomMessageHandler(Action handle) { - _handle = handle; - ServerCertificateCustomValidationCallback = delegate { return true; }; - } - - protected override Task SendAsync(HttpRequestMessage request, - CancellationToken cancellationToken) { - _handle(request); - return base.SendAsync(request, cancellationToken); - } + gossip.Members.Last(x => x.State == ClusterMessages.VNodeState.Follower).EndPoint.Port); } - public Task InitializeAsync() => _fixture.InitializeAsync(); - public Task DisposeAsync() => _fixture.DisposeAsync(); - - public class Fixture : IAsyncLifetime { - public readonly string Host = "localhost"; - public readonly int SecurePort = GetFreePort(); - public readonly int InsecurePort = GetFreePort(); - internal readonly ClusterMessages.ClusterInfo CurrentClusterInfo = new ClusterMessages.ClusterInfo(); - private Server? _server; - - private static int GetFreePort() { - using var socket = - new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp) { - ExclusiveAddressUse = false - }; - socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); - socket.Bind(new IPEndPoint(IPAddress.Loopback, 0)); - return ((IPEndPoint)socket.LocalEndPoint).Port; - } + private class CallbackTestGossipClient : IGossipClient { + private readonly ClusterMessages.ClusterInfo _gossip; + private readonly Action? _callback; - private void StartGrpcServer() { - var keyCertificatePair = GenerateKeyCertificatePair(); - _server = new Server - { - Services = { Gossip.Gossip.BindService(new GossipImplementation(CurrentClusterInfo)) }, - Ports = { - new ServerPort(Host, SecurePort, new SslServerCredentials(new [] {keyCertificatePair})), - new ServerPort(Host, InsecurePort, ServerCredentials.Insecure) - } - }; - AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); //for clients - _server.Start(); + public CallbackTestGossipClient(ClusterMessages.ClusterInfo gossip, Action? callback = null) { + _gossip = gossip; + _callback = callback; } - private KeyCertificatePair GenerateKeyCertificatePair() { - using (RSA rsa = RSA.Create()) - { - var certReq = new CertificateRequest("CN=hello", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); - var certificate = certReq.CreateSelfSigned(DateTimeOffset.UtcNow.AddMonths(-1), DateTimeOffset.UtcNow.AddMonths(1)); - var pemCertificateBuilder = new StringBuilder(); - pemCertificateBuilder.AppendLine("-----BEGIN CERTIFICATE-----"); - pemCertificateBuilder.AppendLine(Convert.ToBase64String(certificate.Export(X509ContentType.Cert), Base64FormattingOptions.InsertLineBreaks)); - pemCertificateBuilder.AppendLine("-----END CERTIFICATE-----"); - var pemCertificate = pemCertificateBuilder.ToString(); - - var pemKeyBuilder = new StringBuilder(); - pemKeyBuilder.AppendLine("-----BEGIN RSA PRIVATE KEY-----"); - pemKeyBuilder.AppendLine(Convert.ToBase64String(rsa.ExportRSAPrivateKey(), Base64FormattingOptions.InsertLineBreaks)); - pemKeyBuilder.AppendLine("-----END RSA PRIVATE KEY-----"); - var pemKey = pemKeyBuilder.ToString(); - - return new KeyCertificatePair(pemCertificate, pemKey); - } - } - private class GossipImplementation : Gossip.Gossip.GossipBase { - private readonly ClusterMessages.ClusterInfo _currentClusterInfo; - - public GossipImplementation(ClusterMessages.ClusterInfo currentClusterInfo) { - _currentClusterInfo = currentClusterInfo; - } - public override Task Read(Empty request, ServerCallContext context) { - if (_currentClusterInfo.Members == null) { - return Task.FromResult(new ClusterInfo()); - } - var members = Array.ConvertAll(_currentClusterInfo.Members, x => new MemberInfo { - InstanceId = Uuid.FromGuid(x.InstanceId).ToDto(), - State = (MemberInfo.Types.VNodeState)x.State, - IsAlive = x.IsAlive, - HttpEndPoint = new Gossip.EndPoint { - Address = x.HttpEndPointAddress, - Port = (uint) x.HttpEndPointPort - } - }).ToArray(); - var info = new ClusterInfo(); - info.Members.AddRange(members); - return Task.FromResult(info); - } + public ValueTask GetAsync(EndPoint endPoint, + CancellationToken cancellationToken = default) { + _callback?.Invoke(); + return new ValueTask(_gossip); } + } - public Task InitializeAsync() { - StartGrpcServer(); - return Task.CompletedTask; - } + private class MultiGossipCallback : IGossipClient { + private readonly ClusterMessages.ClusterInfo[] _gossip; + private int _currentIndex; - public Task DisposeAsync() { - return _server == null ? Task.CompletedTask : _server.ShutdownAsync(); + public MultiGossipCallback(params ClusterMessages.ClusterInfo[] gossip) { + _gossip = gossip; + _currentIndex = -1; } - public int GetPort(bool secure){ - return secure ? SecurePort : InsecurePort; - } + public ValueTask GetAsync(EndPoint endPoint, + CancellationToken cancellationToken = default) => + new ValueTask( + _gossip[Interlocked.Increment(ref _currentIndex) % _gossip.Length]); } } } diff --git a/test/EventStore.Client.Tests/Interceptors/HostSelectorInterceptorTests.cs b/test/EventStore.Client.Tests/Interceptors/HostSelectorInterceptorTests.cs new file mode 100644 index 000000000..12f27132e --- /dev/null +++ b/test/EventStore.Client.Tests/Interceptors/HostSelectorInterceptorTests.cs @@ -0,0 +1,177 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Grpc.Core; +using Grpc.Core.Interceptors; +using Xunit; + +namespace EventStore.Client.Interceptors { + public class HostSelectorInterceptorTests { + private static readonly Marshaller _marshaller = + new Marshaller(_ => Array.Empty(), _ => new object()); + + public delegate Task<(Metadata metadata, string host)> GrpcCall(Interceptor interceptor, + Task response = null); + + private static IEnumerable Calls() { + yield return MakeUnaryCall; + yield return MakeClientStreamingCall; + yield return MakeServerStreamingCall; + yield return MakeDuplexStreamingCall; + } + + private static IEnumerable<(NodePreference nodePreference, bool requriesLeader)> NodePreferences() { + yield return (NodePreference.Leader, true); + yield return (NodePreference.Follower, false); + yield return (NodePreference.Random, false); + yield return (NodePreference.ReadOnlyReplica, false); + } + + public static IEnumerable RequiresLeaderCases() => + from _ in NodePreferences() + from call in Calls() + select new object[] {_.nodePreference, _.requriesLeader, call}; + + [Theory, MemberData(nameof(RequiresLeaderCases))] + public async Task RequiresLeaderSetToExpectedResult(NodePreference nodePreference, bool expectedRequiresLeader, + GrpcCall makeCall) { + var sut = new HostSelectorInterceptor(new TestEndpointDiscoverer(new IPEndPoint(IPAddress.Any, 2113)), + nodePreference); + var (metadata, host) = await makeCall(sut); + Assert.True(metadata.TryGetValue("requires-leader", out var requiresLeaderValue)); + Assert.True(bool.TryParse(requiresLeaderValue, out var requiresLeader)); + Assert.Equal(expectedRequiresLeader, requiresLeader); + Assert.Equal($"{IPAddress.Any}:2113", host); + } + + public static IEnumerable SelectsGossipCases() => + from call in Calls() + select new object[] {call}; + + [Theory, MemberData(nameof(SelectsGossipCases))] + public async Task SelectsNewGossipEndPointAfterFailure(GrpcCall makeCall) { + var endpoints = new EndPoint[] { + new IPEndPoint(IPAddress.Loopback, 2113), + new IPEndPoint(IPAddress.Loopback, 2114), + new IPEndPoint(IPAddress.Loopback, 2115), + }; + + var sut = new HostSelectorInterceptor(new TestEndpointDiscoverer(endpoints), NodePreference.Leader); + + await Assert.ThrowsAsync(() => + makeCall(sut, Task.FromException(new DummyException()))); + + var (_, host) = await makeCall(sut, Task.FromResult(new object())); + + Assert.Equal($"{IPAddress.Loopback}:2114", host); + } + + private static async Task<(Metadata metadata, string host)> MakeUnaryCall(Interceptor interceptor, + Task response = null) { + var metadata = new Metadata(); + string host = null; + + using var call = interceptor.AsyncUnaryCall(new object(), + CreateClientInterceptorContext(metadata, MethodType.Unary), + (_, context) => { + host = context.Host; + return new AsyncUnaryCall(response ?? Task.FromResult(new object()), + Task.FromResult(context.Options.Headers), GetSuccess, GetTrailers, OnDispose); + }); + await call.ResponseAsync; + return (metadata, host); + } + + private static async Task<(Metadata metadata, string host)> MakeClientStreamingCall(Interceptor interceptor, + Task response = null) { + var metadata = new Metadata(); + string host = null; + + using var call = interceptor.AsyncClientStreamingCall( + CreateClientInterceptorContext(metadata, MethodType.ClientStreaming), + context => { + host = context.Host; + return new AsyncClientStreamingCall(null, response ?? Task.FromResult(new object()), + Task.FromResult(context.Options.Headers), GetSuccess, GetTrailers, OnDispose); + }); + await call.ResponseAsync; + return (metadata, host); + } + + private static async Task<(Metadata metadata, string host)> MakeServerStreamingCall(Interceptor interceptor, + Task response = null) { + var metadata = new Metadata(); + string host = null; + + using var call = interceptor.AsyncServerStreamingCall(new object(), + CreateClientInterceptorContext(metadata, MethodType.ServerStreaming), + (_, context) => { + host = context.Host; + return new AsyncServerStreamingCall(new TestAsyncStreamReader(response), + Task.FromResult(context.Options.Headers), GetSuccess, GetTrailers, OnDispose); + }); + await call.ResponseStream.ReadAllAsync().ToArrayAsync(); + return (metadata, host); + } + + private static async Task<(Metadata metadata, string host)> MakeDuplexStreamingCall(Interceptor interceptor, + Task response = null) { + var metadata = new Metadata(); + string host = null; + + using var call = interceptor.AsyncDuplexStreamingCall( + CreateClientInterceptorContext(metadata, MethodType.ServerStreaming), + context => { + host = context.Host; + return new AsyncDuplexStreamingCall(null, new TestAsyncStreamReader(response), + Task.FromResult(context.Options.Headers), GetSuccess, GetTrailers, OnDispose); + }); + await call.ResponseStream.ReadAllAsync().ToArrayAsync(); + return (metadata, host); + } + + private static Status GetSuccess() => Status.DefaultSuccess; + + private static Metadata GetTrailers() => Metadata.Empty; + + private static void OnDispose() { } + + private static ClientInterceptorContext CreateClientInterceptorContext(Metadata metadata, + MethodType methodType) => new ClientInterceptorContext( + new Method(methodType, string.Empty, string.Empty, _marshaller, _marshaller), + null, new CallOptions(metadata)); + + + private class TestEndpointDiscoverer : IEndpointDiscoverer { + private readonly EndPoint[] _endPoints; + private int _index = -1; + + public TestEndpointDiscoverer(params EndPoint[] endPoints) { + _endPoints = endPoints; + } + + public Task DiscoverAsync(CancellationToken cancellationToken = default) => + Task.FromResult(_endPoints[Interlocked.Increment(ref _index) % _endPoints.Length]); + } + + private class TestAsyncStreamReader : IAsyncStreamReader { + private readonly Task _response; + + public Task MoveNext(CancellationToken cancellationToken) => _response.IsFaulted + ? Task.FromException(_response.Exception!.GetBaseException()) + : Task.FromResult(false); + + public object Current => _response.Result; + + public TestAsyncStreamReader(Task response = null) { + _response = response ?? Task.FromResult(new object()); + } + } + + private class DummyException : Exception { + } + } +} From fe45d5280a7cbf5e4a46fecc9f9589343a945ce2 Mon Sep 17 00:00:00 2001 From: thefringeninja <495495+thefringeninja@users.noreply.github.com> Date: Tue, 5 Jan 2021 09:56:04 +0100 Subject: [PATCH 03/21] update scan vulnerabilities tool --- .config/dotnet-tools.json | 4 ++-- .github/workflows/ci.yml | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 2bee4adea..96667b34e 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -9,7 +9,7 @@ ] }, "dotnet-retire": { - "version": "4.0.1", + "version": "5.0.0", "commands": [ "dotnet-retire" ] @@ -21,4 +21,4 @@ ] } } -} \ No newline at end of file +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 84cb3efb7..e9c766837 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,9 +10,13 @@ on: jobs: vulnerability-scan: + strategy: + fail-fast: false + matrix: + sdk: ['5.0-focal', '3.1-bionic'] runs-on: ubuntu-latest - name: ci/github/scan-vulnerabilities - container: mcr.microsoft.com/dotnet/sdk:5.0-bionic + name: scan-vulnerabilities/${{ matrix.sdk }} + container: mcr.microsoft.com/dotnet/sdk:${{ matrix.sdk }} steps: - name: Checkout uses: actions/checkout@v2 From 06aad4102581d1085794c18ccdd1fe88a9c2a383 Mon Sep 17 00:00:00 2001 From: thefringeninja <495495+thefringeninja@users.noreply.github.com> Date: Wed, 6 Jan 2021 10:12:04 +0100 Subject: [PATCH 04/21] use channel per node for grpc.core implementations --- .../EventStoreOperationsClient.Admin.cs | 17 +- .../EventStoreOperationsClient.Scavenge.cs | 19 +- .../EventStoreOperationsClient.cs | 2 - ...orePersistentSubscriptionsClient.Create.cs | 3 +- ...orePersistentSubscriptionsClient.Delete.cs | 3 +- ...StorePersistentSubscriptionsClient.Read.cs | 20 +- ...orePersistentSubscriptionsClient.Update.cs | 3 +- ...EventStorePersistentSubscriptionsClient.cs | 2 - ...StoreProjectionManagementClient.Control.cs | 12 +- ...tStoreProjectionManagementClient.Create.cs | 9 +- ...ntStoreProjectionManagementClient.State.cs | 6 +- ...reProjectionManagementClient.Statistics.cs | 3 +- ...tStoreProjectionManagementClient.Update.cs | 3 +- .../EventStoreProjectionManagementClient.cs | 2 - .../EventStoreClient.Append.cs | 5 +- .../EventStoreClient.Delete.cs | 3 +- .../EventStoreClient.Metadata.cs | 3 +- .../EventStoreClient.Read.cs | 95 +++++----- .../EventStoreClient.Tombstone.cs | 3 +- .../EventStoreClient.cs | 2 - .../EventStoreUserManagementClient.cs | 53 +++--- src/EventStore.Client/ChannelFactory.cs | 10 +- .../ClusterAwareHttpHandler.cs | 70 ------- src/EventStore.Client/EventStoreClientBase.cs | 47 +++-- src/EventStore.Client/GrpcGossipClient.cs | 20 +- .../Interceptors/HostSelectorInterceptor.cs | 104 ---------- .../Interceptors/ReportLeaderInterceptor.cs | 85 +++++++++ src/EventStore.Client/MultiChannel.cs | 54 ++++++ test/Directory.Build.props | 3 + ...ubscribing_to_normal_events_manual_nack.cs | 6 +- .../EventStoreClientFixtureBase.cs | 23 ++- .../ClusterAwareHttpHandlerTests.cs | 144 -------------- .../HostSelectorInterceptorTests.cs | 177 ------------------ .../ReportLeaderInterceptorTests.cs | 94 ++++++++++ 34 files changed, 449 insertions(+), 656 deletions(-) delete mode 100644 src/EventStore.Client/ClusterAwareHttpHandler.cs delete mode 100644 src/EventStore.Client/Interceptors/HostSelectorInterceptor.cs create mode 100644 src/EventStore.Client/Interceptors/ReportLeaderInterceptor.cs create mode 100644 src/EventStore.Client/MultiChannel.cs delete mode 100644 test/EventStore.Client.Tests/ClusterAwareHttpHandlerTests.cs delete mode 100644 test/EventStore.Client.Tests/Interceptors/HostSelectorInterceptorTests.cs create mode 100644 test/EventStore.Client.Tests/Interceptors/ReportLeaderInterceptorTests.cs diff --git a/src/EventStore.Client.Operations/EventStoreOperationsClient.Admin.cs b/src/EventStore.Client.Operations/EventStoreOperationsClient.Admin.cs index 3575cb50c..599c8707f 100644 --- a/src/EventStore.Client.Operations/EventStoreOperationsClient.Admin.cs +++ b/src/EventStore.Client.Operations/EventStoreOperationsClient.Admin.cs @@ -16,7 +16,8 @@ public partial class EventStoreOperationsClient { public async Task ShutdownAsync( UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - await _client.ShutdownAsync(EmptyResult, + await new Operations.Operations.OperationsClient( + await SelectCallInvoker(cancellationToken).ConfigureAwait(false)).ShutdownAsync(EmptyResult, EventStoreCallOptions.Create(Settings, Settings.OperationOptions, userCredentials, cancellationToken)); } @@ -29,7 +30,8 @@ await _client.ShutdownAsync(EmptyResult, public async Task MergeIndexesAsync( UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - await _client.MergeIndexesAsync(EmptyResult, + await new Operations.Operations.OperationsClient( + await SelectCallInvoker(cancellationToken).ConfigureAwait(false)).MergeIndexesAsync(EmptyResult, EventStoreCallOptions.Create(Settings, Settings.OperationOptions, userCredentials, cancellationToken)); } @@ -42,7 +44,8 @@ await _client.MergeIndexesAsync(EmptyResult, public async Task ResignNodeAsync( UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - await _client.ResignNodeAsync(EmptyResult, + await new Operations.Operations.OperationsClient( + await SelectCallInvoker(cancellationToken).ConfigureAwait(false)).ResignNodeAsync(EmptyResult, EventStoreCallOptions.Create(Settings, Settings.OperationOptions, userCredentials, cancellationToken)); } @@ -56,7 +59,9 @@ await _client.ResignNodeAsync(EmptyResult, public async Task SetNodePriorityAsync(int nodePriority, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - await _client.SetNodePriorityAsync(new SetNodePriorityReq {Priority = nodePriority}, + await new Operations.Operations.OperationsClient( + await SelectCallInvoker(cancellationToken).ConfigureAwait(false)).SetNodePriorityAsync( + new SetNodePriorityReq {Priority = nodePriority}, EventStoreCallOptions.Create(Settings, Settings.OperationOptions, userCredentials, cancellationToken)); } @@ -68,7 +73,9 @@ await _client.SetNodePriorityAsync(new SetNodePriorityReq {Priority = nodePriori /// public async Task RestartPersistentSubscriptions(UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - await _client.RestartPersistentSubscriptionsAsync(EmptyResult, + await new Operations.Operations.OperationsClient( + await SelectCallInvoker(cancellationToken).ConfigureAwait(false)).RestartPersistentSubscriptionsAsync( + EmptyResult, EventStoreCallOptions.Create(Settings, Settings.OperationOptions, userCredentials, cancellationToken)); } } diff --git a/src/EventStore.Client.Operations/EventStoreOperationsClient.Scavenge.cs b/src/EventStore.Client.Operations/EventStoreOperationsClient.Scavenge.cs index d3b326ef9..ab7f6702a 100644 --- a/src/EventStore.Client.Operations/EventStoreOperationsClient.Scavenge.cs +++ b/src/EventStore.Client.Operations/EventStoreOperationsClient.Scavenge.cs @@ -28,12 +28,16 @@ public async Task StartScavengeAsync( throw new ArgumentOutOfRangeException(nameof(startFromChunk)); } - var result = await _client.StartScavengeAsync(new StartScavengeReq { - Options = new StartScavengeReq.Types.Options { - ThreadCount = threadCount, - StartFromChunk = startFromChunk - } - }, EventStoreCallOptions.Create(Settings, Settings.OperationOptions, userCredentials, cancellationToken)); + var result = await new Operations.Operations.OperationsClient( + await SelectCallInvoker(cancellationToken).ConfigureAwait(false)).StartScavengeAsync( + new StartScavengeReq { + Options = new StartScavengeReq.Types.Options { + ThreadCount = threadCount, + StartFromChunk = startFromChunk + } + }, + EventStoreCallOptions.Create(Settings, Settings.OperationOptions, userCredentials, + cancellationToken)); return result.ScavengeResult switch { ScavengeResp.Types.ScavengeResult.Started => DatabaseScavengeResult.Started(result.ScavengeId), @@ -54,7 +58,8 @@ public async Task StopScavengeAsync( string scavengeId, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var result = await _client.StopScavengeAsync(new StopScavengeReq { + var result = await new Operations.Operations.OperationsClient( + await SelectCallInvoker(cancellationToken).ConfigureAwait(false)).StopScavengeAsync(new StopScavengeReq { Options = new StopScavengeReq.Types.Options { ScavengeId = scavengeId } diff --git a/src/EventStore.Client.Operations/EventStoreOperationsClient.cs b/src/EventStore.Client.Operations/EventStoreOperationsClient.cs index 50f2e1808..5836537c1 100644 --- a/src/EventStore.Client.Operations/EventStoreOperationsClient.cs +++ b/src/EventStore.Client.Operations/EventStoreOperationsClient.cs @@ -18,7 +18,6 @@ public partial class EventStoreOperationsClient : EventStoreClientBase { .FirstOrDefault(x => x.Key == Constants.Exceptions.ScavengeId)?.Value) }; - private readonly Operations.Operations.OperationsClient _client; private readonly ILogger _log; /// @@ -33,7 +32,6 @@ public EventStoreOperationsClient(IOptions options) : /// /// public EventStoreOperationsClient(EventStoreClientSettings? settings = null) : base(settings, ExceptionMap) { - _client = new Operations.Operations.OperationsClient(CallInvoker); _log = Settings.LoggerFactory?.CreateLogger() ?? new NullLogger(); } diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Create.cs b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Create.cs index 234525f65..e7b37b8cc 100644 --- a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Create.cs +++ b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Create.cs @@ -39,7 +39,8 @@ public async Task CreateAsync(string streamName, string groupName, throw new ArgumentNullException(nameof(settings)); } - await _client.CreateAsync(new CreateReq { + await new PersistentSubscriptions.PersistentSubscriptions.PersistentSubscriptionsClient( + await SelectCallInvoker(cancellationToken).ConfigureAwait(false)).CreateAsync(new CreateReq { Options = new CreateReq.Types.Options { StreamIdentifier = streamName, GroupName = groupName, diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Delete.cs b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Delete.cs index 8117ea77f..962acd256 100644 --- a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Delete.cs +++ b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Delete.cs @@ -15,7 +15,8 @@ partial class EventStorePersistentSubscriptionsClient { /// public async Task DeleteAsync(string streamName, string groupName, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - await _client.DeleteAsync(new DeleteReq { + await new PersistentSubscriptions.PersistentSubscriptions.PersistentSubscriptionsClient( + await SelectCallInvoker(cancellationToken).ConfigureAwait(false)).DeleteAsync(new DeleteReq { Options = new DeleteReq.Types.Options { StreamIdentifier = streamName, GroupName = groupName diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Read.cs b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Read.cs index 68730beef..279231f4a 100644 --- a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Read.cs +++ b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Read.cs @@ -21,7 +21,7 @@ partial class EventStorePersistentSubscriptionsClient { /// /// /// - public Task SubscribeAsync(string streamName, string groupName, + public async Task SubscribeAsync(string streamName, string groupName, Func eventAppeared, Action? subscriptionDropped = null, UserCredentials? userCredentials = null, int bufferSize = 10, bool autoAck = true, @@ -53,16 +53,16 @@ public Task SubscribeAsync(string streamName, string gro var operationOptions = Settings.OperationOptions.Clone(); operationOptions.TimeoutAfter = new TimeSpan?(); - var call = _client.Read(EventStoreCallOptions.Create(Settings, operationOptions, userCredentials, - cancellationToken)); + var call = new PersistentSubscriptions.PersistentSubscriptions.PersistentSubscriptionsClient( + await SelectCallInvoker(cancellationToken).ConfigureAwait(false)).Read(EventStoreCallOptions.Create( + Settings, operationOptions, userCredentials, cancellationToken)); - return PersistentSubscription.Confirm(call, new ReadReq.Types.Options { - BufferSize = bufferSize, - GroupName = groupName, - StreamIdentifier = streamName, - UuidOption = new ReadReq.Types.Options.Types.UUIDOption {Structured = new Empty()} - }, autoAck, eventAppeared, - subscriptionDropped ?? delegate { }, cancellationToken); + return await PersistentSubscription.Confirm(call, new ReadReq.Types.Options { + BufferSize = bufferSize, + GroupName = groupName, + StreamIdentifier = streamName, + UuidOption = new ReadReq.Types.Options.Types.UUIDOption {Structured = new Empty()} + }, autoAck, eventAppeared, subscriptionDropped ?? delegate { }, cancellationToken).ConfigureAwait(false); } } } diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Update.cs b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Update.cs index 69e5a6d2b..c299deeed 100644 --- a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Update.cs +++ b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Update.cs @@ -39,7 +39,8 @@ public async Task UpdateAsync(string streamName, string groupName, PersistentSub throw new ArgumentNullException(nameof(settings)); } - await _client.UpdateAsync(new UpdateReq { + await new PersistentSubscriptions.PersistentSubscriptions.PersistentSubscriptionsClient( + await SelectCallInvoker(cancellationToken).ConfigureAwait(false)).UpdateAsync(new UpdateReq { Options = new UpdateReq.Types.Options { StreamIdentifier = streamName, GroupName = groupName, diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.cs b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.cs index 8f12531dc..f59ff96e6 100644 --- a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.cs +++ b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.cs @@ -11,7 +11,6 @@ namespace EventStore.Client { /// The client used to manage persistent subscriptions in the EventStoreDB. /// public partial class EventStorePersistentSubscriptionsClient : EventStoreClientBase { - private readonly PersistentSubscriptions.PersistentSubscriptions.PersistentSubscriptionsClient _client; private readonly ILogger _log; /// @@ -33,7 +32,6 @@ public EventStorePersistentSubscriptionsClient(EventStoreClientSettings? setting ex.Trailers.First(x => x.Key == Constants.Exceptions.StreamName).Value, ex.Trailers.First(x => x.Key == Constants.Exceptions.GroupName).Value, ex) }) { - _client = new PersistentSubscriptions.PersistentSubscriptions.PersistentSubscriptionsClient(CallInvoker); _log = Settings.LoggerFactory?.CreateLogger() ?? new NullLogger(); } diff --git a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Control.cs b/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Control.cs index de9cf6691..c469702bc 100644 --- a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Control.cs +++ b/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Control.cs @@ -14,7 +14,8 @@ public partial class EventStoreProjectionManagementClient { /// public async Task EnableAsync(string name, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - using var call = _client.EnableAsync(new EnableReq { + using var call = new Projections.Projections.ProjectionsClient( + await SelectCallInvoker(cancellationToken).ConfigureAwait(false)).EnableAsync(new EnableReq { Options = new EnableReq.Types.Options { Name = name } @@ -31,7 +32,8 @@ public async Task EnableAsync(string name, UserCredentials? userCredentials = nu /// public async Task ResetAsync(string name, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - using var call = _client.ResetAsync(new ResetReq { + using var call = new Projections.Projections.ProjectionsClient( + await SelectCallInvoker(cancellationToken).ConfigureAwait(false)).ResetAsync(new ResetReq { Options = new ResetReq.Types.Options { Name = name, WriteCheckpoint = true @@ -64,7 +66,8 @@ public Task DisableAsync(string name, UserCredentials? userCredentials = null, private async Task DisableInternalAsync(string name, bool writeCheckpoint, UserCredentials? userCredentials, CancellationToken cancellationToken) { - using var call = _client.DisableAsync(new DisableReq { + using var call = new Projections.Projections.ProjectionsClient( + await SelectCallInvoker(cancellationToken).ConfigureAwait(false)).DisableAsync(new DisableReq { Options = new DisableReq.Types.Options { Name = name, WriteCheckpoint = writeCheckpoint @@ -81,7 +84,8 @@ private async Task DisableInternalAsync(string name, bool writeCheckpoint, UserC /// public async Task RestartSubsystemAsync(UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - await _client.RestartSubsystemAsync(new Empty(), + await new Projections.Projections.ProjectionsClient( + await SelectCallInvoker(cancellationToken).ConfigureAwait(false)).RestartSubsystemAsync(new Empty(), EventStoreCallOptions.Create(Settings, Settings.OperationOptions, userCredentials, cancellationToken)); } } diff --git a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Create.cs b/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Create.cs index 594901c15..d86490d6e 100644 --- a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Create.cs +++ b/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Create.cs @@ -14,7 +14,8 @@ public partial class EventStoreProjectionManagementClient { /// public async Task CreateOneTimeAsync(string query, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - using var call = _client.CreateAsync(new CreateReq { + using var call = new Projections.Projections.ProjectionsClient( + await SelectCallInvoker(cancellationToken).ConfigureAwait(false)).CreateAsync(new CreateReq { Options = new CreateReq.Types.Options { OneTime = new Empty(), Query = query @@ -34,7 +35,8 @@ public async Task CreateOneTimeAsync(string query, UserCredentials? userCredenti /// public async Task CreateContinuousAsync(string name, string query, bool trackEmittedStreams = false, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - using var call = _client.CreateAsync(new CreateReq { + using var call = new Projections.Projections.ProjectionsClient( + await SelectCallInvoker(cancellationToken).ConfigureAwait(false)).CreateAsync(new CreateReq { Options = new CreateReq.Types.Options { Continuous = new CreateReq.Types.Options.Types.Continuous { Name = name, @@ -56,7 +58,8 @@ public async Task CreateContinuousAsync(string name, string query, bool trackEmi /// public async Task CreateTransientAsync(string name, string query, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - using var call = _client.CreateAsync(new CreateReq { + using var call = new Projections.Projections.ProjectionsClient( + await SelectCallInvoker(cancellationToken).ConfigureAwait(false)).CreateAsync(new CreateReq { Options = new CreateReq.Types.Options { Transient = new CreateReq.Types.Options.Types.Transient { Name = name diff --git a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.State.cs b/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.State.cs index cff716c74..c346f3e23 100644 --- a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.State.cs +++ b/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.State.cs @@ -61,7 +61,8 @@ public async Task GetResultAsync(string name, string? partition = null, private async ValueTask GetResultInternalAsync(string name, string? partition, UserCredentials? userCredentials, CancellationToken cancellationToken) { - using var call = _client.ResultAsync(new ResultReq { + using var call = new Projections.Projections.ProjectionsClient( + await SelectCallInvoker(cancellationToken).ConfigureAwait(false)).ResultAsync(new ResultReq { Options = new ResultReq.Types.Options { Name = name, Partition = partition ?? string.Empty @@ -124,7 +125,8 @@ public async Task GetStateAsync(string name, string? partition = null, private async ValueTask GetStateInternalAsync(string name, string? partition, UserCredentials? userCredentials, CancellationToken cancellationToken) { - using var call = _client.StateAsync(new StateReq { + using var call = new Projections.Projections.ProjectionsClient( + await SelectCallInvoker(cancellationToken).ConfigureAwait(false)).StateAsync(new StateReq { Options = new StateReq.Types.Options { Name = name, Partition = partition ?? string.Empty diff --git a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Statistics.cs b/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Statistics.cs index 83498129c..7a2b4183a 100644 --- a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Statistics.cs +++ b/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Statistics.cs @@ -49,7 +49,8 @@ public Task GetStatusAsync(string name, UserCredentials? user private async IAsyncEnumerable ListInternalAsync(StatisticsReq.Types.Options options, UserCredentials? userCredentials, [EnumeratorCancellation] CancellationToken cancellationToken) { - using var call = _client.Statistics(new StatisticsReq { + using var call = new Projections.Projections.ProjectionsClient( + await SelectCallInvoker(cancellationToken).ConfigureAwait(false)).Statistics(new StatisticsReq { Options = options }, EventStoreCallOptions.Create(Settings, Settings.OperationOptions, userCredentials, cancellationToken)); diff --git a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Update.cs b/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Update.cs index a872a2afb..df0349cdc 100644 --- a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Update.cs +++ b/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Update.cs @@ -26,7 +26,8 @@ public async Task UpdateAsync(string name, string query, bool? emitEnabled = nul options.NoEmitOptions = new Empty(); } - using var call = _client.UpdateAsync(new UpdateReq { + using var call = new Projections.Projections.ProjectionsClient( + await SelectCallInvoker(cancellationToken).ConfigureAwait(false)).UpdateAsync(new UpdateReq { Options = options }, EventStoreCallOptions.Create(Settings, Settings.OperationOptions, userCredentials, cancellationToken)); diff --git a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.cs b/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.cs index 7e8d99f8f..a2c3e1b21 100644 --- a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.cs +++ b/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.cs @@ -11,7 +11,6 @@ namespace EventStore.Client { ///The client used to manage projections on the EventStoreDB. /// public partial class EventStoreProjectionManagementClient : EventStoreClientBase { - private readonly Projections.Projections.ProjectionsClient _client; private readonly ILogger _log; /// @@ -27,7 +26,6 @@ public EventStoreProjectionManagementClient(IOptions o /// public EventStoreProjectionManagementClient(EventStoreClientSettings? settings) : base(settings, new Dictionary>()) { - _client = new Projections.Projections.ProjectionsClient(CallInvoker); _log = settings?.LoggerFactory?.CreateLogger() ?? new NullLogger(); } diff --git a/src/EventStore.Client.Streams/EventStoreClient.Append.cs b/src/EventStore.Client.Streams/EventStoreClient.Append.cs index 91bd1a88e..e7f9294d4 100644 --- a/src/EventStore.Client.Streams/EventStoreClient.Append.cs +++ b/src/EventStore.Client.Streams/EventStoreClient.Append.cs @@ -96,8 +96,9 @@ private async Task AppendToStreamInternal( EventStoreClientOperationOptions operationOptions, UserCredentials? userCredentials, CancellationToken cancellationToken) { - using var call = _client.Append(EventStoreCallOptions.Create(Settings, operationOptions, - userCredentials, cancellationToken)); + using var call = new Streams.Streams.StreamsClient( + await SelectCallInvoker(cancellationToken).ConfigureAwait(false)).Append(EventStoreCallOptions.Create( + Settings, operationOptions, userCredentials, cancellationToken)); IWriteResult writeResult; try { diff --git a/src/EventStore.Client.Streams/EventStoreClient.Delete.cs b/src/EventStore.Client.Streams/EventStoreClient.Delete.cs index 964c08f54..69f6aa552 100644 --- a/src/EventStore.Client.Streams/EventStoreClient.Delete.cs +++ b/src/EventStore.Client.Streams/EventStoreClient.Delete.cs @@ -81,7 +81,8 @@ private async Task DeleteInternal(DeleteReq request, UserCredentials? userCredentials, CancellationToken cancellationToken) { _log.LogDebug("Deleting stream {streamName}.", request.Options.StreamIdentifier); - var result = await _client.DeleteAsync(request, + var result = await new Streams.Streams.StreamsClient( + await SelectCallInvoker(cancellationToken).ConfigureAwait(false)).DeleteAsync(request, EventStoreCallOptions.Create(Settings, operationOptions, userCredentials, cancellationToken)); return new DeleteResult(new Position(result.Position.CommitPosition, result.Position.PreparePosition)); diff --git a/src/EventStore.Client.Streams/EventStoreClient.Metadata.cs b/src/EventStore.Client.Streams/EventStoreClient.Metadata.cs index f70abfca8..3bd2d752e 100644 --- a/src/EventStore.Client.Streams/EventStoreClient.Metadata.cs +++ b/src/EventStore.Client.Streams/EventStoreClient.Metadata.cs @@ -17,8 +17,7 @@ private async Task GetStreamMetadataAsync(string streamNam _log.LogDebug("Read stream metadata for {streamName}."); try { - metadata = await ReadStreamAsync(Direction.Backwards, SystemStreams.MetastreamOf(streamName), - StreamPosition.End, 1, operationOptions, false, userCredentials, cancellationToken) + metadata = await ReadStreamAsync(Direction.Backwards, SystemStreams.MetastreamOf(streamName), StreamPosition.End, 1, operationOptions, false, userCredentials, cancellationToken) .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); } catch (StreamNotFoundException) { _log.LogWarning("Stream metadata for {streamName} not found."); diff --git a/src/EventStore.Client.Streams/EventStoreClient.Read.cs b/src/EventStore.Client.Streams/EventStoreClient.Read.cs index 036f37ae1..2dceeede7 100644 --- a/src/EventStore.Client.Streams/EventStoreClient.Read.cs +++ b/src/EventStore.Client.Streams/EventStoreClient.Read.cs @@ -67,30 +67,24 @@ public IAsyncEnumerable ReadAllAsync( cancellationToken); } - private ReadStreamResult ReadStreamAsync( - Direction direction, - string streamName, - StreamPosition revision, - long maxCount, - EventStoreClientOperationOptions operationOptions, - bool resolveLinkTos = false, - UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) => new ReadStreamResult(_client, new ReadReq { - Options = new ReadReq.Types.Options { - ReadDirection = direction switch { - Direction.Backwards => ReadReq.Types.Options.Types.ReadDirection.Backwards, - Direction.Forwards => ReadReq.Types.Options.Types.ReadDirection.Forwards, - _ => throw new InvalidOperationException() - }, - ResolveLinks = resolveLinkTos, - Stream = ReadReq.Types.Options.Types.StreamOptions.FromStreamNameAndRevision(streamName, revision), - Count = (ulong)maxCount - } - }, - Settings, - operationOptions, - userCredentials, - cancellationToken); + private ReadStreamResult ReadStreamAsync(Direction direction, + string streamName, StreamPosition revision, long maxCount, + EventStoreClientOperationOptions operationOptions, bool resolveLinkTos = false, + UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { + return new ReadStreamResult(SelectCallInvoker, new ReadReq { + Options = new ReadReq.Types.Options { + ReadDirection = direction switch { + Direction.Backwards => ReadReq.Types.Options.Types.ReadDirection.Backwards, + Direction.Forwards => ReadReq.Types.Options.Types.ReadDirection.Forwards, + _ => throw new InvalidOperationException() + }, + ResolveLinks = resolveLinkTos, + Stream = ReadReq.Types.Options.Types.StreamOptions.FromStreamNameAndRevision(streamName, + revision), + Count = (ulong)maxCount + } + }, Settings, operationOptions, userCredentials, cancellationToken); + } /// /// Asynchronously reads all the events from a stream. @@ -118,21 +112,21 @@ public ReadStreamResult ReadStreamAsync( var operationOptions = Settings.OperationOptions.Clone(); configureOperationOptions?.Invoke(operationOptions); - return ReadStreamAsync(direction, streamName, revision, maxCount, operationOptions, resolveLinkTos, - userCredentials, cancellationToken); + return ReadStreamAsync(direction, streamName, revision, maxCount, operationOptions, resolveLinkTos, userCredentials, cancellationToken); } /// /// A class that represents the result of a read operation. /// public class ReadStreamResult : IAsyncEnumerable, IAsyncEnumerator { - private readonly IAsyncEnumerator _call; + private readonly string _streamName; + private readonly TaskCompletionSource> _callSource; + private bool _moved; private CancellationToken _cancellationToken; - private readonly string _streamName; internal ReadStreamResult( - Streams.Streams.StreamsClient client, + Func> selectCallInvoker, ReadReq request, EventStoreClientSettings settings, EventStoreClientOperationOptions operationOptions, @@ -149,16 +143,24 @@ internal ReadStreamResult( } request.Options.UuidOption = new ReadReq.Types.Options.Types.UUIDOption {Structured = new Empty()}; + _callSource = new TaskCompletionSource>(); + _ = Task.Run(async () => { + var client = new Streams.Streams.StreamsClient(await selectCallInvoker(cancellationToken) + .ConfigureAwait(false)); + _callSource.SetResult(client + .Read(request, + EventStoreCallOptions.Create(settings, operationOptions, userCredentials, + cancellationToken)).ResponseStream.ReadAllAsync().GetAsyncEnumerator()); + }); _moved = false; - _call = client.Read(request, - EventStoreCallOptions.Create(settings, operationOptions, userCredentials, cancellationToken)) - .ResponseStream.ReadAllAsync().GetAsyncEnumerator(); ReadState = GetStateInternal(); async Task GetStateInternal() { - _moved = await _call.MoveNextAsync(cancellationToken).ConfigureAwait(false); - return _call.Current?.ContentCase switch { + var call = await _callSource.Task.ConfigureAwait(false); + + _moved = await call.MoveNextAsync(cancellationToken).ConfigureAwait(false); + return call.Current?.ContentCase switch { ReadResp.ContentOneofCase.StreamNotFound => Client.ReadState.StreamNotFound, _ => Client.ReadState.Ok }; @@ -180,7 +182,8 @@ public IAsyncEnumerator GetAsyncEnumerator( } /// - public ValueTask DisposeAsync() => _call.DisposeAsync(); + public async ValueTask DisposeAsync() => + await (await _callSource.Task.ConfigureAwait(false)).DisposeAsync().ConfigureAwait(false); /// public async ValueTask MoveNextAsync() { @@ -189,20 +192,26 @@ public async ValueTask MoveNextAsync() { throw ExceptionFromState(state, _streamName); } + var call = await _callSource.Task.ConfigureAwait(false); + if (_moved) { _moved = false; - if (IsCurrentItemEvent()) return true; + if (IsCurrentItemEvent()) { + return true; + } } - while (await _call.MoveNextAsync(_cancellationToken).ConfigureAwait(false)) { - if (IsCurrentItemEvent()) return true; + while (await call.MoveNextAsync(_cancellationToken).ConfigureAwait(false)) { + if (IsCurrentItemEvent()) { + return true; + } } Current = default; return false; bool IsCurrentItemEvent() { - var (confirmation, position, @event) = ConvertToItem(_call.Current); + var (confirmation, position, @event) = ConvertToItem(call.Current); if (confirmation == SubscriptionConfirmation.None && position == null) { Current = @event; return true; @@ -212,12 +221,11 @@ bool IsCurrentItemEvent() { } } - private static Exception ExceptionFromState(ReadState state, string streamName) { - return state switch { + private static Exception ExceptionFromState(ReadState state, string streamName) => + state switch { Client.ReadState.StreamNotFound => new StreamNotFoundException(streamName), _ => throw new ArgumentOutOfRangeException(nameof(state), state, null) }; - } /// public ResolvedEvent Current { get; private set; } @@ -240,7 +248,8 @@ private static Exception ExceptionFromState(ReadState state, string streamName) request.Options.UuidOption = new ReadReq.Types.Options.Types.UUIDOption {Structured = new Empty()}; - using var call = _client.Read(request, + using var call = new Streams.Streams.StreamsClient( + await SelectCallInvoker(cancellationToken).ConfigureAwait(false)).Read(request, EventStoreCallOptions.Create(Settings, operationOptions, userCredentials, cancellationToken)); await foreach (var e in call.ResponseStream diff --git a/src/EventStore.Client.Streams/EventStoreClient.Tombstone.cs b/src/EventStore.Client.Streams/EventStoreClient.Tombstone.cs index 70aa762ba..e8d524c0c 100644 --- a/src/EventStore.Client.Streams/EventStoreClient.Tombstone.cs +++ b/src/EventStore.Client.Streams/EventStoreClient.Tombstone.cs @@ -81,7 +81,8 @@ private async Task TombstoneInternal(TombstoneReq request, CancellationToken cancellationToken) { _log.LogDebug("Tombstoning stream {streamName}.", request.Options.StreamIdentifier); - var result = await _client.TombstoneAsync(request, + var result = await new Streams.Streams.StreamsClient( + await SelectCallInvoker(cancellationToken).ConfigureAwait(false)).TombstoneAsync(request, EventStoreCallOptions.Create(Settings, operationOptions, userCredentials, cancellationToken)); return new DeleteResult(new Position(result.Position.CommitPosition, result.Position.PreparePosition)); diff --git a/src/EventStore.Client.Streams/EventStoreClient.cs b/src/EventStore.Client.Streams/EventStoreClient.cs index 407ad0009..11eb23c97 100644 --- a/src/EventStore.Client.Streams/EventStoreClient.cs +++ b/src/EventStore.Client.Streams/EventStoreClient.cs @@ -20,7 +20,6 @@ public partial class EventStoreClient : EventStoreClientBase { }, }; - private readonly Streams.Streams.StreamsClient _client; private readonly ILogger _log; private static readonly Dictionary> ExceptionMap = @@ -60,7 +59,6 @@ public EventStoreClient(IOptions options) : this(optio /// /// public EventStoreClient(EventStoreClientSettings? settings = null) : base(settings, ExceptionMap) { - _client = new Streams.Streams.StreamsClient(CallInvoker); _log = Settings.LoggerFactory?.CreateLogger() ?? new NullLogger(); } diff --git a/src/EventStore.Client.UserManagement/EventStoreUserManagementClient.cs b/src/EventStore.Client.UserManagement/EventStoreUserManagementClient.cs index 49e5e52b7..7692e1f99 100644 --- a/src/EventStore.Client.UserManagement/EventStoreUserManagementClient.cs +++ b/src/EventStore.Client.UserManagement/EventStoreUserManagementClient.cs @@ -15,7 +15,6 @@ namespace EventStore.Client { /// The client used for operations on internal users. /// public class EventStoreUserManagementClient : EventStoreClientBase { - private readonly Users.Users.UsersClient _client; private readonly ILogger _log; /// @@ -24,7 +23,6 @@ public class EventStoreUserManagementClient : EventStoreClientBase { /// public EventStoreUserManagementClient(EventStoreClientSettings? settings = null) : base(settings, ExceptionMap) { - _client = new Users.Users.UsersClient(CallInvoker); _log = Settings.LoggerFactory?.CreateLogger() ?? new NullLogger(); } @@ -52,7 +50,8 @@ public async Task CreateUserAsync(string loginName, string fullName, string[] gr if (fullName == string.Empty) throw new ArgumentOutOfRangeException(nameof(fullName)); if (password == string.Empty) throw new ArgumentOutOfRangeException(nameof(password)); - await _client.CreateAsync(new CreateReq { + await new Users.Users.UsersClient( + await SelectCallInvoker(cancellationToken).ConfigureAwait(false)).CreateAsync(new CreateReq { Options = new CreateReq.Types.Options { LoginName = loginName, FullName = fullName, @@ -81,7 +80,8 @@ public async Task GetUserAsync(string loginName, UserCredentials? u throw new ArgumentOutOfRangeException(nameof(loginName)); } - using var call = _client.Details(new DetailsReq { + using var call = new Users.Users.UsersClient( + await SelectCallInvoker(cancellationToken).ConfigureAwait(false)).Details(new DetailsReq { Options = new DetailsReq.Types.Options { LoginName = loginName } @@ -110,11 +110,13 @@ public async Task DeleteUserAsync(string loginName, UserCredentials? userCredent if (loginName == null) { throw new ArgumentNullException(nameof(loginName)); } + if (loginName == string.Empty) { throw new ArgumentOutOfRangeException(nameof(loginName)); } - await _client.DeleteAsync(new DeleteReq { + await new Users.Users.UsersClient( + await SelectCallInvoker(cancellationToken).ConfigureAwait(false)).DeleteAsync(new DeleteReq { Options = new DeleteReq.Types.Options { LoginName = loginName } @@ -140,7 +142,8 @@ public async Task EnableUserAsync(string loginName, UserCredentials? userCredent throw new ArgumentOutOfRangeException(nameof(loginName)); } - await _client.EnableAsync(new EnableReq { + await new Users.Users.UsersClient( + await SelectCallInvoker(cancellationToken).ConfigureAwait(false)).EnableAsync(new EnableReq { Options = new EnableReq.Types.Options { LoginName = loginName } @@ -159,7 +162,8 @@ public async Task DisableUserAsync(string loginName, UserCredentials? userCreden CancellationToken cancellationToken = default) { if (loginName == string.Empty) throw new ArgumentOutOfRangeException(nameof(loginName)); - await _client.DisableAsync(new DisableReq { + await new Users.Users.UsersClient( + await SelectCallInvoker(cancellationToken).ConfigureAwait(false)).DisableAsync(new DisableReq { Options = new DisableReq.Types.Options { LoginName = loginName } @@ -174,7 +178,8 @@ await _client.DisableAsync(new DisableReq { /// public async IAsyncEnumerable ListAllAsync(UserCredentials? userCredentials = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { - using var call = _client.Details(new DetailsReq(), + using var call = new Users.Users.UsersClient( + await SelectCallInvoker(cancellationToken).ConfigureAwait(false)).Details(new DetailsReq(), EventStoreCallOptions.Create(Settings, Settings.OperationOptions, userCredentials, cancellationToken)); await foreach (var userDetail in call.ResponseStream @@ -206,13 +211,16 @@ public async Task ChangePasswordAsync(string loginName, string currentPassword, if (currentPassword == string.Empty) throw new ArgumentOutOfRangeException(nameof(currentPassword)); if (newPassword == string.Empty) throw new ArgumentOutOfRangeException(nameof(newPassword)); - await _client.ChangePasswordAsync(new ChangePasswordReq { - Options = new ChangePasswordReq.Types.Options { - CurrentPassword = currentPassword, - NewPassword = newPassword, - LoginName = loginName - } - }, EventStoreCallOptions.Create(Settings, Settings.OperationOptions, userCredentials, cancellationToken)); + await new Users.Users.UsersClient( + await SelectCallInvoker(cancellationToken).ConfigureAwait(false)).ChangePasswordAsync( + new ChangePasswordReq { + Options = new ChangePasswordReq.Types.Options { + CurrentPassword = currentPassword, + NewPassword = newPassword, + LoginName = loginName + } + }, + EventStoreCallOptions.Create(Settings, Settings.OperationOptions, userCredentials, cancellationToken)); } /// @@ -232,12 +240,15 @@ public async Task ResetPasswordAsync(string loginName, string newPassword, if (loginName == string.Empty) throw new ArgumentOutOfRangeException(nameof(loginName)); if (newPassword == string.Empty) throw new ArgumentOutOfRangeException(nameof(newPassword)); - await _client.ResetPasswordAsync(new ResetPasswordReq { - Options = new ResetPasswordReq.Types.Options { - NewPassword = newPassword, - LoginName = loginName - } - }, EventStoreCallOptions.Create(Settings, Settings.OperationOptions, userCredentials, cancellationToken)); + await new Users.Users.UsersClient( + await SelectCallInvoker(cancellationToken).ConfigureAwait(false)).ResetPasswordAsync( + new ResetPasswordReq { + Options = new ResetPasswordReq.Types.Options { + NewPassword = newPassword, + LoginName = loginName + } + }, + EventStoreCallOptions.Create(Settings, Settings.OperationOptions, userCredentials, cancellationToken)); } private static readonly IDictionary> ExceptionMap = diff --git a/src/EventStore.Client/ChannelFactory.cs b/src/EventStore.Client/ChannelFactory.cs index 66a8e4c34..ee54e9eb2 100644 --- a/src/EventStore.Client/ChannelFactory.cs +++ b/src/EventStore.Client/ChannelFactory.cs @@ -22,14 +22,8 @@ public static ChannelBase CreateChannel(EventStoreClientSettings settings, Uri? AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); } return GrpcChannel.ForAddress(address, new GrpcChannelOptions { - HttpClient = new HttpClient(new ClusterAwareHttpHandler(settings.ConnectivitySettings.GossipOverHttps, - settings.ConnectivitySettings.NodePreference == NodePreference.Leader, - settings.ConnectivitySettings.IsSingleNode - ? (IEndpointDiscoverer)new SingleNodeEndpointDiscoverer(settings.ConnectivitySettings.Address) - : new GossipBasedEndpointDiscoverer(settings.ConnectivitySettings, - new GrpcGossipClient(settings))) { - InnerHandler = settings.CreateHttpMessageHandler?.Invoke() ?? new SocketsHttpHandler() - }, true) { + HttpClient = new HttpClient(settings.CreateHttpMessageHandler?.Invoke() ?? new SocketsHttpHandler(), + true) { Timeout = Timeout.InfiniteTimeSpan, DefaultRequestVersion = new Version(2, 0), }, diff --git a/src/EventStore.Client/ClusterAwareHttpHandler.cs b/src/EventStore.Client/ClusterAwareHttpHandler.cs deleted file mode 100644 index d563cd872..000000000 --- a/src/EventStore.Client/ClusterAwareHttpHandler.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; - -#nullable enable -namespace EventStore.Client { - /// - internal class ClusterAwareHttpHandler : DelegatingHandler { - private readonly bool _useHttps; - private readonly bool _requiresLeader; - private readonly IEndpointDiscoverer _endpointDiscoverer; - private Lazy> _endpoint; - - public ClusterAwareHttpHandler(bool useHttps, bool requiresLeader, IEndpointDiscoverer endpointDiscoverer) { - _useHttps = useHttps; - _requiresLeader = requiresLeader; - _endpointDiscoverer = endpointDiscoverer; - _endpoint = new Lazy>(() => endpointDiscoverer.DiscoverAsync(), - LazyThreadSafetyMode.ExecutionAndPublication); - } - - /// - protected override async Task SendAsync(HttpRequestMessage request, - CancellationToken cancellationToken) { - var endpointResolver = _endpoint; - try { - var endpoint = await endpointResolver.Value.ConfigureAwait(false); - - request.RequestUri = new UriBuilder(request.RequestUri!) { - Host = endpoint.GetHost(), - Port = endpoint.GetPort(), - Scheme = _useHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp - }.Uri; - request.Headers.Add("requires-leader", _requiresLeader.ToString()); - var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); - - if (!response.TrailingHeaders.TryGetValues(Constants.Exceptions.ExceptionKey, out var key) || - !key.Contains(Constants.Exceptions.NotLeader) || - !response.TrailingHeaders.TryGetValues(Constants.Exceptions.LeaderEndpointHost, out var hosts) || - !response.TrailingHeaders.TryGetValues(Constants.Exceptions.LeaderEndpointPort, out var ports)) { - return response; - } - - foreach (var host in hosts) { - foreach (var port in ports) { - if (!int.TryParse(port, out var p)) { - continue; - } - - Interlocked.Exchange(ref _endpoint, - new Lazy>(Task.FromResult(new DnsEndPoint(host, p)))); - - return response; - } - } - - return response; - } catch (Exception) { - Interlocked.CompareExchange(ref _endpoint, - new Lazy>(() => _endpointDiscoverer.DiscoverAsync(cancellationToken), - LazyThreadSafetyMode.ExecutionAndPublication), endpointResolver); - - throw; - } - } - } -} diff --git a/src/EventStore.Client/EventStoreClientBase.cs b/src/EventStore.Client/EventStoreClientBase.cs index 9a3607581..1b9d8931d 100644 --- a/src/EventStore.Client/EventStoreClientBase.cs +++ b/src/EventStore.Client/EventStoreClientBase.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using EventStore.Client.Interceptors; using Grpc.Core; using Grpc.Core.Interceptors; @@ -11,12 +13,12 @@ namespace EventStore.Client { /// The base class used by clients used to communicate with the EventStoreDB. /// public abstract class EventStoreClientBase : IDisposable { - private readonly ChannelBase _channel; - + private readonly IDictionary> _exceptionMap; + private readonly MultiChannel _channels; /// - /// The . + /// The name of the connection. /// - protected CallInvoker CallInvoker { get; } + public string ConnectionName { get; } /// /// The . @@ -30,35 +32,30 @@ public abstract class EventStoreClientBase : IDisposable { /// protected EventStoreClientBase(EventStoreClientSettings? settings, IDictionary> exceptionMap) { + _exceptionMap = exceptionMap; Settings = settings ?? new EventStoreClientSettings(); - var connectionName = Settings.ConnectionName ?? $"ES-{Guid.NewGuid()}"; + ConnectionName = Settings.ConnectionName ?? $"ES-{Guid.NewGuid()}"; - _channel = ChannelFactory.CreateChannel(Settings, Settings.ConnectivitySettings.Address); + _channels = new MultiChannel(Settings); + } - CallInvoker = (Settings.Interceptors ?? Array.Empty()).Aggregate( - _channel.CreateCallInvoker() - .Intercept(new TypedExceptionInterceptor(exceptionMap, ex => { })) - .Intercept(new ConnectionNameInterceptor(connectionName)) -#if GRPC_CORE - .Intercept(new HostSelectorInterceptor(Settings.ConnectivitySettings.IsSingleNode - ? (IEndpointDiscoverer)new SingleNodeEndpointDiscoverer( - Settings.ConnectivitySettings.Address) - : new GossipBasedEndpointDiscoverer(Settings.ConnectivitySettings, - new GrpcGossipClient(Settings)), - Settings.ConnectivitySettings.NodePreference)) -#endif - , + /// + /// + /// + /// + /// + protected async Task SelectCallInvoker(CancellationToken cancellationToken) => + (Settings.Interceptors ?? Array.Empty()).Aggregate( + (await _channels.GetCurrentChannel(cancellationToken).ConfigureAwait(false)).CreateCallInvoker() + .Intercept(new TypedExceptionInterceptor(_exceptionMap, ex => { })) + .Intercept(new ConnectionNameInterceptor(ConnectionName)) + .Intercept(new ReportLeaderInterceptor(_channels.SetEndPoint)), (invoker, interceptor) => invoker.Intercept(interceptor)); - } /// public void Dispose() { - // ReSharper disable SuspiciousTypeConversion.Global - if (_channel is IDisposable disposable) { - // ReSharper restore SuspiciousTypeConversion.Global - disposable.Dispose(); - } + _channels.Dispose(); } } } diff --git a/src/EventStore.Client/GrpcGossipClient.cs b/src/EventStore.Client/GrpcGossipClient.cs index a1aa041b8..1f1b58608 100644 --- a/src/EventStore.Client/GrpcGossipClient.cs +++ b/src/EventStore.Client/GrpcGossipClient.cs @@ -4,16 +4,17 @@ using System.Net; using System.Threading; using System.Threading.Tasks; +using Grpc.Core; namespace EventStore.Client { internal class GrpcGossipClient : IGossipClient, IDisposable { private readonly EventStoreClientSettings _settings; - private readonly ConcurrentDictionary _clients; + private readonly ConcurrentDictionary _channels; private int _disposed; public GrpcGossipClient(EventStoreClientSettings settings) { _settings = settings; - _clients = new ConcurrentDictionary(); + _channels = new ConcurrentDictionary(); } public async ValueTask GetAsync(EndPoint endPoint, @@ -22,19 +23,20 @@ public GrpcGossipClient(EventStoreClientSettings settings) { throw new ObjectDisposedException(GetType().ToString()); } - var client = _clients.GetOrAdd(endPoint, - endpoint => new Gossip.Gossip.GossipClient(ChannelFactory.CreateChannel(_settings, endpoint))); + var channel = _channels.GetOrAdd(endPoint, endpoint => ChannelFactory.CreateChannel(_settings, endpoint)); + + var client = new Gossip.Gossip.GossipClient(channel); var result = await client.ReadAsync(new Empty(), cancellationToken: cancellationToken); return new ClusterMessages.ClusterInfo { - Members = Array.ConvertAll(result.Members.ToArray(), x => + Members = result.Members.Select(x => new ClusterMessages.MemberInfo { InstanceId = Uuid.FromDto(x.InstanceId).ToGuid(), State = (ClusterMessages.VNodeState)x.State, IsAlive = x.IsAlive, EndPoint = new DnsEndPoint(x.HttpEndPoint.Address, (int)x.HttpEndPoint.Port) - }) + }).ToArray() }; } @@ -43,10 +45,12 @@ public void Dispose() { return; } - foreach (var client in _clients.Values) { + foreach (var channel in _channels.Values) { // ReSharper disable once SuspiciousTypeConversion.Global - if (client is IDisposable disposable) { + if (channel is IDisposable disposable) { disposable.Dispose(); + } else { + channel.ShutdownAsync(); } } } diff --git a/src/EventStore.Client/Interceptors/HostSelectorInterceptor.cs b/src/EventStore.Client/Interceptors/HostSelectorInterceptor.cs deleted file mode 100644 index 5dc0021eb..000000000 --- a/src/EventStore.Client/Interceptors/HostSelectorInterceptor.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using Grpc.Core; -using Grpc.Core.Interceptors; - -#nullable enable -namespace EventStore.Client.Interceptors { - internal class HostSelectorInterceptor : Interceptor { - private readonly string _requiresLeaderHeaderValue; - private readonly IEndpointDiscoverer _endpointDiscoverer; - private Lazy> _selectedEndpoint; - - public HostSelectorInterceptor(IEndpointDiscoverer endpointDiscoverer, NodePreference nodePreference) { - _endpointDiscoverer = endpointDiscoverer; - _requiresLeaderHeaderValue = nodePreference == NodePreference.Leader ? bool.TrueString : bool.FalseString; - _selectedEndpoint = DeferEndpointSelection(); - } - - private Lazy> DeferEndpointSelection(CancellationToken cancellationToken = default) => - new Lazy>(() => _endpointDiscoverer.DiscoverAsync(cancellationToken), - LazyThreadSafetyMode.ExecutionAndPublication); - - private void ScheduleEndpointSelection(Task _) => - Interlocked.Exchange(ref _selectedEndpoint, DeferEndpointSelection()); - - public override AsyncUnaryCall AsyncUnaryCall(TRequest request, - ClientInterceptorContext context, - AsyncUnaryCallContinuation continuation) { - var call = continuation(request, CreateClientInterceptorContext(context)); - - call.ResponseAsync.ContinueWith(ScheduleEndpointSelection, - TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously); - - return new AsyncUnaryCall(call.ResponseAsync, call.ResponseHeadersAsync, call.GetStatus, - call.GetTrailers, - call.Dispose); - } - - public override AsyncClientStreamingCall AsyncClientStreamingCall( - ClientInterceptorContext context, - AsyncClientStreamingCallContinuation continuation) { - var call = continuation(CreateClientInterceptorContext(context)); - - call.ResponseAsync.ContinueWith(ScheduleEndpointSelection, - TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously); - - return new AsyncClientStreamingCall(call.RequestStream, call.ResponseAsync, - call.ResponseHeadersAsync, call.GetStatus, call.GetTrailers, call.Dispose); - } - - public override AsyncServerStreamingCall AsyncServerStreamingCall( - TRequest request, ClientInterceptorContext context, - AsyncServerStreamingCallContinuation continuation) { - var call = continuation(request, CreateClientInterceptorContext(context)); - - return new AsyncServerStreamingCall( - new StreamReader(call.ResponseStream, ScheduleEndpointSelection), call.ResponseHeadersAsync, - call.GetStatus, call.GetTrailers, call.Dispose); - } - - public override AsyncDuplexStreamingCall AsyncDuplexStreamingCall( - ClientInterceptorContext context, - AsyncDuplexStreamingCallContinuation continuation) { - var call = continuation(CreateClientInterceptorContext(context)); - - return new AsyncDuplexStreamingCall(call.RequestStream, - new StreamReader(call.ResponseStream, ScheduleEndpointSelection), call.ResponseHeadersAsync, - call.GetStatus, call.GetTrailers, call.Dispose); - } - - private ClientInterceptorContext CreateClientInterceptorContext( - ClientInterceptorContext context) where TRequest : class where TResponse : class { - context.Options.Headers.Add("requires-leader", _requiresLeaderHeaderValue); - - return new ClientInterceptorContext(context.Method, SelectHost(), context.Options); - } - - private string SelectHost() => - $"{_selectedEndpoint.Value.Result.GetHost()}:{_selectedEndpoint.Value.Result.GetPort()}"; - - private class StreamReader : IAsyncStreamReader { - private readonly IAsyncStreamReader _inner; - private readonly Action> _callback; - - public StreamReader(IAsyncStreamReader inner, Action> callback) { - _inner = inner; - _callback = callback; - } - - public Task MoveNext(CancellationToken cancellationToken) { - var task = _inner.MoveNext(cancellationToken); - - task.ContinueWith(_callback, - TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously); - - return task; - } - - public T Current { get; } = default!; - } - } -} diff --git a/src/EventStore.Client/Interceptors/ReportLeaderInterceptor.cs b/src/EventStore.Client/Interceptors/ReportLeaderInterceptor.cs new file mode 100644 index 000000000..ffec87d03 --- /dev/null +++ b/src/EventStore.Client/Interceptors/ReportLeaderInterceptor.cs @@ -0,0 +1,85 @@ +using System; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Grpc.Core; +using Grpc.Core.Interceptors; + +namespace EventStore.Client.Interceptors { + internal class ReportLeaderInterceptor : Interceptor { + private readonly Action _reportNewLeader; + + private const TaskContinuationOptions ContinuationOptions = + TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.OnlyOnFaulted; + + public ReportLeaderInterceptor(Action reportNewLeader) { + _reportNewLeader = reportNewLeader; + } + + public override AsyncUnaryCall AsyncUnaryCall(TRequest request, + ClientInterceptorContext context, + AsyncUnaryCallContinuation continuation) { + var response = continuation(request, context); + + response.ResponseAsync.ContinueWith(ReportNewLeader, ContinuationOptions); + + return new AsyncUnaryCall(response.ResponseAsync, response.ResponseHeadersAsync, + response.GetStatus, response.GetTrailers, response.Dispose); + } + + public override AsyncClientStreamingCall AsyncClientStreamingCall( + ClientInterceptorContext context, + AsyncClientStreamingCallContinuation continuation) { + var response = continuation(context); + + response.ResponseAsync.ContinueWith(ReportNewLeader, ContinuationOptions); + + return new AsyncClientStreamingCall(response.RequestStream, response.ResponseAsync, + response.ResponseHeadersAsync, response.GetStatus, response.GetTrailers, response.Dispose); + } + + public override AsyncDuplexStreamingCall AsyncDuplexStreamingCall( + ClientInterceptorContext context, + AsyncDuplexStreamingCallContinuation continuation) { + var response = continuation(context); + + return new AsyncDuplexStreamingCall(response.RequestStream, + new StreamReader(response.ResponseStream, ReportNewLeader), response.ResponseHeadersAsync, + response.GetStatus, response.GetTrailers, response.Dispose); + } + + public override AsyncServerStreamingCall AsyncServerStreamingCall( + TRequest request, ClientInterceptorContext context, + AsyncServerStreamingCallContinuation continuation) { + var response = continuation(request, context); + + return new AsyncServerStreamingCall( + new StreamReader(response.ResponseStream, ReportNewLeader), response.ResponseHeadersAsync, + response.GetStatus, response.GetTrailers, response.Dispose); + } + + private void ReportNewLeader(Task task) { + if (task.Exception!.InnerException is NotLeaderException ex) { + _reportNewLeader(ex.LeaderEndpoint); + } + } + + private class StreamReader : IAsyncStreamReader { + private readonly IAsyncStreamReader _inner; + private readonly Action> _reportNewLeader; + + public StreamReader(IAsyncStreamReader inner, Action> reportNewLeader) { + _inner = inner; + _reportNewLeader = reportNewLeader; + } + + public Task MoveNext(CancellationToken cancellationToken) { + var task = _inner.MoveNext(cancellationToken); + task.ContinueWith(_reportNewLeader, ContinuationOptions); + return task; + } + + public T Current => _inner.Current; + } + } +} diff --git a/src/EventStore.Client/MultiChannel.cs b/src/EventStore.Client/MultiChannel.cs new file mode 100644 index 000000000..2125850b8 --- /dev/null +++ b/src/EventStore.Client/MultiChannel.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Concurrent; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Grpc.Core; + +#nullable enable +namespace EventStore.Client { + internal class MultiChannel : IDisposable { + private readonly EventStoreClientSettings _settings; + private readonly IEndpointDiscoverer _endpointDiscoverer; + private readonly ConcurrentDictionary _channels; + + private EndPoint? _current; + + private int _disposed; + + public MultiChannel(EventStoreClientSettings settings) { + _settings = settings; + _endpointDiscoverer = settings.ConnectivitySettings.IsSingleNode + ? (IEndpointDiscoverer)new SingleNodeEndpointDiscoverer(settings.ConnectivitySettings.Address) + : new GossipBasedEndpointDiscoverer(settings.ConnectivitySettings, new GrpcGossipClient(settings)); + _channels = new ConcurrentDictionary(); + } + + public void SetEndPoint(EndPoint value) => _current = value; + + public async Task GetCurrentChannel(CancellationToken cancellationToken = default) { + if (Interlocked.CompareExchange(ref _disposed, 0, 0) != 0) { + throw new ObjectDisposedException(GetType().ToString()); + } + + var current = _current ??= await _endpointDiscoverer.DiscoverAsync(cancellationToken).ConfigureAwait(false); + return _channels.GetOrAdd(current, ChannelFactory.CreateChannel(_settings, current)); + } + + + + public void Dispose() { + if (Interlocked.Exchange(ref _disposed, 1) == 1) { + return; + } + + foreach (var channel in _channels.Values) { + if (channel is IDisposable disposable) { + disposable.Dispose(); + } else { + channel.ShutdownAsync(); + } + } + } + } +} diff --git a/test/Directory.Build.props b/test/Directory.Build.props index fd1a068d5..77b190f22 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -15,6 +15,9 @@ runtime; build; native; contentfiles; analyzers + + + diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/when_writing_and_subscribing_to_normal_events_manual_nack.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/when_writing_and_subscribing_to_normal_events_manual_nack.cs index f17925c3d..da7b3932c 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/when_writing_and_subscribing_to_normal_events_manual_nack.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/when_writing_and_subscribing_to_normal_events_manual_nack.cs @@ -41,14 +41,12 @@ await Client.CreateAsync(Stream, Group, new PersistentSubscriptionSettings(startFrom: StreamPosition.Start, resolveLinkTos: true), TestCredentials.Root); _subscription = await Client.SubscribeAsync(Stream, Group, - (subscription, e, retryCount, ct) => { - subscription.Nack(PersistentSubscriptionNakEventAction.Park, "fail", e); + async (subscription, e, retryCount, ct) => { + await subscription.Nack(PersistentSubscriptionNakEventAction.Park, "fail", e); if (Interlocked.Increment(ref _eventReceivedCount) == _events.Length) { _eventsReceived.TrySetResult(true); } - - return Task.CompletedTask; }, (s, r, e) => { if (e != null) { _eventsReceived.TrySetException(e); diff --git a/test/EventStore.Client.Tests.Common/EventStoreClientFixtureBase.cs b/test/EventStore.Client.Tests.Common/EventStoreClientFixtureBase.cs index c5405659f..24a2221c5 100644 --- a/test/EventStore.Client.Tests.Common/EventStoreClientFixtureBase.cs +++ b/test/EventStore.Client.Tests.Common/EventStoreClientFixtureBase.cs @@ -14,6 +14,10 @@ using Ductus.FluentDocker.Builders; using Ductus.FluentDocker.Model.Builders; using Ductus.FluentDocker.Services; +#if GRPC_CORE +using System.Security.Cryptography.X509Certificates; +using Grpc.Core; +#endif using Polly; using Serilog; using Serilog.Events; @@ -27,6 +31,14 @@ namespace EventStore.Client { public abstract class EventStoreClientFixtureBase : IAsyncLifetime { public const string TestEventType = "-"; + private const string ConnectionString = +#if GRPC_CORE + "esdb://127.0.0.1:2113/?tlsVerifyCert=false" +#else + "esdb://localhost:2113/?tlsVerifyCert=false" +#endif + ; + private static readonly Subject LogEventSubject = new Subject(); private static readonly string HostCertificatePath = @@ -73,12 +85,19 @@ protected EventStoreClientFixtureBase(EventStoreClientSettings? clientSettings, _disposables = new List(); ServicePointManager.ServerCertificateValidationCallback = delegate { return true; }; - Settings = clientSettings ?? EventStoreClientSettings.Create("esdb://localhost:2113/?tlsVerifyCert=false"); + Settings = clientSettings ?? EventStoreClientSettings.Create(ConnectionString); Settings.OperationOptions.TimeoutAfter = Debugger.IsAttached ? new TimeSpan?() : TimeSpan.FromSeconds(30); +#if GRPC_CORE + Settings.ChannelCredentials ??= GetServerCertificate(); + + static SslCredentials GetServerCertificate() => new SslCredentials( + File.ReadAllText(Path.Combine(HostCertificatePath, "ca", "ca.crt")), null, _ => true); +#endif + Settings.LoggerFactory ??= new SerilogLoggerFactory(); TestServer = new EventStoreTestServer(Settings.ConnectivitySettings.Address, env); @@ -159,7 +178,6 @@ public EventStoreTestServer(Uri address, IDictionary? envOverrid } }) { BaseAddress = address, - }; var tag = Environment.GetEnvironmentVariable("ES_DOCKER_TAG") ?? "ci"; @@ -174,6 +192,7 @@ public EventStoreTestServer(Uri address, IDictionary? envOverrid foreach (var (key, value) in envOverrides ?? Enumerable.Empty>()) { env[key] = value; } + _eventStore = new Builder() .UseContainer() .UseImage($"docker.pkg.github.com/eventstore/eventstore/eventstore:{tag}") diff --git a/test/EventStore.Client.Tests/ClusterAwareHttpHandlerTests.cs b/test/EventStore.Client.Tests/ClusterAwareHttpHandlerTests.cs deleted file mode 100644 index 95591754d..000000000 --- a/test/EventStore.Client.Tests/ClusterAwareHttpHandlerTests.cs +++ /dev/null @@ -1,144 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Xunit; - -namespace EventStore.Client { - public class ClusterAwareHttpHandlerTests { - [Theory, - InlineData(true, true), - InlineData(true, false), - InlineData(false, true), - InlineData(false, false) - ] - public async Task should_set_requires_leader_header(bool useHttps, bool requiresLeader) { - var sut = new ClusterAwareHttpHandler( - useHttps, requiresLeader, - new FakeEndpointDiscoverer(() => new IPEndPoint(IPAddress.Parse("0.0.0.0"), 2113))) { - InnerHandler = new TestMessageHandler() - }; - - var client = new HttpClient(sut); - - var request = new HttpRequestMessage(HttpMethod.Get, new UriBuilder().Uri); - - await client.SendAsync(request); - - Assert.True(request.Headers.TryGetValues("requires-leader", out var value)); - Assert.True(bool.Parse(value.First()) == requiresLeader); - } - - [Theory, InlineData(true), InlineData(false)] - public async Task should_issue_request_to_discovered_endpoint(bool useHttps) { - var discoveredEndpoint = new IPEndPoint(IPAddress.Parse("0.0.0.0"), 2113); - - var sut = new ClusterAwareHttpHandler( - useHttps, true, new FakeEndpointDiscoverer(() => discoveredEndpoint)) { - InnerHandler = new TestMessageHandler() - }; - - var client = new HttpClient(sut); - - await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, new UriBuilder().Uri)); - - var request = new HttpRequestMessage(HttpMethod.Get, new UriBuilder().Uri); - await client.SendAsync(request); - - Assert.Equal(discoveredEndpoint.Address.ToString(), request.RequestUri.Host); - Assert.Equal(discoveredEndpoint.Port, request.RequestUri.Port); - } - - [Theory, InlineData(true), InlineData(false)] - public async Task should_attempt_endpoint_discovery_on_next_request_when_request_fails(bool useHttps) { - int discoveryAttempts = 0; - - var sut = new ClusterAwareHttpHandler( - useHttps, true, new FakeEndpointDiscoverer(() => { - discoveryAttempts++; - throw new Exception(); - })) { - InnerHandler = new TestMessageHandler() - }; - - var client = new HttpClient(sut); - - await Assert.ThrowsAsync(() => - client.SendAsync(new HttpRequestMessage(HttpMethod.Get, new UriBuilder().Uri))); - await Assert.ThrowsAsync(() => - client.SendAsync(new HttpRequestMessage(HttpMethod.Get, new UriBuilder().Uri))); - - Assert.Equal(2, discoveryAttempts); - } - - [Theory, ClassData(typeof(EndPoints))] - public async Task should_set_endpoint_to_leader_endpoint_on_exception(bool useHttps, EndPoint endpoint) { - var sut = new ClusterAwareHttpHandler( - useHttps, true, new FakeEndpointDiscoverer(() => new IPEndPoint(IPAddress.Loopback, 2113))) { - InnerHandler = new TestNotLeaderMessageHandler(endpoint) - }; - - using var client = new HttpClient(sut); - - using var _ = await client.GetAsync(new UriBuilder().Uri); // first request results in leadernotfound - - using var request = new HttpRequestMessage(HttpMethod.Get, new UriBuilder().Uri); - - using var __ = await client.SendAsync(request); - - Assert.Equal(endpoint.GetHost(), request.RequestUri.Host); - Assert.Equal(endpoint.GetPort(), request.RequestUri.Port); - } - } - - public class EndPoints : IEnumerable { - public IEnumerator GetEnumerator() { - yield return new object[] {true, new IPEndPoint(IPAddress.Any, 2113)}; - yield return new object[] {true, new DnsEndPoint("nodea.eventstore.dev", 2113),}; - yield return new object[] {false, new IPEndPoint(IPAddress.Any, 2113)}; - yield return new object[] {false, new DnsEndPoint("nodea.eventstore.dev", 2113),}; - } - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - } - - internal class FakeEndpointDiscoverer : IEndpointDiscoverer { - private readonly Func _function; - - public FakeEndpointDiscoverer(Func function) { - _function = function; - } - - public Task DiscoverAsync(CancellationToken cancellationToken = default) => - Task.FromResult(_function()); - } - - internal class TestMessageHandler : HttpMessageHandler { - protected override Task SendAsync(HttpRequestMessage request, - CancellationToken cancellationToken) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)); - } - - internal class TestNotLeaderMessageHandler : TestMessageHandler { - private readonly EndPoint _endpoint; - private int _messageCount = -1; - - public TestNotLeaderMessageHandler(EndPoint endpoint) { - _endpoint = endpoint; - } - - protected override Task SendAsync(HttpRequestMessage request, - CancellationToken cancellationToken) => Interlocked.Increment(ref _messageCount) == 0 - ? Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { - TrailingHeaders = { - {Constants.Exceptions.ExceptionKey, Constants.Exceptions.NotLeader}, - {Constants.Exceptions.LeaderEndpointHost, _endpoint.GetHost()}, - {Constants.Exceptions.LeaderEndpointPort, _endpoint.GetPort().ToString()} - } - }) - : base.SendAsync(request, cancellationToken); - } -} diff --git a/test/EventStore.Client.Tests/Interceptors/HostSelectorInterceptorTests.cs b/test/EventStore.Client.Tests/Interceptors/HostSelectorInterceptorTests.cs deleted file mode 100644 index 12f27132e..000000000 --- a/test/EventStore.Client.Tests/Interceptors/HostSelectorInterceptorTests.cs +++ /dev/null @@ -1,177 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using Grpc.Core; -using Grpc.Core.Interceptors; -using Xunit; - -namespace EventStore.Client.Interceptors { - public class HostSelectorInterceptorTests { - private static readonly Marshaller _marshaller = - new Marshaller(_ => Array.Empty(), _ => new object()); - - public delegate Task<(Metadata metadata, string host)> GrpcCall(Interceptor interceptor, - Task response = null); - - private static IEnumerable Calls() { - yield return MakeUnaryCall; - yield return MakeClientStreamingCall; - yield return MakeServerStreamingCall; - yield return MakeDuplexStreamingCall; - } - - private static IEnumerable<(NodePreference nodePreference, bool requriesLeader)> NodePreferences() { - yield return (NodePreference.Leader, true); - yield return (NodePreference.Follower, false); - yield return (NodePreference.Random, false); - yield return (NodePreference.ReadOnlyReplica, false); - } - - public static IEnumerable RequiresLeaderCases() => - from _ in NodePreferences() - from call in Calls() - select new object[] {_.nodePreference, _.requriesLeader, call}; - - [Theory, MemberData(nameof(RequiresLeaderCases))] - public async Task RequiresLeaderSetToExpectedResult(NodePreference nodePreference, bool expectedRequiresLeader, - GrpcCall makeCall) { - var sut = new HostSelectorInterceptor(new TestEndpointDiscoverer(new IPEndPoint(IPAddress.Any, 2113)), - nodePreference); - var (metadata, host) = await makeCall(sut); - Assert.True(metadata.TryGetValue("requires-leader", out var requiresLeaderValue)); - Assert.True(bool.TryParse(requiresLeaderValue, out var requiresLeader)); - Assert.Equal(expectedRequiresLeader, requiresLeader); - Assert.Equal($"{IPAddress.Any}:2113", host); - } - - public static IEnumerable SelectsGossipCases() => - from call in Calls() - select new object[] {call}; - - [Theory, MemberData(nameof(SelectsGossipCases))] - public async Task SelectsNewGossipEndPointAfterFailure(GrpcCall makeCall) { - var endpoints = new EndPoint[] { - new IPEndPoint(IPAddress.Loopback, 2113), - new IPEndPoint(IPAddress.Loopback, 2114), - new IPEndPoint(IPAddress.Loopback, 2115), - }; - - var sut = new HostSelectorInterceptor(new TestEndpointDiscoverer(endpoints), NodePreference.Leader); - - await Assert.ThrowsAsync(() => - makeCall(sut, Task.FromException(new DummyException()))); - - var (_, host) = await makeCall(sut, Task.FromResult(new object())); - - Assert.Equal($"{IPAddress.Loopback}:2114", host); - } - - private static async Task<(Metadata metadata, string host)> MakeUnaryCall(Interceptor interceptor, - Task response = null) { - var metadata = new Metadata(); - string host = null; - - using var call = interceptor.AsyncUnaryCall(new object(), - CreateClientInterceptorContext(metadata, MethodType.Unary), - (_, context) => { - host = context.Host; - return new AsyncUnaryCall(response ?? Task.FromResult(new object()), - Task.FromResult(context.Options.Headers), GetSuccess, GetTrailers, OnDispose); - }); - await call.ResponseAsync; - return (metadata, host); - } - - private static async Task<(Metadata metadata, string host)> MakeClientStreamingCall(Interceptor interceptor, - Task response = null) { - var metadata = new Metadata(); - string host = null; - - using var call = interceptor.AsyncClientStreamingCall( - CreateClientInterceptorContext(metadata, MethodType.ClientStreaming), - context => { - host = context.Host; - return new AsyncClientStreamingCall(null, response ?? Task.FromResult(new object()), - Task.FromResult(context.Options.Headers), GetSuccess, GetTrailers, OnDispose); - }); - await call.ResponseAsync; - return (metadata, host); - } - - private static async Task<(Metadata metadata, string host)> MakeServerStreamingCall(Interceptor interceptor, - Task response = null) { - var metadata = new Metadata(); - string host = null; - - using var call = interceptor.AsyncServerStreamingCall(new object(), - CreateClientInterceptorContext(metadata, MethodType.ServerStreaming), - (_, context) => { - host = context.Host; - return new AsyncServerStreamingCall(new TestAsyncStreamReader(response), - Task.FromResult(context.Options.Headers), GetSuccess, GetTrailers, OnDispose); - }); - await call.ResponseStream.ReadAllAsync().ToArrayAsync(); - return (metadata, host); - } - - private static async Task<(Metadata metadata, string host)> MakeDuplexStreamingCall(Interceptor interceptor, - Task response = null) { - var metadata = new Metadata(); - string host = null; - - using var call = interceptor.AsyncDuplexStreamingCall( - CreateClientInterceptorContext(metadata, MethodType.ServerStreaming), - context => { - host = context.Host; - return new AsyncDuplexStreamingCall(null, new TestAsyncStreamReader(response), - Task.FromResult(context.Options.Headers), GetSuccess, GetTrailers, OnDispose); - }); - await call.ResponseStream.ReadAllAsync().ToArrayAsync(); - return (metadata, host); - } - - private static Status GetSuccess() => Status.DefaultSuccess; - - private static Metadata GetTrailers() => Metadata.Empty; - - private static void OnDispose() { } - - private static ClientInterceptorContext CreateClientInterceptorContext(Metadata metadata, - MethodType methodType) => new ClientInterceptorContext( - new Method(methodType, string.Empty, string.Empty, _marshaller, _marshaller), - null, new CallOptions(metadata)); - - - private class TestEndpointDiscoverer : IEndpointDiscoverer { - private readonly EndPoint[] _endPoints; - private int _index = -1; - - public TestEndpointDiscoverer(params EndPoint[] endPoints) { - _endPoints = endPoints; - } - - public Task DiscoverAsync(CancellationToken cancellationToken = default) => - Task.FromResult(_endPoints[Interlocked.Increment(ref _index) % _endPoints.Length]); - } - - private class TestAsyncStreamReader : IAsyncStreamReader { - private readonly Task _response; - - public Task MoveNext(CancellationToken cancellationToken) => _response.IsFaulted - ? Task.FromException(_response.Exception!.GetBaseException()) - : Task.FromResult(false); - - public object Current => _response.Result; - - public TestAsyncStreamReader(Task response = null) { - _response = response ?? Task.FromResult(new object()); - } - } - - private class DummyException : Exception { - } - } -} diff --git a/test/EventStore.Client.Tests/Interceptors/ReportLeaderInterceptorTests.cs b/test/EventStore.Client.Tests/Interceptors/ReportLeaderInterceptorTests.cs new file mode 100644 index 000000000..1e5f4c7f5 --- /dev/null +++ b/test/EventStore.Client.Tests/Interceptors/ReportLeaderInterceptorTests.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Grpc.Core; +using Grpc.Core.Interceptors; +using Xunit; + +namespace EventStore.Client.Interceptors { + public class ReportLeaderInterceptorTests { + private static readonly Marshaller _marshaller = + new Marshaller(_ => Array.Empty(), _ => new object()); + + public delegate Task GrpcCall(Interceptor interceptor, Task response = null); + + private static IEnumerable GrpcCalls() { + yield return MakeUnaryCall; + yield return MakeClientStreamingCall; + yield return MakeDuplexStreamingCall; + yield return MakeServerStreamingCall; + } + + public static IEnumerable TestCases() => GrpcCalls().Select(call => new object[] {call}); + + [Theory, MemberData(nameof(TestCases))] + public async Task ReportsNewLeader(GrpcCall call) { + EndPoint actual = default; + var sut = new ReportLeaderInterceptor(ex => actual = ex); + + var result = await Assert.ThrowsAsync(() => + call(sut, Task.FromException(new NotLeaderException("a.host", 2112)))); + Assert.Equal(result.LeaderEndpoint, actual); + } + + private static async Task MakeUnaryCall(Interceptor interceptor, Task response = null) { + using var call = interceptor.AsyncUnaryCall(new object(), + CreateClientInterceptorContext(MethodType.Unary), + (_, context) => new AsyncUnaryCall(response ?? Task.FromResult(new object()), + Task.FromResult(context.Options.Headers), GetSuccess, GetTrailers, OnDispose)); + await call.ResponseAsync; + } + + private static async Task MakeClientStreamingCall(Interceptor interceptor, Task response = null) { + using var call = interceptor.AsyncClientStreamingCall( + CreateClientInterceptorContext(MethodType.ClientStreaming), + context => new AsyncClientStreamingCall(null, response ?? Task.FromResult(new object()), + Task.FromResult(context.Options.Headers), GetSuccess, GetTrailers, OnDispose)); + await call.ResponseAsync; + } + + private static async Task MakeServerStreamingCall(Interceptor interceptor, Task response = null) { + using var call = interceptor.AsyncServerStreamingCall(new object(), + CreateClientInterceptorContext(MethodType.ServerStreaming), + (_, context) => new AsyncServerStreamingCall(new TestAsyncStreamReader(response), + Task.FromResult(context.Options.Headers), GetSuccess, GetTrailers, OnDispose)); + await call.ResponseStream.ReadAllAsync().ToArrayAsync(); + } + + private static async Task MakeDuplexStreamingCall(Interceptor interceptor, Task response = null) { + using var call = interceptor.AsyncDuplexStreamingCall( + CreateClientInterceptorContext(MethodType.ServerStreaming), + context => new AsyncDuplexStreamingCall(null, new TestAsyncStreamReader(response), + Task.FromResult(context.Options.Headers), GetSuccess, GetTrailers, OnDispose)); + await call.ResponseStream.ReadAllAsync().ToArrayAsync(); + } + + private static Status GetSuccess() => Status.DefaultSuccess; + + private static Metadata GetTrailers() => Metadata.Empty; + + private static void OnDispose() { } + + private static ClientInterceptorContext CreateClientInterceptorContext(MethodType methodType) => + new ClientInterceptorContext( + new Method(methodType, string.Empty, string.Empty, _marshaller, _marshaller), + null, new CallOptions(new Metadata())); + + private class TestAsyncStreamReader : IAsyncStreamReader { + private readonly Task _response; + + public Task MoveNext(CancellationToken cancellationToken) => _response.IsFaulted + ? Task.FromException(_response.Exception!.GetBaseException()) + : Task.FromResult(false); + + public object Current => _response.Result; + + public TestAsyncStreamReader(Task response = null) { + _response = response ?? Task.FromResult(new object()); + } + } + } +} From 1e913d125841cfdc2adc1ff9cc6fbf412d66edc7 Mon Sep 17 00:00:00 2001 From: thefringeninja <495495+thefringeninja@users.noreply.github.com> Date: Wed, 6 Jan 2021 19:51:03 +0100 Subject: [PATCH 05/21] add net48 support --- .github/workflows/ci.yml | 2 +- Directory.Build.props | 6 +++- .../DeconstructionExtensions.cs | 13 ++++++++ .../EpochExtensions.cs | 12 +++++-- ...ntStoreProjectionManagementClient.State.cs | 21 ++++++++++--- ...entStoreClientSettings.ConnectionString.cs | 10 +++--- src/EventStore.Client/StreamIdentifier.cs | 9 +++++- src/EventStore.Client/Uuid.cs | 31 ++++++++++++++++++- test/Directory.Build.props | 2 +- .../EventDataComparer.cs | 8 ++--- .../is_json.cs | 4 +-- .../EventStoreClientFixtureBase.cs | 20 +++++++----- 12 files changed, 108 insertions(+), 30 deletions(-) create mode 100644 src/EventStore.Client.Common/DeconstructionExtensions.cs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e9c766837..51b0362ff 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: strategy: fail-fast: false matrix: - framework: [netcoreapp3.1, net5.0] + framework: [netcoreapp3.1, net5.0, net48] os: [ubuntu-18.04] test: ["", .Streams, .PersistentSubscriptions, .Operations, .UserManagement, .ProjectionManagement] configuration: [release] diff --git a/Directory.Build.props b/Directory.Build.props index c197af600..83a279cfe 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - netcoreapp3.1;net5.0 + net48;netcoreapp3.1;net5.0 x64 true disable @@ -21,4 +21,8 @@ + + + + diff --git a/src/EventStore.Client.Common/DeconstructionExtensions.cs b/src/EventStore.Client.Common/DeconstructionExtensions.cs new file mode 100644 index 000000000..a3c63481c --- /dev/null +++ b/src/EventStore.Client.Common/DeconstructionExtensions.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +#nullable enable +namespace EventStore.Client { +#if NETFRAMEWORK + internal static class DeconstructionExtensions { + public static void Deconstruct(this KeyValuePair source, out TKey key, + out TValue value) { + key = source.Key; + value = source.Value; + } + } +#endif +} diff --git a/src/EventStore.Client.Common/EpochExtensions.cs b/src/EventStore.Client.Common/EpochExtensions.cs index ccedca24d..525c65132 100644 --- a/src/EventStore.Client.Common/EpochExtensions.cs +++ b/src/EventStore.Client.Common/EpochExtensions.cs @@ -3,10 +3,18 @@ #nullable enable namespace EventStore.Client { internal static class EpochExtensions { + private static readonly DateTime UnixEpoch = +#if NETFRAMEWORK + new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc) +#else + DateTime.UnixEpoch +#endif + ; + public static DateTime FromTicksSinceEpoch(this long value) => - new DateTime(DateTime.UnixEpoch.Ticks + value, DateTimeKind.Utc); + new DateTime(UnixEpoch.Ticks + value, DateTimeKind.Utc); public static long ToTicksSinceEpoch(this DateTime value) => - (value - DateTime.UnixEpoch).Ticks; + (value - UnixEpoch).Ticks; } } diff --git a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.State.cs b/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.State.cs index c346f3e23..8c2106749 100644 --- a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.State.cs +++ b/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.State.cs @@ -22,7 +22,10 @@ public async Task GetResultAsync(string name, string? partition = var value = await GetResultInternalAsync(name, partition, userCredentials, cancellationToken) .ConfigureAwait(false); - await using var stream = new MemoryStream(); +#if !NETFRAMEWORK + await +#endif + using var stream = new MemoryStream(); await using var writer = new Utf8JsonWriter(stream); var serializer = new ValueSerializer(); serializer.Write(writer, value, new JsonSerializerOptions()); @@ -47,8 +50,10 @@ public async Task GetResultAsync(string name, string? partition = null, CancellationToken cancellationToken = default) { var value = await GetResultInternalAsync(name, partition, userCredentials, cancellationToken) .ConfigureAwait(false); - - await using var stream = new MemoryStream(); +#if !NETFRAMEWORK + await +#endif + using var stream = new MemoryStream(); await using var writer = new Utf8JsonWriter(stream); var serializer = new ValueSerializer(); serializer.Write(writer, value, new JsonSerializerOptions()); @@ -86,7 +91,10 @@ public async Task GetStateAsync(string name, string? partition = n var value = await GetStateInternalAsync(name, partition, userCredentials, cancellationToken) .ConfigureAwait(false); - await using var stream = new MemoryStream(); +#if !NETFRAMEWORK + await +#endif + using var stream = new MemoryStream(); await using var writer = new Utf8JsonWriter(stream); var serializer = new ValueSerializer(); serializer.Write(writer, value, new JsonSerializerOptions()); @@ -112,7 +120,10 @@ public async Task GetStateAsync(string name, string? partition = null, var value = await GetStateInternalAsync(name, partition, userCredentials, cancellationToken) .ConfigureAwait(false); - await using var stream = new MemoryStream(); +#if !NETFRAMEWORK + await +#endif + using var stream = new MemoryStream(); await using var writer = new Utf8JsonWriter(stream); var serializer = new ValueSerializer(); serializer.Write(writer, value, new JsonSerializerOptions()); diff --git a/src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs b/src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs index d815f7f5a..7fca9c29a 100644 --- a/src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs +++ b/src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs @@ -196,16 +196,16 @@ private static string ParseScheme(string s) => !Schemes.Contains(s) ? throw new InvalidSchemeException(s, Schemes) : s; private static (string, string) ParseUserInfo(string s) { - var tokens = s.Split(Colon); + var tokens = s.Split(Colon[0]); if (tokens.Length != 2) throw new InvalidUserCredentialsException(s); return (tokens[0], tokens[1]); } private static EndPoint[] ParseHosts(string s) { - var hostsTokens = s.Split(Comma); + var hostsTokens = s.Split(Comma[0]); var hosts = new List(); foreach (var hostToken in hostsTokens) { - var hostPortToken = hostToken.Split(Colon); + var hostPortToken = hostToken.Split(Colon[0]); string host; int port; switch (hostPortToken.Length) { @@ -239,7 +239,7 @@ private static EndPoint[] ParseHosts(string s) { private static Dictionary ParseKeyValuePairs(string s) { var options = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - var optionsTokens = s.Split(Ampersand); + var optionsTokens = s.Split(Ampersand[0]); foreach (var optionToken in optionsTokens) { var (key, val) = ParseKeyValuePair(optionToken); try { @@ -253,7 +253,7 @@ private static Dictionary ParseKeyValuePairs(string s) { } private static (string, string) ParseKeyValuePair(string s) { - var keyValueToken = s.Split(Equal); + var keyValueToken = s.Split(Equal[0]); if (keyValueToken.Length != 2) { throw new InvalidKeyValuePairException(s); } diff --git a/src/EventStore.Client/StreamIdentifier.cs b/src/EventStore.Client/StreamIdentifier.cs index c8f75d5e2..f83dcec6a 100644 --- a/src/EventStore.Client/StreamIdentifier.cs +++ b/src/EventStore.Client/StreamIdentifier.cs @@ -8,7 +8,14 @@ public partial class StreamIdentifier { public static implicit operator string(StreamIdentifier source) { if (source._cached != null || source.StreamName.IsEmpty) return source._cached; - var tmp = Encoding.UTF8.GetString(source.StreamName.Span); + + var tmp = Encoding.UTF8.GetString( +#if NETFRAMEWORK + source.StreamName.ToByteArray() +#else + source.StreamName.Span +#endif + ); //this doesn't have to be thread safe, its just a cache in case the identifier is turned into a string several times source._cached = tmp; return source._cached; diff --git a/src/EventStore.Client/Uuid.cs b/src/EventStore.Client/Uuid.cs index dcfc8bdd4..455d5f9ba 100644 --- a/src/EventStore.Client/Uuid.cs +++ b/src/EventStore.Client/Uuid.cs @@ -65,6 +65,18 @@ private Uuid(Guid value) { throw new NotSupportedException(); } +#if NETFRAMEWORK + var data = value.ToByteArray(); + + Array.Reverse(data, 0, 8); + Array.Reverse(data, 0, 2); + Array.Reverse(data, 2, 2); + Array.Reverse(data, 4, 4); + Array.Reverse(data, 8, 8); + + _msb = BitConverter.ToInt64(data, 0); + _lsb = BitConverter.ToInt64(data, 8); +#else Span data = stackalloc byte[16]; if (!value.TryWriteBytes(data)) { @@ -79,6 +91,7 @@ private Uuid(Guid value) { _msb = BitConverter.ToInt64(data); _lsb = BitConverter.ToInt64(data.Slice(8)); +#endif } private Uuid(string value) : this(value == null @@ -147,7 +160,21 @@ public Guid ToGuid() { if (!BitConverter.IsLittleEndian) { throw new NotSupportedException(); } - +#if NETFRAMEWORK + var msb = BitConverter.GetBytes(_msb); + Array.Reverse(msb, 0, 8); + Array.Reverse(msb, 0, 4); + Array.Reverse(msb, 4, 2); + Array.Reverse(msb, 6, 2); + + var lsb = BitConverter.GetBytes(_lsb); + Array.Reverse(lsb); + + var data = new byte[16]; + Array.Copy(msb, data, 8); + Array.Copy(lsb, 0, data, 8, 8); + return new Guid(data); +#else Span data = stackalloc byte[16]; if (!BitConverter.TryWriteBytes(data, _msb) || !BitConverter.TryWriteBytes(data.Slice(8), _lsb)) { @@ -161,6 +188,8 @@ public Guid ToGuid() { data.Slice(8).Reverse(); return new Guid(data); + +#endif } } } diff --git a/test/Directory.Build.props b/test/Directory.Build.props index 77b190f22..467d205d3 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -16,7 +16,7 @@ - + diff --git a/test/EventStore.Client.Streams.Tests/EventDataComparer.cs b/test/EventStore.Client.Streams.Tests/EventDataComparer.cs index 57cbe506d..ae811849c 100644 --- a/test/EventStore.Client.Streams.Tests/EventDataComparer.cs +++ b/test/EventStore.Client.Streams.Tests/EventDataComparer.cs @@ -9,11 +9,11 @@ public static bool Equal(EventData expected, EventRecord actual) { if (expected.Type != actual.EventType) return false; - var expectedDataString = Encoding.UTF8.GetString(expected.Data.Span); - var expectedMetadataString = Encoding.UTF8.GetString(expected.Metadata.Span); + var expectedDataString = Encoding.UTF8.GetString(expected.Data.ToArray()); + var expectedMetadataString = Encoding.UTF8.GetString(expected.Metadata.ToArray()); - var actualDataString = Encoding.UTF8.GetString(actual.Data.Span); - var actualMetadataDataString = Encoding.UTF8.GetString(actual.Metadata.Span); + var actualDataString = Encoding.UTF8.GetString(actual.Data.ToArray()); + var actualMetadataDataString = Encoding.UTF8.GetString(actual.Metadata.ToArray()); return expectedDataString == actualDataString && expectedMetadataString == actualMetadataDataString; } diff --git a/test/EventStore.Client.Streams.Tests/is_json.cs b/test/EventStore.Client.Streams.Tests/is_json.cs index 96a0af0fb..2ff8acd8c 100644 --- a/test/EventStore.Client.Streams.Tests/is_json.cs +++ b/test/EventStore.Client.Streams.Tests/is_json.cs @@ -47,8 +47,8 @@ public async Task is_preserved(bool isJson, string data, string metadata) { Assert.Equal(isJson ? Constants.Metadata.ContentTypes.ApplicationJson : Constants.Metadata.ContentTypes.ApplicationOctetStream, @event.Event.ContentType); - Assert.Equal(data, encoding.GetString(@event.Event.Data.Span)); - Assert.Equal(metadata, encoding.GetString(@event.Event.Metadata.Span)); + Assert.Equal(data, encoding.GetString(@event.Event.Data.ToArray())); + Assert.Equal(metadata, encoding.GetString(@event.Event.Metadata.ToArray())); } private string GetStreamName(bool isJson, string data, string metadata, diff --git a/test/EventStore.Client.Tests.Common/EventStoreClientFixtureBase.cs b/test/EventStore.Client.Tests.Common/EventStoreClientFixtureBase.cs index 24a2221c5..f9d92e51a 100644 --- a/test/EventStore.Client.Tests.Common/EventStoreClientFixtureBase.cs +++ b/test/EventStore.Client.Tests.Common/EventStoreClientFixtureBase.cs @@ -15,7 +15,6 @@ using Ductus.FluentDocker.Model.Builders; using Ductus.FluentDocker.Services; #if GRPC_CORE -using System.Security.Cryptography.X509Certificates; using Grpc.Core; #endif using Polly; @@ -42,8 +41,7 @@ public abstract class EventStoreClientFixtureBase : IAsyncLifetime { private static readonly Subject LogEventSubject = new Subject(); private static readonly string HostCertificatePath = - Path.GetFullPath(Path.Combine("..", "..", "..", "..", "certs"), Environment.CurrentDirectory); - + Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, "..", "..", "..", "..", "certs")); private readonly IList _disposables; protected EventStoreTestServer TestServer { get; } protected EventStoreClientSettings Settings { get; } @@ -172,11 +170,19 @@ protected class EventStoreTestServer : IAsyncDisposable { private static readonly string ContainerName = "es-client-dotnet-test"; public EventStoreTestServer(Uri address, IDictionary? envOverrides) { - _httpClient = new HttpClient(new SocketsHttpHandler { - SslOptions = { - RemoteCertificateValidationCallback = delegate { return true; } + _httpClient = new HttpClient( +#if NETFRAMEWORK + new HttpClientHandler { + ServerCertificateCustomValidationCallback = delegate { return true; } + } +#else + new SocketsHttpHandler { + SslOptions = { + RemoteCertificateValidationCallback = delegate { return true; } + } } - }) { +#endif + ) { BaseAddress = address, }; var tag = Environment.GetEnvironmentVariable("ES_DOCKER_TAG") ?? "ci"; From 9672b43696e571a121d783750eaeb7835da9e6fe Mon Sep 17 00:00:00 2001 From: thefringeninja <495495+thefringeninja@users.noreply.github.com> Date: Wed, 6 Jan 2021 22:28:35 +0100 Subject: [PATCH 06/21] remove/obsolete createHttpMessageHandler --- ...ationsClientServiceCollectionExtensions.cs | 30 ++++++++++++++ ...SubscriptionsClientCollectionExtensions.cs | 39 +++++++++++++++++-- ...ionManagementClientCollectionExtensions.cs | 33 ++++++++++++++++ ...tStoreClientServiceCollectionExtensions.cs | 31 ++++++++++++++- ...serManagementClientCollectionExtensions.cs | 34 ++++++++++++++++ .../EventStoreClientSettings.cs | 5 +++ .../ConnectionStringTests.cs | 15 +++++++ 7 files changed, 182 insertions(+), 5 deletions(-) diff --git a/src/EventStore.Client.Operations/EventStoreOperationsClientServiceCollectionExtensions.cs b/src/EventStore.Client.Operations/EventStoreOperationsClientServiceCollectionExtensions.cs index f2563da51..2ebc3a7d9 100644 --- a/src/EventStore.Client.Operations/EventStoreOperationsClientServiceCollectionExtensions.cs +++ b/src/EventStore.Client.Operations/EventStoreOperationsClientServiceCollectionExtensions.cs @@ -15,6 +15,19 @@ namespace Microsoft.Extensions.DependencyInjection { /// A set of extension methods for which provide support for an . /// public static class EventStoreOperationsClientServiceCollectionExtensions { +#if GRPC_CORE + /// + /// Adds an to the . + /// + /// + /// + /// + /// + public static IServiceCollection AddEventStoreOperationsClient(this IServiceCollection services, Uri address) + => services.AddEventStoreOperationsClient(options => { + options.ConnectivitySettings.Address = address; + }); +#else /// /// Adds an to the . /// @@ -29,7 +42,24 @@ public static IServiceCollection AddEventStoreOperationsClient(this IServiceColl options.ConnectivitySettings.Address = address; options.CreateHttpMessageHandler = createHttpMessageHandler; }); +#endif +#if NETCOREAPP3_1 + /// + /// Adds an to the . + /// + /// + /// + /// + /// + /// + public static IServiceCollection AddEventStoreOperationsClient(this IServiceCollection services, Uri address, + // ReSharper disable once MethodOverloadWithOptionalParameter + Func? createHttpMessageHandler = null) + => services.AddEventStoreOperationsClient(options => { + options.ConnectivitySettings.Address = address; + }); +#endif /// /// Adds an to the . /// diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClientCollectionExtensions.cs b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClientCollectionExtensions.cs index a9164d660..00c742be5 100644 --- a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClientCollectionExtensions.cs +++ b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClientCollectionExtensions.cs @@ -13,6 +13,20 @@ namespace Microsoft.Extensions.DependencyInjection { /// A set of extension methods for which provide support for an . /// public static class EventStorePersistentSubscriptionsClientCollectionExtensions { +#if GRPC_CORE + /// + /// Adds an to the . + /// + /// + /// + /// + /// + public static IServiceCollection AddEventStorePersistentSubscriptionsClient(this IServiceCollection services, + Uri address) + => services.AddEventStorePersistentSubscriptionsClient(options => { + options.ConnectivitySettings.Address = address; + }); +#else /// /// Adds an to the . /// @@ -22,12 +36,31 @@ public static class EventStorePersistentSubscriptionsClientCollectionExtensions /// /// public static IServiceCollection AddEventStorePersistentSubscriptionsClient(this IServiceCollection services, - Uri address, - Func? createHttpMessageHandler = null) + Uri address, Func? createHttpMessageHandler = null) => services.AddEventStorePersistentSubscriptionsClient(options => { options.ConnectivitySettings.Address = address; options.CreateHttpMessageHandler = createHttpMessageHandler; }); +#endif + +#if NETCOREAPP3_1 + /// + /// Adds an to the . + /// + /// + /// + /// + /// + /// + [Obsolete] + public static IServiceCollection AddEventStorePersistentSubscriptionsClient(this IServiceCollection services, + // ReSharper disable once MethodOverloadWithOptionalParameter + Uri address, Func? createHttpMessageHandler = null) + => services.AddEventStorePersistentSubscriptionsClient(options => { + options.ConnectivitySettings.Address = address; + options.CreateHttpMessageHandler = createHttpMessageHandler; + }); +#endif /// /// Adds an to the . @@ -61,14 +94,12 @@ private static IServiceCollection AddEventStorePersistentSubscriptionsClient(thi } configureSettings?.Invoke(settings); - services.TryAddSingleton(provider => { settings.LoggerFactory ??= provider.GetService(); settings.Interceptors ??= provider.GetServices(); return new EventStorePersistentSubscriptionsClient(settings); }); - return services; } } diff --git a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClientCollectionExtensions.cs b/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClientCollectionExtensions.cs index 19ca8f948..4d9eaa1f1 100644 --- a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClientCollectionExtensions.cs +++ b/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClientCollectionExtensions.cs @@ -13,6 +13,20 @@ namespace Microsoft.Extensions.DependencyInjection { /// A set of extension methods for which provide support for an . /// public static class EventStoreProjectionManagementClientCollectionExtensions { +#if GRPC_CORE + /// + /// Adds an to the . + /// + /// + /// + /// + /// + public static IServiceCollection AddEventStoreProjectionManagementClient(this IServiceCollection services, + Uri address) + => services.AddEventStoreProjectionManagementClient(options => { + options.ConnectivitySettings.Address = address; + }); +#else /// /// Adds an to the . /// @@ -28,7 +42,26 @@ public static IServiceCollection AddEventStoreProjectionManagementClient(this IS options.ConnectivitySettings.Address = address; options.CreateHttpMessageHandler = createHttpMessageHandler; }); +#endif +#if NETCOREAPP3_1 + /// + /// Adds an to the . + /// + /// + /// + /// + /// + /// + [Obsolete] + public static IServiceCollection AddEventStoreProjectionManagementClient(this IServiceCollection services, + // ReSharper disable once MethodOverloadWithOptionalParameter + Uri address, Func? createHttpMessageHandler = null) + => services.AddEventStoreProjectionManagementClient(options => { + options.ConnectivitySettings.Address = address; + options.CreateHttpMessageHandler = createHttpMessageHandler; + }); +#endif /// /// Adds an to the . /// diff --git a/src/EventStore.Client.Streams/EventStoreClientServiceCollectionExtensions.cs b/src/EventStore.Client.Streams/EventStoreClientServiceCollectionExtensions.cs index 6be63701b..dda74c58e 100644 --- a/src/EventStore.Client.Streams/EventStoreClientServiceCollectionExtensions.cs +++ b/src/EventStore.Client.Streams/EventStoreClientServiceCollectionExtensions.cs @@ -13,6 +13,19 @@ namespace Microsoft.Extensions.DependencyInjection { /// A set of extension methods for which provide support for an . /// public static class EventStoreClientServiceCollectionExtensions { +#if GRPC_CORE + /// + /// Adds an to the . + /// + /// + /// + /// + /// + public static IServiceCollection AddEventStoreClient(this IServiceCollection services, Uri address) + => services.AddEventStoreClient(options => { + options.ConnectivitySettings.Address = address; + }); +#else /// /// Adds an to the . /// @@ -27,8 +40,24 @@ public static IServiceCollection AddEventStoreClient(this IServiceCollection ser options.ConnectivitySettings.Address = address; options.CreateHttpMessageHandler = createHttpMessageHandler; }); +#endif - +#if NETCOREAPP3_1 + /// + /// Adds an to the . + /// + /// + /// + /// + /// + /// + public static IServiceCollection AddEventStoreClient(this IServiceCollection services, Uri address, + // ReSharper disable once MethodOverloadWithOptionalParameter + Func? createHttpMessageHandler = null) + => services.AddEventStoreClient(options => { + options.ConnectivitySettings.Address = address; + }); +#endif /// /// Adds an to the . /// diff --git a/src/EventStore.Client.UserManagement/EventStoreUserManagementClientCollectionExtensions.cs b/src/EventStore.Client.UserManagement/EventStoreUserManagementClientCollectionExtensions.cs index f9da623b0..32fdfeabe 100644 --- a/src/EventStore.Client.UserManagement/EventStoreUserManagementClientCollectionExtensions.cs +++ b/src/EventStore.Client.UserManagement/EventStoreUserManagementClientCollectionExtensions.cs @@ -13,6 +13,20 @@ namespace Microsoft.Extensions.DependencyInjection { /// A set of extension methods for which provide support for an . /// public static class EventStoreUserManagementClientCollectionExtensions { +#if GRPC_CORE + /// + /// Adds an to the . + /// + /// + /// + /// + /// + public static IServiceCollection AddEventStoreUserManagementClient(this IServiceCollection services, + Uri address) + => services.AddEventStoreUserManagementClient(options => { + options.ConnectivitySettings.Address = address; + }); +#else /// /// Adds an to the . /// @@ -27,6 +41,26 @@ public static IServiceCollection AddEventStoreUserManagementClient(this IService options.ConnectivitySettings.Address = address; options.CreateHttpMessageHandler = createHttpMessageHandler; }); +#endif + +#if NETCOREAPP3_1 + /// + /// Adds an to the . + /// + /// + /// + /// + /// + /// + [Obsolete] + public static IServiceCollection AddEventStoreUserManagementClient(this IServiceCollection services, + // ReSharper disable once MethodOverloadWithOptionalParameter + Uri address, Func? createHttpMessageHandler = null) + => services.AddEventStoreUserManagementClient(options => { + options.ConnectivitySettings.Address = address; + }); + +#endif /// /// Adds an to the . diff --git a/src/EventStore.Client/EventStoreClientSettings.cs b/src/EventStore.Client/EventStoreClientSettings.cs index 08016b52d..a6df7dba5 100644 --- a/src/EventStore.Client/EventStoreClientSettings.cs +++ b/src/EventStore.Client/EventStoreClientSettings.cs @@ -22,11 +22,16 @@ public partial class EventStoreClientSettings { /// public string? ConnectionName { get; set; } +#if !NETFRAMEWORK /// /// An optional factory. /// +#if NETCOREAPP3_1 + [Obsolete] +#endif public Func? CreateHttpMessageHandler { get; set; } +#endif /// /// An optional to use. /// diff --git a/test/EventStore.Client.Tests/ConnectionStringTests.cs b/test/EventStore.Client.Tests/ConnectionStringTests.cs index 5b3c9c6f1..bc4a1d15b 100644 --- a/test/EventStore.Client.Tests/ConnectionStringTests.cs +++ b/test/EventStore.Client.Tests/ConnectionStringTests.cs @@ -121,8 +121,12 @@ public void with_valid_single_node_connection_string() { Assert.Equal(37, settings.ConnectivitySettings.DiscoveryInterval.TotalMilliseconds); Assert.Equal(33, settings.ConnectivitySettings.GossipTimeout.TotalMilliseconds); Assert.Equal(NodePreference.Follower, settings.ConnectivitySettings.NodePreference); +#if !GRPC_CORE +#if !GRPC_CORE Assert.NotNull(settings.CreateHttpMessageHandler); +#endif +#endif settings = EventStoreClientSettings.Create( "esdb://127.0.0.1?connectionName=test&maxDiscoverAttempts=13&DiscoveryInterval=37&nOdEPrEfErence=FoLLoWer&tls=true&tlsVerifyCert=true&operationTimeout=330&throwOnAppendFailure=faLse"); Assert.Null(settings.DefaultCredentials); @@ -135,7 +139,10 @@ public void with_valid_single_node_connection_string() { Assert.Equal(13, settings.ConnectivitySettings.MaxDiscoverAttempts); Assert.Equal(37, settings.ConnectivitySettings.DiscoveryInterval.TotalMilliseconds); Assert.Equal(NodePreference.Follower, settings.ConnectivitySettings.NodePreference); +#if !GRPC_CORE Assert.Null(settings.CreateHttpMessageHandler); +#endif + Assert.Equal(330, settings.OperationOptions.TimeoutAfter.Value.TotalMilliseconds); Assert.False(settings.OperationOptions.ThrowOnAppendFailure); @@ -146,7 +153,9 @@ public void with_valid_single_node_connection_string() { Assert.Null(settings.ConnectivitySettings.IpGossipSeeds); Assert.Null(settings.ConnectivitySettings.DnsGossipSeeds); Assert.True(settings.ConnectivitySettings.GossipOverHttps); +#if !GRPC_CORE Assert.Null(settings.CreateHttpMessageHandler); +#endif } [Fact] @@ -198,7 +207,9 @@ public void with_valid_cluster_connection_string() { Assert.Equal(13, settings.ConnectivitySettings.MaxDiscoverAttempts); Assert.Equal(37, settings.ConnectivitySettings.DiscoveryInterval.TotalMilliseconds); Assert.Equal(NodePreference.Follower, settings.ConnectivitySettings.NodePreference); +#if !GRPC_CORE Assert.NotNull(settings.CreateHttpMessageHandler); +#endif settings = EventStoreClientSettings.Create( @@ -219,7 +230,9 @@ public void with_valid_cluster_connection_string() { Assert.Equal(13, settings.ConnectivitySettings.MaxDiscoverAttempts); Assert.Equal(37, settings.ConnectivitySettings.DiscoveryInterval.TotalMilliseconds); Assert.Equal(NodePreference.Follower, settings.ConnectivitySettings.NodePreference); +#if !GRPC_CORE Assert.NotNull(settings.CreateHttpMessageHandler); +#endif } [Fact] @@ -245,6 +258,7 @@ public void with_different_tls_settings() { Assert.False(settings.ConnectivitySettings.GossipOverHttps); } +#if !GRPC_CORE [Fact] public void with_different_tls_verify_cert_settings() { EventStoreClientSettings settings; @@ -268,6 +282,7 @@ public void with_different_tls_verify_cert_settings() { "esdb://127.0.0.1,127.0.0.2:3321,127.0.0.3/?tlsVerifyCert=false"); Assert.NotNull(settings.CreateHttpMessageHandler); } +#endif public static IEnumerable DiscoverSchemeCases() { yield return new object[] { From 3ad6b3c75273b0bec44278aa4c53b2f3629edc5a Mon Sep 17 00:00:00 2001 From: thefringeninja <495495+thefringeninja@users.noreply.github.com> Date: Thu, 7 Jan 2021 07:10:38 +0100 Subject: [PATCH 07/21] add reasonable timeouts to gh action jobs --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 51b0362ff..02d363ee6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,7 @@ on: jobs: vulnerability-scan: + timeout-minutes: 5 strategy: fail-fast: false matrix: @@ -26,6 +27,7 @@ jobs: dotnet restore dotnet tool run dotnet-retire build-dotnet: + timeout-minutes: 20 strategy: fail-fast: false matrix: @@ -93,6 +95,7 @@ jobs: name: test-results-${{ matrix.configuration }}-EventStore.Client${{ matrix.test }} path: test-results publish: + timeout-minutes: 5 needs: [vulnerability-scan, build-dotnet] runs-on: ubuntu-latest name: publish From a2d45b8a61fb3bf841f5f694b36a1dcc97a4ebba Mon Sep 17 00:00:00 2001 From: thefringeninja <495495+thefringeninja@users.noreply.github.com> Date: Thu, 7 Jan 2021 07:23:49 +0100 Subject: [PATCH 08/21] await ack/nack in tests to avoid 'Only one write can be pending at a time' --- .../happy_case_catching_up_to_link_to_events_manual_ack.cs | 6 ++---- .../happy_case_catching_up_to_normal_events_manual_ack.cs | 6 ++---- ...e_writing_and_subscribing_to_normal_events_manual_ack.cs | 6 ++---- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/happy_case_catching_up_to_link_to_events_manual_ack.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/happy_case_catching_up_to_link_to_events_manual_ack.cs index fd2017b24..33a4dcbd6 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/happy_case_catching_up_to_link_to_events_manual_ack.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/happy_case_catching_up_to_link_to_events_manual_ack.cs @@ -49,14 +49,12 @@ await Client.CreateAsync(Stream, Group, new PersistentSubscriptionSettings(startFrom: StreamPosition.Start, resolveLinkTos: true), TestCredentials.Root); _subscription = await Client.SubscribeAsync(Stream, Group, - (subscription, e, retryCount, ct) => { - subscription.Ack(e); + async (subscription, e, retryCount, ct) => { + await subscription.Ack(e); if (Interlocked.Increment(ref _eventReceivedCount) == _events.Length) { _eventsReceived.TrySetResult(true); } - - return Task.CompletedTask; }, (s, r, e) => { if (e != null) { _eventsReceived.TrySetException(e); diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/happy_case_catching_up_to_normal_events_manual_ack.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/happy_case_catching_up_to_normal_events_manual_ack.cs index 81e81be68..f09f2147a 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/happy_case_catching_up_to_normal_events_manual_ack.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/happy_case_catching_up_to_normal_events_manual_ack.cs @@ -44,14 +44,12 @@ await Client.CreateAsync(Stream, Group, new PersistentSubscriptionSettings(startFrom: StreamPosition.Start, resolveLinkTos: true), TestCredentials.Root); _subscription = await Client.SubscribeAsync(Stream, Group, - (subscription, e, retryCount, ct) => { - subscription.Ack(e); + async(subscription, e, retryCount, ct) => { + await subscription.Ack(e); if (Interlocked.Increment(ref _eventReceivedCount) == _events.Length) { _eventsReceived.TrySetResult(true); } - - return Task.CompletedTask; }, (s, r, e) => { if (e != null) { _eventsReceived.TrySetException(e); diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/happy_case_writing_and_subscribing_to_normal_events_manual_ack.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/happy_case_writing_and_subscribing_to_normal_events_manual_ack.cs index a25b62d0d..ddbcfdbd3 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/happy_case_writing_and_subscribing_to_normal_events_manual_ack.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/happy_case_writing_and_subscribing_to_normal_events_manual_ack.cs @@ -40,13 +40,11 @@ await Client.CreateAsync(Stream, Group, new PersistentSubscriptionSettings(startFrom: StreamPosition.End, resolveLinkTos: true), TestCredentials.Root); _subscription = await Client.SubscribeAsync(Stream, Group, - (subscription, e, retryCount, ct) => { - subscription.Ack(e); + async (subscription, e, retryCount, ct) => { + await subscription.Ack(e); if (Interlocked.Increment(ref _eventReceivedCount) == _events.Length) { _eventsReceived.TrySetResult(true); } - - return Task.CompletedTask; }, (s, r, e) => { if (e != null) { _eventsReceived.TrySetException(e); From 567f80dafe1bc666c7d3937277980d14dba32fd2 Mon Sep 17 00:00:00 2001 From: thefringeninja <495495+thefringeninja@users.noreply.github.com> Date: Thu, 7 Jan 2021 07:41:11 +0100 Subject: [PATCH 09/21] grpc.core may use unavailable instead of deadline exceeded --- .../Interceptors/TypedExceptionInterceptor.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/EventStore.Client/Interceptors/TypedExceptionInterceptor.cs b/src/EventStore.Client/Interceptors/TypedExceptionInterceptor.cs index f4bc0dc2b..48c599b86 100644 --- a/src/EventStore.Client/Interceptors/TypedExceptionInterceptor.cs +++ b/src/EventStore.Client/Interceptors/TypedExceptionInterceptor.cs @@ -99,9 +99,11 @@ private static Exception ConvertRpcException(RpcException ex, return (ex.Trailers.TryGetValue(Constants.Exceptions.ExceptionKey, out var key) && exceptionMap.TryGetValue(key!, out factory)) switch { true => factory!.Invoke(ex), - false => ex.StatusCode switch { - StatusCode.DeadlineExceeded => ex, - StatusCode.Unauthenticated => new NotAuthenticatedException(ex.Message, ex), + false => (ex.StatusCode, ex.Status.Detail) switch { + (StatusCode.Unavailable, "Deadline Exceeded") => new RpcException(new Status( + StatusCode.DeadlineExceeded, ex.Status.Detail, ex.Status.DebugException)), + (StatusCode.DeadlineExceeded, _) => ex, + (StatusCode.Unauthenticated, _) => new NotAuthenticatedException(ex.Message, ex), _ => new InvalidOperationException(ex.Message, ex) } }; From 3489103b965cf4ecbe93c7d3dc54e8ec5cfbb9fb Mon Sep 17 00:00:00 2001 From: thefringeninja <495495+thefringeninja@users.noreply.github.com> Date: Thu, 7 Jan 2021 08:07:30 +0100 Subject: [PATCH 10/21] include framework in test results file names --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 02d363ee6..67652fc15 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,12 +87,12 @@ jobs: if: always() with: path: test-results.html - name: test-results-${{ matrix.configuration }}-EventStore.Client${{ matrix.test }}.html + name: test-results-${{ matrix.configuration }}-${{ matrix.framework }}-EventStore.Client${{ matrix.test }}.html - name: Publish Test Results (All) uses: actions/upload-artifact@v1 if: always() with: - name: test-results-${{ matrix.configuration }}-EventStore.Client${{ matrix.test }} + name: test-results-${{ matrix.configuration }}-${{ matrix.framework }}-EventStore.Client${{ matrix.test }} path: test-results publish: timeout-minutes: 5 From 38774a3ea240d9434ab61c7a230ca84aa7f6535a Mon Sep 17 00:00:00 2001 From: thefringeninja <495495+thefringeninja@users.noreply.github.com> Date: Thu, 7 Jan 2021 08:52:08 +0100 Subject: [PATCH 11/21] skip installing frameworks for targets that don't need it --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 67652fc15..0daa1031c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: strategy: fail-fast: false matrix: - sdk: ['5.0-focal', '3.1-bionic'] + sdk: [5.0-focal, 3.1-bionic] runs-on: ubuntu-latest name: scan-vulnerabilities/${{ matrix.sdk }} container: mcr.microsoft.com/dotnet/sdk:${{ matrix.sdk }} @@ -56,10 +56,12 @@ jobs: docker pull docker.pkg.github.com/eventstore/eventstore/eventstore:${{ matrix.docker-tag }} - name: Install netcoreapp3.1 uses: actions/setup-dotnet@v1 + if: matrix.framework == 'netcoreapp3.1' with: dotnet-version: 3.1.x - name: Install net5.0 uses: actions/setup-dotnet@v1 + if: matrix.framework == 'net5.0' with: dotnet-version: 5.0.x - name: Compile From 8ea4c583f24bdeafa77d39b889528af759ff45cb Mon Sep 17 00:00:00 2001 From: thefringeninja <495495+thefringeninja@users.noreply.github.com> Date: Thu, 7 Jan 2021 09:12:29 +0100 Subject: [PATCH 12/21] implement DisposeAsync for better cleanup --- src/EventStore.Client/EventStoreClientBase.cs | 9 +++++---- src/EventStore.Client/MultiChannel.cs | 8 ++++---- .../EventStoreClientFixture.cs | 6 +++--- .../EventStoreClientFixture.cs | 10 +++++----- .../EventStoreClientFixture.cs | 10 +++++----- .../EventStoreClientFixture.cs | 6 +++--- .../EventStoreClientFixture.cs | 6 +++--- 7 files changed, 28 insertions(+), 27 deletions(-) diff --git a/src/EventStore.Client/EventStoreClientBase.cs b/src/EventStore.Client/EventStoreClientBase.cs index 1b9d8931d..0e52afeee 100644 --- a/src/EventStore.Client/EventStoreClientBase.cs +++ b/src/EventStore.Client/EventStoreClientBase.cs @@ -12,7 +12,7 @@ namespace EventStore.Client { /// /// The base class used by clients used to communicate with the EventStoreDB. /// - public abstract class EventStoreClientBase : IDisposable { + public abstract class EventStoreClientBase : IDisposable, IAsyncDisposable { private readonly IDictionary> _exceptionMap; private readonly MultiChannel _channels; /// @@ -54,8 +54,9 @@ protected async Task SelectCallInvoker(CancellationToken cancellati (invoker, interceptor) => invoker.Intercept(interceptor)); /// - public void Dispose() { - _channels.Dispose(); - } + public void Dispose() => _channels.Dispose(); + + /// + public ValueTask DisposeAsync() => _channels.DisposeAsync(); } } diff --git a/src/EventStore.Client/MultiChannel.cs b/src/EventStore.Client/MultiChannel.cs index 2125850b8..c3881c0a4 100644 --- a/src/EventStore.Client/MultiChannel.cs +++ b/src/EventStore.Client/MultiChannel.cs @@ -7,7 +7,7 @@ #nullable enable namespace EventStore.Client { - internal class MultiChannel : IDisposable { + internal class MultiChannel : IDisposable, IAsyncDisposable { private readonly EventStoreClientSettings _settings; private readonly IEndpointDiscoverer _endpointDiscoverer; private readonly ConcurrentDictionary _channels; @@ -35,9 +35,9 @@ public async Task GetCurrentChannel(CancellationToken cancellationT return _channels.GetOrAdd(current, ChannelFactory.CreateChannel(_settings, current)); } + public void Dispose() => DisposeAsync().GetAwaiter().GetResult(); - - public void Dispose() { + public async ValueTask DisposeAsync() { if (Interlocked.Exchange(ref _disposed, 1) == 1) { return; } @@ -46,7 +46,7 @@ public void Dispose() { if (channel is IDisposable disposable) { disposable.Dispose(); } else { - channel.ShutdownAsync(); + await channel.ShutdownAsync().ConfigureAwait(false); } } } diff --git a/test/EventStore.Client.Operations.Tests/EventStoreClientFixture.cs b/test/EventStore.Client.Operations.Tests/EventStoreClientFixture.cs index e485d25c3..a879aa354 100644 --- a/test/EventStore.Client.Operations.Tests/EventStoreClientFixture.cs +++ b/test/EventStore.Client.Operations.Tests/EventStoreClientFixture.cs @@ -9,9 +9,9 @@ protected EventStoreClientFixture(EventStoreClientSettings? settings = null) : b Client = new EventStoreOperationsClient(Settings); } - public override Task DisposeAsync() { - Client?.Dispose(); - return base.DisposeAsync(); + public override async Task DisposeAsync() { + await Client.DisposeAsync(); + await base.DisposeAsync(); } } } diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/EventStoreClientFixture.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/EventStoreClientFixture.cs index c2c3cb341..b0f1f37c1 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/EventStoreClientFixture.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/EventStoreClientFixture.cs @@ -23,11 +23,11 @@ await UserManagementClient.CreateUserWithRetry(TestCredentials.TestUser1.Usernam await When().WithTimeout(TimeSpan.FromMinutes(5)); } - public override Task DisposeAsync() { - UserManagementClient?.Dispose(); - StreamsClient?.Dispose(); - Client?.Dispose(); - return base.DisposeAsync(); + public override async Task DisposeAsync() { + await UserManagementClient.DisposeAsync(); + await StreamsClient.DisposeAsync(); + await Client.DisposeAsync(); + await base.DisposeAsync(); } } } diff --git a/test/EventStore.Client.ProjectionManagement.Tests/EventStoreClientFixture.cs b/test/EventStore.Client.ProjectionManagement.Tests/EventStoreClientFixture.cs index 99836a66c..94db67ead 100644 --- a/test/EventStore.Client.ProjectionManagement.Tests/EventStoreClientFixture.cs +++ b/test/EventStore.Client.ProjectionManagement.Tests/EventStoreClientFixture.cs @@ -38,11 +38,11 @@ await Task.WhenAll(StandardProjections.Names.Select(name => await When().WithTimeout(TimeSpan.FromMinutes(5)); } - public override Task DisposeAsync() { - StreamsClient?.Dispose(); - UserManagementClient?.Dispose(); - Client?.Dispose(); - return base.DisposeAsync(); + public override async Task DisposeAsync() { + await StreamsClient.DisposeAsync(); + await UserManagementClient.DisposeAsync(); + await Client.DisposeAsync(); + await base.DisposeAsync(); } } } diff --git a/test/EventStore.Client.Streams.Tests/EventStoreClientFixture.cs b/test/EventStore.Client.Streams.Tests/EventStoreClientFixture.cs index 592c5ed45..a9143a64f 100644 --- a/test/EventStore.Client.Streams.Tests/EventStoreClientFixture.cs +++ b/test/EventStore.Client.Streams.Tests/EventStoreClientFixture.cs @@ -10,9 +10,9 @@ protected EventStoreClientFixture(EventStoreClientSettings? settings = null, Client = new EventStoreClient(Settings); } - public override Task DisposeAsync() { - Client?.Dispose(); - return base.DisposeAsync(); + public override async Task DisposeAsync() { + await Client.DisposeAsync(); + await base.DisposeAsync(); } } } diff --git a/test/EventStore.Client.UserManagement.Tests/EventStoreClientFixture.cs b/test/EventStore.Client.UserManagement.Tests/EventStoreClientFixture.cs index 7734587e0..2d08f2a6c 100644 --- a/test/EventStore.Client.UserManagement.Tests/EventStoreClientFixture.cs +++ b/test/EventStore.Client.UserManagement.Tests/EventStoreClientFixture.cs @@ -8,9 +8,9 @@ protected EventStoreClientFixture(EventStoreClientSettings? settings = null) : b Client = new EventStoreUserManagementClient(Settings); } - public override Task DisposeAsync() { - Client?.Dispose(); - return base.DisposeAsync(); + public override async Task DisposeAsync() { + await Client.DisposeAsync(); + await base.DisposeAsync(); } } } From a19d48ed37d75d695a6bc569f995a71c8170a59d Mon Sep 17 00:00:00 2001 From: thefringeninja <495495+thefringeninja@users.noreply.github.com> Date: Fri, 8 Jan 2021 08:49:44 +0100 Subject: [PATCH 13/21] support keep alives --- src/EventStore.Client/ChannelFactory.cs | 33 +++++++++-- .../EventStoreClientConnectivitySettings.cs | 4 ++ ...entStoreClientSettings.ConnectionString.cs | 57 +++++++++++-------- .../ConnectionStringTests.cs | 45 ++++++++------- 4 files changed, 90 insertions(+), 49 deletions(-) diff --git a/src/EventStore.Client/ChannelFactory.cs b/src/EventStore.Client/ChannelFactory.cs index ee54e9eb2..bca134087 100644 --- a/src/EventStore.Client/ChannelFactory.cs +++ b/src/EventStore.Client/ChannelFactory.cs @@ -1,11 +1,15 @@ using System; using System.Net; using Grpc.Core; + #if !GRPC_CORE using System.Net.Http; using System.Threading; using Grpc.Net.Client; +#else +using System.Collections.Generic; #endif + #nullable enable namespace EventStore.Client { internal static class ChannelFactory { @@ -16,13 +20,13 @@ public static ChannelBase CreateChannel(EventStoreClientSettings settings, Uri? address ??= settings.ConnectivitySettings.Address; #if !GRPC_CORE - if (address.Scheme == Uri.UriSchemeHttp || - !settings.ConnectivitySettings.GossipOverHttps) { + if (address.Scheme == Uri.UriSchemeHttp ||!settings.ConnectivitySettings.GossipOverHttps) { //this must be switched on before creation of the HttpMessageHandler AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); } + return GrpcChannel.ForAddress(address, new GrpcChannelOptions { - HttpClient = new HttpClient(settings.CreateHttpMessageHandler?.Invoke() ?? new SocketsHttpHandler(), + HttpClient = new HttpClient(CreateHandler(), true) { Timeout = Timeout.InfiniteTimeSpan, DefaultRequestVersion = new Version(2, 0), @@ -31,8 +35,29 @@ public static ChannelBase CreateChannel(EventStoreClientSettings settings, Uri? Credentials = settings.ChannelCredentials, DisposeHttpClient = true }); + + HttpMessageHandler CreateHandler() { + if (settings.CreateHttpMessageHandler != null) { + return settings.CreateHttpMessageHandler.Invoke(); + } + + var handler = new SocketsHttpHandler(); + if (settings.ConnectivitySettings.KeepAlive.HasValue) { + handler.KeepAlivePingDelay = settings.ConnectivitySettings.KeepAlive.Value; + } + + return handler; + } #else - return new Channel(address.Host, address.Port, settings.ChannelCredentials ?? ChannelCredentials.Insecure); + return new Channel(address.Host, address.Port, settings.ChannelCredentials ?? ChannelCredentials.Insecure, + GetChannelOptions()); + + IEnumerable GetChannelOptions() { + if (settings.ConnectivitySettings.KeepAlive.HasValue) { + yield return new ChannelOption("grpc.keepalive_time_ms", + (int)settings.ConnectivitySettings.KeepAlive.Value.TotalMilliseconds); + } + } #endif } } diff --git a/src/EventStore.Client/EventStoreClientConnectivitySettings.cs b/src/EventStore.Client/EventStoreClientConnectivitySettings.cs index 9c81717a9..4f7150d05 100644 --- a/src/EventStore.Client/EventStoreClientConnectivitySettings.cs +++ b/src/EventStore.Client/EventStoreClientConnectivitySettings.cs @@ -60,6 +60,10 @@ public class EventStoreClientConnectivitySettings { /// public NodePreference NodePreference { get; set; } + /// + /// The optional amount of time to wait after which a keepalive ping is sent on the transport. + /// + public TimeSpan? KeepAlive { get; set; } /// /// True if pointing to a single EventStoreDB node. diff --git a/src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs b/src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs index 7fca9c29a..9c5f51a4b 100644 --- a/src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs +++ b/src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs @@ -13,9 +13,8 @@ public partial class EventStoreClientSettings { /// /// /// - public static EventStoreClientSettings Create(string connectionString) { - return ConnectionStringParser.Parse(connectionString); - } + public static EventStoreClientSettings Create(string connectionString) => + ConnectionStringParser.Parse(connectionString); private static class ConnectionStringParser { private const string SchemeSeparator = "://"; @@ -28,14 +27,15 @@ private static class ConnectionStringParser { private const string QuestionMark = "?"; private const string Tls = nameof(Tls); - private const string ConnectionName = "ConnectionName"; - private const string MaxDiscoverAttempts = "MaxDiscoverAttempts"; - private const string DiscoveryInterval = "DiscoveryInterval"; - private const string GossipTimeout = "GossipTimeout"; - private const string NodePreference = "NodePreference"; - private const string TlsVerifyCert = "TlsVerifyCert"; - private const string OperationTimeout = "OperationTimeout"; - private const string ThrowOnAppendFailure = "ThrowOnAppendFailure"; + private const string ConnectionName = nameof(ConnectionName); + private const string MaxDiscoverAttempts = nameof(MaxDiscoverAttempts); + private const string DiscoveryInterval = nameof(DiscoveryInterval); + private const string GossipTimeout = nameof(GossipTimeout); + private const string NodePreference = nameof(NodePreference); + private const string TlsVerifyCert = nameof(TlsVerifyCert); + private const string OperationTimeout = nameof(OperationTimeout); + private const string ThrowOnAppendFailure = nameof(ThrowOnAppendFailure); + private const string KeepAlive = nameof(KeepAlive); private const string UriSchemeDiscover = "esdb+discover"; @@ -53,7 +53,8 @@ private static class ConnectionStringParser { {Tls, typeof(bool)}, {TlsVerifyCert, typeof(bool)}, {OperationTimeout, typeof(int)}, - {ThrowOnAppendFailure, typeof(bool)} + {ThrowOnAppendFailure, typeof(bool)}, + {KeepAlive, typeof(int)} }; public static EventStoreClientSettings Parse(string connectionString) { @@ -159,24 +160,16 @@ private static EventStoreClientSettings CreateSettings(string scheme, (string us useTls = (bool)tls; } - if (typedOptions.TryGetValue(TlsVerifyCert, out object tlsVerifyCert)) { - if (!(bool)tlsVerifyCert) { -#if !GRPC_CORE - settings.CreateHttpMessageHandler = () => new SocketsHttpHandler { - SslOptions = { - RemoteCertificateValidationCallback = delegate { return true; } - } - }; -#endif - } - } - if (typedOptions.TryGetValue(OperationTimeout, out object operationTimeout)) settings.OperationOptions.TimeoutAfter = TimeSpan.FromMilliseconds((int)operationTimeout); if (typedOptions.TryGetValue(ThrowOnAppendFailure, out object throwOnAppendFailure)) settings.OperationOptions.ThrowOnAppendFailure = (bool)throwOnAppendFailure; + if (typedOptions.TryGetValue(KeepAlive, out var keepAliveMs)) { + settings.ConnectivitySettings.KeepAlive = TimeSpan.FromMilliseconds((int)keepAliveMs); + } + if (hosts.Length == 1 && scheme != UriSchemeDiscover) { connSettings.Address = hosts[0].ToUri(useTls); } else { @@ -189,6 +182,22 @@ private static EventStoreClientSettings CreateSettings(string scheme, (string us connSettings.GossipOverHttps = useTls; } +#if !GRPC_CORE + settings.CreateHttpMessageHandler = () => { + var handler = new SocketsHttpHandler(); + + if (typedOptions.TryGetValue(TlsVerifyCert, out var tlsVerifyCert) && !(bool)tlsVerifyCert) { + handler.SslOptions.RemoteCertificateValidationCallback = delegate { return true; }; + } + + if (settings.ConnectivitySettings.KeepAlive.HasValue) { + handler.KeepAlivePingDelay = settings.ConnectivitySettings.KeepAlive.Value; + } + + return handler; + }; +#endif + return settings; } diff --git a/test/EventStore.Client.Tests/ConnectionStringTests.cs b/test/EventStore.Client.Tests/ConnectionStringTests.cs index bc4a1d15b..e4343b0db 100644 --- a/test/EventStore.Client.Tests/ConnectionStringTests.cs +++ b/test/EventStore.Client.Tests/ConnectionStringTests.cs @@ -89,7 +89,8 @@ public void connection_string_with_duplicate_key_should_throw(string connectionS InlineData("esdb://user:pass@127.0.0.1/?discoveryInterval=abcd"), InlineData("esdb://user:pass@127.0.0.1/?gossipTimeout=defg"), InlineData("esdb://user:pass@127.0.0.1/?tlsVerifyCert=truee"), - InlineData("esdb://user:pass@127.0.0.1/?nodePreference=blabla")] + InlineData("esdb://user:pass@127.0.0.1/?nodePreference=blabla"), + InlineData("esdb://user:pass@127.0.0.1/?keepAlive=blabla")] public void connection_string_with_invalid_settings_should_throw(string connectionString) { Assert.Throws(() => EventStoreClientSettings.Create(connectionString)); } @@ -107,10 +108,8 @@ public void with_different_node_preferences(string nodePreference, NodePreferenc [Fact] public void with_valid_single_node_connection_string() { - EventStoreClientSettings settings; - - settings = EventStoreClientSettings.Create( - "esdb://user:pass@127.0.0.1/?maxDiscoverAttempts=13&DiscoveryInterval=37&gossipTimeout=33&nOdEPrEfErence=FoLLoWer&tlsVerifyCert=false"); + var settings = EventStoreClientSettings.Create( + "esdb://user:pass@127.0.0.1/?maxDiscoverAttempts=13&DiscoveryInterval=37&gossipTimeout=33&nOdEPrEfErence=FoLLoWer&tlsVerifyCert=false&keepAlive=10"); Assert.Equal("user", settings.DefaultCredentials.Username); Assert.Equal("pass", settings.DefaultCredentials.Password); Assert.Equal("https://127.0.0.1:2113/", settings.ConnectivitySettings.Address.ToString()); @@ -121,6 +120,7 @@ public void with_valid_single_node_connection_string() { Assert.Equal(37, settings.ConnectivitySettings.DiscoveryInterval.TotalMilliseconds); Assert.Equal(33, settings.ConnectivitySettings.GossipTimeout.TotalMilliseconds); Assert.Equal(NodePreference.Follower, settings.ConnectivitySettings.NodePreference); + Assert.Equal(TimeSpan.FromMilliseconds(10), settings.ConnectivitySettings.KeepAlive); #if !GRPC_CORE #if !GRPC_CORE Assert.NotNull(settings.CreateHttpMessageHandler); @@ -128,7 +128,7 @@ public void with_valid_single_node_connection_string() { #endif settings = EventStoreClientSettings.Create( - "esdb://127.0.0.1?connectionName=test&maxDiscoverAttempts=13&DiscoveryInterval=37&nOdEPrEfErence=FoLLoWer&tls=true&tlsVerifyCert=true&operationTimeout=330&throwOnAppendFailure=faLse"); + "esdb://127.0.0.1?connectionName=test&maxDiscoverAttempts=13&DiscoveryInterval=37&nOdEPrEfErence=FoLLoWer&tls=true&tlsVerifyCert=true&operationTimeout=330&throwOnAppendFailure=faLse&KEepAlive=10"); Assert.Null(settings.DefaultCredentials); Assert.Equal("test", settings.ConnectionName); Assert.Equal("https://127.0.0.1:2113/", settings.ConnectivitySettings.Address.ToString()); @@ -139,8 +139,9 @@ public void with_valid_single_node_connection_string() { Assert.Equal(13, settings.ConnectivitySettings.MaxDiscoverAttempts); Assert.Equal(37, settings.ConnectivitySettings.DiscoveryInterval.TotalMilliseconds); Assert.Equal(NodePreference.Follower, settings.ConnectivitySettings.NodePreference); + Assert.Equal(TimeSpan.FromMilliseconds(10), settings.ConnectivitySettings.KeepAlive); #if !GRPC_CORE - Assert.Null(settings.CreateHttpMessageHandler); + Assert.NotNull(settings.CreateHttpMessageHandler); #endif Assert.Equal(330, settings.OperationOptions.TimeoutAfter.Value.TotalMilliseconds); @@ -153,15 +154,15 @@ public void with_valid_single_node_connection_string() { Assert.Null(settings.ConnectivitySettings.IpGossipSeeds); Assert.Null(settings.ConnectivitySettings.DnsGossipSeeds); Assert.True(settings.ConnectivitySettings.GossipOverHttps); + Assert.Null(settings.ConnectivitySettings.KeepAlive); #if !GRPC_CORE - Assert.Null(settings.CreateHttpMessageHandler); + Assert.NotNull(settings.CreateHttpMessageHandler); #endif } [Fact] public void with_default_settings() { - EventStoreClientSettings settings; - settings = EventStoreClientSettings.Create("esdb://hostname:4321/"); + var settings = EventStoreClientSettings.Create("esdb://hostname:4321/"); Assert.Null(settings.ConnectionName); Assert.Equal(EventStoreClientConnectivitySettings.Default.Address.Scheme, @@ -179,18 +180,18 @@ public void with_default_settings() { settings.ConnectivitySettings.NodePreference); Assert.Equal(EventStoreClientConnectivitySettings.Default.GossipOverHttps, settings.ConnectivitySettings.GossipOverHttps); - Assert.Equal(EventStoreClientOperationOptions.Default.TimeoutAfter.Value.TotalMilliseconds, - settings.OperationOptions.TimeoutAfter.Value.TotalMilliseconds); + Assert.Equal(EventStoreClientOperationOptions.Default.TimeoutAfter!.Value.TotalMilliseconds, + settings.OperationOptions.TimeoutAfter!.Value.TotalMilliseconds); Assert.Equal(EventStoreClientOperationOptions.Default.ThrowOnAppendFailure, settings.OperationOptions.ThrowOnAppendFailure); + Assert.Equal(EventStoreClientConnectivitySettings.Default.KeepAlive, + settings.ConnectivitySettings.KeepAlive); } [Fact] public void with_valid_cluster_connection_string() { - EventStoreClientSettings settings; - - settings = EventStoreClientSettings.Create( - "esdb://user:pass@127.0.0.1,127.0.0.2:3321,127.0.0.3/?maxDiscoverAttempts=13&DiscoveryInterval=37&nOdEPrEfErence=FoLLoWer&tlsVerifyCert=false"); + var settings = EventStoreClientSettings.Create( + "esdb://user:pass@127.0.0.1,127.0.0.2:3321,127.0.0.3/?maxDiscoverAttempts=13&DiscoveryInterval=37&nOdEPrEfErence=FoLLoWer&tlsVerifyCert=false&KEepAlive=10"); Assert.Equal("user", settings.DefaultCredentials.Username); Assert.Equal("pass", settings.DefaultCredentials.Password); Assert.NotEmpty(settings.ConnectivitySettings.GossipSeeds); @@ -207,13 +208,14 @@ public void with_valid_cluster_connection_string() { Assert.Equal(13, settings.ConnectivitySettings.MaxDiscoverAttempts); Assert.Equal(37, settings.ConnectivitySettings.DiscoveryInterval.TotalMilliseconds); Assert.Equal(NodePreference.Follower, settings.ConnectivitySettings.NodePreference); + Assert.Equal(TimeSpan.FromMilliseconds(10), settings.ConnectivitySettings.KeepAlive); #if !GRPC_CORE Assert.NotNull(settings.CreateHttpMessageHandler); #endif settings = EventStoreClientSettings.Create( - "esdb://user:pass@host1,host2:3321,127.0.0.3/?tls=false&maxDiscoverAttempts=13&DiscoveryInterval=37&nOdEPrEfErence=FoLLoWer&tlsVerifyCert=false"); + "esdb://user:pass@host1,host2:3321,127.0.0.3/?tls=false&maxDiscoverAttempts=13&DiscoveryInterval=37&nOdEPrEfErence=FoLLoWer&tlsVerifyCert=false&KEepAlive=10"); Assert.Equal("user", settings.DefaultCredentials.Username); Assert.Equal("pass", settings.DefaultCredentials.Password); Assert.NotEmpty(settings.ConnectivitySettings.GossipSeeds); @@ -230,6 +232,7 @@ public void with_valid_cluster_connection_string() { Assert.Equal(13, settings.ConnectivitySettings.MaxDiscoverAttempts); Assert.Equal(37, settings.ConnectivitySettings.DiscoveryInterval.TotalMilliseconds); Assert.Equal(NodePreference.Follower, settings.ConnectivitySettings.NodePreference); + Assert.Equal(TimeSpan.FromMilliseconds(10), settings.ConnectivitySettings.KeepAlive); #if !GRPC_CORE Assert.NotNull(settings.CreateHttpMessageHandler); #endif @@ -264,19 +267,19 @@ public void with_different_tls_verify_cert_settings() { EventStoreClientSettings settings; settings = EventStoreClientSettings.Create("esdb://127.0.0.1/"); - Assert.Null(settings.CreateHttpMessageHandler); + Assert.NotNull(settings.CreateHttpMessageHandler); settings = EventStoreClientSettings.Create("esdb://127.0.0.1/?tlsVerifyCert=TrUe"); - Assert.Null(settings.CreateHttpMessageHandler); + Assert.NotNull(settings.CreateHttpMessageHandler); settings = EventStoreClientSettings.Create("esdb://127.0.0.1/?tlsVerifyCert=FaLsE"); Assert.NotNull(settings.CreateHttpMessageHandler); settings = EventStoreClientSettings.Create("esdb://127.0.0.1,127.0.0.2:3321,127.0.0.3/"); - Assert.Null(settings.CreateHttpMessageHandler); + Assert.NotNull(settings.CreateHttpMessageHandler); settings = EventStoreClientSettings.Create("esdb://127.0.0.1,127.0.0.2:3321,127.0.0.3/?tlsVerifyCert=true"); - Assert.Null(settings.CreateHttpMessageHandler); + Assert.NotNull(settings.CreateHttpMessageHandler); settings = EventStoreClientSettings.Create( "esdb://127.0.0.1,127.0.0.2:3321,127.0.0.3/?tlsVerifyCert=false"); From 92804a726deabae0ee1dd036ae748fe32f86c6f7 Mon Sep 17 00:00:00 2001 From: thefringeninja <495495+thefringeninja@users.noreply.github.com> Date: Mon, 11 Jan 2021 10:09:38 +0100 Subject: [PATCH 14/21] skip flakey regression on net .framework --- .../Bugs/Issue_1125.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/Bugs/Issue_1125.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/Bugs/Issue_1125.cs index 89d2bd9ba..0b4b6c104 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/Bugs/Issue_1125.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/Bugs/Issue_1125.cs @@ -16,9 +16,16 @@ public Issue_1125(Fixture fixture) { public static IEnumerable TestCases() => Enumerable.Range(0, 50) .Select(i => new object[] {i}); - [Theory, MemberData(nameof(TestCases))] + [Theory( +#if NETFRAMEWORK + Skip = "Really flaky on .net frameork" +#endif + ), MemberData(nameof(TestCases))] public async Task persistent_subscription_delivers_all_events(int iteration) { - const int eventCount = 250; + if (Environment.OSVersion.IsWindows()) { + + } + const int eventCount = 250; const int totalEvents = eventCount * 2; var completed = new TaskCompletionSource(); From 3b75d24fc93da262991de8fede54afb715569601 Mon Sep 17 00:00:00 2001 From: thefringeninja <495495+thefringeninja@users.noreply.github.com> Date: Wed, 13 Jan 2021 09:58:03 +0100 Subject: [PATCH 15/21] downgrade to gRPC 2.32.1 until grpc/grpc#24935 is resolved --- src/Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 5992e66b0..ef1de0437 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -25,7 +25,7 @@ Copyright 2012-2020 Event Store Ltd v true - 2.34.0 + 2.33.1 From 9d24daadf5816214f3c37ab364a06808bcc3e335 Mon Sep 17 00:00:00 2001 From: thefringeninja <495495+thefringeninja@users.noreply.github.com> Date: Thu, 14 Jan 2021 12:02:08 +0100 Subject: [PATCH 16/21] added dns name to generated certificates --- gencert.sh | 4 +++- .../EventStoreClientFixtureBase.cs | 8 +------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/gencert.sh b/gencert.sh index 4a22d0515..ceb2d188b 100755 --- a/gencert.sh +++ b/gencert.sh @@ -2,10 +2,12 @@ mkdir -p certs +chmod 0755 ./certs + docker pull docker.pkg.github.com/eventstore/es-gencert-cli/es-gencert-cli:1.0.1 docker run --rm --volume $PWD/certs:/tmp --user $(id -u):$(id -g) docker.pkg.github.com/eventstore/es-gencert-cli/es-gencert-cli:1.0.1 create-ca -out /tmp/ca -docker run --rm --volume $PWD/certs:/tmp --user $(id -u):$(id -g) docker.pkg.github.com/eventstore/es-gencert-cli/es-gencert-cli:1.0.1 create-node -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/node -ip-addresses 127.0.0.1 +docker run --rm --volume $PWD/certs:/tmp --user $(id -u):$(id -g) docker.pkg.github.com/eventstore/es-gencert-cli/es-gencert-cli:1.0.1 create-node -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/node -ip-addresses 127.0.0.1 -dns-names localhost chmod 0755 -R ./certs diff --git a/test/EventStore.Client.Tests.Common/EventStoreClientFixtureBase.cs b/test/EventStore.Client.Tests.Common/EventStoreClientFixtureBase.cs index f9d92e51a..4eb31468a 100644 --- a/test/EventStore.Client.Tests.Common/EventStoreClientFixtureBase.cs +++ b/test/EventStore.Client.Tests.Common/EventStoreClientFixtureBase.cs @@ -30,13 +30,7 @@ namespace EventStore.Client { public abstract class EventStoreClientFixtureBase : IAsyncLifetime { public const string TestEventType = "-"; - private const string ConnectionString = -#if GRPC_CORE - "esdb://127.0.0.1:2113/?tlsVerifyCert=false" -#else - "esdb://localhost:2113/?tlsVerifyCert=false" -#endif - ; + private const string ConnectionString = "esdb://localhost:2113/?tlsVerifyCert=false"; private static readonly Subject LogEventSubject = new Subject(); From f73f2c13e345e99e698ff8a609ce663baae4f920 Mon Sep 17 00:00:00 2001 From: thefringeninja <495495+thefringeninja@users.noreply.github.com> Date: Thu, 14 Jan 2021 12:02:49 +0100 Subject: [PATCH 17/21] remove unused test project --- .../EventStore.Client.IntegrationTests.csproj | 11 -- .../EventStoreClientFixture.cs | 154 ------------------ 2 files changed, 165 deletions(-) delete mode 100644 test/EventStore.Client.IntegrationTests/EventStore.Client.IntegrationTests.csproj delete mode 100644 test/EventStore.Client.IntegrationTests/EventStoreClientFixture.cs diff --git a/test/EventStore.Client.IntegrationTests/EventStore.Client.IntegrationTests.csproj b/test/EventStore.Client.IntegrationTests/EventStore.Client.IntegrationTests.csproj deleted file mode 100644 index 7223670c0..000000000 --- a/test/EventStore.Client.IntegrationTests/EventStore.Client.IntegrationTests.csproj +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - Always - - - diff --git a/test/EventStore.Client.IntegrationTests/EventStoreClientFixture.cs b/test/EventStore.Client.IntegrationTests/EventStoreClientFixture.cs deleted file mode 100644 index 549efa8b4..000000000 --- a/test/EventStore.Client.IntegrationTests/EventStoreClientFixture.cs +++ /dev/null @@ -1,154 +0,0 @@ -using System; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Ductus.FluentDocker.Builders; -using Ductus.FluentDocker.Services; -using EventStore.Client.Gossip; -using Grpc.Core; -#if !GRPC_CORE -using Grpc.Net.Client; -#endif -using Polly; -using Xunit; -using EndPoint = System.Net.EndPoint; - -#nullable enable -namespace EventStore.Client { - public class Somerting { - [Fact] - public async Task YouSuck() { - await using var fixture = new EventStoreClientInsecureClusterFixture(); - await fixture.Start(); - using var client = fixture.CreateEventStoreClient( - Array.ConvertAll(fixture.Ports, port => new IPEndPoint(IPAddress.Loopback, port))); - - var leader = await fixture.GetLeaderNode(); - - leader!.ToString(); - } - } - - public class EventStoreClientInsecureClusterFixture : EventStoreClientClusterFixture { - public EventStoreClientInsecureClusterFixture() : base(new UriBuilder { - Scheme = Uri.UriSchemeHttp, - Port = 2113, - Query = "?tls=false" - }.Uri, "insecure") { - } - } - - public abstract class EventStoreClientClusterFixture : IAsyncDisposable { - private readonly Uri _address; - private readonly EventStoreTestServer _server; - public Gossip.Gossip.GossipClient GossipClient => _server.GossipClient; - - public int[] Ports => _server.Ports.ToArray(); - - protected EventStoreClientClusterFixture(Uri address, string composeFileName) { - _address = address; - _server = new EventStoreTestServer(address, composeFileName); - } - - public EventStoreClient CreateEventStoreClient(DnsEndPoint[] dnsGossipSeeds) { - var settings = EventStoreClientSettings.Create(_address.ToString()); - settings.ConnectivitySettings.DnsGossipSeeds = dnsGossipSeeds; - return new EventStoreClient(settings); - } - - public EventStoreClient CreateEventStoreClient(IPEndPoint[] ipGossipSeeds) { - var settings = EventStoreClientSettings.Create(new UriBuilder(_address) { - Scheme = "esdb+discover" - }.Uri.ToString()); - settings.ConnectivitySettings.IpGossipSeeds = ipGossipSeeds; - return new EventStoreClient(settings); - } - - public async ValueTask GetLeaderNode() { - var clusterInfo = await GossipClient.ReadAsync(new Empty()); - return clusterInfo.Members.Where(x => x.State == MemberInfo.Types.VNodeState.Leader) - .Select(x => new DnsEndPoint(x.HttpEndPoint.Address, (int)x.HttpEndPoint.Port)) - .FirstOrDefault(); - } - - public ValueTask Start() => _server.Start(); - public ValueTask DisposeAsync() => _server.DisposeAsync(); - - private class EventStoreTestServer : IAsyncDisposable { - private readonly ICompositeService _eventStore; - private readonly HttpClient _httpClient; - private readonly ChannelBase _channel; - public Gossip.Gossip.GossipClient GossipClient { get; } - - public int[] Ports => - (from container in _eventStore.Containers - where container.Name.Contains("esdb-node") - from pair in container.GetConfiguration().NetworkSettings.Ports - where pair.Key == "2113/tcp" - select pair.Value.First().Port) - .ToArray(); - - public EventStoreTestServer(Uri address, string mode) { - _httpClient = new HttpClient(new SocketsHttpHandler { - SslOptions = { - RemoteCertificateValidationCallback = delegate { return true; } - } - }) { - BaseAddress = address - }; - - _eventStore = new Builder() - .UseContainer() - .UseCompose() - .FromFile($"docker-compose.{mode}.yml") - .Build(); - _channel = CreateChannel(address); - GossipClient = new Gossip.Gossip.GossipClient(_channel); - } - - private ChannelBase CreateChannel(Uri address) { -#if !GRPC_CORE - return GrpcChannel.ForAddress(address); -#else - return new Channel(address.Host, address.Port, - address.Scheme == Uri.UriSchemeHttps - ? new SslCredentials() - : ChannelCredentials.Insecure); -#endif - } - - public ValueTask DisposeAsync() { - if (_channel is IDisposable disposable) { - disposable.Dispose(); - } - - try { - _eventStore.Dispose(); - } - catch {} - - return new ValueTask(); - } - - public async ValueTask Start(CancellationToken cancellationToken = default) { - _eventStore.Start(); - try { - await Policy.Handle() - .WaitAndRetryAsync(5, retryCount => TimeSpan.FromSeconds(retryCount * retryCount)) - .ExecuteAsync(async () => { - using var response = await _httpClient.GetAsync("/health/live", cancellationToken); - if (response.StatusCode >= HttpStatusCode.BadRequest) { - throw new Exception($"Health check failed with status code: {response.StatusCode}."); - } - }); - } catch (Exception) { - _httpClient.Dispose(); - _eventStore.Dispose(); - throw; - } - } - } - } -} From 18ba7c9310b5dfc9c79ec524abe37d65310b26a3 Mon Sep 17 00:00:00 2001 From: thefringeninja <495495+thefringeninja@users.noreply.github.com> Date: Thu, 14 Jan 2021 12:08:28 +0100 Subject: [PATCH 18/21] improve array shuffling --- src/EventStore.Client/ArrayExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EventStore.Client/ArrayExtensions.cs b/src/EventStore.Client/ArrayExtensions.cs index 05be5b980..e5f6a9bda 100644 --- a/src/EventStore.Client/ArrayExtensions.cs +++ b/src/EventStore.Client/ArrayExtensions.cs @@ -6,7 +6,7 @@ public static void RandomShuffle(this T[] arr, int i, int j) { if (i >= j) return; var rnd = new Random(Guid.NewGuid().GetHashCode()); - for (int k = i; k <= j; ++k) { + for (int k = i; k < j; ++k) { var index = rnd.Next(k, j + 1); var tmp = arr[index]; arr[index] = arr[k]; From 57b5f23a8804e943205ad8148bf1c5731d2f7136 Mon Sep 17 00:00:00 2001 From: thefringeninja <495495+thefringeninja@users.noreply.github.com> Date: Mon, 18 Jan 2021 13:43:44 +0100 Subject: [PATCH 19/21] obsolete GossipOverHttps --- src/EventStore.Client/ChannelFactory.cs | 7 +++---- .../EventStoreClientConnectivitySettings.cs | 10 +++++++++- ...ventStoreClientSettings.ConnectionString.cs | 4 ++-- .../ConnectionStringTests.cs | 18 +++++++++--------- .../GossipBasedEndpointDiscovererTests.cs | 12 ++++++------ 5 files changed, 29 insertions(+), 22 deletions(-) diff --git a/src/EventStore.Client/ChannelFactory.cs b/src/EventStore.Client/ChannelFactory.cs index bca134087..dfbbbf8a3 100644 --- a/src/EventStore.Client/ChannelFactory.cs +++ b/src/EventStore.Client/ChannelFactory.cs @@ -14,20 +14,19 @@ namespace EventStore.Client { internal static class ChannelFactory { public static ChannelBase CreateChannel(EventStoreClientSettings settings, EndPoint endPoint) => - CreateChannel(settings, endPoint.ToUri(settings.ConnectivitySettings.GossipOverHttps)); + CreateChannel(settings, endPoint.ToUri(!settings.ConnectivitySettings.Insecure)); public static ChannelBase CreateChannel(EventStoreClientSettings settings, Uri? address) { address ??= settings.ConnectivitySettings.Address; #if !GRPC_CORE - if (address.Scheme == Uri.UriSchemeHttp ||!settings.ConnectivitySettings.GossipOverHttps) { + if (address.Scheme == Uri.UriSchemeHttp ||settings.ConnectivitySettings.Insecure) { //this must be switched on before creation of the HttpMessageHandler AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); } return GrpcChannel.ForAddress(address, new GrpcChannelOptions { - HttpClient = new HttpClient(CreateHandler(), - true) { + HttpClient = new HttpClient(CreateHandler(), true) { Timeout = Timeout.InfiniteTimeSpan, DefaultRequestVersion = new Version(2, 0), }, diff --git a/src/EventStore.Client/EventStoreClientConnectivitySettings.cs b/src/EventStore.Client/EventStoreClientConnectivitySettings.cs index 4f7150d05..3af4ee9ae 100644 --- a/src/EventStore.Client/EventStoreClientConnectivitySettings.cs +++ b/src/EventStore.Client/EventStoreClientConnectivitySettings.cs @@ -45,10 +45,14 @@ public class EventStoreClientConnectivitySettings { /// public TimeSpan GossipTimeout { get; set; } +#if !NETFRAMEWORK + /// /// Whether or not to use HTTPS when communicating via gossip. /// + [Obsolete] public bool GossipOverHttps { get; set; } = true; +#endif /// /// The polling interval used to discover the . @@ -70,13 +74,17 @@ public class EventStoreClientConnectivitySettings { /// public bool IsSingleNode => GossipSeeds.Length == 0; + /// + /// True if communicating over a secure channel; otherwise false. + /// + public bool Insecure { get; set; } + /// /// The default . /// public static EventStoreClientConnectivitySettings Default => new EventStoreClientConnectivitySettings { MaxDiscoverAttempts = 10, GossipTimeout = TimeSpan.FromSeconds(5), - GossipOverHttps = true, DiscoveryInterval = TimeSpan.FromMilliseconds(100), NodePreference = NodePreference.Leader }; diff --git a/src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs b/src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs index 9c5f51a4b..01ab7cdab 100644 --- a/src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs +++ b/src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs @@ -170,6 +170,8 @@ private static EventStoreClientSettings CreateSettings(string scheme, (string us settings.ConnectivitySettings.KeepAlive = TimeSpan.FromMilliseconds((int)keepAliveMs); } + connSettings.Insecure = !useTls; + if (hosts.Length == 1 && scheme != UriSchemeDiscover) { connSettings.Address = hosts[0].ToUri(useTls); } else { @@ -178,8 +180,6 @@ private static EventStoreClientSettings CreateSettings(string scheme, (string us Array.ConvertAll(hosts, x => new DnsEndPoint(x.GetHost(), x.GetPort())); else connSettings.IpGossipSeeds = Array.ConvertAll(hosts, x => x as IPEndPoint); - - connSettings.GossipOverHttps = useTls; } #if !GRPC_CORE diff --git a/test/EventStore.Client.Tests/ConnectionStringTests.cs b/test/EventStore.Client.Tests/ConnectionStringTests.cs index e4343b0db..e8cdebc60 100644 --- a/test/EventStore.Client.Tests/ConnectionStringTests.cs +++ b/test/EventStore.Client.Tests/ConnectionStringTests.cs @@ -135,7 +135,7 @@ public void with_valid_single_node_connection_string() { Assert.Empty(settings.ConnectivitySettings.GossipSeeds); Assert.Null(settings.ConnectivitySettings.IpGossipSeeds); Assert.Null(settings.ConnectivitySettings.DnsGossipSeeds); - Assert.True(settings.ConnectivitySettings.GossipOverHttps); + Assert.False(settings.ConnectivitySettings.Insecure); Assert.Equal(13, settings.ConnectivitySettings.MaxDiscoverAttempts); Assert.Equal(37, settings.ConnectivitySettings.DiscoveryInterval.TotalMilliseconds); Assert.Equal(NodePreference.Follower, settings.ConnectivitySettings.NodePreference); @@ -153,7 +153,7 @@ public void with_valid_single_node_connection_string() { Assert.Empty(settings.ConnectivitySettings.GossipSeeds); Assert.Null(settings.ConnectivitySettings.IpGossipSeeds); Assert.Null(settings.ConnectivitySettings.DnsGossipSeeds); - Assert.True(settings.ConnectivitySettings.GossipOverHttps); + Assert.True(settings.ConnectivitySettings.Insecure); Assert.Null(settings.ConnectivitySettings.KeepAlive); #if !GRPC_CORE Assert.NotNull(settings.CreateHttpMessageHandler); @@ -178,8 +178,8 @@ public void with_default_settings() { settings.ConnectivitySettings.MaxDiscoverAttempts); Assert.Equal(EventStoreClientConnectivitySettings.Default.NodePreference, settings.ConnectivitySettings.NodePreference); - Assert.Equal(EventStoreClientConnectivitySettings.Default.GossipOverHttps, - settings.ConnectivitySettings.GossipOverHttps); + Assert.Equal(EventStoreClientConnectivitySettings.Default.Insecure, + settings.ConnectivitySettings.Insecure); Assert.Equal(EventStoreClientOperationOptions.Default.TimeoutAfter!.Value.TotalMilliseconds, settings.OperationOptions.TimeoutAfter!.Value.TotalMilliseconds); Assert.Equal(EventStoreClientOperationOptions.Default.ThrowOnAppendFailure, @@ -197,7 +197,7 @@ public void with_valid_cluster_connection_string() { Assert.NotEmpty(settings.ConnectivitySettings.GossipSeeds); Assert.NotNull(settings.ConnectivitySettings.IpGossipSeeds); Assert.Null(settings.ConnectivitySettings.DnsGossipSeeds); - Assert.True(settings.ConnectivitySettings.GossipOverHttps); + Assert.False(settings.ConnectivitySettings.Insecure); Assert.True(settings.ConnectivitySettings.IpGossipSeeds.Length == 3 && Equals(settings.ConnectivitySettings.IpGossipSeeds[0].Address, IPAddress.Parse("127.0.0.1")) && Equals(settings.ConnectivitySettings.IpGossipSeeds[0].Port, 2113) && @@ -221,7 +221,7 @@ public void with_valid_cluster_connection_string() { Assert.NotEmpty(settings.ConnectivitySettings.GossipSeeds); Assert.Null(settings.ConnectivitySettings.IpGossipSeeds); Assert.NotNull(settings.ConnectivitySettings.DnsGossipSeeds); - Assert.False(settings.ConnectivitySettings.GossipOverHttps); + Assert.True(settings.ConnectivitySettings.Insecure); Assert.True(settings.ConnectivitySettings.DnsGossipSeeds.Length == 3 && Equals(settings.ConnectivitySettings.DnsGossipSeeds[0].Host, "host1") && Equals(settings.ConnectivitySettings.DnsGossipSeeds[0].Port, 2113) && @@ -252,13 +252,13 @@ public void with_different_tls_settings() { Assert.Equal(Uri.UriSchemeHttp, settings.ConnectivitySettings.Address.Scheme); settings = EventStoreClientSettings.Create("esdb://127.0.0.1,127.0.0.2:3321,127.0.0.3/"); - Assert.True(settings.ConnectivitySettings.GossipOverHttps); + Assert.False(settings.ConnectivitySettings.Insecure); settings = EventStoreClientSettings.Create("esdb://127.0.0.1,127.0.0.2:3321,127.0.0.3?tls=true"); - Assert.True(settings.ConnectivitySettings.GossipOverHttps); + Assert.False(settings.ConnectivitySettings.Insecure); settings = EventStoreClientSettings.Create("esdb://127.0.0.1,127.0.0.2:3321,127.0.0.3/?tls=fAlSe"); - Assert.False(settings.ConnectivitySettings.GossipOverHttps); + Assert.True(settings.ConnectivitySettings.Insecure); } #if !GRPC_CORE diff --git a/test/EventStore.Client.Tests/GossipBasedEndpointDiscovererTests.cs b/test/EventStore.Client.Tests/GossipBasedEndpointDiscovererTests.cs index cbf843f12..d72a6acb2 100644 --- a/test/EventStore.Client.Tests/GossipBasedEndpointDiscovererTests.cs +++ b/test/EventStore.Client.Tests/GossipBasedEndpointDiscovererTests.cs @@ -33,7 +33,7 @@ public async Task should_issue_gossip_to_gossip_seed(bool useHttps) { new EventStoreClientConnectivitySettings { MaxDiscoverAttempts = 1, GossipTimeout = Timeout.InfiniteTimeSpan, - GossipOverHttps = useHttps, + Insecure = !useHttps, DiscoveryInterval = TimeSpan.Zero, NodePreference = NodePreference.Leader, DnsGossipSeeds = new[] {_gossipSeed} @@ -78,7 +78,7 @@ public async Task should_be_able_to_discover_twice(bool useHttps) { new EventStoreClientConnectivitySettings { MaxDiscoverAttempts = 5, GossipTimeout = Timeout.InfiniteTimeSpan, - GossipOverHttps = useHttps, + Insecure = !useHttps, DiscoveryInterval = TimeSpan.Zero, NodePreference = NodePreference.Leader, DnsGossipSeeds = new[] {_gossipSeed} @@ -107,7 +107,7 @@ public async Task should_not_exceed_max_discovery_attempts(bool useHttps) { new EventStoreClientConnectivitySettings { MaxDiscoverAttempts = 5, GossipTimeout = Timeout.InfiniteTimeSpan, - GossipOverHttps = useHttps, + Insecure = !useHttps, DiscoveryInterval = TimeSpan.Zero, NodePreference = NodePreference.Leader, DnsGossipSeeds = new[] {_gossipSeed} @@ -156,7 +156,7 @@ internal async Task should_not_be_able_to_pick_invalid_node(bool useHttps, new EventStoreClientConnectivitySettings { MaxDiscoverAttempts = 1, GossipTimeout = Timeout.InfiniteTimeSpan, - GossipOverHttps = useHttps, + Insecure = !useHttps, DiscoveryInterval = TimeSpan.Zero, NodePreference = NodePreference.Leader, DnsGossipSeeds = new[] {_gossipSeed} @@ -209,7 +209,7 @@ internal async Task should_pick_node_based_on_preference(bool useHttps, NodePref new EventStoreClientConnectivitySettings { MaxDiscoverAttempts = 1, GossipTimeout = Timeout.InfiniteTimeSpan, - GossipOverHttps = useHttps, + Insecure = !useHttps, DiscoveryInterval = TimeSpan.Zero, NodePreference = preference, DnsGossipSeeds = new[] {_gossipSeed} @@ -243,7 +243,7 @@ public async Task falls_back_to_first_alive_node_if_a_preferred_node_is_not_foun new EventStoreClientConnectivitySettings { MaxDiscoverAttempts = 1, GossipTimeout = Timeout.InfiniteTimeSpan, - GossipOverHttps = useHttps, + Insecure = !useHttps, DiscoveryInterval = TimeSpan.Zero, NodePreference = NodePreference.Leader, DnsGossipSeeds = new[] {_gossipSeed} From 4dceb12cba5320240193f30894157279e4599ece Mon Sep 17 00:00:00 2001 From: thefringeninja <495495+thefringeninja@users.noreply.github.com> Date: Mon, 18 Jan 2021 13:53:16 +0100 Subject: [PATCH 20/21] don't use CallCredentials when none supplied --- ...Extensions.cs => EventStoreCallOptions.cs} | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) rename src/EventStore.Client.Common/{EventStoreClientSettingsExtensions.cs => EventStoreCallOptions.cs} (62%) diff --git a/src/EventStore.Client.Common/EventStoreClientSettingsExtensions.cs b/src/EventStore.Client.Common/EventStoreCallOptions.cs similarity index 62% rename from src/EventStore.Client.Common/EventStoreClientSettingsExtensions.cs rename to src/EventStore.Client.Common/EventStoreCallOptions.cs index 816308f71..58b2fe750 100644 --- a/src/EventStore.Client.Common/EventStoreClientSettingsExtensions.cs +++ b/src/EventStore.Client.Common/EventStoreCallOptions.cs @@ -11,18 +11,16 @@ public static CallOptions Create(EventStoreClientSettings settings, cancellationToken: cancellationToken, deadline: DeadlineAfter(operationOptions.TimeoutAfter), headers: new Metadata(), - credentials: CallCredentials.FromInterceptor(async (context, metadata) => { - var credentials = settings.DefaultCredentials ?? userCredentials; + credentials: (settings.DefaultCredentials ?? userCredentials) == null + ? null + : CallCredentials.FromInterceptor(async (context, metadata) => { + var credentials = settings.DefaultCredentials ?? userCredentials; - if (credentials == null) { - return; - } - - var authorizationHeader = await settings.OperationOptions - .GetAuthenticationHeaderValue(credentials, CancellationToken.None) - .ConfigureAwait(false); - metadata.Add(Constants.Headers.Authorization, authorizationHeader); - }) + var authorizationHeader = await settings.OperationOptions + .GetAuthenticationHeaderValue(credentials!, CancellationToken.None) + .ConfigureAwait(false); + metadata.Add(Constants.Headers.Authorization, authorizationHeader); + }) ); private static DateTime? DeadlineAfter(TimeSpan? timeoutAfter) => !timeoutAfter.HasValue From 22f4075a6e487e9e2762f1eb5305f26a16610561 Mon Sep 17 00:00:00 2001 From: thefringeninja <495495+thefringeninja@users.noreply.github.com> Date: Wed, 20 Jan 2021 16:42:50 +0100 Subject: [PATCH 21/21] remove duplicate compiler directive --- test/EventStore.Client.Tests/ConnectionStringTests.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/EventStore.Client.Tests/ConnectionStringTests.cs b/test/EventStore.Client.Tests/ConnectionStringTests.cs index e8cdebc60..d84d7ae80 100644 --- a/test/EventStore.Client.Tests/ConnectionStringTests.cs +++ b/test/EventStore.Client.Tests/ConnectionStringTests.cs @@ -121,11 +121,8 @@ public void with_valid_single_node_connection_string() { Assert.Equal(33, settings.ConnectivitySettings.GossipTimeout.TotalMilliseconds); Assert.Equal(NodePreference.Follower, settings.ConnectivitySettings.NodePreference); Assert.Equal(TimeSpan.FromMilliseconds(10), settings.ConnectivitySettings.KeepAlive); -#if !GRPC_CORE #if !GRPC_CORE Assert.NotNull(settings.CreateHttpMessageHandler); -#endif - #endif settings = EventStoreClientSettings.Create( "esdb://127.0.0.1?connectionName=test&maxDiscoverAttempts=13&DiscoveryInterval=37&nOdEPrEfErence=FoLLoWer&tls=true&tlsVerifyCert=true&operationTimeout=330&throwOnAppendFailure=faLse&KEepAlive=10");