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");