From cba38a17e1d0342a1866cbfc329feb1cc23fb6de Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Wed, 3 Feb 2021 14:12:46 +0000 Subject: [PATCH 1/2] Implement meta header for client requests (cherry picked (and adapted) from commit fec16efac5b30c1bb11b866d71e40b40450cf2f0) --- .../connection/configuration-options.asciidoc | 4 + .../Configuration/ConnectionConfiguration.cs | 41 +-- .../IConnectionConfigurationValues.cs | 10 + .../Configuration/RequestConfiguration.cs | 14 + .../RequestConfigurationExtensions.cs | 22 ++ .../Configuration/RequestMetaData.cs | 30 +++ .../Connection/ConnectionInfo.cs | 48 ++++ .../Connection/HttpConnection-CoreFx.cs | 8 + .../Connection/HttpWebRequestConnection.cs | 8 + .../Connection/MetaData/ClientVersionInfo.cs | 50 ++++ .../Connection/MetaData/MetaDataHeader.cs | 44 ++++ .../Connection/MetaData/MetaHeaderProvider.cs | 50 ++++ .../Connection/MetaData/RuntimeVersionInfo.cs | 243 +++++++++++++++++ .../Connection/MetaData/VersionInfo.cs | 47 ++++ .../RequestParametersExtensions.cs | 24 ++ .../Extensions/Extensions.cs | 11 + .../Transport/Pipeline/RequestData.cs | 7 + .../Multiple/BulkAll/BulkAllObservable.cs | 26 +- .../Multiple/BulkAll/BulkAllRequest.cs | 9 +- .../Multiple/Reindex/ReindexObservable.cs | 25 +- .../Multiple/ScrollAll/ScrollAllObservable.cs | 26 ++ .../Multiple/ScrollAll/ScrollAllRequest.cs | 6 +- src/Nest/Helpers/HelperIdentifiers.cs | 15 ++ src/Nest/Helpers/IHelperCallable.cs | 25 ++ src/Nest/Helpers/RequestMetaDataExtensions.cs | 66 +++++ .../RestoreObservable/RestoreObservable.cs | 13 +- .../SnapshotObservable/SnapshotObservable.cs | 13 +- .../Connection/HttpConnectionTests.cs | 93 ++++++- .../MetaData/MetaHeaderHelperTests.cs | 248 ++++++++++++++++++ .../MetaData/MetaHeaderProviderTests.cs | 105 ++++++++ .../Connection/MetaData/VersionInfoTests.cs | 35 +++ 31 files changed, 1318 insertions(+), 48 deletions(-) create mode 100644 src/Elasticsearch.Net/Configuration/RequestConfigurationExtensions.cs create mode 100644 src/Elasticsearch.Net/Configuration/RequestMetaData.cs create mode 100644 src/Elasticsearch.Net/Connection/ConnectionInfo.cs create mode 100644 src/Elasticsearch.Net/Connection/MetaData/ClientVersionInfo.cs create mode 100644 src/Elasticsearch.Net/Connection/MetaData/MetaDataHeader.cs create mode 100644 src/Elasticsearch.Net/Connection/MetaData/MetaHeaderProvider.cs create mode 100644 src/Elasticsearch.Net/Connection/MetaData/RuntimeVersionInfo.cs create mode 100644 src/Elasticsearch.Net/Connection/MetaData/VersionInfo.cs create mode 100644 src/Elasticsearch.Net/Domain/RequestParameters/RequestParametersExtensions.cs create mode 100644 src/Nest/Helpers/HelperIdentifiers.cs create mode 100644 src/Nest/Helpers/IHelperCallable.cs create mode 100644 src/Nest/Helpers/RequestMetaDataExtensions.cs create mode 100644 src/Tests/Tests/Connection/MetaData/MetaHeaderHelperTests.cs create mode 100644 src/Tests/Tests/Connection/MetaData/MetaHeaderProviderTests.cs create mode 100644 src/Tests/Tests/Connection/MetaData/VersionInfoTests.cs diff --git a/docs/client-concepts/connection/configuration-options.asciidoc b/docs/client-concepts/connection/configuration-options.asciidoc index e99fa325d91..8e1d0c7499d 100644 --- a/docs/client-concepts/connection/configuration-options.asciidoc +++ b/docs/client-concepts/connection/configuration-options.asciidoc @@ -64,6 +64,10 @@ Ensures the response bytes are always available on the `ElasticsearchResponse + IMPORTANT: Depending on the registered serializer, this may cause the response to be buffered in memory first, potentially affecting performance. +`DisableMetaHeader`:: + +Disables the meta header which is included on all requests by default. This header contains lightweight information about the client and runtime. + `DisablePing`:: 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 diff --git a/src/Elasticsearch.Net/Configuration/ConnectionConfiguration.cs b/src/Elasticsearch.Net/Configuration/ConnectionConfiguration.cs index da8bf909cd9..bd265c9e5ec 100644 --- a/src/Elasticsearch.Net/Configuration/ConnectionConfiguration.cs +++ b/src/Elasticsearch.Net/Configuration/ConnectionConfiguration.cs @@ -28,36 +28,7 @@ public class ConnectionConfiguration : ConnectionConfigurationhttps://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 - } - } + private static bool UsingCurlHandler => ConnectionInfo.UsingCurlHandler; /// /// The default ping timeout. Defaults to 2 seconds @@ -174,6 +145,7 @@ public abstract class ConnectionConfiguration : IConnectionConfigurationValue private bool _disableAutomaticProxyDetection = false; private bool _disableDirectStreaming = false; + private bool _disableMetaHeader; private bool _disablePings; private bool _enableHttpCompression; private bool _enableHttpPipelining = true; @@ -235,6 +207,7 @@ protected ConnectionConfiguration(IConnectionPool connectionPool, IConnection co TimeSpan? IConnectionConfigurationValues.DeadTimeout => _deadTimeout; bool IConnectionConfigurationValues.DisableAutomaticProxyDetection => _disableAutomaticProxyDetection; bool IConnectionConfigurationValues.DisableDirectStreaming => _disableDirectStreaming; + bool IConnectionConfigurationValues.DisableMetaHeader => _disableMetaHeader; bool IConnectionConfigurationValues.DisablePings => _disablePings; bool IConnectionConfigurationValues.EnableHttpCompression => _enableHttpCompression; NameValueCollection IConnectionConfigurationValues.Headers => _headers; @@ -270,6 +243,8 @@ protected ConnectionConfiguration(IConnectionPool connectionPool, IConnection co ElasticsearchUrlFormatter IConnectionConfigurationValues.UrlFormatter => _urlFormatter; string IConnectionConfigurationValues.UserAgent => _userAgent; + MetaHeaderProvider IConnectionConfigurationValues.MetaHeaderProvider { get; } = new MetaHeaderProvider(); + void IDisposable.Dispose() => DisposeManagedResources(); private static void DefaultCompletedRequestHandler(IApiCallDetails response) { } @@ -362,6 +337,12 @@ public T SniffOnConnectionFault(bool sniffsOnConnectionFault = true) => /// public T DisableAutomaticProxyDetection(bool disable = true) => Assign(disable, (a, v) => a._disableAutomaticProxyDetection = v); + /// + /// Disables the meta header which is included on all requests by default. This header contains lightweight information + /// about the client and runtime. + /// + public T DisableMetaHeader(bool disable = true) => Assign(disable, (a, v) => a._disableMetaHeader = v); + /// /// Instead of following a c/go like error checking on response.IsValid always throw an exception /// on the client when a call resulted in an exception on either the client or the Elasticsearch server. diff --git a/src/Elasticsearch.Net/Configuration/IConnectionConfigurationValues.cs b/src/Elasticsearch.Net/Configuration/IConnectionConfigurationValues.cs index 432d4de9417..9325628c72a 100644 --- a/src/Elasticsearch.Net/Configuration/IConnectionConfigurationValues.cs +++ b/src/Elasticsearch.Net/Configuration/IConnectionConfigurationValues.cs @@ -61,6 +61,11 @@ public interface IConnectionConfigurationValues : IDisposable /// bool DisableDirectStreaming { get; } + /// + /// When set to true will disable sending the meta header on requests. Defaults to false + /// + bool DisableMetaHeader { get; } + /// /// This signals that we do not want to send initial pings to unknown/previously dead nodes /// and just send the call straightaway @@ -229,5 +234,10 @@ public interface IConnectionConfigurationValues : IDisposable #endif /// TimeSpan DnsRefreshTimeout { get; } + + /// + /// Produces the client meta header for a request. + /// + MetaHeaderProvider MetaHeaderProvider { get; } } } diff --git a/src/Elasticsearch.Net/Configuration/RequestConfiguration.cs b/src/Elasticsearch.Net/Configuration/RequestConfiguration.cs index f90d00d14e1..b143d6b316a 100644 --- a/src/Elasticsearch.Net/Configuration/RequestConfiguration.cs +++ b/src/Elasticsearch.Net/Configuration/RequestConfiguration.cs @@ -93,6 +93,11 @@ public interface IRequestConfiguration /// Reasons for such exceptions could be search parser errors, index missing exceptions, etc... /// bool ThrowExceptions { get; set; } + + /// + /// Holds additional meta data about the request. + /// + RequestMetaData RequestMetaData { get; set; } } public class RequestConfiguration : IRequestConfiguration @@ -113,6 +118,7 @@ public class RequestConfiguration : IRequestConfiguration public string OpaqueId { get; set; } public TimeSpan? PingTimeout { get; set; } public TimeSpan? RequestTimeout { get; set; } + public RequestMetaData RequestMetaData { get; set; } /// /// Submit the request on behalf in the context of a different user @@ -162,6 +168,7 @@ public RequestConfigurationDescriptor(IRequestConfiguration config) string IRequestConfiguration.RunAs { get; set; } private IRequestConfiguration Self => this; bool IRequestConfiguration.ThrowExceptions { get; set; } + RequestMetaData IRequestConfiguration.RequestMetaData { get; set; } /// /// Submit the request on behalf in the context of a different shield user @@ -284,5 +291,12 @@ public RequestConfigurationDescriptor ClientCertificate(X509Certificate certific /// Use the following client certificate to authenticate this request to Elasticsearch public RequestConfigurationDescriptor ClientCertificate(string certificatePath) => ClientCertificates(new X509Certificate2Collection { new X509Certificate(certificatePath) }); + + /// + public RequestConfigurationDescriptor RequestMetaData(RequestMetaData metaData) + { + Self.RequestMetaData = metaData; + return this; + } } } diff --git a/src/Elasticsearch.Net/Configuration/RequestConfigurationExtensions.cs b/src/Elasticsearch.Net/Configuration/RequestConfigurationExtensions.cs new file mode 100644 index 00000000000..7621302079f --- /dev/null +++ b/src/Elasticsearch.Net/Configuration/RequestConfigurationExtensions.cs @@ -0,0 +1,22 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; + +namespace Elasticsearch.Net +{ + public static class RequestConfigurationExtensions + { + public static void SetRequestMetaData(this IRequestConfiguration requestConfiguration, RequestMetaData requestMetaData) + { + if (requestConfiguration is null) + throw new ArgumentNullException(nameof(requestConfiguration)); + + if (requestMetaData is null) + throw new ArgumentNullException(nameof(requestMetaData)); + + requestConfiguration.RequestMetaData = requestMetaData; + } + } +} diff --git a/src/Elasticsearch.Net/Configuration/RequestMetaData.cs b/src/Elasticsearch.Net/Configuration/RequestMetaData.cs new file mode 100644 index 00000000000..07a4d291c8b --- /dev/null +++ b/src/Elasticsearch.Net/Configuration/RequestMetaData.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; + +namespace Elasticsearch.Net +{ + /// + /// Holds meta data about a client request. + /// + public sealed class RequestMetaData + { + /// + /// Reserved key for a meta data entry which identifies the helper which produced the request. + /// + public const string HelperKey = "helper"; + + private Dictionary _metaDataItems; + + public bool TryAddMetaData (string key, string value) + { + _metaDataItems ??= new Dictionary(); + + if (_metaDataItems.ContainsKey(key)) + return false; + + _metaDataItems.Add(key, value); + return true; + } + + public IReadOnlyDictionary Items => _metaDataItems ?? EmptyReadOnly.Dictionary; + } +} diff --git a/src/Elasticsearch.Net/Connection/ConnectionInfo.cs b/src/Elasticsearch.Net/Connection/ConnectionInfo.cs new file mode 100644 index 00000000000..3e0ec831c6b --- /dev/null +++ b/src/Elasticsearch.Net/Connection/ConnectionInfo.cs @@ -0,0 +1,48 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +#if DOTNETCORE +using System.Net.Http; +#endif + +namespace Elasticsearch.Net +{ + public static class ConnectionInfo + { + public 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 + } + } + } +} \ No newline at end of file diff --git a/src/Elasticsearch.Net/Connection/HttpConnection-CoreFx.cs b/src/Elasticsearch.Net/Connection/HttpConnection-CoreFx.cs index 2feadcfade5..f477dfc240b 100644 --- a/src/Elasticsearch.Net/Connection/HttpConnection-CoreFx.cs +++ b/src/Elasticsearch.Net/Connection/HttpConnection-CoreFx.cs @@ -99,6 +99,7 @@ public virtual async Task RequestAsync(RequestData request Stream responseStream = null; Exception ex = null; string mimeType = null; + requestData.IsAsync = true; try { var requestMessage = CreateHttpRequestMessage(requestData); @@ -233,6 +234,13 @@ protected virtual HttpRequestMessage CreateRequestMessage(RequestData requestDat stream.Position = 0; } + if (requestData.MetaHeaderProvider is object) { + var value = requestData.MetaHeaderProvider.ProduceHeaderValue(requestData); + + if (!string.IsNullOrEmpty(value)) + requestMessage.Headers.TryAddWithoutValidation(requestData.MetaHeaderProvider.HeaderName, value); + } + return requestMessage; } diff --git a/src/Elasticsearch.Net/Connection/HttpWebRequestConnection.cs b/src/Elasticsearch.Net/Connection/HttpWebRequestConnection.cs index c9676031799..35f17d63752 100644 --- a/src/Elasticsearch.Net/Connection/HttpWebRequestConnection.cs +++ b/src/Elasticsearch.Net/Connection/HttpWebRequestConnection.cs @@ -82,6 +82,7 @@ CancellationToken cancellationToken Stream responseStream = null; Exception ex = null; string mimeType = null; + requestData.IsAsync = true; try { var data = requestData.PostData; @@ -195,6 +196,13 @@ protected virtual HttpWebRequest CreateWebRequest(RequestData requestData) if (requestData.Headers != null && requestData.Headers.HasKeys()) request.Headers.Add(requestData.Headers); + if (requestData.MetaHeaderProvider is object) { + var value = requestData.MetaHeaderProvider.ProduceHeaderValue(requestData); + + if (!string.IsNullOrEmpty(value)) + request.Headers.Add(requestData.MetaHeaderProvider.HeaderName, requestData.MetaHeaderProvider.ProduceHeaderValue(requestData)); + } + var timeout = (int)requestData.RequestTimeout.TotalMilliseconds; request.Timeout = timeout; request.ReadWriteTimeout = timeout; diff --git a/src/Elasticsearch.Net/Connection/MetaData/ClientVersionInfo.cs b/src/Elasticsearch.Net/Connection/MetaData/ClientVersionInfo.cs new file mode 100644 index 00000000000..6fadd6f7b80 --- /dev/null +++ b/src/Elasticsearch.Net/Connection/MetaData/ClientVersionInfo.cs @@ -0,0 +1,50 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Diagnostics; +using System.Reflection; +using System.Text.RegularExpressions; + +namespace Elasticsearch.Net +{ + internal sealed class ClientVersionInfo : VersionInfo + { + private static readonly Regex VersionRegex = new Regex(@"(\d+\.)(\d+\.)(\d)"); + + public static readonly ClientVersionInfo Empty = new ClientVersionInfo { Version = new Version(0, 0, 0), IsPrerelease = false }; + + private ClientVersionInfo() { } + + public static ClientVersionInfo Create() + { + var fullVersion = DetermineClientVersion(typeof(T)); + + var clientVersion = new ClientVersionInfo(); + clientVersion.StoreVersion(fullVersion); + return clientVersion; + } + + private static string DetermineClientVersion(Type type) + { + try + { + var productVersion = FileVersionInfo.GetVersionInfo(type.GetTypeInfo().Assembly.Location)?.ProductVersion ?? EmptyVersion; + + if (productVersion == EmptyVersion) + productVersion = Assembly.GetAssembly(type).GetName().Version.ToString(); + + var match = VersionRegex.Match(productVersion); + + return match.Success ? match.Value : EmptyVersion; + } + catch + { + // ignore failures and fall through + } + + return EmptyVersion; + } + } +} \ No newline at end of file diff --git a/src/Elasticsearch.Net/Connection/MetaData/MetaDataHeader.cs b/src/Elasticsearch.Net/Connection/MetaData/MetaDataHeader.cs new file mode 100644 index 00000000000..b238c492d4d --- /dev/null +++ b/src/Elasticsearch.Net/Connection/MetaData/MetaDataHeader.cs @@ -0,0 +1,44 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Text; + +namespace Elasticsearch.Net +{ + internal sealed class MetaDataHeader + { + private const char _separator = ','; + + private readonly string _headerValue; + + public MetaDataHeader(VersionInfo version, string serviceIdentifier, bool isAsync) + { + ClientVersion = version.ToString(); + RuntimeVersion = new RuntimeVersionInfo().ToString(); + ServiceIdentifier = serviceIdentifier; + + // This code is expected to be called infrequently so we're not concerns with over optimising this + + _headerValue = new StringBuilder(64) + .Append(serviceIdentifier).Append("=").Append(ClientVersion).Append(_separator) + .Append("a=").Append(isAsync ? "1" : "0").Append(_separator) + .Append("net=").Append(RuntimeVersion).Append(_separator) + .Append(_httpClientIdentifier).Append("=").Append(RuntimeVersion) + .ToString(); + } + + private static readonly string _httpClientIdentifier = +#if DOTNETCORE + ConnectionInfo.UsingCurlHandler ? "cu" : "so"; +#else + "wr"; +#endif + + public string ServiceIdentifier { get; private set; } + public string ClientVersion { get; private set; } + public string RuntimeVersion { get; private set; } + + public override string ToString() => _headerValue; + } +} \ No newline at end of file diff --git a/src/Elasticsearch.Net/Connection/MetaData/MetaHeaderProvider.cs b/src/Elasticsearch.Net/Connection/MetaData/MetaHeaderProvider.cs new file mode 100644 index 00000000000..6875f6ffecd --- /dev/null +++ b/src/Elasticsearch.Net/Connection/MetaData/MetaHeaderProvider.cs @@ -0,0 +1,50 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +namespace Elasticsearch.Net +{ + /// + /// Produces the meta header when this functionality is enabled in the . + /// + public class MetaHeaderProvider + { + private const string MetaHeaderName = "x-elastic-client-meta"; + + private readonly MetaDataHeader _asyncMetaDataHeader; + private readonly MetaDataHeader _syncMetaDataHeader; + + public MetaHeaderProvider() + { + var clientVersionInfo = ClientVersionInfo.Create(); + _asyncMetaDataHeader = new MetaDataHeader(clientVersionInfo, "es", true); + _syncMetaDataHeader = new MetaDataHeader(clientVersionInfo, "es", false); + } + + public string HeaderName => MetaHeaderName; + + public string ProduceHeaderValue(RequestData requestData) + { + try + { + if (requestData.ConnectionSettings.DisableMetaHeader) + return null; + + var headerValue = requestData.IsAsync + ? _asyncMetaDataHeader.ToString() + : _syncMetaDataHeader.ToString(); + + if (requestData.RequestMetaData.TryGetValue(RequestMetaData.HelperKey, out var helperSuffix)) + headerValue = $"{headerValue},h={helperSuffix}"; + + return headerValue; + } + catch + { + // don't fail the application just because we cannot create this optional header + } + + return string.Empty; + } + } +} \ No newline at end of file diff --git a/src/Elasticsearch.Net/Connection/MetaData/RuntimeVersionInfo.cs b/src/Elasticsearch.Net/Connection/MetaData/RuntimeVersionInfo.cs new file mode 100644 index 00000000000..d15f9a38b86 --- /dev/null +++ b/src/Elasticsearch.Net/Connection/MetaData/RuntimeVersionInfo.cs @@ -0,0 +1,243 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +// Adapted from BenchmarkDotNet source https://github.com/dotnet/BenchmarkDotNet/blob/master/src/BenchmarkDotNet/Environments/Runtimes/CoreRuntime.cs +#region BenchmarkDotNet License https://github.com/dotnet/BenchmarkDotNet/blob/master/LICENSE.md +// The MIT License +// Copyright (c) 2013–2020.NET Foundation and contributors + +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software +// and associated documentation files (the "Software"), to deal in the Software without restriction, +// including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all copies or substantial +// portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +// LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#endregion + +using System; +#if DOTNETCORE +using System.Diagnostics; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +#else +using Microsoft.Win32; +using System.Linq; +#endif + +namespace Elasticsearch.Net +{ + /// + /// Represents the current .NET Runtime version. + /// + internal sealed class RuntimeVersionInfo : VersionInfo + { + public static readonly RuntimeVersionInfo Default = new RuntimeVersionInfo { Version = new Version(0, 0, 0), IsPrerelease = false }; + + public RuntimeVersionInfo() => StoreVersion(GetRuntimeVersion()); + + private static string GetRuntimeVersion() => +#if !DOTNETCORE + GetFullFrameworkRuntime(); +#else + GetNetCoreVersion(); +#endif + +#if DOTNETCORE + private static string GetNetCoreVersion() + { + // for .NET 5+ we can use Environment.Version + if (Environment.Version.Major >= 5) + { + const string dotNet = ".NET "; + var index = RuntimeInformation.FrameworkDescription.IndexOf(dotNet, StringComparison.OrdinalIgnoreCase); + if (index >= 0) + { + return RuntimeInformation.FrameworkDescription.Substring(dotNet.Length); + } + } + + // next, try using file version info + var systemPrivateCoreLib = FileVersionInfo.GetVersionInfo(typeof(object).Assembly.Location); + if (TryGetVersionFromProductInfo(systemPrivateCoreLib.ProductVersion, systemPrivateCoreLib.ProductName, out var runtimeVersion)) + { + return runtimeVersion; + } + + var assembly = typeof(System.Runtime.GCSettings).GetTypeInfo().Assembly; + if (TryGetVersionFromAssemblyPath(assembly, out runtimeVersion)) + { + return runtimeVersion; + } + + //At this point, we can't identify whether this is a prerelease, but a version is better than nothing! + + var frameworkName = Assembly.GetEntryAssembly()?.GetCustomAttribute()?.FrameworkName; + if (TryGetVersionFromFrameworkName(frameworkName, out runtimeVersion)) + { + return runtimeVersion; + } + + if (IsRunningInContainer) + { + var dotNetVersion = Environment.GetEnvironmentVariable("DOTNET_VERSION"); + var aspNetCoreVersion = Environment.GetEnvironmentVariable("ASPNETCORE_VERSION"); + + return dotNetVersion ?? aspNetCoreVersion; + } + + return null; + } + + private static bool TryGetVersionFromAssemblyPath(Assembly assembly, out string runtimeVersion) + { + var assemblyPath = assembly.CodeBase.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries); + var netCoreAppIndex = Array.IndexOf(assemblyPath, "Microsoft.NETCore.App"); + if (netCoreAppIndex > 0 && netCoreAppIndex < assemblyPath.Length - 2) + { + runtimeVersion = assemblyPath[netCoreAppIndex + 1]; + return true; + } + + runtimeVersion = null; + return false; + } + + // NOTE: 5.0.1 FrameworkDescription returns .NET 5.0.1-servicing.20575.16, so we special case servicing as NOT prerelease + protected override bool ContainsPrerelease(string version) => base.ContainsPrerelease(version) && !version.Contains("-servicing"); + + // sample input: + // 2.0: 4.6.26614.01 @BuiltBy: dlab14-DDVSOWINAGE018 @Commit: a536e7eec55c538c94639cefe295aa672996bf9b, Microsoft .NET Framework + // 2.1: 4.6.27817.01 @BuiltBy: dlab14-DDVSOWINAGE101 @Branch: release/2.1 @SrcCode: https://github.com/dotnet/coreclr/tree/6f78fbb3f964b4f407a2efb713a186384a167e5c, Microsoft .NET Framework + // 2.2: 4.6.27817.03 @BuiltBy: dlab14-DDVSOWINAGE101 @Branch: release/2.2 @SrcCode: https://github.com/dotnet/coreclr/tree/ce1d090d33b400a25620c0145046471495067cc7, Microsoft .NET Framework + // 3.0: 3.0.0-preview8.19379.2+ac25be694a5385a6a1496db40de932df0689b742, Microsoft .NET Core + // 5.0: 5.0.0-alpha1.19413.7+0ecefa44c9d66adb8a997d5778dc6c246ad393a7, Microsoft .NET Core + private static bool TryGetVersionFromProductInfo(string productVersion, string productName, out string version) + { + if (string.IsNullOrEmpty(productVersion) || string.IsNullOrEmpty(productName)) + { + version = null; + return false; + } + + // yes, .NET Core 2.X has a product name == .NET Framework... + if (productName.IndexOf(".NET Framework", StringComparison.OrdinalIgnoreCase) >= 0) + { + const string releaseVersionPrefix = "release/"; + var releaseVersionIndex = productVersion.IndexOf(releaseVersionPrefix); + if (releaseVersionIndex > 0) + { + version = productVersion.Substring(releaseVersionIndex + releaseVersionPrefix.Length); + return true; + } + } + + // matches .NET Core and also .NET 5+ + if (productName.IndexOf(".NET", StringComparison.OrdinalIgnoreCase) >= 0) + { + version = productVersion; + return true; + } + + version = null; + return false; + } + + // sample input: + // .NETCoreApp,Version=v2.0 + // .NETCoreApp,Version=v2.1 + private static bool TryGetVersionFromFrameworkName(string frameworkName, out string runtimeVersion) + { + const string versionPrefix = ".NETCoreApp,Version=v"; + if (!string.IsNullOrEmpty(frameworkName) && frameworkName.StartsWith(versionPrefix)) + { + runtimeVersion = frameworkName.Substring(versionPrefix.Length); + return true; + } + + runtimeVersion = null; + return false; + } + + private static bool IsRunningInContainer => string.Equals(Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER"), "true"); +#endif + +#if !DOTNETCORE + private static string GetFullFrameworkRuntime() + { + const string subkey = @"SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full\"; + + using (var ndpKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32).OpenSubKey(subkey)) + { + if (ndpKey != null && ndpKey.GetValue("Release") != null) + { + var version = CheckFor45PlusVersion((int)ndpKey.GetValue("Release")); + + if (!string.IsNullOrEmpty(version) ) + return version; + } + } + + var fullName = RuntimeInformation.FrameworkDescription; + var servicingVersion = new string(fullName.SkipWhile(c => !char.IsDigit(c)).ToArray()); + var servicingVersionRelease = MapToReleaseVersion(servicingVersion); + + return servicingVersionRelease; + + static string MapToReleaseVersion(string servicingVersion) + { + // the following code assumes that .NET 4.6.1 is the oldest supported version + if (string.Compare(servicingVersion, "4.6.2") < 0) + return "4.6.1"; + if (string.Compare(servicingVersion, "4.7") < 0) + return "4.6.2"; + if (string.Compare(servicingVersion, "4.7.1") < 0) + return "4.7"; + if (string.Compare(servicingVersion, "4.7.2") < 0) + return "4.7.1"; + if (string.Compare(servicingVersion, "4.8") < 0) + return "4.7.2"; + + return "4.8.0"; // most probably the last major release of Full .NET Framework + } + + // Checking the version using >= enables forward compatibility. + static string CheckFor45PlusVersion(int releaseKey) + { + if (releaseKey >= 528040) + return "4.8.0"; + if (releaseKey >= 461808) + return "4.7.2"; + if (releaseKey >= 461308) + return "4.7.1"; + if (releaseKey >= 460798) + return "4.7"; + if (releaseKey >= 394802) + return "4.6.2"; + if (releaseKey >= 394254) + return "4.6.1"; + if (releaseKey >= 393295) + return "4.6"; + if (releaseKey >= 379893) + return "4.5.2"; + if (releaseKey >= 378675) + return "4.5.1"; + if (releaseKey >= 378389) + return "4.5.0"; + // This code should never execute. A non-null release key should mean + // that 4.5 or later is installed. + return null; + } + } +#endif + } +} \ No newline at end of file diff --git a/src/Elasticsearch.Net/Connection/MetaData/VersionInfo.cs b/src/Elasticsearch.Net/Connection/MetaData/VersionInfo.cs new file mode 100644 index 00000000000..10c1422bc55 --- /dev/null +++ b/src/Elasticsearch.Net/Connection/MetaData/VersionInfo.cs @@ -0,0 +1,47 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Linq; + +namespace Elasticsearch.Net +{ + public abstract class VersionInfo + { + protected const string EmptyVersion = "0.0.0"; + + public Version Version { get; protected set; } + public bool IsPrerelease { get; protected set; } + + protected void StoreVersion(string fullVersion) + { + if (string.IsNullOrEmpty(fullVersion)) + fullVersion = EmptyVersion; + + var clientVersion = GetParsableVersionPart(fullVersion); + + if (!Version.TryParse(clientVersion, out var parsedVersion)) + throw new ArgumentException("Invalid version string", nameof(fullVersion)); + + var finalVersion = parsedVersion; + + if (parsedVersion.Minor == -1 || parsedVersion.Build == -1) + finalVersion = new Version(parsedVersion.Major, parsedVersion.Minor > -1 + ? parsedVersion.Minor + : 0, parsedVersion.Build > -1 + ? parsedVersion.Build + : 0); + + Version = finalVersion; + IsPrerelease = ContainsPrerelease(fullVersion); + } + + protected virtual bool ContainsPrerelease(string version) => version.Contains("-"); + + private static string GetParsableVersionPart(string fullVersionName) => + new string(fullVersionName.TakeWhile(c => char.IsDigit(c) || c == '.').ToArray()); + + public override string ToString() => IsPrerelease ? Version.ToString() + "p" : Version.ToString(); + } +} diff --git a/src/Elasticsearch.Net/Domain/RequestParameters/RequestParametersExtensions.cs b/src/Elasticsearch.Net/Domain/RequestParameters/RequestParametersExtensions.cs new file mode 100644 index 00000000000..fc2573b1dbe --- /dev/null +++ b/src/Elasticsearch.Net/Domain/RequestParameters/RequestParametersExtensions.cs @@ -0,0 +1,24 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; + +namespace Elasticsearch.Net +{ + internal static class RequestParametersExtensions + { + internal static void SetRequestMetaData(this IRequestParameters parameters, RequestMetaData requestMetaData) + { + if (parameters is null) + throw new ArgumentNullException(nameof(parameters)); + + if (requestMetaData is null) + throw new ArgumentNullException(nameof(requestMetaData)); + + parameters.RequestConfiguration ??= new RequestConfiguration(); + + parameters.RequestConfiguration.RequestMetaData = requestMetaData; + } + } +} \ No newline at end of file diff --git a/src/Elasticsearch.Net/Extensions/Extensions.cs b/src/Elasticsearch.Net/Extensions/Extensions.cs index 091fa59851c..9c3f64a8e06 100644 --- a/src/Elasticsearch.Net/Extensions/Extensions.cs +++ b/src/Elasticsearch.Net/Extensions/Extensions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Globalization; using System.IO; using System.Linq; @@ -7,6 +8,16 @@ namespace Elasticsearch.Net { + internal static class EmptyReadOnly + { + public static readonly IReadOnlyCollection Collection = new ReadOnlyCollection(new TElement[0]); + } + + internal static class EmptyReadOnly + { + public static readonly IReadOnlyDictionary Dictionary = new ReadOnlyDictionary(new Dictionary(0)); + } + internal static class Extensions { private const long MillisecondsInAWeek = MillisecondsInADay * 7; diff --git a/src/Elasticsearch.Net/Transport/Pipeline/RequestData.cs b/src/Elasticsearch.Net/Transport/Pipeline/RequestData.cs index 370735beba7..0550f135a9b 100644 --- a/src/Elasticsearch.Net/Transport/Pipeline/RequestData.cs +++ b/src/Elasticsearch.Net/Transport/Pipeline/RequestData.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Collections.Specialized; using System.IO; using System.Linq; @@ -69,6 +70,8 @@ IMemoryStreamFactory memoryStreamFactory AllowedStatusCodes = local?.AllowedStatusCodes ?? Enumerable.Empty(); ClientCertificates = local?.ClientCertificates ?? global.ClientCertificates; UserAgent = global.UserAgent; + MetaHeaderProvider = global.MetaHeaderProvider; + RequestMetaData = local?.RequestMetaData?.Items ?? EmptyReadOnly.Dictionary; } public string Accept { get; } @@ -111,6 +114,10 @@ IMemoryStreamFactory memoryStreamFactory public Uri Uri => Node != null ? new Uri(Node.Uri, PathAndQuery) : null; public TimeSpan DnsRefreshTimeout { get; } + public MetaHeaderProvider MetaHeaderProvider { get; } + public IReadOnlyDictionary RequestMetaData { get; } + public bool IsAsync { get; set; } + private string CreatePathWithQueryStrings(string path, IConnectionConfigurationValues global, IRequestParameters request) { path = path ?? string.Empty; diff --git a/src/Nest/Document/Multiple/BulkAll/BulkAllObservable.cs b/src/Nest/Document/Multiple/BulkAll/BulkAllObservable.cs index 7f5a0f2eb36..c410fd8293c 100644 --- a/src/Nest/Document/Multiple/BulkAll/BulkAllObservable.cs +++ b/src/Nest/Document/Multiple/BulkAll/BulkAllObservable.cs @@ -107,7 +107,21 @@ private void RefreshOnCompleted() var indices = _partitionedBulkRequest.RefreshIndices ?? _partitionedBulkRequest.Index; if (indices == null) return; - var refresh = _client.Refresh(indices); + var refresh = _client.Refresh(indices, r => r.RequestConfiguration(rc => + { + switch (_partitionedBulkRequest) + { + case IHelperCallable helperCallable when helperCallable.ParentMetaData is object: + rc.RequestMetaData(helperCallable.ParentMetaData); + break; + default: + rc.RequestMetaData(RequestMetaDataFactory.BulkHelperRequestMetaData()); + break; + } + + return rc; + })); + if (!refresh.IsValid) throw Throw($"Refreshing after all documents have indexed failed", refresh.ApiCall); } @@ -129,6 +143,16 @@ private async Task BulkAsync(IList buffer, long page, int b if (request.Routing != null) s.Routing(request.Routing); if (request.WaitForActiveShards.HasValue) s.WaitForActiveShards(request.WaitForActiveShards.ToString()); + switch (_partitionedBulkRequest) + { + case IHelperCallable helperCallable when helperCallable.ParentMetaData is object: + s.RequestConfiguration(rc => rc.RequestMetaData(helperCallable.ParentMetaData)); + break; + default: + s.RequestConfiguration(rc => rc.RequestMetaData(RequestMetaDataFactory.BulkHelperRequestMetaData())); + break; + } + return s; }, _compositeCancelToken) .ConfigureAwait(false); diff --git a/src/Nest/Document/Multiple/BulkAll/BulkAllRequest.cs b/src/Nest/Document/Multiple/BulkAll/BulkAllRequest.cs index 37b16001875..f0f5f5a0fc8 100644 --- a/src/Nest/Document/Multiple/BulkAll/BulkAllRequest.cs +++ b/src/Nest/Document/Multiple/BulkAll/BulkAllRequest.cs @@ -111,7 +111,7 @@ public interface IBulkAllRequest where T : class Action BulkResponseCallback { get; set; } } - public class BulkAllRequest : IBulkAllRequest + public class BulkAllRequest : IBulkAllRequest, IHelperCallable where T : class { public BulkAllRequest(IEnumerable documents) @@ -180,9 +180,13 @@ public BulkAllRequest(IEnumerable documents) /// public Action BulkResponseCallback { get; set; } + + internal RequestMetaData ParentMetaData { get; set; } + + RequestMetaData IHelperCallable.ParentMetaData { get => ParentMetaData; set => ParentMetaData = value; } } - public class BulkAllDescriptor : DescriptorBase, IBulkAllRequest>, IBulkAllRequest + public class BulkAllDescriptor : DescriptorBase, IBulkAllRequest>, IBulkAllRequest, IHelperCallable where T : class { private readonly IEnumerable _documents; @@ -215,6 +219,7 @@ public BulkAllDescriptor(IEnumerable documents) TypeName IBulkAllRequest.Type { get; set; } int? IBulkAllRequest.WaitForActiveShards { get; set; } Action IBulkAllRequest.BulkResponseCallback { get; set; } + RequestMetaData IHelperCallable.ParentMetaData { get; set; } /// public BulkAllDescriptor MaxDegreeOfParallelism(int? parallelism) => diff --git a/src/Nest/Document/Multiple/Reindex/ReindexObservable.cs b/src/Nest/Document/Multiple/Reindex/ReindexObservable.cs index fc990aee6fc..c944c610d6e 100644 --- a/src/Nest/Document/Multiple/Reindex/ReindexObservable.cs +++ b/src/Nest/Document/Multiple/Reindex/ReindexObservable.cs @@ -123,6 +123,13 @@ private BulkAllObservable> BulkAll(IEnumerable> ScrollAll(int slices, ProducerC RoutingField = scrollAll.RoutingField, MaxDegreeOfParallelism = scrollAll.MaxDegreeOfParallelism ?? slices, Search = scrollAll.Search, - BackPressure = backPressure + BackPressure = backPressure, + ParentMetaData = RequestMetaDataFactory.ReindexHelperRequestMetaData() }; var scrollObservable = _client.ScrollAll(scrollAllRequest, _compositeCancelToken); @@ -217,11 +225,14 @@ private int CreateIndex(string toIndex, IScrollAllRequest scrollAll) /// Either the number of shards from to source or the target as a slice hint to ScrollAll private int? CreateIndexIfNeeded(Indices fromIndices, string resolvedTo) { + var requestMetaData = RequestMetaDataFactory.ReindexHelperRequestMetaData(); + if (_reindexRequest.OmitIndexCreation) return null; var pointsToSingleSourceIndex = fromIndices.Match((a) => false, (m) => m.Indices.Count == 1); - var targetExistsAlready = _client.IndexExists(resolvedTo); - if (targetExistsAlready.Exists) return null; + var targetExistsAlready = _client.IndexExists(resolvedTo, e => e.RequestConfiguration(rc => rc.RequestMetaData(requestMetaData))); + if (targetExistsAlready.Exists) + return null; _compositeCancelToken.ThrowIfCancellationRequested(); IndexState originalIndexState = null; @@ -229,7 +240,7 @@ private int CreateIndex(string toIndex, IScrollAllRequest scrollAll) if (pointsToSingleSourceIndex) { - var getIndexResponse = _client.GetIndex(resolvedFrom); + var getIndexResponse = _client.GetIndex(resolvedFrom, i => i.RequestConfiguration(rc => rc.RequestMetaData(requestMetaData))); _compositeCancelToken.ThrowIfCancellationRequested(); originalIndexState = getIndexResponse.Indices[resolvedFrom]; if (_reindexRequest.OmitIndexCreation) @@ -240,6 +251,12 @@ private int CreateIndex(string toIndex, IScrollAllRequest scrollAll) (originalIndexState != null ? new CreateIndexRequest(resolvedTo, originalIndexState) : new CreateIndexRequest(resolvedTo)); + + if (createIndexRequest.RequestParameters.RequestConfiguration == null) + createIndexRequest.RequestParameters.RequestConfiguration = new RequestConfiguration(); + + createIndexRequest.RequestParameters.RequestConfiguration.SetRequestMetaData(requestMetaData); + var createIndexResponse = _client.CreateIndex(createIndexRequest); _compositeCancelToken.ThrowIfCancellationRequested(); if (!createIndexResponse.IsValid) diff --git a/src/Nest/Document/Multiple/ScrollAll/ScrollAllObservable.cs b/src/Nest/Document/Multiple/ScrollAll/ScrollAllObservable.cs index 916d07bd394..93a08719fa9 100644 --- a/src/Nest/Document/Multiple/ScrollAll/ScrollAllObservable.cs +++ b/src/Nest/Document/Multiple/ScrollAll/ScrollAllObservable.cs @@ -29,6 +29,20 @@ public ScrollAllObservable( { _scrollAllRequest = scrollAllRequest; _searchRequest = scrollAllRequest?.Search ?? new SearchRequest(); + + if (_searchRequest.RequestParameters.RequestConfiguration is null) + _searchRequest.RequestParameters.RequestConfiguration = new RequestConfiguration(); + + switch (_scrollAllRequest) + { + case IHelperCallable helperCallable when helperCallable.ParentMetaData is object: + _searchRequest.RequestParameters.RequestConfiguration.SetRequestMetaData(helperCallable.ParentMetaData); + break; + default: + _searchRequest.RequestParameters.RequestConfiguration.SetRequestMetaData(RequestMetaDataFactory.ScrollHelperRequestMetaData()); + break; + } + if (_searchRequest.Sort == null) _searchRequest.Sort = SortField.ByDocumentOrder; _searchRequest.RequestParameters.Scroll = _scrollAllRequest.ScrollTime.ToTimeSpan(); @@ -107,6 +121,18 @@ private async Task ScrollToCompletionAsync(int slice, IObserver(request, _compositeCancelToken).ConfigureAwait(false); ThrowOnBadSearchResult(searchResult, slice, page); } diff --git a/src/Nest/Document/Multiple/ScrollAll/ScrollAllRequest.cs b/src/Nest/Document/Multiple/ScrollAll/ScrollAllRequest.cs index 2088128eae1..889f33eaa11 100644 --- a/src/Nest/Document/Multiple/ScrollAll/ScrollAllRequest.cs +++ b/src/Nest/Document/Multiple/ScrollAll/ScrollAllRequest.cs @@ -1,5 +1,6 @@ using System; using System.Linq.Expressions; +using Elasticsearch.Net; namespace Nest { @@ -42,7 +43,7 @@ public interface IScrollAllRequest int Slices { get; set; } } - public class ScrollAllRequest : IScrollAllRequest + public class ScrollAllRequest : IScrollAllRequest, IHelperCallable { public ScrollAllRequest(Time scrollTime, int numberOfSlices) { @@ -63,8 +64,11 @@ public ScrollAllRequest(Time scrollTime, int numberOfSlices, Field routingField) /// public Field RoutingField { get; set; } + internal RequestMetaData ParentMetaData { get; set; } + /// public ISearchRequest Search { get; set; } + RequestMetaData IHelperCallable.ParentMetaData { get => ParentMetaData; set => ParentMetaData = value; } Time IScrollAllRequest.ScrollTime { get; set; } int IScrollAllRequest.Slices { get; set; } diff --git a/src/Nest/Helpers/HelperIdentifiers.cs b/src/Nest/Helpers/HelperIdentifiers.cs new file mode 100644 index 00000000000..d6b016da729 --- /dev/null +++ b/src/Nest/Helpers/HelperIdentifiers.cs @@ -0,0 +1,15 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +namespace Nest +{ + internal static class HelperIdentifiers + { + public const string SnapshotHelper = "sn"; + public const string ScrollHelper = "s"; + public const string ReindexHelper = "r"; + public const string BulkHelper = "b"; + public const string RestoreHelper = "sr"; + } +} \ No newline at end of file diff --git a/src/Nest/Helpers/IHelperCallable.cs b/src/Nest/Helpers/IHelperCallable.cs new file mode 100644 index 00000000000..19bb5f1e28e --- /dev/null +++ b/src/Nest/Helpers/IHelperCallable.cs @@ -0,0 +1,25 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using Elasticsearch.Net; + +namespace Nest +{ + /// + /// May be applied to helper requests where they may be called by an upstream helper. + /// + /// + /// For example, the reindex helper calls down into the bulk helper and scroll helpers. + /// and therefore both + /// implement this interface. + /// + internal interface IHelperCallable + { + /// + /// The of the parent helper when this requestis created by a parent + /// helper. + /// + RequestMetaData ParentMetaData { get; internal set; } + } +} \ No newline at end of file diff --git a/src/Nest/Helpers/RequestMetaDataExtensions.cs b/src/Nest/Helpers/RequestMetaDataExtensions.cs new file mode 100644 index 00000000000..c627c3b1964 --- /dev/null +++ b/src/Nest/Helpers/RequestMetaDataExtensions.cs @@ -0,0 +1,66 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using Elasticsearch.Net; + +namespace Nest +{ + internal static class RequestMetaDataExtensions + { + internal static void AddHelper(this RequestMetaData metaData, string helperValue) + { + if (!metaData.TryAddMetaData(RequestMetaData.HelperKey, helperValue)) + throw new InvalidOperationException("A helper value has already been added."); + } + + internal static void AddSnapshotHelper(this RequestMetaData metaData) => metaData.AddHelper(HelperIdentifiers.SnapshotHelper); + + internal static void AddScrollHelper(this RequestMetaData metaData) => metaData.AddHelper(HelperIdentifiers.ScrollHelper); + + internal static void AddReindexHelper(this RequestMetaData metaData) => metaData.AddHelper(HelperIdentifiers.ReindexHelper); + + internal static void AddBulkHelper(this RequestMetaData metaData) => metaData.AddHelper(HelperIdentifiers.BulkHelper); + + internal static void AddRestoreHelper(this RequestMetaData metaData) => metaData.AddHelper(HelperIdentifiers.RestoreHelper); + } + + internal static class RequestMetaDataFactory + { + internal static RequestMetaData ReindexHelperRequestMetaData() + { + var metaData = new RequestMetaData(); + metaData.AddReindexHelper(); + return metaData; + } + + internal static RequestMetaData ScrollHelperRequestMetaData() + { + var metaData = new RequestMetaData(); + metaData.AddScrollHelper(); + return metaData; + } + + internal static RequestMetaData BulkHelperRequestMetaData() + { + var metaData = new RequestMetaData(); + metaData.AddBulkHelper(); + return metaData; + } + + internal static RequestMetaData SnapshotHelperRequestMetaData() + { + var metaData = new RequestMetaData(); + metaData.AddSnapshotHelper(); + return metaData; + } + + internal static RequestMetaData RestoreHelperRequestMetaData() + { + var metaData = new RequestMetaData(); + metaData.AddRestoreHelper(); + return metaData; + } + } +} \ No newline at end of file diff --git a/src/Nest/Modules/SnapshotAndRestore/Restore/RestoreObservable/RestoreObservable.cs b/src/Nest/Modules/SnapshotAndRestore/Restore/RestoreObservable/RestoreObservable.cs index fea3f641494..784bbca20ae 100644 --- a/src/Nest/Modules/SnapshotAndRestore/Restore/RestoreObservable/RestoreObservable.cs +++ b/src/Nest/Modules/SnapshotAndRestore/Restore/RestoreObservable/RestoreObservable.cs @@ -26,7 +26,11 @@ public RestoreObservable(IElasticClient elasticClient, IRestoreRequest restoreRe _elasticClient = elasticClient; _restoreRequest = restoreRequest; + + if (_restoreRequest.RequestParameters.RequestConfiguration is null) + _restoreRequest.RequestParameters.RequestConfiguration = new RequestConfiguration(); + _restoreRequest.RequestParameters.RequestConfiguration.SetRequestMetaData(RequestMetaDataFactory.RestoreHelperRequestMetaData()); _restoreStatusHumbleObject = new RestoreStatusHumbleObject(elasticClient, restoreRequest); _restoreStatusHumbleObject.Completed += StopTimer; _restoreStatusHumbleObject.Error += StopTimer; @@ -179,10 +183,13 @@ public void CheckStatus() )) .ToArray(); - var recoveryStatus = _elasticClient.RecoveryStatus(new RecoveryStatusRequest(indices) - { + var recoveryStatusRequest = new RecoveryStatusRequest(indices) + { Detailed = true, - }); + RequestConfiguration = new RequestConfiguration() + }; + recoveryStatusRequest.RequestConfiguration.SetRequestMetaData(RequestMetaDataFactory.RestoreHelperRequestMetaData()); + var recoveryStatus = _elasticClient.RecoveryStatus(recoveryStatusRequest); if (!recoveryStatus.IsValid) throw new ElasticsearchClientException(PipelineFailure.BadResponse, "Failed getting recovery status.", recoveryStatus.ApiCall); diff --git a/src/Nest/Modules/SnapshotAndRestore/Snapshot/SnapshotObservable/SnapshotObservable.cs b/src/Nest/Modules/SnapshotAndRestore/Snapshot/SnapshotObservable/SnapshotObservable.cs index 5dc19b4a821..44c02873602 100644 --- a/src/Nest/Modules/SnapshotAndRestore/Snapshot/SnapshotObservable/SnapshotObservable.cs +++ b/src/Nest/Modules/SnapshotAndRestore/Snapshot/SnapshotObservable/SnapshotObservable.cs @@ -25,6 +25,9 @@ public SnapshotObservable(IElasticClient elasticClient, ISnapshotRequest snapsho _elasticClient = elasticClient; _snapshotRequest = snapshotRequest; + if (_snapshotRequest.RequestParameters.RequestConfiguration is null) + _snapshotRequest.RequestParameters.RequestConfiguration = new RequestConfiguration(); + _snapshotRequest.RequestParameters.RequestConfiguration.SetRequestMetaData(RequestMetaDataFactory.SnapshotHelperRequestMetaData()); _snapshotStatusHumbleObject = new SnapshotStatusHumbleObject(elasticClient, snapshotRequest); _snapshotStatusHumbleObject.Completed += StopTimer; _snapshotStatusHumbleObject.Error += StopTimer; @@ -165,9 +168,15 @@ public void CheckStatus() { try { + var snapshotRequest = new SnapshotStatusRequest(_snapshotRequest.RepositoryName, _snapshotRequest.Snapshot) + { + RequestConfiguration = new RequestConfiguration() + }; + + snapshotRequest.RequestConfiguration.SetRequestMetaData(RequestMetaDataFactory.SnapshotHelperRequestMetaData()); + var snapshotStatusResponse = - _elasticClient.SnapshotStatus(new SnapshotStatusRequest(_snapshotRequest.RepositoryName, - _snapshotRequest.Snapshot)); + _elasticClient.SnapshotStatus(snapshotRequest); if (!snapshotStatusResponse.IsValid) throw new ElasticsearchClientException(PipelineFailure.BadResponse, "Failed to get snapshot status.", diff --git a/src/Tests/Tests/ClientConcepts/Connection/HttpConnectionTests.cs b/src/Tests/Tests/ClientConcepts/Connection/HttpConnectionTests.cs index 983f5430dea..11005a04d16 100644 --- a/src/Tests/Tests/ClientConcepts/Connection/HttpConnectionTests.cs +++ b/src/Tests/Tests/ClientConcepts/Connection/HttpConnectionTests.cs @@ -1,6 +1,8 @@ #if DOTNETCORE using System; +using System.Linq; using System.Net.Http; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Elastic.Elasticsearch.Xunit.XunitPlumbing; @@ -60,7 +62,9 @@ private static RequestData CreateRequestData( TimeSpan requestTimeout = default(TimeSpan), Uri proxyAddress = null, bool disableAutomaticProxyDetection = false, - bool httpCompression = false + bool httpCompression = false, + bool disableMetaHeader = false, + Action requestMetaData = null ) { if (requestTimeout == default(TimeSpan)) requestTimeout = TimeSpan.FromSeconds(10); @@ -68,12 +72,22 @@ private static RequestData CreateRequestData( var connectionSettings = new ConnectionSettings(new Uri("http://localhost:9200")) .RequestTimeout(requestTimeout) .DisableAutomaticProxyDetection(disableAutomaticProxyDetection) - .EnableHttpCompression(httpCompression); + .EnableHttpCompression(httpCompression) + .DisableMetaHeader(disableMetaHeader); if (proxyAddress != null) connectionSettings.Proxy(proxyAddress, null, null); - var requestData = new RequestData(HttpMethod.GET, "/", null, connectionSettings, new PingRequestParameters(), + var requestParameters = new SearchRequestParameters(); + + if (requestMetaData is object) + { + requestParameters.RequestConfiguration ??= new RequestConfiguration(); + requestParameters.RequestConfiguration.RequestMetaData ??= new RequestMetaData(); + requestMetaData(requestParameters.RequestConfiguration.RequestMetaData); + } + + var requestData = new RequestData(HttpMethod.GET, "/", null, connectionSettings, requestParameters, new MemoryStreamFactory()) { Node = new Node(new Uri("http://localhost:9200")) @@ -109,8 +123,64 @@ [I] public async Task HttpClientUseProxyShouldBeTrueWhenEnabledAutoProxyDetectio connection.LastUsedHttpClientHandler.UseProxy.Should().BeTrue(); } + [U] public async Task HttpClientSetsMetaHeaderWhenNotDisabled() + { + var regex = new Regex(@"^[a-z]{1,}=[a-z0-9\.\-]{1,}(?:,[a-z]{1,}=[a-z0-9\.\-]+)*$"); + + var requestData = CreateRequestData(); + var connection = new TestableHttpConnection(responseMessage => + { + responseMessage.RequestMessage.Headers.TryGetValues("x-elastic-client-meta", out var headerValue).Should().BeTrue(); + headerValue.Should().HaveCount(1); + headerValue.Single().Should().NotBeNullOrEmpty(); + regex.Match(headerValue.Single()).Success.Should().BeTrue(); + }); + + connection.Request(requestData); + await connection.RequestAsync(requestData, CancellationToken.None).ConfigureAwait(false); + } + + [U] public async Task HttpClientSetsMetaHeaderWithHelperWhenNotDisabled() + { + var regex = new Regex(@"^[a-z]{1,}=[a-z0-9\.\-]{1,}(?:,[a-z]{1,}=[a-z0-9\.\-]+)*$"); + + var requestData = CreateRequestData(requestMetaData: m => m.TryAddMetaData("helper", "r")); + var connection = new TestableHttpConnection(responseMessage => + { + responseMessage.RequestMessage.Headers.TryGetValues("x-elastic-client-meta", out var headerValue).Should().BeTrue(); + headerValue.Should().HaveCount(1); + headerValue.Single().Should().NotBeNullOrEmpty(); + headerValue.Single().Should().EndWith(",h=r"); + regex.Match(headerValue.Single()).Success.Should().BeTrue(); + }); + + connection.Request(requestData); + await connection.RequestAsync(requestData, CancellationToken.None).ConfigureAwait(false); + } + + [U] public async Task HttpClientShouldNotSetMetaHeaderWhenDisabled() + { + var requestData = CreateRequestData(disableMetaHeader: true); + var connection = new TestableHttpConnection(responseMessage => + { + responseMessage.RequestMessage.Headers.TryGetValues("x-elastic-client-meta", out var headerValue).Should().BeFalse(); + }); + + connection.Request(requestData); + await connection.RequestAsync(requestData, CancellationToken.None).ConfigureAwait(false); + } + public class TestableHttpConnection : HttpConnection { + private readonly Action _response; + + private TestableClientHandler _handler; + + public TestableHttpConnection(Action response) => _response = response; + + public TestableHttpConnection() { + } + public int CallCount { get; private set; } public int ClientCount => Clients.Count; @@ -130,8 +200,21 @@ public override Task RequestAsync(RequestData requestData, protected override HttpClientHandler CreateHttpClientHandler(RequestData requestData) { - LastUsedHttpClientHandler = base.CreateHttpClientHandler(requestData); - return LastUsedHttpClientHandler; + _handler = new TestableClientHandler(base.CreateHttpClientHandler(requestData), _response); + return _handler; + } + } + + public class TestableClientHandler : HttpClientHandler { + private readonly Action _responseAction; + + public TestableClientHandler(HttpMessageHandler handler, Action responseAction) => + _responseAction = responseAction; + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { + var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + _responseAction?.Invoke(response); + return response; } } } diff --git a/src/Tests/Tests/Connection/MetaData/MetaHeaderHelperTests.cs b/src/Tests/Tests/Connection/MetaData/MetaHeaderHelperTests.cs new file mode 100644 index 00000000000..667d244b711 --- /dev/null +++ b/src/Tests/Tests/Connection/MetaData/MetaHeaderHelperTests.cs @@ -0,0 +1,248 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Elastic.Elasticsearch.Xunit.XunitPlumbing; +using Elasticsearch.Net; +using FluentAssertions; +using Nest; + +namespace Tests.MetaHeader +{ + public class MetaHeaderHelperTests + { + [U] public void BulkAllHelperRequestsIncludeExpectedHelperMetaData() + { + var pool = new SingleNodeConnectionPool(new Uri("http://localhost:9200")); + + // We can avoid specifying response bodies and this still exercises all requests. + var responses = new List<(int, string)> + { + (200, "{}"), + (200, "{}"), + (200, "{}") + }; + + var connection = new TestableInMemoryConnection(a => + a.RequestMetaData.Single(x => x.Key == "helper").Value.Should().Be("b"), responses); + var settings = new ConnectionSettings(pool, connection); + var client = new ElasticClient(settings); + + var documents = CreateLazyStreamOfDocuments(20); + + var observableBulk = client.BulkAll(documents, f => f + .MaxDegreeOfParallelism(8) + .BackOffTime(TimeSpan.FromSeconds(10)) + .BackOffRetries(2) + .Size(10) + .RefreshOnCompleted() + .Index("an-index") + ); + + var bulkObserver = observableBulk.Wait(TimeSpan.FromMinutes(5), b => + { + foreach (var item in b.Items) + { + item.IsValid.Should().BeTrue(); + item.Id.Should().NotBeNullOrEmpty(); + } + }); + + connection.AssertExpectedCallCount(); + } + + [U] public void ScrollAllHelperRequestsIncludeExpectedHelperMetaData() + { + var pool = new SingleNodeConnectionPool(new Uri("http://localhost:9200")); + + var responses = new List<(int, string)> + { + (200, "{\"_scroll_id\":\"SCROLLID\",\"took\":0,\"timed_out\":false,\"_shards\":{\"total\":1,\"successful\":1,\"skipped\":0,\"failed\":0},\"hits\":{\"total\":0,\"max_score\":null,\"hits\":[]}}"), + (200, "{\"_scroll_id\":\"SCROLLID\",\"took\":0,\"timed_out\":false,\"_shards\":{\"total\":1,\"successful\":1,\"skipped\":0,\"failed\":0},\"hits\":{\"total\":1,\"max_score\":null,\"hits\":[{\"_index\":\"index-a\",\"_type\":\"_doc\",\"_id\":\"ISXw0HYBAJbnbq7-Utq6\",\"_score\":null,\"_source\":{\"name\": \"name-a\"},\"sort\":[0]}]}}"), + (200, "{\"_scroll_id\":\"SCROLLID\",\"took\":1,\"timed_out\":false,\"terminated_early\":false,\"_shards\":{\"total\":1,\"successful\":1,\"skipped\":0,\"failed\":0},\"hits\":{\"total\":1,\"max_score\":null,\"hits\":[]}}") + }; + + var connection = new TestableInMemoryConnection(a => + a.RequestMetaData.Single(x => x.Key == "helper").Value.Should().Be("s"), responses); + var settings = new ConnectionSettings(pool, connection); + var client = new ElasticClient(settings); + + var documents = CreateLazyStreamOfDocuments(20); + + var observableScroll = client.ScrollAll("5s", 2, s => s.Search(ss => ss.Size(2).Index("index-a"))); + var bulkObserver = observableScroll.Wait(TimeSpan.FromMinutes(5), _ => { }); + + connection.AssertExpectedCallCount(); + } + + [U] public void ReindexHelperRequestsIncludeExpectedHelperMetaData() + { + var pool = new SingleNodeConnectionPool(new Uri("http://localhost:9200")); + + var responses = new List<(int, string)> + { + (404, "{}"), + (200, "{\"index-a\":{\"aliases\":{},\"mappings\":{\"properties\":{\"name\":{\"type\":\"keyword\"}}},\"settings\":{\"index\":{\"routing\":{\"allocation\":{\"include\":{\"_tier_preference\":\"data_content\"}}},\"number_of_shards\":\"1\",\"provided_name\":\"index-a\",\"creation_date\":\"1609823178261\",\"number_of_replicas\":\"1\",\"uuid\":\"2R4H1VfTR5imfmIPkNIIxw\",\"version\":{\"created\":\"7100099\"}}}}}"), + (200, "{\"acknowledged\":true,\"shards_acknowledged\":true,\"index\":\"index-b\"}"), + (200, "{\"_scroll_id\":\"SCROLLID\",\"took\":0,\"timed_out\":false,\"_shards\":{\"total\":1,\"successful\":1,\"skipped\":0,\"failed\":0},\"hits\":{\"total\":0,\"max_score\":null,\"hits\":[]}}"), + (200, "{\"_scroll_id\":\"SCROLLID\",\"took\":0,\"timed_out\":false,\"_shards\":{\"total\":1,\"successful\":1,\"skipped\":0,\"failed\":0},\"hits\":{\"total\":0,\"max_score\":null,\"hits\":[{\"_index\":\"index-a\",\"_type\":\"_doc\",\"_id\":\"ISXw0HYBAJbnbq7-Utq6\",\"_score\":null,\"_source\":{\"name\": \"name-a\"},\"sort\":[0]}]}}"), + (200, "{\"_scroll_id\":\"SCROLLID\",\"took\":1,\"timed_out\":false,\"terminated_early\":false,\"_shards\":{\"total\":1,\"successful\":1,\"skipped\":0,\"failed\":0},\"hits\":{\"total\":0,\"max_score\":null,\"hits\":[]}}"), + (200, "{\"took\":4,\"errors\":false,\"items\":[{\"index\":{\"_index\":\"index-b\",\"_type\":\"_doc\",\"_id\":\"ISXw0HYBAJbnbq7-Utq6\",\"_version\":1,\"result\":\"created\",\"_shards\":{\"total\":2,\"successful\":1,\"failed\":0},\"_seq_no\":0,\"_primary_term\":1,\"status\":201}}]}") + }; + + var connection = new TestableInMemoryConnection(a => + a.RequestMetaData.Single(x => x.Key == "helper").Value.Should().Be("r"), responses); + var settings = new ConnectionSettings(pool, connection); + var client = new ElasticClient(settings); + + var reindexObserver = client.Reindex(r => r + .ScrollAll("5s", 2, s => s.Search(ss => ss.Size(2).Index("index-a"))) + .BulkAll(b => b.Size(1).Index("index-b"))) + .Wait(TimeSpan.FromMinutes(1), _ => { }); + + connection.AssertExpectedCallCount(); + } + + [U] public void SnapshotHelperRequestsIncludeExpectedHelperMetaData() + { + var pool = new SingleNodeConnectionPool(new Uri("http://localhost:9200")); + + // We can avoid specifying response bodies and this still exercises all requests. + var responses = new List<(int, string)> + { + (200, "{}"), + (200, "{}") + }; + + var connection = new TestableInMemoryConnection(a => + a.RequestMetaData.Single(x => x.Key == "helper").Value.Should().Be("sn"), responses); + var settings = new ConnectionSettings(pool, connection); + var client = new ElasticClient(settings); + + var observableSnapshot = new SnapshotObservable(client, new SnapshotRequest("repository-a", "snapshot-a")); + var observer = new SnapshotObserver(connection); + using var subscription = observableSnapshot.Subscribe(observer); + } + + private class SnapshotObserver : IObserver + { + private readonly TestableInMemoryConnection _connection; + + public SnapshotObserver(TestableInMemoryConnection connection) => _connection = connection; + + public void OnCompleted() => _connection.AssertExpectedCallCount(); + public void OnError(Exception error) => throw new NotImplementedException(); + public void OnNext(ISnapshotStatusResponse value) { } + } + + [U] public void RestoreHelperRequestsIncludeExpectedHelperMetaData() + { + var pool = new SingleNodeConnectionPool(new Uri("http://localhost:9200")); + + // We can avoid specifying response bodies and this still exercises all requests. + var responses = new List<(int, string)> + { + (200, "{}"), + (200, "{}") + }; + + var connection = new TestableInMemoryConnection(a => + a.RequestMetaData.Single(x => x.Key == "helper").Value.Should().Be("sr"), responses); + var settings = new ConnectionSettings(pool, connection); + var client = new ElasticClient(settings); + + var observableRestore = new RestoreObservable(client, new RestoreRequest("repository-a", "snapshot-a")); + var observer = new RestoreObserver(connection); + using var subscription = observableRestore.Subscribe(observer); + } + + private class RestoreObserver : IObserver + { + private readonly TestableInMemoryConnection _connection; + + public RestoreObserver(TestableInMemoryConnection connection) => _connection = connection; + + public void OnCompleted() => _connection.AssertExpectedCallCount(); + public void OnError(Exception error) => throw new NotImplementedException(); + public void OnNext(IRecoveryStatusResponse value) { } + } + + protected static IEnumerable CreateLazyStreamOfDocuments(int count) + { + for (var i = 0; i < count; i++) + yield return new SmallObject { Name = i.ToString() }; + } + + protected class SmallObject + { + public string Name { get; set; } + } + + protected class TestableInMemoryConnection : IConnection + { + internal static readonly byte[] EmptyBody = Encoding.UTF8.GetBytes(""); + + private readonly Action _perRequestAssertion; + private readonly List<(int, string)> _responses; + private int _requestCounter = -1; + + public TestableInMemoryConnection(Action assertion, List<(int, string)> responses) + { + _perRequestAssertion = assertion; + _responses = responses; + } + + public void AssertExpectedCallCount() => _requestCounter.Should().Be(_responses.Count - 1); + + async Task IConnection.RequestAsync(RequestData requestData, CancellationToken cancellationToken) + { + Interlocked.Increment(ref _requestCounter); + + _perRequestAssertion(requestData); + + await Task.Yield(); // avoids test deadlocks + + int statusCode; + string response; + + if (_responses.Count > _requestCounter) + (statusCode, response) = _responses[_requestCounter]; + else + (statusCode, response) = (500, (string)null); + + var stream = !string.IsNullOrEmpty(response) ? requestData.MemoryStreamFactory.Create(Encoding.UTF8.GetBytes(response)) : requestData.MemoryStreamFactory.Create(EmptyBody); + + return await ResponseBuilder + .ToResponseAsync(requestData, null, statusCode, null, stream, RequestData.MimeType, cancellationToken) + .ConfigureAwait(false); + } + + TResponse IConnection.Request(RequestData requestData) + { + Interlocked.Increment(ref _requestCounter); + + _perRequestAssertion(requestData); + + int statusCode; + string response; + + if (_responses.Count > _requestCounter) + (statusCode, response) = _responses[_requestCounter]; + else + (statusCode, response) = (200, (string)null); + + var stream = !string.IsNullOrEmpty(response) ? requestData.MemoryStreamFactory.Create(Encoding.UTF8.GetBytes(response)) : requestData.MemoryStreamFactory.Create(EmptyBody); + + return ResponseBuilder.ToResponse(requestData, null, statusCode, null, stream, RequestData.MimeType); + } + + public void Dispose() { } + } + } +} diff --git a/src/Tests/Tests/Connection/MetaData/MetaHeaderProviderTests.cs b/src/Tests/Tests/Connection/MetaData/MetaHeaderProviderTests.cs new file mode 100644 index 00000000000..9840639fb99 --- /dev/null +++ b/src/Tests/Tests/Connection/MetaData/MetaHeaderProviderTests.cs @@ -0,0 +1,105 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Text.RegularExpressions; +using Elastic.Elasticsearch.Xunit.XunitPlumbing; +using Elasticsearch.Net; +using FluentAssertions; +using Nest; + +namespace Tests.Core.Connection.MetaData +{ + public class MetaHeaderProviderTests + { + private readonly Regex _validHeaderRegex = new Regex(@"^[a-z]{1,}=[a-z0-9\.\-]{1,}(?:,[a-z]{1,}=[a-z0-9\.\-]+)*$"); + private readonly Regex _validVersionRegex = new Regex(@"^[0-9]{1,2}\.[0-9]{1,2}(?:\.[0-9]{1,3})?p?$"); + private readonly Regex _validHttpClientPart = new Regex(@"^[a-z]{2,3}=[0-9]{1,2}\.[0-9]{1,2}(?:\.[0-9]{1,3})?p?$"); + + [U] public void HeaderName_ReturnsExpectedValue() + { + var sut = new MetaHeaderProvider(); + sut.HeaderName.Should().Be("x-elastic-client-meta"); + } + + [U] public void HeaderName_ReturnsNullWhenDisabled() + { + var sut = new MetaHeaderProvider(); + + var connectionSettings = new ConnectionSettings() + .DisableMetaHeader(true); + + var requestData = new RequestData(HttpMethod.POST, "/_search", "{}", connectionSettings, + new SearchRequestParameters(), + new RecyclableMemoryStreamFactory()); + + sut.ProduceHeaderValue(requestData).Should().BeNull(); + } + + [U] public void HeaderName_ReturnsExpectedValue_ForSyncRequest_WhenNotDisabled() + { + var sut = new MetaHeaderProvider(); + + var connectionSettings = new ConnectionSettings(); + + var requestData = new RequestData(HttpMethod.POST, "/_search", "{}", connectionSettings, + new SearchRequestParameters(), + new RecyclableMemoryStreamFactory()) + { + IsAsync = false + }; + + var result = sut.ProduceHeaderValue(requestData); + + _validHeaderRegex.Match(result).Success.Should().BeTrue(); + + var parts = result.Split(','); + parts.Length.Should().Be(4); + + parts[0].Should().StartWith("es="); + var clientVersion = parts[0].Substring(3); + _validVersionRegex.Match(clientVersion).Success.Should().BeTrue(); + + parts[1].Should().Be("a=0"); + + parts[2].Should().StartWith("net="); + var runtimeVersion = parts[2].Substring(4); + _validVersionRegex.Match(runtimeVersion).Success.Should().BeTrue(); + + _validHttpClientPart.Match(parts[3]).Success.Should().BeTrue(); + } + + [U] public void HeaderName_ReturnsExpectedValue_ForAsyncRequest_WhenNotDisabled() + { + var sut = new MetaHeaderProvider(); + + var connectionSettings = new ConnectionSettings(); + + var requestData = new RequestData(HttpMethod.POST, "/_search", "{}", connectionSettings, + new SearchRequestParameters(), + new RecyclableMemoryStreamFactory()) + { + IsAsync = true + }; + + var result = sut.ProduceHeaderValue(requestData); + + _validHeaderRegex.Match(result).Success.Should().BeTrue(); + + var parts = result.Split(','); + parts.Length.Should().Be(4); + + parts[0].Should().StartWith("es="); + var clientVersion = parts[0].Substring(3); + _validVersionRegex.Match(clientVersion).Success.Should().BeTrue(); + + parts[1].Should().Be("a=1"); + + parts[2].Should().StartWith("net="); + var runtimeVersion = parts[2].Substring(4); + _validVersionRegex.Match(runtimeVersion).Success.Should().BeTrue(); + + _validHttpClientPart.Match(parts[3]).Success.Should().BeTrue(); + } + } +} \ No newline at end of file diff --git a/src/Tests/Tests/Connection/MetaData/VersionInfoTests.cs b/src/Tests/Tests/Connection/MetaData/VersionInfoTests.cs new file mode 100644 index 00000000000..8b2c3cf87be --- /dev/null +++ b/src/Tests/Tests/Connection/MetaData/VersionInfoTests.cs @@ -0,0 +1,35 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using Elastic.Elasticsearch.Xunit.XunitPlumbing; +using Elasticsearch.Net; +using FluentAssertions; + +namespace Tests.Core.Connection.MetaData +{ + public class VersionInfoTests + { + [U] public void ToString_ReturnsExpectedValue_ForNonPrerelease() + { + var sut = new TestVersionInfo("1.2.3", false); + sut.ToString().Should().Be("1.2.3"); + } + + [U] public void ToString_ReturnsExpectedValue_ForPrerelease() + { + var sut = new TestVersionInfo("1.2.3", true); + sut.ToString().Should().Be("1.2.3p"); + } + + private class TestVersionInfo : VersionInfo + { + public TestVersionInfo(string version, bool isPrerelease) + { + Version = new Version(version); + IsPrerelease = isPrerelease; + } + } + } +} \ No newline at end of file From 5dd30a05f3c47c8d73b21b01fb5950c8722b41af Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Thu, 4 Feb 2021 12:16:03 +0000 Subject: [PATCH 2/2] Skip nested sort test prior to 6.1 --- src/Tests/Tests/Search/Request/SortUsageTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Tests/Tests/Search/Request/SortUsageTests.cs b/src/Tests/Tests/Search/Request/SortUsageTests.cs index 9cff34dc70b..a91731ffc83 100644 --- a/src/Tests/Tests/Search/Request/SortUsageTests.cs +++ b/src/Tests/Tests/Search/Request/SortUsageTests.cs @@ -14,6 +14,7 @@ namespace Tests.Search.Request * Allows to add one or more sort on specific fields. Each sort can be reversed as well. * The sort is defined on a per field level, with special field name for `_score` to sort by score. */ + [SkipVersion("<6.1.0", "Nested sort parameter not available prior to 6.1.0")] public class SortUsageTests : SearchUsageTestBase { public SortUsageTests(ReadOnlyCluster cluster, EndpointUsage usage) : base(cluster, usage) { }