From ffc9dd8ad5d1cc1fc353f5ccf535fd8f44ea8ad7 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Wed, 22 Apr 2020 14:16:03 +0200 Subject: [PATCH 1/8] Fix check for agressive connectionlimit In https://github.com/dotnet/runtime/issues/22366 we found that if HttpClient is using the curl handler it will lead to TCP connections bleeding. Our DefaultConnectionLimit is very restrictive if this is true. Our check however is too lenient and will default to true always on .NET core since netcoreapp still ships with CurlHandler as recent as `3.1.x` --- .../Configuration/ConnectionConfiguration.cs | 1235 +++++++++-------- 1 file changed, 618 insertions(+), 617 deletions(-) diff --git a/src/Elasticsearch.Net/Configuration/ConnectionConfiguration.cs b/src/Elasticsearch.Net/Configuration/ConnectionConfiguration.cs index d13c7345abb..1520dfa5def 100644 --- a/src/Elasticsearch.Net/Configuration/ConnectionConfiguration.cs +++ b/src/Elasticsearch.Net/Configuration/ConnectionConfiguration.cs @@ -1,617 +1,618 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Collections.Specialized; -using System.ComponentModel; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -#if DOTNETCORE -using System.Net.Http; -using System.Runtime.InteropServices; -#endif -using System.Net.Security; -using System.Reflection; -using System.Security; -using System.Security.Cryptography.X509Certificates; -using System.Threading; -using Elasticsearch.Net.Extensions; - -namespace Elasticsearch.Net -{ - /// - /// Allows you to control how behaves and where/how it connects to Elasticsearch - /// - public class ConnectionConfiguration : ConnectionConfiguration - { - /// - /// Detects whether we are running on .NET Core with CurlHandler. - /// If this is true, we will set a very restrictive - /// As the old curl based handler is known to bleed TCP connections: - /// https://github.com/dotnet/runtime/issues/22366 - /// - private static bool UsingCurlHandler - { - get - { -#if !DOTNETCORE - return false; -#else - var curlHandlerExists = typeof(HttpClientHandler).Assembly.GetType("System.Net.Http.CurlHandler") != null; - if (!curlHandlerExists) return false; - - var socketsHandlerExists = typeof(HttpClientHandler).Assembly.GetType("System.Net.Http.SocketsHttpHandler") != null; - // running on a .NET core version with CurlHandler, before the existence of SocketsHttpHandler. - // Must be using CurlHandler. - if (!socketsHandlerExists) return true; - - if (AppContext.TryGetSwitch("System.Net.Http.UseSocketsHttpHandler", out var isEnabled)) - return !isEnabled; - - var environmentVariable = - Environment.GetEnvironmentVariable("DOTNET_SYSTEM_NET_HTTP_USESOCKETSHTTPHANDLER"); - - // SocketsHandler exists and no environment variable exists to disable it. - // Must be using SocketsHandler and not CurlHandler - if (environmentVariable == null) return false; - - return environmentVariable.Equals("false", StringComparison.OrdinalIgnoreCase) || - environmentVariable.Equals("0"); -#endif - } - } - - /// - /// The default ping timeout. Defaults to 2 seconds - /// - public static readonly TimeSpan DefaultPingTimeout = TimeSpan.FromSeconds(2); - - /// - /// The default ping timeout when the connection is over HTTPS. Defaults to - /// 5 seconds - /// - public static readonly TimeSpan DefaultPingTimeoutOnSSL = TimeSpan.FromSeconds(5); - - /// - /// The default timeout before the client aborts a request to Elasticsearch. - /// Defaults to 1 minute - /// - public static readonly TimeSpan DefaultTimeout = TimeSpan.FromMinutes(1); - - /// - /// The default connection limit for both Elasticsearch.Net and Nest. Defaults to 80 -#if DOTNETCORE - /// Except for implementations based on curl, which defaults to -#endif - /// - public static readonly int DefaultConnectionLimit = UsingCurlHandler ? Environment.ProcessorCount : 80; - - /// - /// The default user agent for Elasticsearch.Net - /// - public static readonly string DefaultUserAgent = $"elasticsearch-net/{typeof(IConnectionConfigurationValues).Assembly.GetCustomAttribute().InformationalVersion} ({RuntimeInformation.OSDescription}; {RuntimeInformation.FrameworkDescription}; Elasticsearch.Net)"; - - /// - /// Creates a new instance of - /// - /// The root of the Elasticsearch node we want to connect to. Defaults to http://localhost:9200 - [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")] - public ConnectionConfiguration(Uri uri = null) - : this(new SingleNodeConnectionPool(uri ?? new Uri("http://localhost:9200"))) { } - - /// - /// Sets up the client to communicate to Elastic Cloud using , - /// documentation for more information on how to obtain your Cloud Id - /// - public ConnectionConfiguration(string cloudId, BasicAuthenticationCredentials credentials) : this(new CloudConnectionPool(cloudId, credentials)) { } - - /// - /// Sets up the client to communicate to Elastic Cloud using , - /// documentation for more information on how to obtain your Cloud Id - /// - public ConnectionConfiguration(string cloudId, ApiKeyAuthenticationCredentials credentials) : this(new CloudConnectionPool(cloudId, credentials)) { } - - /// - /// Creates a new instance of - /// - /// A connection pool implementation that tells the client what nodes are available - public ConnectionConfiguration(IConnectionPool connectionPool) - // ReSharper disable once IntroduceOptionalParameters.Global - : this(connectionPool, null, null) { } - - /// - /// Creates a new instance of - /// - /// A connection pool implementation that tells the client what nodes are available - /// An connection implementation that can make API requests - public ConnectionConfiguration(IConnectionPool connectionPool, IConnection connection) - // ReSharper disable once IntroduceOptionalParameters.Global - : this(connectionPool, connection, null) { } - - /// - /// Creates a new instance of - /// - /// A connection pool implementation that tells the client what nodes are available - /// A serializer implementation used to serialize requests and deserialize responses - public ConnectionConfiguration(IConnectionPool connectionPool, IElasticsearchSerializer serializer) - : this(connectionPool, null, serializer) { } - - /// - /// Creates a new instance of - /// - /// A connection pool implementation that tells the client what nodes are available - /// An connection implementation that can make API requests - /// A serializer implementation used to serialize requests and deserialize responses - public ConnectionConfiguration(IConnectionPool connectionPool, IConnection connection, IElasticsearchSerializer serializer) - : base(connectionPool, connection, serializer) { } - - } - - [Browsable(false)] - [EditorBrowsable(EditorBrowsableState.Never)] - public abstract class ConnectionConfiguration : IConnectionConfigurationValues - where T : ConnectionConfiguration - { - private readonly IConnection _connection; - private readonly IConnectionPool _connectionPool; - private readonly NameValueCollection _headers = new NameValueCollection(); - private readonly NameValueCollection _queryString = new NameValueCollection(); - private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); - private readonly ElasticsearchUrlFormatter _urlFormatter; - - private BasicAuthenticationCredentials _basicAuthCredentials; - private ApiKeyAuthenticationCredentials _apiKeyAuthCredentials; - private X509CertificateCollection _clientCertificates; - private Action _completedRequestHandler = DefaultCompletedRequestHandler; - private int _connectionLimit; - private TimeSpan? _deadTimeout; - private bool _disableAutomaticProxyDetection; - private bool _disableDirectStreaming; - private bool _disablePings; - private bool _enableHttpCompression; - private bool _enableHttpPipelining = true; - private TimeSpan? _keepAliveInterval; - private TimeSpan? _keepAliveTime; - private TimeSpan? _maxDeadTimeout; - private int? _maxRetries; - private TimeSpan? _maxRetryTimeout; - private Func _nodePredicate = DefaultNodePredicate; - private Action _onRequestDataCreated = DefaultRequestDataCreated; - private TimeSpan? _pingTimeout; - private bool _prettyJson; - private string _proxyAddress; - private SecureString _proxyPassword; - private string _proxyUsername; - private TimeSpan _requestTimeout; - private Func _serverCertificateValidationCallback; - private IReadOnlyCollection _skipDeserializationForStatusCodes = new ReadOnlyCollection(new int[] { }); - private TimeSpan? _sniffLifeSpan; - private bool _sniffOnConnectionFault; - private bool _sniffOnStartup; - private bool _throwExceptions; - private bool _transferEncodingChunked; - private IMemoryStreamFactory _memoryStreamFactory = RecyclableMemoryStreamFactory.Default; - - private string _userAgent = ConnectionConfiguration.DefaultUserAgent; - private Func _statusCodeToResponseSuccess; - - protected ConnectionConfiguration(IConnectionPool connectionPool, IConnection connection, IElasticsearchSerializer requestResponseSerializer) - { - _connectionPool = connectionPool; - _connection = connection ?? new HttpConnection(); - var serializer = requestResponseSerializer ?? new LowLevelRequestResponseSerializer(); - UseThisRequestResponseSerializer = new DiagnosticsSerializerProxy(serializer); - - _connectionLimit = ConnectionConfiguration.DefaultConnectionLimit; - _requestTimeout = ConnectionConfiguration.DefaultTimeout; - _sniffOnConnectionFault = true; - _sniffOnStartup = true; - _sniffLifeSpan = TimeSpan.FromHours(1); - if (_connectionPool.SupportsReseeding) - _nodePredicate = DefaultReseedableNodePredicate; - - _urlFormatter = new ElasticsearchUrlFormatter(this); - _statusCodeToResponseSuccess = (m, i) => HttpStatusCodeClassifier(m, i); - - if (connectionPool is CloudConnectionPool cloudPool) - { - _basicAuthCredentials = cloudPool.BasicCredentials; - _apiKeyAuthCredentials = cloudPool.ApiKeyCredentials; - _enableHttpCompression = true; - } - - } - - protected IElasticsearchSerializer UseThisRequestResponseSerializer { get; set; } - BasicAuthenticationCredentials IConnectionConfigurationValues.BasicAuthenticationCredentials => _basicAuthCredentials; - ApiKeyAuthenticationCredentials IConnectionConfigurationValues.ApiKeyAuthenticationCredentials => _apiKeyAuthCredentials; - SemaphoreSlim IConnectionConfigurationValues.BootstrapLock => _semaphore; - X509CertificateCollection IConnectionConfigurationValues.ClientCertificates => _clientCertificates; - IConnection IConnectionConfigurationValues.Connection => _connection; - int IConnectionConfigurationValues.ConnectionLimit => _connectionLimit; - IConnectionPool IConnectionConfigurationValues.ConnectionPool => _connectionPool; - TimeSpan? IConnectionConfigurationValues.DeadTimeout => _deadTimeout; - bool IConnectionConfigurationValues.DisableAutomaticProxyDetection => _disableAutomaticProxyDetection; - bool IConnectionConfigurationValues.DisableDirectStreaming => _disableDirectStreaming; - bool IConnectionConfigurationValues.DisablePings => _disablePings; - bool IConnectionConfigurationValues.EnableHttpCompression => _enableHttpCompression; - NameValueCollection IConnectionConfigurationValues.Headers => _headers; - bool IConnectionConfigurationValues.HttpPipeliningEnabled => _enableHttpPipelining; - TimeSpan? IConnectionConfigurationValues.KeepAliveInterval => _keepAliveInterval; - TimeSpan? IConnectionConfigurationValues.KeepAliveTime => _keepAliveTime; - TimeSpan? IConnectionConfigurationValues.MaxDeadTimeout => _maxDeadTimeout; - int? IConnectionConfigurationValues.MaxRetries => _maxRetries; - TimeSpan? IConnectionConfigurationValues.MaxRetryTimeout => _maxRetryTimeout; - IMemoryStreamFactory IConnectionConfigurationValues.MemoryStreamFactory => _memoryStreamFactory; - - Func IConnectionConfigurationValues.NodePredicate => _nodePredicate; - Action IConnectionConfigurationValues.OnRequestCompleted => _completedRequestHandler; - Action IConnectionConfigurationValues.OnRequestDataCreated => _onRequestDataCreated; - TimeSpan? IConnectionConfigurationValues.PingTimeout => _pingTimeout; - bool IConnectionConfigurationValues.PrettyJson => _prettyJson; - string IConnectionConfigurationValues.ProxyAddress => _proxyAddress; - SecureString IConnectionConfigurationValues.ProxyPassword => _proxyPassword; - string IConnectionConfigurationValues.ProxyUsername => _proxyUsername; - NameValueCollection IConnectionConfigurationValues.QueryStringParameters => _queryString; - IElasticsearchSerializer IConnectionConfigurationValues.RequestResponseSerializer => UseThisRequestResponseSerializer; - TimeSpan IConnectionConfigurationValues.RequestTimeout => _requestTimeout; - - Func IConnectionConfigurationValues.ServerCertificateValidationCallback => - _serverCertificateValidationCallback; - - IReadOnlyCollection IConnectionConfigurationValues.SkipDeserializationForStatusCodes => _skipDeserializationForStatusCodes; - TimeSpan? IConnectionConfigurationValues.SniffInformationLifeSpan => _sniffLifeSpan; - bool IConnectionConfigurationValues.SniffsOnConnectionFault => _sniffOnConnectionFault; - bool IConnectionConfigurationValues.SniffsOnStartup => _sniffOnStartup; - bool IConnectionConfigurationValues.ThrowExceptions => _throwExceptions; - ElasticsearchUrlFormatter IConnectionConfigurationValues.UrlFormatter => _urlFormatter; - string IConnectionConfigurationValues.UserAgent => _userAgent; - Func IConnectionConfigurationValues.StatusCodeToResponseSuccess => _statusCodeToResponseSuccess; - bool IConnectionConfigurationValues.TransferEncodingChunked => _transferEncodingChunked; - - void IDisposable.Dispose() => DisposeManagedResources(); - - private static void DefaultCompletedRequestHandler(IApiCallDetails response) { } - - private static void DefaultRequestDataCreated(RequestData response) { } - - /// - /// The default predicate for implementations that return true for - /// - /// in which case master only nodes are excluded from API calls. - /// - private static bool DefaultReseedableNodePredicate(Node node) => !node.MasterOnlyNode; - - private static bool DefaultNodePredicate(Node node) => true; - - protected T Assign(TValue value, Action assigner) => Fluent.Assign((T)this, value, assigner); - - /// - /// Sets the keep-alive option on a TCP connection. - /// For Desktop CLR, sets ServicePointManager.SetTcpKeepAlive - /// - /// Specifies the timeout with no activity until the first keep-alive packet is sent. - /// - /// Specifies the interval between when successive keep-alive packets are sent if no acknowledgement is - /// received. - /// - public T EnableTcpKeepAlive(TimeSpan keepAliveTime, TimeSpan keepAliveInterval) => - Assign(keepAliveTime, (a, v) => a._keepAliveTime = v) - .Assign(keepAliveInterval, (a, v) => a._keepAliveInterval = v); - - /// The maximum number of retries for a given request - public T MaximumRetries(int maxRetries) => Assign(maxRetries, (a, v) => a._maxRetries = v); - - /// - /// Limits the number of concurrent connections that can be opened to an endpoint. Defaults to 80 for all IConnection - /// implementations that are not based on System.Net.Http.CurlHandler. For those based on System.Net.Http.CurlHandler, defaults - /// to Environment.ProcessorCount. - /// - /// For Desktop CLR, this setting applies to the DefaultConnectionLimit property on the ServicePointManager object when creating - /// ServicePoint objects, affecting the default implementation. - /// - /// - /// For Core CLR, this setting applies to the MaxConnectionsPerServer property on the HttpClientHandler instances used by the HttpClient - /// inside the default implementation - /// - /// - /// The connection limit, a value lower then 0 will cause the connection limit not to be set at all - public T ConnectionLimit(int connectionLimit) => Assign(connectionLimit, (a, v) => a._connectionLimit = v); - - /// - /// Enables resniffing of the cluster when a call fails, if the connection pool supports reseeding. Defaults to true - /// - public T SniffOnConnectionFault(bool sniffsOnConnectionFault = true) => - Assign(sniffsOnConnectionFault, (a, v) => a._sniffOnConnectionFault = v); - - /// - /// Enables sniffing on first usage of a connection pool if that pool supports reseeding. Defaults to true - /// - public T SniffOnStartup(bool sniffsOnStartup = true) => Assign(sniffsOnStartup, (a, v) => a._sniffOnStartup = v); - - /// - /// Set the duration after which a cluster state is considered stale and a sniff should be performed again. - /// An has to signal it supports reseeding, otherwise sniffing will never happen. - /// Defaults to 1 hour. - /// Set to null to disable completely. Sniffing will only ever happen on ConnectionPools that return true for SupportsReseeding - /// - /// The duration a clusterstate is considered fresh, set to null to disable periodic sniffing - public T SniffLifeSpan(TimeSpan? sniffLifeSpan) => Assign(sniffLifeSpan, (a, v) => a._sniffLifeSpan = v); - - /// - /// Enables gzip compressed requests and responses. - /// IMPORTANT: You need to configure http compression on Elasticsearch to be able to use this - /// https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-http.html - /// - public T EnableHttpCompression(bool enabled = true) => Assign(enabled, (a, v) => a._enableHttpCompression = v); - - /// - /// Disables the automatic detection of a proxy - /// - public T DisableAutomaticProxyDetection(bool disable = true) => Assign(disable, (a, v) => a._disableAutomaticProxyDetection = v); - - /// - /// Instead of following a c/go like error checking on response.IsValid do throw an exception (except when is false) - /// on the client when a call resulted in an exception on either the client or the Elasticsearch server. - /// Reasons for such exceptions could be search parser errors, index missing exceptions, etc... - /// - public T ThrowExceptions(bool alwaysThrow = true) => Assign(alwaysThrow, (a, v) => a._throwExceptions = v); - - /// - /// 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 - /// 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. - /// You can disable these pings globally here if you rather have it fail on the possible slower original request - /// - public T DisablePing(bool disable = true) => Assign(disable, (a, v) => a._disablePings = v); - - /// - /// A collection of query string parameters that will be sent with every request. Useful in situations where you always need to pass a - /// parameter e.g. an API key. - /// - public T GlobalQueryStringParameters(NameValueCollection queryStringParameters) => Assign(queryStringParameters, (a, v) => a._queryString.Add(v)); - - /// - /// 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 - /// auth header - /// - public T GlobalHeaders(NameValueCollection headers) => Assign(headers, (a, v) => a._headers.Add(v)); - - /// - /// Sets the default timeout in milliseconds for each request to Elasticsearch. Defaults to 60 seconds. - /// NOTE: You can set this to a high value here, and specify a timeout on Elasticsearch's side. - /// - /// time out in milliseconds - public T RequestTimeout(TimeSpan timeout) => Assign(timeout, (a, v) => a._requestTimeout = v); - - /// - /// Sets the default ping timeout in milliseconds for ping requests, which are used - /// to determine whether a node is alive. Pings should fail as fast as possible. - /// - /// The ping timeout in milliseconds defaults to 1000, or 2000 if using SSL. - public T PingTimeout(TimeSpan timeout) => Assign(timeout, (a, v) => a._pingTimeout = v); - - /// - /// Sets the default dead timeout factor when a node has been marked dead. - /// - /// Some connection pools may use a flat timeout whilst others take this factor and increase it exponentially - /// - public T DeadTimeout(TimeSpan timeout) => Assign(timeout, (a, v) => a._deadTimeout = v); - - /// - /// Sets the maximum time a node can be marked dead. - /// Different implementations of may choose a different default. - /// - /// The timeout in milliseconds - public T MaxDeadTimeout(TimeSpan timeout) => Assign(timeout, (a, v) => a._maxDeadTimeout = v); - - /// - /// Limits the total runtime, including retries, separately from - /// - /// When not specified, defaults to , which itself defaults to 60 seconds - /// - /// - public T MaxRetryTimeout(TimeSpan maxRetryTimeout) => Assign(maxRetryTimeout, (a, v) => a._maxRetryTimeout = v); - - /// - /// If your connection has to go through proxy, use this method to specify the proxy url - /// - public T Proxy(Uri proxyAddress, string username, string password) => - Assign(proxyAddress.ToString(), (a, v) => a._proxyAddress = v) - .Assign(username, (a, v) => a._proxyUsername = v) - .Assign(password, (a, v) => a._proxyPassword = v.CreateSecureString()); - - /// - /// If your connection has to go through proxy, use this method to specify the proxy url - /// - public T Proxy(Uri proxyAddress, string username, SecureString password) => - Assign(proxyAddress.ToString(), (a, v) => a._proxyAddress = v) - .Assign(username, (a, v) => a._proxyUsername = v) - .Assign(password, (a, v) => a._proxyPassword = v); - - /// - /// Forces all requests to have ?pretty=true querystring parameter appended, - /// causing Elasticsearch to return formatted JSON. - /// Also forces the client to send out formatted JSON. Defaults to false - /// - public T PrettyJson(bool b = true) => Assign(b, (a, v) => - { - a._prettyJson = v; - const string key = "pretty"; - if (!v && a._queryString[key] != null) a._queryString.Remove(key); - else if (v && a._queryString[key] == null) - a.GlobalQueryStringParameters(new NameValueCollection { { key, "true" } }); - }); - - /// - /// Forces all requests to have ?error_trace=true querystring parameter appended, - /// causing Elasticsearch to return stack traces as part of serialized exceptions - /// Defaults to false - /// - public T IncludeServerStackTraceOnError(bool b = true) => Assign(b, (a, v) => - { - const string key = "error_trace"; - if (!v && a._queryString[key] != null) a._queryString.Remove(key); - else if (v && a._queryString[key] == null) - a.GlobalQueryStringParameters(new NameValueCollection { { key, "true" } }); - }); - - /// - /// Ensures the response bytes are always available on the - /// - /// IMPORTANT: Depending on the registered serializer, - /// this may cause the response to be buffered in memory first, potentially affecting performance. - /// - /// - public T DisableDirectStreaming(bool b = true) => Assign(b, (a, v) => a._disableDirectStreaming = v); - - /// - /// Registers an that is called when a response is received from Elasticsearch. - /// This can be useful for implementing custom logging. - /// Multiple callbacks can be registered by calling this multiple times - /// - public T OnRequestCompleted(Action handler) => - Assign(handler, (a, v) => a._completedRequestHandler += v ?? DefaultCompletedRequestHandler); - - /// - /// Registers an that is called when is created. - /// Multiple callbacks can be registered by calling this multiple times - /// - public T OnRequestDataCreated(Action handler) => - Assign(handler, (a, v) => a._onRequestDataCreated += v ?? DefaultRequestDataCreated); - - /// - /// Basic Authentication credentials to send with all requests to Elasticsearch - /// - public T BasicAuthentication(string username, string password) => - Assign(new BasicAuthenticationCredentials(username, password), (a, v) => a._basicAuthCredentials = v); - - /// - /// Basic Authentication credentials to send with all requests to Elasticsearch - /// - public T BasicAuthentication(string username, SecureString password) => - Assign(new BasicAuthenticationCredentials(username, password), (a, v) => a._basicAuthCredentials = v); - - /// - /// Api Key to send with all requests to Elasticsearch - /// - public T ApiKeyAuthentication(string id, SecureString apiKey) => - Assign(new ApiKeyAuthenticationCredentials(id, apiKey), (a, v) => a._apiKeyAuthCredentials = v); - - /// - /// Api Key to send with all requests to Elasticsearch - /// - public T ApiKeyAuthentication(string id, string apiKey) => - Assign(new ApiKeyAuthenticationCredentials(id, apiKey), (a, v) => a._apiKeyAuthCredentials = v); - - /// - /// Api Key to send with all requests to Elasticsearch - /// - public T ApiKeyAuthentication(ApiKeyAuthenticationCredentials credentials) => - Assign(credentials, (a, v) => a._apiKeyAuthCredentials = v); - - /// - /// Allows for requests to be pipelined. http://en.wikipedia.org/wiki/HTTP_pipelining - /// NOTE: HTTP pipelining must also be enabled in Elasticsearch for this to work properly. - /// - public T EnableHttpPipelining(bool enabled = true) => Assign(enabled, (a, v) => a._enableHttpPipelining = v); - - /// - /// Register a predicate to select which nodes that you want to execute API calls on. Note that sniffing requests omit this predicate and - /// always execute on all nodes. - /// When using an implementation that supports reseeding of nodes, this will default to omitting master only - /// node from regular API calls. - /// When using static or single node connection pooling it is assumed the list of node you instantiate the client with should be taken - /// verbatim. - /// - /// Return true if you want the node to be used for API calls - public T NodePredicate(Func predicate) => Assign(predicate ?? DefaultNodePredicate, (a, v) => a._nodePredicate = v); - - /// - /// Turns on settings that aid in debugging like DisableDirectStreaming() and PrettyJson() - /// so that the original request and response JSON can be inspected. It also always asks the server for the full stack trace on errors - /// - /// - /// An optional callback to be performed when the request completes. This will - /// not overwrite the global OnRequestCompleted callback that is set directly on - /// ConnectionSettings. If no callback is passed, DebugInformation from the response - /// will be written to the debug output by default. - /// - public T EnableDebugMode(Action onRequestCompleted = null) => - PrettyJson() - .IncludeServerStackTraceOnError() - .DisableDirectStreaming() - .Assign(onRequestCompleted, (a, v) => - { - var originalCompletedRequestHandler = _completedRequestHandler; - var debugCompletedRequestHandler = v ?? (d => Debug.WriteLine(d.DebugInformation)); - _completedRequestHandler = d => - { - originalCompletedRequestHandler?.Invoke(d); - debugCompletedRequestHandler.Invoke(d); - }; - }); - - /// - /// Register a ServerCertificateValidationCallback, this is called per endpoint until it returns true. - /// After this callback returns true that endpoint is validated for the lifetime of the ServiceEndpoint - /// for that host. - /// - public T ServerCertificateValidationCallback(Func callback) => - Assign(callback, (a, v) => a._serverCertificateValidationCallback = v); - - /// - /// Use the following certificates to authenticate all HTTP requests. You can also set them on individual - /// request using - /// - public T ClientCertificates(X509CertificateCollection certificates) => - Assign(certificates, (a, v) => a._clientCertificates = v); - - /// - /// Use a to authenticate all HTTP requests. You can also set them on individual request using - /// - public T ClientCertificate(X509Certificate certificate) => - Assign(new X509Certificate2Collection { certificate }, (a, v) => a._clientCertificates = v); - - /// - /// Use a file path to a certificate to authenticate all HTTP requests. You can also set them on individual request using - /// - public T ClientCertificate(string certificatePath) => - Assign(new X509Certificate2Collection { new X509Certificate(certificatePath) }, (a, v) => a._clientCertificates = v); - - /// - /// Configure the client to skip deserialization of certain status codes e.g: you run Elasticsearch behind a proxy that returns a HTML for 401, - /// 500 - /// - public T SkipDeserializationForStatusCodes(params int[] statusCodes) => - Assign(new ReadOnlyCollection(statusCodes), (a, v) => a._skipDeserializationForStatusCodes = v); - - /// - /// The user agent string to send with requests. Useful for debugging purposes to understand client and framework - /// versions that initiate requests to Elasticsearch - /// - public T UserAgent(string userAgent) => Assign(userAgent, (a, v) => a._userAgent = v); - - /// - /// Whether the request should be sent with chunked Transfer-Encoding. Default is false - /// - public T TransferEncodingChunked(bool transferEncodingChunked = true) => Assign(transferEncodingChunked, (a, v) => a._transferEncodingChunked = v); - - /// - /// The memory stream factory to use, defaults to - /// - public T MemoryStreamFactory(IMemoryStreamFactory memoryStreamFactory) => Assign(memoryStreamFactory, (a, v) => a._memoryStreamFactory = v); - - protected virtual void DisposeManagedResources() - { - _connectionPool?.Dispose(); - _connection?.Dispose(); - _semaphore?.Dispose(); - _proxyPassword?.Dispose(); - _basicAuthCredentials?.Dispose(); - _apiKeyAuthCredentials?.Dispose(); - } - - protected virtual bool HttpStatusCodeClassifier(HttpMethod method, int statusCode) => - statusCode >= 200 && statusCode < 300; - - } -} +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Net; +#if DOTNETCORE +using System.Net.Http; +using System.Runtime.InteropServices; +#endif +using System.Net.Security; +using System.Reflection; +using System.Security; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using Elasticsearch.Net.Extensions; + +namespace Elasticsearch.Net +{ + /// + /// Allows you to control how behaves and where/how it connects to Elasticsearch + /// + public class ConnectionConfiguration : ConnectionConfiguration + { + /// + /// Detects whether we are running on .NET Core without SocketsHttpHandler existing or being enabled + /// If this is true we will set a very restrictive + /// As the old curl based handler is known to bleed TCP connections: + /// https://github.com/dotnet/runtime/issues/22366 + /// + private static bool UsingCurlHandler + { + get + { +#if !DOTNETCORE + return false; +#else + var curlHandlerExists = typeof(HttpClientHandler).Assembly.GetType("System.Net.Http.CurlHandler") != null; + if (!curlHandlerExists) return false; + + var socketsHandlerExists = typeof(HttpClientHandler).Assembly.GetType("System.Net.Http.SocketsHttpHandler") != null; + // running on a .NET core version with CurlHandler, before the existence of SocketsHttpHandler. + // Must be using CurlHandler. + if (!socketsHandlerExists) return true; + + if (AppContext.TryGetSwitch("System.Net.Http.UseSocketsHttpHandler", out var isEnabled)) + return !isEnabled; + + var environmentVariable = + Environment.GetEnvironmentVariable("DOTNET_SYSTEM_NET_HTTP_USESOCKETSHTTPHANDLER"); + + // SocketsHandler exists and no environment variable exists to disable it. + // Must be using SocketsHandler and not CurlHandler + if (environmentVariable == null) return false; + + return environmentVariable.Equals("false", StringComparison.OrdinalIgnoreCase) || + environmentVariable.Equals("0"); +#endif + } + } + + /// + /// The default ping timeout. Defaults to 2 seconds + /// + public static readonly TimeSpan DefaultPingTimeout = TimeSpan.FromSeconds(2); + + /// + /// The default ping timeout when the connection is over HTTPS. Defaults to + /// 5 seconds + /// + public static readonly TimeSpan DefaultPingTimeoutOnSSL = TimeSpan.FromSeconds(5); + + /// + /// The default timeout before the client aborts a request to Elasticsearch. + /// Defaults to 1 minute + /// + public static readonly TimeSpan DefaultTimeout = TimeSpan.FromMinutes(1); + + /// + /// The default connection limit for both Elasticsearch.Net and Nest. Defaults to 80 +#if DOTNETCORE + /// Except for implementations based on curl, which defaults to +#endif + /// + public static readonly int DefaultConnectionLimit = UsingCurlHandler ? Environment.ProcessorCount : 80; + + /// + /// The default user agent for Elasticsearch.Net + /// + public static readonly string DefaultUserAgent = $"elasticsearch-net/{typeof(IConnectionConfigurationValues).Assembly.GetCustomAttribute().InformationalVersion} ({RuntimeInformation.OSDescription}; {RuntimeInformation.FrameworkDescription}; Elasticsearch.Net)"; + + /// + /// Creates a new instance of + /// + /// The root of the Elasticsearch node we want to connect to. Defaults to http://localhost:9200 + [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")] + public ConnectionConfiguration(Uri uri = null) + : this(new SingleNodeConnectionPool(uri ?? new Uri("http://localhost:9200"))) { } + + /// + /// Sets up the client to communicate to Elastic Cloud using , + /// documentation for more information on how to obtain your Cloud Id + /// + public ConnectionConfiguration(string cloudId, BasicAuthenticationCredentials credentials) : this(new CloudConnectionPool(cloudId, credentials)) { } + + /// + /// Sets up the client to communicate to Elastic Cloud using , + /// documentation for more information on how to obtain your Cloud Id + /// + public ConnectionConfiguration(string cloudId, ApiKeyAuthenticationCredentials credentials) : this(new CloudConnectionPool(cloudId, credentials)) { } + + /// + /// Creates a new instance of + /// + /// A connection pool implementation that tells the client what nodes are available + public ConnectionConfiguration(IConnectionPool connectionPool) + // ReSharper disable once IntroduceOptionalParameters.Global + : this(connectionPool, null, null) { } + + /// + /// Creates a new instance of + /// + /// A connection pool implementation that tells the client what nodes are available + /// An connection implementation that can make API requests + public ConnectionConfiguration(IConnectionPool connectionPool, IConnection connection) + // ReSharper disable once IntroduceOptionalParameters.Global + : this(connectionPool, connection, null) { } + + /// + /// Creates a new instance of + /// + /// A connection pool implementation that tells the client what nodes are available + /// A serializer implementation used to serialize requests and deserialize responses + public ConnectionConfiguration(IConnectionPool connectionPool, IElasticsearchSerializer serializer) + : this(connectionPool, null, serializer) { } + + /// + /// Creates a new instance of + /// + /// A connection pool implementation that tells the client what nodes are available + /// An connection implementation that can make API requests + /// A serializer implementation used to serialize requests and deserialize responses + public ConnectionConfiguration(IConnectionPool connectionPool, IConnection connection, IElasticsearchSerializer serializer) + : base(connectionPool, connection, serializer) { } + + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public abstract class ConnectionConfiguration : IConnectionConfigurationValues + where T : ConnectionConfiguration + { + private readonly IConnection _connection; + private readonly IConnectionPool _connectionPool; + private readonly NameValueCollection _headers = new NameValueCollection(); + private readonly NameValueCollection _queryString = new NameValueCollection(); + private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); + private readonly ElasticsearchUrlFormatter _urlFormatter; + + private BasicAuthenticationCredentials _basicAuthCredentials; + private ApiKeyAuthenticationCredentials _apiKeyAuthCredentials; + private X509CertificateCollection _clientCertificates; + private Action _completedRequestHandler = DefaultCompletedRequestHandler; + private int _connectionLimit; + private TimeSpan? _deadTimeout; + private bool _disableAutomaticProxyDetection; + private bool _disableDirectStreaming; + private bool _disablePings; + private bool _enableHttpCompression; + private bool _enableHttpPipelining = true; + private TimeSpan? _keepAliveInterval; + private TimeSpan? _keepAliveTime; + private TimeSpan? _maxDeadTimeout; + private int? _maxRetries; + private TimeSpan? _maxRetryTimeout; + private Func _nodePredicate = DefaultNodePredicate; + private Action _onRequestDataCreated = DefaultRequestDataCreated; + private TimeSpan? _pingTimeout; + private bool _prettyJson; + private string _proxyAddress; + private SecureString _proxyPassword; + private string _proxyUsername; + private TimeSpan _requestTimeout; + private Func _serverCertificateValidationCallback; + private IReadOnlyCollection _skipDeserializationForStatusCodes = new ReadOnlyCollection(new int[] { }); + private TimeSpan? _sniffLifeSpan; + private bool _sniffOnConnectionFault; + private bool _sniffOnStartup; + private bool _throwExceptions; + private bool _transferEncodingChunked; + private IMemoryStreamFactory _memoryStreamFactory = RecyclableMemoryStreamFactory.Default; + + private string _userAgent = ConnectionConfiguration.DefaultUserAgent; + private Func _statusCodeToResponseSuccess; + + protected ConnectionConfiguration(IConnectionPool connectionPool, IConnection connection, IElasticsearchSerializer requestResponseSerializer) + { + _connectionPool = connectionPool; + _connection = connection ?? new HttpConnection(); + var serializer = requestResponseSerializer ?? new LowLevelRequestResponseSerializer(); + UseThisRequestResponseSerializer = new DiagnosticsSerializerProxy(serializer); + + _connectionLimit = ConnectionConfiguration.DefaultConnectionLimit; + _requestTimeout = ConnectionConfiguration.DefaultTimeout; + _sniffOnConnectionFault = true; + _sniffOnStartup = true; + _sniffLifeSpan = TimeSpan.FromHours(1); + if (_connectionPool.SupportsReseeding) + _nodePredicate = DefaultReseedableNodePredicate; + + _urlFormatter = new ElasticsearchUrlFormatter(this); + _statusCodeToResponseSuccess = (m, i) => HttpStatusCodeClassifier(m, i); + + if (connectionPool is CloudConnectionPool cloudPool) + { + _basicAuthCredentials = cloudPool.BasicCredentials; + _apiKeyAuthCredentials = cloudPool.ApiKeyCredentials; + _enableHttpCompression = true; + } + + } + + protected IElasticsearchSerializer UseThisRequestResponseSerializer { get; set; } + BasicAuthenticationCredentials IConnectionConfigurationValues.BasicAuthenticationCredentials => _basicAuthCredentials; + ApiKeyAuthenticationCredentials IConnectionConfigurationValues.ApiKeyAuthenticationCredentials => _apiKeyAuthCredentials; + SemaphoreSlim IConnectionConfigurationValues.BootstrapLock => _semaphore; + X509CertificateCollection IConnectionConfigurationValues.ClientCertificates => _clientCertificates; + IConnection IConnectionConfigurationValues.Connection => _connection; + int IConnectionConfigurationValues.ConnectionLimit => _connectionLimit; + IConnectionPool IConnectionConfigurationValues.ConnectionPool => _connectionPool; + TimeSpan? IConnectionConfigurationValues.DeadTimeout => _deadTimeout; + bool IConnectionConfigurationValues.DisableAutomaticProxyDetection => _disableAutomaticProxyDetection; + bool IConnectionConfigurationValues.DisableDirectStreaming => _disableDirectStreaming; + bool IConnectionConfigurationValues.DisablePings => _disablePings; + bool IConnectionConfigurationValues.EnableHttpCompression => _enableHttpCompression; + NameValueCollection IConnectionConfigurationValues.Headers => _headers; + bool IConnectionConfigurationValues.HttpPipeliningEnabled => _enableHttpPipelining; + TimeSpan? IConnectionConfigurationValues.KeepAliveInterval => _keepAliveInterval; + TimeSpan? IConnectionConfigurationValues.KeepAliveTime => _keepAliveTime; + TimeSpan? IConnectionConfigurationValues.MaxDeadTimeout => _maxDeadTimeout; + int? IConnectionConfigurationValues.MaxRetries => _maxRetries; + TimeSpan? IConnectionConfigurationValues.MaxRetryTimeout => _maxRetryTimeout; + IMemoryStreamFactory IConnectionConfigurationValues.MemoryStreamFactory => _memoryStreamFactory; + + Func IConnectionConfigurationValues.NodePredicate => _nodePredicate; + Action IConnectionConfigurationValues.OnRequestCompleted => _completedRequestHandler; + Action IConnectionConfigurationValues.OnRequestDataCreated => _onRequestDataCreated; + TimeSpan? IConnectionConfigurationValues.PingTimeout => _pingTimeout; + bool IConnectionConfigurationValues.PrettyJson => _prettyJson; + string IConnectionConfigurationValues.ProxyAddress => _proxyAddress; + SecureString IConnectionConfigurationValues.ProxyPassword => _proxyPassword; + string IConnectionConfigurationValues.ProxyUsername => _proxyUsername; + NameValueCollection IConnectionConfigurationValues.QueryStringParameters => _queryString; + IElasticsearchSerializer IConnectionConfigurationValues.RequestResponseSerializer => UseThisRequestResponseSerializer; + TimeSpan IConnectionConfigurationValues.RequestTimeout => _requestTimeout; + + Func IConnectionConfigurationValues.ServerCertificateValidationCallback => + _serverCertificateValidationCallback; + + IReadOnlyCollection IConnectionConfigurationValues.SkipDeserializationForStatusCodes => _skipDeserializationForStatusCodes; + TimeSpan? IConnectionConfigurationValues.SniffInformationLifeSpan => _sniffLifeSpan; + bool IConnectionConfigurationValues.SniffsOnConnectionFault => _sniffOnConnectionFault; + bool IConnectionConfigurationValues.SniffsOnStartup => _sniffOnStartup; + bool IConnectionConfigurationValues.ThrowExceptions => _throwExceptions; + ElasticsearchUrlFormatter IConnectionConfigurationValues.UrlFormatter => _urlFormatter; + string IConnectionConfigurationValues.UserAgent => _userAgent; + Func IConnectionConfigurationValues.StatusCodeToResponseSuccess => _statusCodeToResponseSuccess; + bool IConnectionConfigurationValues.TransferEncodingChunked => _transferEncodingChunked; + + void IDisposable.Dispose() => DisposeManagedResources(); + + private static void DefaultCompletedRequestHandler(IApiCallDetails response) { } + + private static void DefaultRequestDataCreated(RequestData response) { } + + /// + /// The default predicate for implementations that return true for + /// + /// in which case master only nodes are excluded from API calls. + /// + private static bool DefaultReseedableNodePredicate(Node node) => !node.MasterOnlyNode; + + private static bool DefaultNodePredicate(Node node) => true; + + protected T Assign(TValue value, Action assigner) => Fluent.Assign((T)this, value, assigner); + + /// + /// Sets the keep-alive option on a TCP connection. + /// For Desktop CLR, sets ServicePointManager.SetTcpKeepAlive + /// + /// Specifies the timeout with no activity until the first keep-alive packet is sent. + /// + /// Specifies the interval between when successive keep-alive packets are sent if no acknowledgement is + /// received. + /// + public T EnableTcpKeepAlive(TimeSpan keepAliveTime, TimeSpan keepAliveInterval) => + Assign(keepAliveTime, (a, v) => a._keepAliveTime = v) + .Assign(keepAliveInterval, (a, v) => a._keepAliveInterval = v); + + /// The maximum number of retries for a given request + public T MaximumRetries(int maxRetries) => Assign(maxRetries, (a, v) => a._maxRetries = v); + + /// + /// Limits the number of concurrent connections that can be opened to an endpoint. Defaults to 80 for all IConnection + /// implementations that are not based on System.Net.Http.CurlHandler. For those based on System.Net.Http.CurlHandler, defaults + /// to Environment.ProcessorCount. + /// + /// For Desktop CLR, this setting applies to the DefaultConnectionLimit property on the ServicePointManager object when creating + /// ServicePoint objects, affecting the default implementation. + /// + /// + /// For Core CLR, this setting applies to the MaxConnectionsPerServer property on the HttpClientHandler instances used by the HttpClient + /// inside the default implementation + /// + /// + /// The connection limit, a value lower then 0 will cause the connection limit not to be set at all + public T ConnectionLimit(int connectionLimit) => Assign(connectionLimit, (a, v) => a._connectionLimit = v); + + /// + /// Enables resniffing of the cluster when a call fails, if the connection pool supports reseeding. Defaults to true + /// + public T SniffOnConnectionFault(bool sniffsOnConnectionFault = true) => + Assign(sniffsOnConnectionFault, (a, v) => a._sniffOnConnectionFault = v); + + /// + /// Enables sniffing on first usage of a connection pool if that pool supports reseeding. Defaults to true + /// + public T SniffOnStartup(bool sniffsOnStartup = true) => Assign(sniffsOnStartup, (a, v) => a._sniffOnStartup = v); + + /// + /// Set the duration after which a cluster state is considered stale and a sniff should be performed again. + /// An has to signal it supports reseeding, otherwise sniffing will never happen. + /// Defaults to 1 hour. + /// Set to null to disable completely. Sniffing will only ever happen on ConnectionPools that return true for SupportsReseeding + /// + /// The duration a clusterstate is considered fresh, set to null to disable periodic sniffing + public T SniffLifeSpan(TimeSpan? sniffLifeSpan) => Assign(sniffLifeSpan, (a, v) => a._sniffLifeSpan = v); + + /// + /// Enables gzip compressed requests and responses. + /// IMPORTANT: You need to configure http compression on Elasticsearch to be able to use this + /// https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-http.html + /// + public T EnableHttpCompression(bool enabled = true) => Assign(enabled, (a, v) => a._enableHttpCompression = v); + + /// + /// Disables the automatic detection of a proxy + /// + public T DisableAutomaticProxyDetection(bool disable = true) => Assign(disable, (a, v) => a._disableAutomaticProxyDetection = v); + + /// + /// Instead of following a c/go like error checking on response.IsValid do throw an exception (except when is false) + /// on the client when a call resulted in an exception on either the client or the Elasticsearch server. + /// Reasons for such exceptions could be search parser errors, index missing exceptions, etc... + /// + public T ThrowExceptions(bool alwaysThrow = true) => Assign(alwaysThrow, (a, v) => a._throwExceptions = v); + + /// + /// 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 + /// 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. + /// You can disable these pings globally here if you rather have it fail on the possible slower original request + /// + public T DisablePing(bool disable = true) => Assign(disable, (a, v) => a._disablePings = v); + + /// + /// A collection of query string parameters that will be sent with every request. Useful in situations where you always need to pass a + /// parameter e.g. an API key. + /// + public T GlobalQueryStringParameters(NameValueCollection queryStringParameters) => Assign(queryStringParameters, (a, v) => a._queryString.Add(v)); + + /// + /// 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 + /// auth header + /// + public T GlobalHeaders(NameValueCollection headers) => Assign(headers, (a, v) => a._headers.Add(v)); + + /// + /// Sets the default timeout in milliseconds for each request to Elasticsearch. Defaults to 60 seconds. + /// NOTE: You can set this to a high value here, and specify a timeout on Elasticsearch's side. + /// + /// time out in milliseconds + public T RequestTimeout(TimeSpan timeout) => Assign(timeout, (a, v) => a._requestTimeout = v); + + /// + /// Sets the default ping timeout in milliseconds for ping requests, which are used + /// to determine whether a node is alive. Pings should fail as fast as possible. + /// + /// The ping timeout in milliseconds defaults to 1000, or 2000 if using SSL. + public T PingTimeout(TimeSpan timeout) => Assign(timeout, (a, v) => a._pingTimeout = v); + + /// + /// Sets the default dead timeout factor when a node has been marked dead. + /// + /// Some connection pools may use a flat timeout whilst others take this factor and increase it exponentially + /// + public T DeadTimeout(TimeSpan timeout) => Assign(timeout, (a, v) => a._deadTimeout = v); + + /// + /// Sets the maximum time a node can be marked dead. + /// Different implementations of may choose a different default. + /// + /// The timeout in milliseconds + public T MaxDeadTimeout(TimeSpan timeout) => Assign(timeout, (a, v) => a._maxDeadTimeout = v); + + /// + /// Limits the total runtime, including retries, separately from + /// + /// When not specified, defaults to , which itself defaults to 60 seconds + /// + /// + public T MaxRetryTimeout(TimeSpan maxRetryTimeout) => Assign(maxRetryTimeout, (a, v) => a._maxRetryTimeout = v); + + /// + /// If your connection has to go through proxy, use this method to specify the proxy url + /// + public T Proxy(Uri proxyAddress, string username, string password) => + Assign(proxyAddress.ToString(), (a, v) => a._proxyAddress = v) + .Assign(username, (a, v) => a._proxyUsername = v) + .Assign(password, (a, v) => a._proxyPassword = v.CreateSecureString()); + + /// + /// If your connection has to go through proxy, use this method to specify the proxy url + /// + public T Proxy(Uri proxyAddress, string username, SecureString password) => + Assign(proxyAddress.ToString(), (a, v) => a._proxyAddress = v) + .Assign(username, (a, v) => a._proxyUsername = v) + .Assign(password, (a, v) => a._proxyPassword = v); + + /// + /// Forces all requests to have ?pretty=true querystring parameter appended, + /// causing Elasticsearch to return formatted JSON. + /// Also forces the client to send out formatted JSON. Defaults to false + /// + public T PrettyJson(bool b = true) => Assign(b, (a, v) => + { + a._prettyJson = v; + const string key = "pretty"; + if (!v && a._queryString[key] != null) a._queryString.Remove(key); + else if (v && a._queryString[key] == null) + a.GlobalQueryStringParameters(new NameValueCollection { { key, "true" } }); + }); + + /// + /// Forces all requests to have ?error_trace=true querystring parameter appended, + /// causing Elasticsearch to return stack traces as part of serialized exceptions + /// Defaults to false + /// + public T IncludeServerStackTraceOnError(bool b = true) => Assign(b, (a, v) => + { + const string key = "error_trace"; + if (!v && a._queryString[key] != null) a._queryString.Remove(key); + else if (v && a._queryString[key] == null) + a.GlobalQueryStringParameters(new NameValueCollection { { key, "true" } }); + }); + + /// + /// Ensures the response bytes are always available on the + /// + /// IMPORTANT: Depending on the registered serializer, + /// this may cause the response to be buffered in memory first, potentially affecting performance. + /// + /// + public T DisableDirectStreaming(bool b = true) => Assign(b, (a, v) => a._disableDirectStreaming = v); + + /// + /// Registers an that is called when a response is received from Elasticsearch. + /// This can be useful for implementing custom logging. + /// Multiple callbacks can be registered by calling this multiple times + /// + public T OnRequestCompleted(Action handler) => + Assign(handler, (a, v) => a._completedRequestHandler += v ?? DefaultCompletedRequestHandler); + + /// + /// Registers an that is called when is created. + /// Multiple callbacks can be registered by calling this multiple times + /// + public T OnRequestDataCreated(Action handler) => + Assign(handler, (a, v) => a._onRequestDataCreated += v ?? DefaultRequestDataCreated); + + /// + /// Basic Authentication credentials to send with all requests to Elasticsearch + /// + public T BasicAuthentication(string username, string password) => + Assign(new BasicAuthenticationCredentials(username, password), (a, v) => a._basicAuthCredentials = v); + + /// + /// Basic Authentication credentials to send with all requests to Elasticsearch + /// + public T BasicAuthentication(string username, SecureString password) => + Assign(new BasicAuthenticationCredentials(username, password), (a, v) => a._basicAuthCredentials = v); + + /// + /// Api Key to send with all requests to Elasticsearch + /// + public T ApiKeyAuthentication(string id, SecureString apiKey) => + Assign(new ApiKeyAuthenticationCredentials(id, apiKey), (a, v) => a._apiKeyAuthCredentials = v); + + /// + /// Api Key to send with all requests to Elasticsearch + /// + public T ApiKeyAuthentication(string id, string apiKey) => + Assign(new ApiKeyAuthenticationCredentials(id, apiKey), (a, v) => a._apiKeyAuthCredentials = v); + + /// + /// Api Key to send with all requests to Elasticsearch + /// + public T ApiKeyAuthentication(ApiKeyAuthenticationCredentials credentials) => + Assign(credentials, (a, v) => a._apiKeyAuthCredentials = v); + + /// + /// Allows for requests to be pipelined. http://en.wikipedia.org/wiki/HTTP_pipelining + /// NOTE: HTTP pipelining must also be enabled in Elasticsearch for this to work properly. + /// + public T EnableHttpPipelining(bool enabled = true) => Assign(enabled, (a, v) => a._enableHttpPipelining = v); + + /// + /// Register a predicate to select which nodes that you want to execute API calls on. Note that sniffing requests omit this predicate and + /// always execute on all nodes. + /// When using an implementation that supports reseeding of nodes, this will default to omitting master only + /// node from regular API calls. + /// When using static or single node connection pooling it is assumed the list of node you instantiate the client with should be taken + /// verbatim. + /// + /// Return true if you want the node to be used for API calls + public T NodePredicate(Func predicate) => Assign(predicate ?? DefaultNodePredicate, (a, v) => a._nodePredicate = v); + + /// + /// Turns on settings that aid in debugging like DisableDirectStreaming() and PrettyJson() + /// so that the original request and response JSON can be inspected. It also always asks the server for the full stack trace on errors + /// + /// + /// An optional callback to be performed when the request completes. This will + /// not overwrite the global OnRequestCompleted callback that is set directly on + /// ConnectionSettings. If no callback is passed, DebugInformation from the response + /// will be written to the debug output by default. + /// + public T EnableDebugMode(Action onRequestCompleted = null) => + PrettyJson() + .IncludeServerStackTraceOnError() + .DisableDirectStreaming() + .Assign(onRequestCompleted, (a, v) => + { + var originalCompletedRequestHandler = _completedRequestHandler; + var debugCompletedRequestHandler = v ?? (d => Debug.WriteLine(d.DebugInformation)); + _completedRequestHandler = d => + { + originalCompletedRequestHandler?.Invoke(d); + debugCompletedRequestHandler.Invoke(d); + }; + }); + + /// + /// Register a ServerCertificateValidationCallback, this is called per endpoint until it returns true. + /// After this callback returns true that endpoint is validated for the lifetime of the ServiceEndpoint + /// for that host. + /// + public T ServerCertificateValidationCallback(Func callback) => + Assign(callback, (a, v) => a._serverCertificateValidationCallback = v); + + /// + /// Use the following certificates to authenticate all HTTP requests. You can also set them on individual + /// request using + /// + public T ClientCertificates(X509CertificateCollection certificates) => + Assign(certificates, (a, v) => a._clientCertificates = v); + + /// + /// Use a to authenticate all HTTP requests. You can also set them on individual request using + /// + public T ClientCertificate(X509Certificate certificate) => + Assign(new X509Certificate2Collection { certificate }, (a, v) => a._clientCertificates = v); + + /// + /// Use a file path to a certificate to authenticate all HTTP requests. You can also set them on individual request using + /// + public T ClientCertificate(string certificatePath) => + Assign(new X509Certificate2Collection { new X509Certificate(certificatePath) }, (a, v) => a._clientCertificates = v); + + /// + /// Configure the client to skip deserialization of certain status codes e.g: you run Elasticsearch behind a proxy that returns a HTML for 401, + /// 500 + /// + public T SkipDeserializationForStatusCodes(params int[] statusCodes) => + Assign(new ReadOnlyCollection(statusCodes), (a, v) => a._skipDeserializationForStatusCodes = v); + + /// + /// The user agent string to send with requests. Useful for debugging purposes to understand client and framework + /// versions that initiate requests to Elasticsearch + /// + public T UserAgent(string userAgent) => Assign(userAgent, (a, v) => a._userAgent = v); + + /// + /// Whether the request should be sent with chunked Transfer-Encoding. Default is false + /// + public T TransferEncodingChunked(bool transferEncodingChunked = true) => Assign(transferEncodingChunked, (a, v) => a._transferEncodingChunked = v); + + /// + /// The memory stream factory to use, defaults to + /// + public T MemoryStreamFactory(IMemoryStreamFactory memoryStreamFactory) => Assign(memoryStreamFactory, (a, v) => a._memoryStreamFactory = v); + + protected virtual void DisposeManagedResources() + { + _connectionPool?.Dispose(); + _connection?.Dispose(); + _semaphore?.Dispose(); + _proxyPassword?.Dispose(); + _basicAuthCredentials?.Dispose(); + _apiKeyAuthCredentials?.Dispose(); + } + + protected virtual bool HttpStatusCodeClassifier(HttpMethod method, int statusCode) => + statusCode >= 200 && statusCode < 300; + + } +} From 1cacf375a4fe9c2bcad9397280d9c7410a484308 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Wed, 22 Apr 2020 14:19:15 +0200 Subject: [PATCH 2/8] remove unused reference --- src/Elasticsearch.Net/Configuration/ConnectionConfiguration.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Elasticsearch.Net/Configuration/ConnectionConfiguration.cs b/src/Elasticsearch.Net/Configuration/ConnectionConfiguration.cs index 1520dfa5def..d422dc378f2 100644 --- a/src/Elasticsearch.Net/Configuration/ConnectionConfiguration.cs +++ b/src/Elasticsearch.Net/Configuration/ConnectionConfiguration.cs @@ -1,11 +1,10 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Net; #if DOTNETCORE using System.Net.Http; using System.Runtime.InteropServices; From 1455f788e7da7c9b66601daad6590f98f8ac38a9 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Thu, 23 Apr 2020 11:39:24 +1000 Subject: [PATCH 3/8] Detect if CurlHandler exists This commit updates the implementation of determining if a conservative default connection limit should be set by also checking if CurlHandler exists when UseSocketsHttpHandler exists and is disabled. --- .../Configuration/ConnectionConfiguration.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Elasticsearch.Net/Configuration/ConnectionConfiguration.cs b/src/Elasticsearch.Net/Configuration/ConnectionConfiguration.cs index d422dc378f2..711abbe8362 100644 --- a/src/Elasticsearch.Net/Configuration/ConnectionConfiguration.cs +++ b/src/Elasticsearch.Net/Configuration/ConnectionConfiguration.cs @@ -24,8 +24,8 @@ namespace Elasticsearch.Net public class ConnectionConfiguration : ConnectionConfiguration { /// - /// Detects whether we are running on .NET Core without SocketsHttpHandler existing or being enabled - /// If this is true we will set a very restrictive + /// Detects whether we are running on .NET Core with CurlHandler. + /// If this is true, we will set a very restrictive /// As the old curl based handler is known to bleed TCP connections: /// https://github.com/dotnet/runtime/issues/22366 /// From fb40e0d91dfd191af5174f2a942f8bc5dfbb50c6 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Fri, 24 Apr 2020 14:50:28 +0200 Subject: [PATCH 4/8] initial commit --- .../ActiveHandlerTrackingEntry.cs | 81 ++++++ .../ExpiredHandlerTrackingEntry.cs | 33 +++ .../LifetimeTrackingHttpMessageHandler.cs | 24 ++ .../RequestDataHttpClientFactory.cs | 253 ++++++++++++++++++ .../HandlerTracking/ValueStopWatch.cs | 40 +++ .../Connection/HttpConnection.cs | 35 +-- tests/Tests.ScratchPad/Program.cs | 8 + .../Connection/HttpConnectionTests.cs | 9 +- 8 files changed, 452 insertions(+), 31 deletions(-) create mode 100644 src/Elasticsearch.Net/Connection/HandlerTracking/ActiveHandlerTrackingEntry.cs create mode 100644 src/Elasticsearch.Net/Connection/HandlerTracking/ExpiredHandlerTrackingEntry.cs create mode 100644 src/Elasticsearch.Net/Connection/HandlerTracking/LifetimeTrackingHttpMessageHandler.cs create mode 100644 src/Elasticsearch.Net/Connection/HandlerTracking/RequestDataHttpClientFactory.cs create mode 100644 src/Elasticsearch.Net/Connection/HandlerTracking/ValueStopWatch.cs diff --git a/src/Elasticsearch.Net/Connection/HandlerTracking/ActiveHandlerTrackingEntry.cs b/src/Elasticsearch.Net/Connection/HandlerTracking/ActiveHandlerTrackingEntry.cs new file mode 100644 index 00000000000..4b06c0b6c8b --- /dev/null +++ b/src/Elasticsearch.Net/Connection/HandlerTracking/ActiveHandlerTrackingEntry.cs @@ -0,0 +1,81 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if DOTNETCORE +using System; +using System.Diagnostics; +using System.Threading; + +namespace Elasticsearch.Net +{ + // Thread-safety: We treat this class as immutable except for the timer. Creating a new object + // for the 'expiry' pool simplifies the threading requirements significantly. + internal class ActiveHandlerTrackingEntry + { + private static readonly TimerCallback TimerCallback = (s) => ((ActiveHandlerTrackingEntry)s).Timer_Tick(); + private readonly object _lock; + private bool _timerInitialized; + private Timer _timer; + private TimerCallback _callback; + + public ActiveHandlerTrackingEntry( + int key, + LifetimeTrackingHttpMessageHandler handler, + TimeSpan lifetime) + { + Key = key; + Handler = handler; + Lifetime = lifetime; + + _lock = new object(); + } + + public LifetimeTrackingHttpMessageHandler Handler { get; private set; } + + public TimeSpan Lifetime { get; } + + public int Key { get; } + + public void StartExpiryTimer(TimerCallback callback) + { + if (Lifetime == Timeout.InfiniteTimeSpan) return; + + if (Volatile.Read(ref _timerInitialized)) return; + + StartExpiryTimerSlow(callback); + } + + private void StartExpiryTimerSlow(TimerCallback callback) + { + Debug.Assert(Lifetime != Timeout.InfiniteTimeSpan); + + lock (_lock) + { + if (Volatile.Read(ref _timerInitialized)) + return; + + _callback = callback; + _timer = NonCapturingTimer.Create(TimerCallback, this, Lifetime, Timeout.InfiniteTimeSpan); + _timerInitialized = true; + } + } + + private void Timer_Tick() + { + Debug.Assert(_callback != null); + Debug.Assert(_timer != null); + + lock (_lock) + { + if (_timer == null) return; + + _timer.Dispose(); + _timer = null; + + _callback(this); + } + } + } +} +#endif diff --git a/src/Elasticsearch.Net/Connection/HandlerTracking/ExpiredHandlerTrackingEntry.cs b/src/Elasticsearch.Net/Connection/HandlerTracking/ExpiredHandlerTrackingEntry.cs new file mode 100644 index 00000000000..185e4ab8a51 --- /dev/null +++ b/src/Elasticsearch.Net/Connection/HandlerTracking/ExpiredHandlerTrackingEntry.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if DOTNETCORE +using System; +using System.Net.Http; + +namespace Elasticsearch.Net +{ + // Thread-safety: This class is immutable + internal class ExpiredHandlerTrackingEntry + { + private readonly WeakReference _livenessTracker; + + // IMPORTANT: don't cache a reference to `other` or `other.Handler` here. + // We need to allow it to be GC'ed. + public ExpiredHandlerTrackingEntry(ActiveHandlerTrackingEntry other) + { + Key = other.Key; + + _livenessTracker = new WeakReference(other.Handler); + InnerHandler = other.Handler.InnerHandler; + } + + public bool CanDispose => !_livenessTracker.IsAlive; + + public HttpMessageHandler InnerHandler { get; } + + public int Key { get; } + } +} +#endif diff --git a/src/Elasticsearch.Net/Connection/HandlerTracking/LifetimeTrackingHttpMessageHandler.cs b/src/Elasticsearch.Net/Connection/HandlerTracking/LifetimeTrackingHttpMessageHandler.cs new file mode 100644 index 00000000000..e346b53ce4b --- /dev/null +++ b/src/Elasticsearch.Net/Connection/HandlerTracking/LifetimeTrackingHttpMessageHandler.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if DOTNETCORE +using System.Net.Http; + +namespace Elasticsearch.Net +{ + // This a marker used to check if the underlying handler should be disposed. HttpClients + // share a reference to an instance of this class, and when it goes out of scope the inner handler + // is eligible to be disposed. + internal class LifetimeTrackingHttpMessageHandler : DelegatingHandler + { + public LifetimeTrackingHttpMessageHandler(HttpMessageHandler innerHandler) + : base(innerHandler) { } + + protected override void Dispose(bool disposing) + { + // The lifetime of this is tracked separately by ActiveHandlerTrackingEntry + } + } +} +#endif diff --git a/src/Elasticsearch.Net/Connection/HandlerTracking/RequestDataHttpClientFactory.cs b/src/Elasticsearch.Net/Connection/HandlerTracking/RequestDataHttpClientFactory.cs new file mode 100644 index 00000000000..403b28779ee --- /dev/null +++ b/src/Elasticsearch.Net/Connection/HandlerTracking/RequestDataHttpClientFactory.cs @@ -0,0 +1,253 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if DOTNETCORE +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Xml; +using Elasticsearch.Net; + +namespace Elasticsearch.Net +{ + /// + /// Heavily modified version of DefaultHttpClientFactory, repurposed for RequestData + /// + internal class RequestDataHttpClientFactory : IDisposable + { + private readonly Func _createHttpClientHandler; + private static readonly TimerCallback CleanupCallback = (s) => ((RequestDataHttpClientFactory)s).CleanupTimer_Tick(); + private readonly Func> _entryFactory; + + // Default time of 10s for cleanup seems reasonable. + // Quick math: + // 10 distinct named clients * expiry time >= 1s = approximate cleanup queue of 100 items + // + // This seems frequent enough. We also rely on GC occurring to actually trigger disposal. + private readonly TimeSpan _defaultCleanupInterval = TimeSpan.FromSeconds(10); + + // We use a new timer for each regular cleanup cycle, protected with a lock. Note that this scheme + // doesn't give us anything to dispose, as the timer is started/stopped as needed. + // + // There's no need for the factory itself to be disposable. If you stop using it, eventually everything will + // get reclaimed. + private Timer _cleanupTimer; + private readonly object _cleanupTimerLock; + private readonly object _cleanupActiveLock; + + // Collection of 'active' handlers. + // + // Using lazy for synchronization to ensure that only one instance of HttpMessageHandler is created + // for each name. + // + private readonly ConcurrentDictionary> _activeHandlers; + + public int InUseHandlers => _activeHandlers.Count; + private int _removedHandlers = 0; + public int RemovedHandlers => _activeHandlers.Count; + + // Collection of 'expired' but not yet disposed handlers. + // + // Used when we're rotating handlers so that we can dispose HttpMessageHandler instances once they + // are eligible for garbage collection. + // + private readonly ConcurrentQueue _expiredHandlers; + private readonly TimerCallback _expiryCallback; + + public RequestDataHttpClientFactory(Func createHttpClientHandler) + { + _createHttpClientHandler = createHttpClientHandler; + // case-sensitive because named options is. + _activeHandlers = new ConcurrentDictionary>(); + _entryFactory = (key, requestData) => + { + return new Lazy(() => CreateHandlerEntry(key, requestData), + LazyThreadSafetyMode.ExecutionAndPublication); + }; + + _expiredHandlers = new ConcurrentQueue(); + _expiryCallback = ExpiryTimer_Tick; + + _cleanupTimerLock = new object(); + _cleanupActiveLock = new object(); + } + + public HttpClient CreateClient(RequestData requestData) + { + if (requestData == null) throw new ArgumentNullException(nameof(requestData)); + + var key = HttpConnection.GetClientKey(requestData); + var handler = CreateHandler(key, requestData); + var client = new HttpClient(handler, disposeHandler: false); + return client; + } + + private HttpMessageHandler CreateHandler(int key, RequestData requestData) + { + if (requestData == null) throw new ArgumentNullException(nameof(requestData)); + + #if NETSTANDARD2_1 + var entry = _activeHandlers.GetOrAdd(key, (k, r) => _entryFactory(k, r), requestData).Value; + #else + var entry = _activeHandlers.GetOrAdd(key, (k) => _entryFactory(k, requestData)).Value; + #endif + + StartHandlerEntryTimer(entry); + + return entry.Handler; + } + + private ActiveHandlerTrackingEntry CreateHandlerEntry(int key, RequestData requestData) + { + // Wrap the handler so we can ensure the inner handler outlives the outer handler. + var handler = new LifetimeTrackingHttpMessageHandler(CreateHandler(key, requestData)); + + // Note that we can't start the timer here. That would introduce a very very subtle race condition + // with very short expiry times. We need to wait until we've actually handed out the handler once + // to start the timer. + // + // Otherwise it would be possible that we start the timer here, immediately expire it (very short + // timer) and then dispose it without ever creating a client. That would be bad. It's unlikely + // this would happen, but we want to be sure. + return new ActiveHandlerTrackingEntry(key, handler, TimeSpan.FromMinutes(1)); + } + + private void ExpiryTimer_Tick(object state) + { + var active = (ActiveHandlerTrackingEntry)state; + + // The timer callback should be the only one removing from the active collection. If we can't find + // our entry in the collection, then this is a bug. + var removed = _activeHandlers.TryRemove(active.Key, out var found); + if (removed) Interlocked.Increment(ref _removedHandlers); + Debug.Assert(removed, "Entry not found. We should always be able to remove the entry"); + Debug.Assert(object.ReferenceEquals(active, found.Value), "Different entry found. The entry should not have been replaced"); + + // At this point the handler is no longer 'active' and will not be handed out to any new clients. + // However we haven't dropped our strong reference to the handler, so we can't yet determine if + // there are still any other outstanding references (we know there is at least one). + // + // We use a different state object to track expired handlers. This allows any other thread that acquired + // the 'active' entry to use it without safety problems. + var expired = new ExpiredHandlerTrackingEntry(active); + _expiredHandlers.Enqueue(expired); + + StartCleanupTimer(); + } + + protected virtual void StartHandlerEntryTimer(ActiveHandlerTrackingEntry entry) => entry.StartExpiryTimer(_expiryCallback); + + protected virtual void StartCleanupTimer() + { + lock (_cleanupTimerLock) + _cleanupTimer ??= NonCapturingTimer.Create(CleanupCallback, this, _defaultCleanupInterval, Timeout.InfiniteTimeSpan); + } + + protected virtual void StopCleanupTimer() + { + lock (_cleanupTimerLock) + { + _cleanupTimer.Dispose(); + _cleanupTimer = null; + } + } + + private void CleanupTimer_Tick() + { + // Stop any pending timers, we'll restart the timer if there's anything left to process after cleanup. + // + // With the scheme we're using it's possible we could end up with some redundant cleanup operations. + // This is expected and fine. + // + // An alternative would be to take a lock during the whole cleanup process. This isn't ideal because it + // would result in threads executing ExpiryTimer_Tick as they would need to block on cleanup to figure out + // whether we need to start the timer. + StopCleanupTimer(); + + if (!Monitor.TryEnter(_cleanupActiveLock)) + { + // We don't want to run a concurrent cleanup cycle. This can happen if the cleanup cycle takes + // a long time for some reason. Since we're running user code inside Dispose, it's definitely + // possible. + // + // If we end up in that position, just make sure the timer gets started again. It should be cheap + // to run a 'no-op' cleanup. + StartCleanupTimer(); + return; + } + + try + { + var initialCount = _expiredHandlers.Count; + + for (var i = 0; i < initialCount; i++) + { + // Since we're the only one removing from _expired, TryDequeue must always succeed. + _expiredHandlers.TryDequeue(out var entry); + Debug.Assert(entry != null, "Entry was null, we should always get an entry back from TryDequeue"); + + if (entry.CanDispose) + { + try + { + entry.InnerHandler.Dispose(); + } + catch (Exception) + { + // ignored (ignored in HttpClientFactory too) + } + } + else + { + // If the entry is still live, put it back in the queue so we can process it + // during the next cleanup cycle. + _expiredHandlers.Enqueue(entry); + } + } + } + finally + { + Monitor.Exit(_cleanupActiveLock); + } + + // We didn't totally empty the cleanup queue, try again later. + if (_expiredHandlers.Count > 0) StartCleanupTimer(); + } + + public void Dispose() + { + //try to cleanup nicely + CleanupTimer_Tick(); + _cleanupTimer?.Dispose(); + + //CleanupTimer might not cleanup everything because it will only dispose if the WeakReference allows it. + // here we forcefully dispose a Client -> ConnectionSettings -> Connection -> RequestDataHttpClientFactory + var attempts = 0; + do + { + attempts++; + var initialCount = _expiredHandlers.Count; + for (var i = 0; i < initialCount; i++) + { + // Since we're the only one removing from _expired, TryDequeue must always succeed. + _expiredHandlers.TryDequeue(out var entry); + try + { + entry?.InnerHandler.Dispose(); + } + catch (Exception) + { + // ignored (ignored in HttpClientFactory too) + } + } + } while (attempts < 5 && _expiredHandlers.Count > 0); + + } + } +} +#endif diff --git a/src/Elasticsearch.Net/Connection/HandlerTracking/ValueStopWatch.cs b/src/Elasticsearch.Net/Connection/HandlerTracking/ValueStopWatch.cs new file mode 100644 index 00000000000..149bc35c506 --- /dev/null +++ b/src/Elasticsearch.Net/Connection/HandlerTracking/ValueStopWatch.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if DOTNETCORE +using System; +using System.Diagnostics; +using System.Threading; + +namespace Elasticsearch.Net +{ + // A convenience API for interacting with System.Threading.Timer in a way + // that doesn't capture the ExecutionContext. We should be using this (or equivalent) + // everywhere we use timers to avoid rooting any values stored in asynclocals. + internal static class NonCapturingTimer + { + public static Timer Create(TimerCallback callback, object state, TimeSpan dueTime, TimeSpan period) + { + if (callback == null) throw new ArgumentNullException(nameof(callback)); + + // Don't capture the current ExecutionContext and its AsyncLocals onto the timer + var restoreFlow = false; + try + { + if (ExecutionContext.IsFlowSuppressed()) return new Timer(callback, state, dueTime, period); + + ExecutionContext.SuppressFlow(); + restoreFlow = true; + + return new Timer(callback, state, dueTime, period); + } + finally + { + // Restore the current ExecutionContext + if (restoreFlow) ExecutionContext.RestoreFlow(); + } + } + } +} +#endif diff --git a/src/Elasticsearch.Net/Connection/HttpConnection.cs b/src/Elasticsearch.Net/Connection/HttpConnection.cs index 75618fa6c63..b310b2c5e7a 100644 --- a/src/Elasticsearch.Net/Connection/HttpConnection.cs +++ b/src/Elasticsearch.Net/Connection/HttpConnection.cs @@ -42,8 +42,13 @@ public class HttpConnection : IConnection + $" please set {nameof(ConnectionConfiguration.ConnectionLimit)} to -1 on your connection configuration/settings." + $" this will cause the {nameof(HttpClientHandler.MaxConnectionsPerServer)} not to be set on {nameof(HttpClientHandler)}"; - protected readonly ConcurrentDictionary Clients = new ConcurrentDictionary(); private readonly object _lock = new object(); + private RequestDataHttpClientFactory HttpClientFactory { get; } + + public int InUseHandlers => HttpClientFactory.InUseHandlers; + public int RemovedHandlers => HttpClientFactory.RemovedHandlers; + + public HttpConnection() => HttpClientFactory = new RequestDataHttpClientFactory(r => CreateHttpClientHandler(r)); public virtual TResponse Request(RequestData requestData) where TResponse : class, IElasticsearchResponse, new() @@ -154,25 +159,7 @@ public virtual async Task RequestAsync(RequestData request void IDisposable.Dispose() => DisposeManagedResources(); - private HttpClient GetClient(RequestData requestData) - { - var key = GetClientKey(requestData); - if (Clients.TryGetValue(key, out var client)) return client; - - lock (_lock) - { - client = Clients.GetOrAdd(key, h => - { - var handler = CreateHttpClientHandler(requestData); - var httpClient = new HttpClient(handler, false) { Timeout = requestData.RequestTimeout }; - - httpClient.DefaultRequestHeaders.ExpectContinue = false; - return httpClient; - }); - } - - return client; - } + private HttpClient GetClient(RequestData requestData) => HttpClientFactory.CreateClient(requestData); protected virtual HttpMessageHandler CreateHttpClientHandler(RequestData requestData) { @@ -393,7 +380,7 @@ private static System.Net.Http.HttpMethod ConvertHttpMethod(HttpMethod httpMetho } } - private static int GetClientKey(RequestData requestData) + internal static int GetClientKey(RequestData requestData) { unchecked { @@ -407,11 +394,7 @@ private static int GetClientKey(RequestData requestData) } } - protected virtual void DisposeManagedResources() - { - foreach (var c in Clients) - c.Value.Dispose(); - } + protected virtual void DisposeManagedResources() => HttpClientFactory.Dispose(); } } #endif diff --git a/tests/Tests.ScratchPad/Program.cs b/tests/Tests.ScratchPad/Program.cs index 0b985defcb3..4babe4338c2 100644 --- a/tests/Tests.ScratchPad/Program.cs +++ b/tests/Tests.ScratchPad/Program.cs @@ -24,6 +24,14 @@ private class ListenerObserver : IObserver public void OnNext(DiagnosticListener value) { + + var client = new ElasticClient(); + + client.Search(); + + client.LowLevel.Search>(PostData.Serializable(new SearchRequest())); + + void WriteToConsole(string eventName, T data) { var a = Activity.Current; diff --git a/tests/Tests/ClientConcepts/Connection/HttpConnectionTests.cs b/tests/Tests/ClientConcepts/Connection/HttpConnectionTests.cs index 6591b12c188..4ce968fba9b 100644 --- a/tests/Tests/ClientConcepts/Connection/HttpConnectionTests.cs +++ b/tests/Tests/ClientConcepts/Connection/HttpConnectionTests.cs @@ -25,12 +25,12 @@ [I] public async Task SingleInstanceOfHttpClient() connection.Request(requestData); connection.CallCount.Should().Be(1); - connection.ClientCount.Should().Be(1); + connection.InUseHandlers.Should().Be(1); await connection.RequestAsync(requestData, CancellationToken.None).ConfigureAwait(false); connection.CallCount.Should().Be(2); - connection.ClientCount.Should().Be(1); + connection.InUseHandlers.Should().Be(1); } [I] public async Task MultipleInstancesOfHttpClientWhenRequestTimeoutChanges() => @@ -53,13 +53,13 @@ private async Task MultipleInstancesOfHttpClientWhen(Func different connection.Request(requestData); connection.CallCount.Should().Be(1); - connection.ClientCount.Should().Be(1); + connection.InUseHandlers.Should().Be(1); requestData = differentRequestData(); await connection.RequestAsync(requestData, CancellationToken.None).ConfigureAwait(false); connection.CallCount.Should().Be(2); - connection.ClientCount.Should().Be(2); + connection.InUseHandlers.Should().Be(2); } private RequestData CreateRequestData( @@ -157,7 +157,6 @@ public class TestableHttpConnection : HttpConnection private readonly Action _response; private TestableClientHandler _handler; public int CallCount { get; private set; } - public int ClientCount => Clients.Count; public HttpClientHandler LastHttpClientHandler => (HttpClientHandler)_handler.InnerHandler; public TestableHttpConnection(Action response) => _response = response; From c88fc621e47eea5f9eed3ebf2270993eac5dd958 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Fri, 24 Apr 2020 16:13:11 +0200 Subject: [PATCH 5/8] add tests and configuration for DnsRefreshTimeout --- .../Configuration/ConnectionConfiguration.cs | 19 ++++++++++++++ .../IConnectionConfigurationValues.cs | 10 +++++++ .../RequestDataHttpClientFactory.cs | 9 ++++--- .../Connection/HttpWebRequestConnection.cs | 1 + .../Transport/Pipeline/RequestData.cs | 2 ++ tests/Tests.Configuration/tests.default.yaml | 2 +- .../Connection/HttpConnectionTests.cs | 26 +++++++++++++++++++ 7 files changed, 64 insertions(+), 5 deletions(-) diff --git a/src/Elasticsearch.Net/Configuration/ConnectionConfiguration.cs b/src/Elasticsearch.Net/Configuration/ConnectionConfiguration.cs index 711abbe8362..049b4cfe5f7 100644 --- a/src/Elasticsearch.Net/Configuration/ConnectionConfiguration.cs +++ b/src/Elasticsearch.Net/Configuration/ConnectionConfiguration.cs @@ -77,6 +77,12 @@ private static bool UsingCurlHandler /// public static readonly TimeSpan DefaultTimeout = TimeSpan.FromMinutes(1); + /// + /// The default timeout before a TCP connection is forcefully recycled so that DNS updates come through + /// Defaults to 5 minutes. + /// + public static readonly TimeSpan DefaultDnsRefreshTimeout = TimeSpan.FromMinutes(5); + /// /// The default connection limit for both Elasticsearch.Net and Nest. Defaults to 80 #if DOTNETCORE @@ -182,6 +188,7 @@ public abstract class ConnectionConfiguration : IConnectionConfigurationValue private SecureString _proxyPassword; private string _proxyUsername; private TimeSpan _requestTimeout; + private TimeSpan _dnsRefreshTimeout; private Func _serverCertificateValidationCallback; private IReadOnlyCollection _skipDeserializationForStatusCodes = new ReadOnlyCollection(new int[] { }); private TimeSpan? _sniffLifeSpan; @@ -203,6 +210,7 @@ protected ConnectionConfiguration(IConnectionPool connectionPool, IConnection co _connectionLimit = ConnectionConfiguration.DefaultConnectionLimit; _requestTimeout = ConnectionConfiguration.DefaultTimeout; + _dnsRefreshTimeout = ConnectionConfiguration.DefaultDnsRefreshTimeout; _sniffOnConnectionFault = true; _sniffOnStartup = true; _sniffLifeSpan = TimeSpan.FromHours(1); @@ -254,6 +262,7 @@ protected ConnectionConfiguration(IConnectionPool connectionPool, IConnection co NameValueCollection IConnectionConfigurationValues.QueryStringParameters => _queryString; IElasticsearchSerializer IConnectionConfigurationValues.RequestResponseSerializer => UseThisRequestResponseSerializer; TimeSpan IConnectionConfigurationValues.RequestTimeout => _requestTimeout; + TimeSpan IConnectionConfigurationValues.DnsRefreshTimeout => _dnsRefreshTimeout; Func IConnectionConfigurationValues.ServerCertificateValidationCallback => _serverCertificateValidationCallback; @@ -411,6 +420,16 @@ public T SniffOnConnectionFault(bool sniffsOnConnectionFault = true) => /// public T MaxRetryTimeout(TimeSpan maxRetryTimeout) => Assign(maxRetryTimeout, (a, v) => a._maxRetryTimeout = v); + /// + /// DnsRefreshTimeout for the connections. Defaults to 5 minutes. + #if DOTNETCORE + /// Will create new instances of after this timeout to force DNS updates + #else + /// Will set both and + #endif + /// + public T DnsRefreshTimeout(TimeSpan timeout) => Assign(timeout, (a, v) => a._dnsRefreshTimeout = v); + /// /// If your connection has to go through proxy, use this method to specify the proxy url /// diff --git a/src/Elasticsearch.Net/Configuration/IConnectionConfigurationValues.cs b/src/Elasticsearch.Net/Configuration/IConnectionConfigurationValues.cs index 2757417b222..6eee7ce3644 100644 --- a/src/Elasticsearch.Net/Configuration/IConnectionConfigurationValues.cs +++ b/src/Elasticsearch.Net/Configuration/IConnectionConfigurationValues.cs @@ -249,5 +249,15 @@ public interface IConnectionConfigurationValues : IDisposable /// Whether the request should be sent with chunked Transfer-Encoding. /// bool TransferEncodingChunked { get; } + + /// + /// DnsRefreshTimeout for the connections. Defaults to 5 minutes. + #if DOTNETCORE + /// Will create new instances of after this timeout to force DNS updates + #else + /// Will set + #endif + /// + TimeSpan DnsRefreshTimeout { get; } } } diff --git a/src/Elasticsearch.Net/Connection/HandlerTracking/RequestDataHttpClientFactory.cs b/src/Elasticsearch.Net/Connection/HandlerTracking/RequestDataHttpClientFactory.cs index 403b28779ee..4cc2f7487c2 100644 --- a/src/Elasticsearch.Net/Connection/HandlerTracking/RequestDataHttpClientFactory.cs +++ b/src/Elasticsearch.Net/Connection/HandlerTracking/RequestDataHttpClientFactory.cs @@ -49,7 +49,7 @@ internal class RequestDataHttpClientFactory : IDisposable public int InUseHandlers => _activeHandlers.Count; private int _removedHandlers = 0; - public int RemovedHandlers => _activeHandlers.Count; + public int RemovedHandlers => _removedHandlers; // Collection of 'expired' but not yet disposed handlers. // @@ -105,7 +105,7 @@ private HttpMessageHandler CreateHandler(int key, RequestData requestData) private ActiveHandlerTrackingEntry CreateHandlerEntry(int key, RequestData requestData) { // Wrap the handler so we can ensure the inner handler outlives the outer handler. - var handler = new LifetimeTrackingHttpMessageHandler(CreateHandler(key, requestData)); + var handler = new LifetimeTrackingHttpMessageHandler(_createHttpClientHandler(requestData)); // Note that we can't start the timer here. That would introduce a very very subtle race condition // with very short expiry times. We need to wait until we've actually handed out the handler once @@ -114,7 +114,7 @@ private ActiveHandlerTrackingEntry CreateHandlerEntry(int key, RequestData reque // Otherwise it would be possible that we start the timer here, immediately expire it (very short // timer) and then dispose it without ever creating a client. That would be bad. It's unlikely // this would happen, but we want to be sure. - return new ActiveHandlerTrackingEntry(key, handler, TimeSpan.FromMinutes(1)); + return new ActiveHandlerTrackingEntry(key, handler, requestData.DnsRefreshTimeout); } private void ExpiryTimer_Tick(object state) @@ -124,7 +124,8 @@ private void ExpiryTimer_Tick(object state) // The timer callback should be the only one removing from the active collection. If we can't find // our entry in the collection, then this is a bug. var removed = _activeHandlers.TryRemove(active.Key, out var found); - if (removed) Interlocked.Increment(ref _removedHandlers); + if (removed) + Interlocked.Increment(ref _removedHandlers); Debug.Assert(removed, "Entry not found. We should always be able to remove the entry"); Debug.Assert(object.ReferenceEquals(active, found.Value), "Different entry found. The entry should not have been replaced"); diff --git a/src/Elasticsearch.Net/Connection/HttpWebRequestConnection.cs b/src/Elasticsearch.Net/Connection/HttpWebRequestConnection.cs index e46dc3b51c5..e6183f12832 100644 --- a/src/Elasticsearch.Net/Connection/HttpWebRequestConnection.cs +++ b/src/Elasticsearch.Net/Connection/HttpWebRequestConnection.cs @@ -219,6 +219,7 @@ protected virtual void AlterServicePoint(ServicePoint requestServicePoint, Reque { requestServicePoint.UseNagleAlgorithm = false; requestServicePoint.Expect100Continue = false; + requestServicePoint.ConnectionLeaseTimeout = (int)requestData.DnsRefreshTimeout.TotalMilliseconds; if (requestData.ConnectionSettings.ConnectionLimit > 0) requestServicePoint.ConnectionLimit = requestData.ConnectionSettings.ConnectionLimit; //looking at http://referencesource.microsoft.com/#System/net/System/Net/ServicePoint.cs diff --git a/src/Elasticsearch.Net/Transport/Pipeline/RequestData.cs b/src/Elasticsearch.Net/Transport/Pipeline/RequestData.cs index d1987246ecc..35a6d727fcb 100644 --- a/src/Elasticsearch.Net/Transport/Pipeline/RequestData.cs +++ b/src/Elasticsearch.Net/Transport/Pipeline/RequestData.cs @@ -73,6 +73,7 @@ IMemoryStreamFactory memoryStreamFactory KeepAliveInterval = (int)(global.KeepAliveInterval?.TotalMilliseconds ?? 2000); KeepAliveTime = (int)(global.KeepAliveTime?.TotalMilliseconds ?? 2000); + DnsRefreshTimeout = global.DnsRefreshTimeout; ProxyAddress = global.ProxyAddress; ProxyUsername = global.ProxyUsername; @@ -130,6 +131,7 @@ IMemoryStreamFactory memoryStreamFactory public bool TransferEncodingChunked { get; } public Uri Uri => Node != null ? new Uri(Node.Uri, PathAndQuery) : null; + public TimeSpan DnsRefreshTimeout { get; } public override string ToString() => $"{Method.GetStringValue()} {_path}"; diff --git a/tests/Tests.Configuration/tests.default.yaml b/tests/Tests.Configuration/tests.default.yaml index 00c6e776d4b..84ec8e76467 100644 --- a/tests/Tests.Configuration/tests.default.yaml +++ b/tests/Tests.Configuration/tests.default.yaml @@ -5,7 +5,7 @@ # tracked by git). # mode either u (unit test), i (integration test) or m (mixed mode) -mode: u +mode: i # the elasticsearch version that should be started # Can be a snapshot version of sonatype or "latest" to get the latest snapshot of sonatype diff --git a/tests/Tests/ClientConcepts/Connection/HttpConnectionTests.cs b/tests/Tests/ClientConcepts/Connection/HttpConnectionTests.cs index 4ce968fba9b..e809f8b1b0a 100644 --- a/tests/Tests/ClientConcepts/Connection/HttpConnectionTests.cs +++ b/tests/Tests/ClientConcepts/Connection/HttpConnectionTests.cs @@ -26,6 +26,7 @@ [I] public async Task SingleInstanceOfHttpClient() connection.CallCount.Should().Be(1); connection.InUseHandlers.Should().Be(1); + connection.RemovedHandlers.Should().Be(0); await connection.RequestAsync(requestData, CancellationToken.None).ConfigureAwait(false); @@ -33,6 +34,27 @@ [I] public async Task SingleInstanceOfHttpClient() connection.InUseHandlers.Should().Be(1); } + [I] public async Task RespectsDnsRefreshTimeout() + { + var connection = new TestableHttpConnection(); + connection.RemovedHandlers.Should().Be(0); + var requestData = CreateRequestData(dnsRefreshTimeout: TimeSpan.FromSeconds(1)); + connection.Request(requestData); + await Task.Delay(TimeSpan.FromSeconds(2)); + connection.Request(requestData); + + connection.CallCount.Should().Be(2); + connection.InUseHandlers.Should().Be(1); + connection.RemovedHandlers.Should().Be(1); + + await connection.RequestAsync(requestData, CancellationToken.None).ConfigureAwait(false); + + connection.CallCount.Should().Be(3); + connection.InUseHandlers.Should().Be(1); + connection.RemovedHandlers.Should().Be(1); + } + + [I] public async Task MultipleInstancesOfHttpClientWhenRequestTimeoutChanges() => await MultipleInstancesOfHttpClientWhen(() => CreateRequestData(TimeSpan.FromSeconds(30))); @@ -54,16 +76,19 @@ private async Task MultipleInstancesOfHttpClientWhen(Func different connection.CallCount.Should().Be(1); connection.InUseHandlers.Should().Be(1); + connection.RemovedHandlers.Should().Be(0); requestData = differentRequestData(); await connection.RequestAsync(requestData, CancellationToken.None).ConfigureAwait(false); connection.CallCount.Should().Be(2); connection.InUseHandlers.Should().Be(2); + connection.RemovedHandlers.Should().Be(0); } private RequestData CreateRequestData( TimeSpan requestTimeout = default, + TimeSpan? dnsRefreshTimeout = default, Uri proxyAddress = null, bool disableAutomaticProxyDetection = false, bool httpCompression = false, @@ -75,6 +100,7 @@ private RequestData CreateRequestData( var node = Client.ConnectionSettings.ConnectionPool.Nodes.First(); var connectionSettings = new ConnectionSettings(node.Uri) .RequestTimeout(requestTimeout) + .DnsRefreshTimeout(dnsRefreshTimeout ?? ConnectionConfiguration.DefaultDnsRefreshTimeout) .DisableAutomaticProxyDetection(disableAutomaticProxyDetection) .TransferEncodingChunked(transferEncodingChunked) .EnableHttpCompression(httpCompression); From 22b1f6ffe7ad18d08970e06bcd5eec712ab4b617 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Fri, 24 Apr 2020 18:25:17 +0200 Subject: [PATCH 6/8] failing readonly test --- .../Framework/EndpointTests/ApiIntegrationTestBase.cs | 8 ++------ tests/Tests/Search/Search/InvalidSearchApiTests.cs | 9 +++++---- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/tests/Tests/Framework/EndpointTests/ApiIntegrationTestBase.cs b/tests/Tests/Framework/EndpointTests/ApiIntegrationTestBase.cs index d6f17178cf3..c0325decaf3 100644 --- a/tests/Tests/Framework/EndpointTests/ApiIntegrationTestBase.cs +++ b/tests/Tests/Framework/EndpointTests/ApiIntegrationTestBase.cs @@ -48,11 +48,8 @@ [I] public virtual async Task ReturnsExpectedIsValid() => [I] public virtual async Task ReturnsExpectedResponse() => await AssertOnAllResponses(ExpectResponse); - protected override Task AssertOnAllResponses(Action assert) - { - if (!ExpectIsValid) return base.AssertOnAllResponses(assert); - - return base.AssertOnAllResponses((r) => + protected override Task AssertOnAllResponses(Action assert) => + base.AssertOnAllResponses((r) => { if (TestClient.Configuration.RunIntegrationTests && !r.IsValid && r.ApiCall.OriginalException != null && !(r.ApiCall.OriginalException is ElasticsearchClientException)) @@ -71,7 +68,6 @@ protected override Task AssertOnAllResponses(Action assert) throw new ResponseAssertionException(ex.SourceException, r).Demystify(); } }); - } } public class ResponseAssertionException : Exception diff --git a/tests/Tests/Search/Search/InvalidSearchApiTests.cs b/tests/Tests/Search/Search/InvalidSearchApiTests.cs index 1c6558455b5..252915e4d5e 100644 --- a/tests/Tests/Search/Search/InvalidSearchApiTests.cs +++ b/tests/Tests/Search/Search/InvalidSearchApiTests.cs @@ -38,7 +38,7 @@ public InvalidSearchApiTests(ReadOnlyCluster cluster, EndpointUsage usage) : bas } }; - protected override int ExpectStatusCode => 500; + protected override int ExpectStatusCode => 400; protected override Func, ISearchRequest> Fluent => s => s .From(10) @@ -67,9 +67,10 @@ protected override void ExpectResponse(ISearchResponse response) response.ShouldNotBeValid(); var serverError = response.ServerError; serverError.Should().NotBeNull(); - serverError.Status.Should().Be(ExpectStatusCode); - serverError.Error.Reason.Should().Be("all shards failed"); - serverError.Error.RootCause.First().Reason.Should().Contain("value source config is invalid"); + serverError.Status.Should().Be(ExpectStatusCode, "{0}", response.DebugInformation); + + serverError.Error.Type.Should().Be("illegal_argument_exception"); + serverError.Error.RootCause.First().Reason.Should().Contain("Required one of fields"); } } } From 67af5839cebc7daa9b099bfc67ab4155fb949186 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Mon, 27 Apr 2020 11:10:17 +0200 Subject: [PATCH 7/8] Apply suggestions from code review Co-Authored-By: Russ Cam --- .../HandlerTracking/RequestDataHttpClientFactory.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Elasticsearch.Net/Connection/HandlerTracking/RequestDataHttpClientFactory.cs b/src/Elasticsearch.Net/Connection/HandlerTracking/RequestDataHttpClientFactory.cs index 4cc2f7487c2..c4187354082 100644 --- a/src/Elasticsearch.Net/Connection/HandlerTracking/RequestDataHttpClientFactory.cs +++ b/src/Elasticsearch.Net/Connection/HandlerTracking/RequestDataHttpClientFactory.cs @@ -16,7 +16,7 @@ namespace Elasticsearch.Net { /// - /// Heavily modified version of DefaultHttpClientFactory, repurposed for RequestData + /// Heavily modified version of DefaultHttpClientFactory, re-purposed for RequestData /// internal class RequestDataHttpClientFactory : IDisposable { @@ -48,7 +48,7 @@ internal class RequestDataHttpClientFactory : IDisposable private readonly ConcurrentDictionary> _activeHandlers; public int InUseHandlers => _activeHandlers.Count; - private int _removedHandlers = 0; + private int _removedHandlers; public int RemovedHandlers => _removedHandlers; // Collection of 'expired' but not yet disposed handlers. From b579a8664573351e2f10b0b4ba2df83af869bc1c Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Mon, 27 Apr 2020 11:20:02 +0200 Subject: [PATCH 8/8] link back to dotnet/runtime source --- .../HandlerTracking/ActiveHandlerTrackingEntry.cs | 7 +++++-- .../HandlerTracking/ExpiredHandlerTrackingEntry.cs | 5 ++++- .../LifetimeTrackingHttpMessageHandler.cs | 9 ++++++--- .../HandlerTracking/RequestDataHttpClientFactory.cs | 1 + .../Connection/HandlerTracking/ValueStopWatch.cs | 9 ++++++--- 5 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/Elasticsearch.Net/Connection/HandlerTracking/ActiveHandlerTrackingEntry.cs b/src/Elasticsearch.Net/Connection/HandlerTracking/ActiveHandlerTrackingEntry.cs index 4b06c0b6c8b..f9c77a46ecd 100644 --- a/src/Elasticsearch.Net/Connection/HandlerTracking/ActiveHandlerTrackingEntry.cs +++ b/src/Elasticsearch.Net/Connection/HandlerTracking/ActiveHandlerTrackingEntry.cs @@ -9,8 +9,11 @@ namespace Elasticsearch.Net { - // Thread-safety: We treat this class as immutable except for the timer. Creating a new object - // for the 'expiry' pool simplifies the threading requirements significantly. + /// + /// Thread-safety: We treat this class as immutable except for the timer. Creating a new object + /// for the 'expiry' pool simplifies the threading requirements significantly. + /// https://github.com/dotnet/runtime/blob/master/src/libraries/Microsoft.Extensions.Http/src/ActiveHandlerTrackingEntry.cs + /// internal class ActiveHandlerTrackingEntry { private static readonly TimerCallback TimerCallback = (s) => ((ActiveHandlerTrackingEntry)s).Timer_Tick(); diff --git a/src/Elasticsearch.Net/Connection/HandlerTracking/ExpiredHandlerTrackingEntry.cs b/src/Elasticsearch.Net/Connection/HandlerTracking/ExpiredHandlerTrackingEntry.cs index 185e4ab8a51..b6f317337c3 100644 --- a/src/Elasticsearch.Net/Connection/HandlerTracking/ExpiredHandlerTrackingEntry.cs +++ b/src/Elasticsearch.Net/Connection/HandlerTracking/ExpiredHandlerTrackingEntry.cs @@ -8,7 +8,10 @@ namespace Elasticsearch.Net { - // Thread-safety: This class is immutable + /// + /// Thread-safety: This class is immutable + /// https://github.com/dotnet/runtime/blob/master/src/libraries/Microsoft.Extensions.Http/src/ExpiredHandlerTrackingEntry.cs + /// internal class ExpiredHandlerTrackingEntry { private readonly WeakReference _livenessTracker; diff --git a/src/Elasticsearch.Net/Connection/HandlerTracking/LifetimeTrackingHttpMessageHandler.cs b/src/Elasticsearch.Net/Connection/HandlerTracking/LifetimeTrackingHttpMessageHandler.cs index e346b53ce4b..bcf414cccbf 100644 --- a/src/Elasticsearch.Net/Connection/HandlerTracking/LifetimeTrackingHttpMessageHandler.cs +++ b/src/Elasticsearch.Net/Connection/HandlerTracking/LifetimeTrackingHttpMessageHandler.cs @@ -7,9 +7,12 @@ namespace Elasticsearch.Net { - // This a marker used to check if the underlying handler should be disposed. HttpClients - // share a reference to an instance of this class, and when it goes out of scope the inner handler - // is eligible to be disposed. + /// + /// This a marker used to check if the underlying handler should be disposed. HttpClients + /// share a reference to an instance of this class, and when it goes out of scope the inner handler + /// is eligible to be disposed. + /// https://github.com/dotnet/runtime/blob/master/src/libraries/Microsoft.Extensions.Http/src/LifetimeTrackingHttpMessageHandler.cs + /// internal class LifetimeTrackingHttpMessageHandler : DelegatingHandler { public LifetimeTrackingHttpMessageHandler(HttpMessageHandler innerHandler) diff --git a/src/Elasticsearch.Net/Connection/HandlerTracking/RequestDataHttpClientFactory.cs b/src/Elasticsearch.Net/Connection/HandlerTracking/RequestDataHttpClientFactory.cs index c4187354082..1629cf80fc8 100644 --- a/src/Elasticsearch.Net/Connection/HandlerTracking/RequestDataHttpClientFactory.cs +++ b/src/Elasticsearch.Net/Connection/HandlerTracking/RequestDataHttpClientFactory.cs @@ -17,6 +17,7 @@ namespace Elasticsearch.Net { /// /// Heavily modified version of DefaultHttpClientFactory, re-purposed for RequestData + /// https://github.com/dotnet/runtime/blob/master/src/libraries/Microsoft.Extensions.Http/src/DefaultHttpClientFactory.cs /// internal class RequestDataHttpClientFactory : IDisposable { diff --git a/src/Elasticsearch.Net/Connection/HandlerTracking/ValueStopWatch.cs b/src/Elasticsearch.Net/Connection/HandlerTracking/ValueStopWatch.cs index 149bc35c506..007e54c332f 100644 --- a/src/Elasticsearch.Net/Connection/HandlerTracking/ValueStopWatch.cs +++ b/src/Elasticsearch.Net/Connection/HandlerTracking/ValueStopWatch.cs @@ -9,9 +9,12 @@ namespace Elasticsearch.Net { - // A convenience API for interacting with System.Threading.Timer in a way - // that doesn't capture the ExecutionContext. We should be using this (or equivalent) - // everywhere we use timers to avoid rooting any values stored in asynclocals. + /// + /// A convenience API for interacting with System.Threading.Timer in a way + /// that doesn't capture the ExecutionContext. We should be using this (or equivalent) + /// everywhere we use timers to avoid rooting any values stored in asynclocals. + /// https://github.com/dotnet/runtime/blob/master/src/libraries/Common/src/Extensions/ValueStopwatch/ValueStopwatch.cs + /// internal static class NonCapturingTimer { public static Timer Create(TimerCallback callback, object state, TimeSpan dueTime, TimeSpan period)