Skip to content

Commit 1dc2e50

Browse files
Mpdreamzrusscam
authored andcommitted
Add a CloudConnectionPool (#4005)
* Add CloudConnectionPool and overloads to the high/low level clients settings constructors to ease connecting to Elastic Cloud * Add tests for the CloudConnectionPool * Apply suggestions from code review Co-Authored-By: Russ Cam <[email protected]> * fix failing tests due to updated exception message (cherry picked from commit fdb1933) (cherry picked from commit fd5716f)
1 parent d81ccdf commit 1dc2e50

File tree

9 files changed

+363
-77
lines changed

9 files changed

+363
-77
lines changed

docs/client-concepts/connection-pooling/building-blocks/connection-pooling.asciidoc

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,12 @@ instance of `ConnectionSettings`. Since a <<lifetimes,single client and connecti
3434
life of the application>>, the lifetime of a single connection pool instance will also be bound to the lifetime
3535
of the application.
3636

37-
There are four types of connection pool
37+
There are five types of connection pool
3838

3939
* <<single-node-connection-pool,SingleNodeConnectionPool>>
4040

41+
* <<cloud-connection-pool,CloudConnectionPool>>
42+
4143
* <<static-connection-pool,StaticConnectionPool>>
4244

4345
* <<sniffing-connection-pool,SniffingConnectionPool>>
@@ -99,6 +101,83 @@ client.ConnectionSettings.ConnectionPool
99101
.Should().BeOfType<SingleNodeConnectionPool>();
100102
----
101103

104+
[[cloud-connection-pool]]
105+
==== CloudConnectionPool
106+
107+
A specialized subclass of `SingleNodeConnectionPool that accepts a Cloud Id and credentials.
108+
When used the client will also pick Elastic Cloud optmized defaults for the connection settings.
109+
110+
A Cloud Id for your cluster can be fetched from your Elastic Cloud cluster administration console.
111+
112+
A Cloud Id should be in the form of `cluster_name:base_64_data` where `base_64_data` are the UUIDs for the services in this cloud instance e.g
113+
114+
`host_name$elasticsearch_uuid$kibana_uuid$apm_uuid`
115+
116+
Out of these, only `host_name` and `elasticsearch_uuid` are always available.
117+
118+
[source,csharp]
119+
----
120+
string ToBase64(string s) => Convert.ToBase64String(Encoding.UTF8.GetBytes(s));
121+
/* Here we obviously use a ficticuous Cloud Id so lets create a fake one. */
122+
123+
var hostName = "cloud-endpoint.example";
124+
var elasticsearchUuid = "3dadf823f05388497ea684236d918a1a";
125+
var services = $"{hostName}${elasticsearchUuid}$3f26e1609cf54a0f80137a80de560da4";
126+
var cloudId = $"my_cluster:{ToBase64(services)}";
127+
128+
/*
129+
* In a real scenario you would be able to copy paste `cloudId`
130+
*
131+
* A cloud connection pool always needs credentials as well here opt for basic auth
132+
*/
133+
var credentials = new BasicAuthenticationCredentials("username", "password");
134+
var pool = new CloudConnectionPool(cloudId, credentials);
135+
pool.UsingSsl.Should().BeTrue();
136+
pool.Nodes.Should().HaveCount(1);
137+
var node = pool.Nodes.First();
138+
node.Uri.Port.Should().Be(443);
139+
node.Uri.Host.Should().Be($"{elasticsearchUuid}.{hostName}");
140+
node.Uri.Scheme.Should().Be("https");
141+
----
142+
143+
This type of pool like its parent the `SingleNodeConnectionPool` is hardwired to opt out of
144+
reseeding (thus, sniffing) as well as pinging
145+
146+
[source,csharp]
147+
----
148+
pool.SupportsReseeding.Should().BeFalse();
149+
pool.SupportsPinging.Should().BeFalse();
150+
----
151+
152+
You can also directly create a cloud enabled connection using the clients constructor
153+
154+
[source,csharp]
155+
----
156+
var client = new ElasticClient(cloudId, credentials);
157+
client.ConnectionSettings.ConnectionPool
158+
.Should()
159+
.BeOfType<CloudConnectionPool>();
160+
----
161+
162+
However we urge that you always pass your connection settings explicitly
163+
164+
[source,csharp]
165+
----
166+
client = new ElasticClient(new ConnectionSettings(pool));
167+
client.ConnectionSettings.ConnectionPool.Should().BeOfType<CloudConnectionPool>();
168+
169+
client.ConnectionSettings.EnableHttpCompression.Should().BeTrue();
170+
client.ConnectionSettings.BasicAuthenticationCredentials.Should().NotBeNull();
171+
client.ConnectionSettings.BasicAuthenticationCredentials.Username.Should().Be("username");
172+
foreach (var id in badCloudIds)
173+
{
174+
Action create = () => new ElasticClient(id, credentials);
175+
176+
create.ShouldThrow<ArgumentException>()
177+
.And.Message.Should().Contain("should be a string in the form of cluster_name:base_64_data");
178+
}
179+
----
180+
102181
[[static-connection-pool]]
103182
==== StaticConnectionPool
104183

src/Elasticsearch.Net/Configuration/ConnectionConfiguration.cs

Lines changed: 57 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
namespace Elasticsearch.Net
1919
{
2020
/// <summary>
21-
/// Allows you to control how <see cref="ElasticLowLevelClient"/> behaves and where/how it connects to Elasticsearch
21+
/// Allows you to control how <see cref="ElasticLowLevelClient" /> behaves and where/how it connects to Elasticsearch
2222
/// </summary>
2323
public class ConnectionConfiguration : ConnectionConfiguration<ConnectionConfiguration>
2424
{
@@ -42,33 +42,42 @@ public class ConnectionConfiguration : ConnectionConfiguration<ConnectionConfigu
4242
/// <summary>
4343
/// The default connection limit for both Elasticsearch.Net and Nest. Defaults to <c>80</c> except for
4444
/// HttpClientHandler implementations based on curl, which defaults to
45-
/// <see cref="Environment.ProcessorCount"/>
45+
/// <see cref="Environment.ProcessorCount" />
4646
/// </summary>
4747
public static readonly int DefaultConnectionLimit = IsCurlHandler ? Environment.ProcessorCount : 80;
4848

49+
4950
/// <summary>
5051
/// The default user agent for Elasticsearch.Net
5152
/// </summary>
52-
public static readonly string DefaultUserAgent = $"elasticsearch-net/{typeof(IConnectionConfigurationValues).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>().InformationalVersion} ({RuntimeInformation.OSDescription}; {RuntimeInformation.FrameworkDescription}; Elasticsearch.Net)";
53+
public static readonly string DefaultUserAgent =
54+
$"elasticsearch-net/{typeof(IConnectionConfigurationValues).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>().InformationalVersion} ({RuntimeInformation.OSDescription}; {RuntimeInformation.FrameworkDescription}; Elasticsearch.Net)";
5355

5456
/// <summary>
55-
/// Creates a new instance of <see cref="ConnectionConfiguration"/>
57+
/// Creates a new instance of <see cref="ConnectionConfiguration" />
5658
/// </summary>
5759
/// <param name="uri">The root of the Elasticsearch node we want to connect to. Defaults to http://localhost:9200</param>
5860
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")]
5961
public ConnectionConfiguration(Uri uri = null)
6062
: this(new SingleNodeConnectionPool(uri ?? new Uri("http://localhost:9200"))) { }
6163

6264
/// <summary>
63-
/// Creates a new instance of <see cref="ConnectionConfiguration"/>
65+
/// Sets up the client to communicate to Elastic Cloud using <paramref name="cloudId" />,
66+
/// <para><see cref="CloudConnectionPool" /> documentation for more information on how to obtain your Cloud Id</para>
67+
/// </summary>
68+
public ConnectionConfiguration(string cloudId, BasicAuthenticationCredentials credentials) : this(
69+
new CloudConnectionPool(cloudId, credentials)) { }
70+
71+
/// <summary>
72+
/// Creates a new instance of <see cref="ConnectionConfiguration" />
6473
/// </summary>
6574
/// <param name="connectionPool">A connection pool implementation that tells the client what nodes are available</param>
6675
public ConnectionConfiguration(IConnectionPool connectionPool)
6776
// ReSharper disable once IntroduceOptionalParameters.Global
6877
: this(connectionPool, null, null) { }
6978

7079
/// <summary>
71-
/// Creates a new instance of <see cref="ConnectionConfiguration"/>
80+
/// Creates a new instance of <see cref="ConnectionConfiguration" />
7281
/// </summary>
7382
/// <param name="connectionPool">A connection pool implementation that tells the client what nodes are available</param>
7483
/// <param name="connection">An connection implementation that can make API requests</param>
@@ -77,15 +86,15 @@ public ConnectionConfiguration(IConnectionPool connectionPool, IConnection conne
7786
: this(connectionPool, connection, null) { }
7887

7988
/// <summary>
80-
/// Creates a new instance of <see cref="ConnectionConfiguration"/>
89+
/// Creates a new instance of <see cref="ConnectionConfiguration" />
8190
/// </summary>
8291
/// <param name="connectionPool">A connection pool implementation that tells the client what nodes are available</param>
8392
/// <param name="serializer">A serializer implementation used to serialize requests and deserialize responses</param>
8493
public ConnectionConfiguration(IConnectionPool connectionPool, IElasticsearchSerializer serializer)
8594
: this(connectionPool, null, serializer) { }
8695

8796
/// <summary>
88-
/// Creates a new instance of <see cref="ConnectionConfiguration"/>
97+
/// Creates a new instance of <see cref="ConnectionConfiguration" />
8998
/// </summary>
9099
/// <param name="connectionPool">A connection pool implementation that tells the client what nodes are available</param>
91100
/// <param name="connection">An connection implementation that can make API requests</param>
@@ -95,7 +104,7 @@ public ConnectionConfiguration(IConnectionPool connectionPool, IConnection conne
95104

96105
internal static bool IsCurlHandler { get; } =
97106
#if DOTNETCORE
98-
typeof(HttpClientHandler).Assembly.GetType("System.Net.Http.CurlHandler") != null;
107+
typeof(HttpClientHandler).Assembly.GetType("System.Net.Http.CurlHandler") != null;
99108
#else
100109
false;
101110
#endif
@@ -107,72 +116,45 @@ public abstract class ConnectionConfiguration<T> : IConnectionConfigurationValue
107116
where T : ConnectionConfiguration<T>
108117
{
109118
private readonly IConnection _connection;
110-
111119
private readonly IConnectionPool _connectionPool;
112-
113120
private readonly NameValueCollection _headers = new NameValueCollection();
114-
115121
private readonly NameValueCollection _queryString = new NameValueCollection();
116122
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
123+
private readonly ElasticsearchUrlFormatter _urlFormatter;
117124

118125
private BasicAuthenticationCredentials _basicAuthCredentials;
119-
120126
private X509CertificateCollection _clientCertificates;
121127
private Action<IApiCallDetails> _completedRequestHandler = DefaultCompletedRequestHandler;
122-
123128
private int _connectionLimit;
124-
125129
private TimeSpan? _deadTimeout;
126130

127131
private bool _disableAutomaticProxyDetection = false;
128132

129133
private bool _disableDirectStreaming = false;
130-
131134
private bool _disablePings;
132-
133135
private bool _enableHttpCompression;
134-
135136
private bool _enableHttpPipelining = true;
136-
137137
private TimeSpan? _keepAliveInterval;
138-
139138
private TimeSpan? _keepAliveTime;
140-
141139
private TimeSpan? _maxDeadTimeout;
142-
143140
private int? _maxRetries;
144-
145141
private TimeSpan? _maxRetryTimeout;
146-
147142
private Func<Node, bool> _nodePredicate = DefaultNodePredicate;
148143
private Action<RequestData> _onRequestDataCreated = DefaultRequestDataCreated;
149-
150144
private TimeSpan? _pingTimeout;
151-
152145
private bool _prettyJson;
153-
154146
private string _proxyAddress;
155147

156148
private string _proxyPassword;
157-
158149
private string _proxyUsername;
159-
160150
private TimeSpan _requestTimeout;
161-
162151
private Func<object, X509Certificate, X509Chain, SslPolicyErrors, bool> _serverCertificateValidationCallback;
163-
164152
private IReadOnlyCollection<int> _skipDeserializationForStatusCodes = new ReadOnlyCollection<int>(new int[] { });
165-
166153
private TimeSpan? _sniffLifeSpan;
167-
168154
private bool _sniffOnConnectionFault;
169-
170155
private bool _sniffOnStartup;
171-
172156
private bool _throwExceptions;
173157

174-
private readonly ElasticsearchUrlFormatter _urlFormatter;
175-
176158
private string _userAgent = ConnectionConfiguration.DefaultUserAgent;
177159

178160
protected ConnectionConfiguration(IConnectionPool connectionPool, IConnection connection, IElasticsearchSerializer requestResponseSerializer)
@@ -190,6 +172,12 @@ protected ConnectionConfiguration(IConnectionPool connectionPool, IConnection co
190172
_nodePredicate = DefaultReseedableNodePredicate;
191173

192174
_urlFormatter = new ElasticsearchUrlFormatter(this);
175+
176+
if (connectionPool is CloudConnectionPool cloudPool)
177+
{
178+
_basicAuthCredentials = cloudPool.BasicCredentials;
179+
_enableHttpCompression = true;
180+
}
193181
}
194182

195183
protected IElasticsearchSerializer UseThisRequestResponseSerializer { get; set; }
@@ -279,15 +267,20 @@ public T EnableTcpKeepAlive(TimeSpan keepAliveTime, TimeSpan keepAliveInterval)
279267
/// <summary>
280268
/// Limits the number of concurrent connections that can be opened to an endpoint. Defaults to <c>80</c>.
281269
/// <para>
282-
/// For Desktop CLR, this setting applies to the DefaultConnectionLimit property on the ServicePointManager object when creating
270+
/// For Desktop CLR, this setting applies to the DefaultConnectionLimit property on the ServicePointManager object when
271+
/// creating
283272
/// ServicePoint objects, affecting the default <see cref="IConnection" /> implementation.
284273
/// </para>
285274
/// <para>
286-
/// For Core CLR, this setting applies to the MaxConnectionsPerServer property on the HttpClientHandler instances used by the HttpClient
275+
/// For Core CLR, this setting applies to the MaxConnectionsPerServer property on the HttpClientHandler instances used by
276+
/// the HttpClient
287277
/// inside the default <see cref="IConnection" /> implementation
288278
/// </para>
289279
/// </summary>
290-
/// <param name="connectionLimit">The connection limit, a value lower then 0 will cause the connection limit not to be set at all</param>
280+
/// <param name="connectionLimit">
281+
/// The connection limit, a value lower then 0 will cause the connection limit not to be set
282+
/// at all
283+
/// </param>
291284
public T ConnectionLimit(int connectionLimit) => Assign(connectionLimit, (a, v) => a._connectionLimit = v);
292285

293286
/// <summary>
@@ -305,7 +298,8 @@ public T SniffOnConnectionFault(bool sniffsOnConnectionFault = true) =>
305298
/// Set the duration after which a cluster state is considered stale and a sniff should be performed again.
306299
/// An <see cref="IConnectionPool" /> has to signal it supports reseeding, otherwise sniffing will never happen.
307300
/// Defaults to 1 hour.
308-
/// Set to null to disable completely. Sniffing will only ever happen on ConnectionPools that return true for SupportsReseeding
301+
/// Set to null to disable completely. Sniffing will only ever happen on ConnectionPools that return true for
302+
/// SupportsReseeding
309303
/// </summary>
310304
/// <param name="sniffLifeSpan">The duration a clusterstate is considered fresh, set to null to disable periodic sniffing</param>
311305
public T SniffLifeSpan(TimeSpan? sniffLifeSpan) => Assign(sniffLifeSpan, (a, v) => a._sniffLifeSpan = v);
@@ -331,19 +325,23 @@ public T SniffOnConnectionFault(bool sniffsOnConnectionFault = true) =>
331325

332326
/// <summary>
333327
/// When a node is used for the very first time or when it's used for the first time after it has been marked dead
334-
/// a ping with a very low timeout is send to the node to make sure that when it's still dead it reports it as fast as possible.
328+
/// a ping with a very low timeout is send to the node to make sure that when it's still dead it reports it as fast as
329+
/// possible.
335330
/// You can disable these pings globally here if you rather have it fail on the possible slower original request
336331
/// </summary>
337332
public T DisablePing(bool disable = true) => Assign(disable, (a, v) => a._disablePings = v);
338333

339334
/// <summary>
340-
/// A collection of query string parameters that will be sent with every request. Useful in situations where you always need to pass a
335+
/// A collection of query string parameters that will be sent with every request. Useful in situations where you always
336+
/// need to pass a
341337
/// parameter e.g. an API key.
342338
/// </summary>
343-
public T GlobalQueryStringParameters(NameValueCollection queryStringParameters) => Assign(queryStringParameters, (a, v) => a._queryString.Add(v));
339+
public T GlobalQueryStringParameters(NameValueCollection queryStringParameters) =>
340+
Assign(queryStringParameters, (a, v) => a._queryString.Add(v));
344341

345342
/// <summary>
346-
/// A collection of headers that will be sent with every request. Useful in situations where you always need to pass a header e.g. a custom
343+
/// A collection of headers that will be sent with every request. Useful in situations where you always need to pass a
344+
/// header e.g. a custom
347345
/// auth header
348346
/// </summary>
349347
public T GlobalHeaders(NameValueCollection headers) => Assign(headers, (a, v) => a._headers.Add(v));
@@ -460,11 +458,14 @@ public T BasicAuthentication(string userName, string password) =>
460458
public T EnableHttpPipelining(bool enabled = true) => Assign(enabled, (a, v) => a._enableHttpPipelining = v);
461459

462460
/// <summary>
463-
/// Register a predicate to select which nodes that you want to execute API calls on. Note that sniffing requests omit this predicate and
461+
/// Register a predicate to select which nodes that you want to execute API calls on. Note that sniffing requests omit this
462+
/// predicate and
464463
/// always execute on all nodes.
465-
/// When using an <see cref="IConnectionPool" /> implementation that supports reseeding of nodes, this will default to omitting master only
464+
/// When using an <see cref="IConnectionPool" /> implementation that supports reseeding of nodes, this will default to
465+
/// omitting master only
466466
/// node from regular API calls.
467-
/// When using static or single node connection pooling it is assumed the list of node you instantiate the client with should be taken
467+
/// When using static or single node connection pooling it is assumed the list of node you instantiate the client with
468+
/// should be taken
468469
/// verbatim.
469470
/// </summary>
470471
/// <param name="predicate">Return true if you want the node to be used for API calls</param>
@@ -478,7 +479,8 @@ public T NodePredicate(Func<Node, bool> predicate)
478479

479480
/// <summary>
480481
/// Turns on settings that aid in debugging like DisableDirectStreaming() and PrettyJson()
481-
/// so that the original request and response JSON can be inspected. It also always asks the server for the full stack trace on errors
482+
/// so that the original request and response JSON can be inspected. It also always asks the server for the full stack
483+
/// trace on errors
482484
/// </summary>
483485
/// <param name="onRequestCompleted">
484486
/// An optional callback to be performed when the request completes. This will
@@ -519,19 +521,22 @@ public T ClientCertificates(X509CertificateCollection certificates) =>
519521
Assign(certificates, (a, v) => a._clientCertificates = v);
520522

521523
/// <summary>
522-
/// Use a <see cref="System.Security.Cryptography.X509Certificates.X509Certificate"/> to authenticate all HTTP requests. You can also set them on individual request using <see cref="RequestConfiguration.ClientCertificates" />
524+
/// Use a <see cref="System.Security.Cryptography.X509Certificates.X509Certificate" /> to authenticate all HTTP requests.
525+
/// You can also set them on individual request using <see cref="RequestConfiguration.ClientCertificates" />
523526
/// </summary>
524527
public T ClientCertificate(X509Certificate certificate) =>
525528
Assign(new X509Certificate2Collection { certificate }, (a, v) => a._clientCertificates = v);
526529

527530
/// <summary>
528-
/// Use a file path to a certificate to authenticate all HTTP requests. You can also set them on individual request using <see cref="RequestConfiguration.ClientCertificates" />
531+
/// Use a file path to a certificate to authenticate all HTTP requests. You can also set them on individual request using
532+
/// <see cref="RequestConfiguration.ClientCertificates" />
529533
/// </summary>
530534
public T ClientCertificate(string certificatePath) =>
531535
Assign(new X509Certificate2Collection { new X509Certificate(certificatePath) }, (a, v) => a._clientCertificates = v);
532536

533537
/// <summary>
534-
/// Configure the client to skip deserialization of certain status codes e.g: you run elasticsearch behind a proxy that returns a HTML for 401,
538+
/// Configure the client to skip deserialization of certain status codes e.g: you run elasticsearch behind a proxy that
539+
/// returns a HTML for 401,
535540
/// 500
536541
/// </summary>
537542
public T SkipDeserializationForStatusCodes(params int[] statusCodes) =>

src/Elasticsearch.Net/Configuration/Security/BasicAuthenticationCredentials.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22
{
33
public class BasicAuthenticationCredentials
44
{
5+
public BasicAuthenticationCredentials() { }
6+
7+
public BasicAuthenticationCredentials(string username, string password)
8+
{
9+
Username = username;
10+
Password = password;
11+
}
12+
513
public string Password { get; set; }
614
public string Username { get; set; }
715

0 commit comments

Comments
 (0)