|
7 | 7 | using System.Linq;
|
8 | 8 | using System.Runtime.CompilerServices;
|
9 | 9 | using System.Text.Json;
|
10 |
| -using System.Threading; |
11 | 10 | using System.Threading.Tasks;
|
12 |
| -using Elastic.Clients.Elasticsearch.Serverless.Requests; |
| 11 | +using System.Threading; |
13 | 12 | using Elastic.Transport;
|
14 | 13 | using Elastic.Transport.Diagnostics;
|
15 |
| -using Elastic.Transport.Products.Elasticsearch; |
16 | 14 |
|
| 15 | +#if ELASTICSEARCH_SERVERLESS |
| 16 | +using Elastic.Clients.Elasticsearch.Serverless.Requests; |
| 17 | +#else |
| 18 | +using Elastic.Clients.Elasticsearch.Requests; |
| 19 | +#endif |
| 20 | + |
| 21 | +#if ELASTICSEARCH_SERVERLESS |
17 | 22 | namespace Elastic.Clients.Elasticsearch.Serverless;
|
| 23 | +#else |
| 24 | + |
| 25 | +namespace Elastic.Clients.Elasticsearch; |
| 26 | +#endif |
18 | 27 |
|
19 | 28 | /// <summary>
|
20 | 29 | /// A strongly-typed client for communicating with Elasticsearch server endpoints.
|
21 | 30 | /// </summary>
|
22 | 31 | public partial class ElasticsearchClient
|
23 | 32 | {
|
24 | 33 | private const string OpenTelemetrySpanAttributePrefix = "db.elasticsearch.";
|
| 34 | + |
25 | 35 | // This should be updated if any of the code uses semantic conventions defined in newer schema versions.
|
26 | 36 | private const string OpenTelemetrySchemaVersion = "https://opentelemetry.io/schemas/1.21.0";
|
27 | 37 |
|
@@ -82,13 +92,14 @@ internal ElasticsearchClient(ITransport<IElasticsearchClientSettings> transport)
|
82 | 92 | public Serializer SourceSerializer => _transport.Configuration.SourceSerializer;
|
83 | 93 | public ITransport<IElasticsearchClientSettings> Transport => _transport;
|
84 | 94 |
|
85 |
| - private ProductCheckStatus _productCheckStatus; |
| 95 | + private int _productCheckStatus; |
86 | 96 |
|
87 | 97 | private enum ProductCheckStatus
|
88 | 98 | {
|
89 |
| - NotChecked, |
90 |
| - Succeeded, |
91 |
| - Failed |
| 99 | + NotChecked = 0, |
| 100 | + InProgress = 1, |
| 101 | + Succeeded = 2, |
| 102 | + Failed = 3 |
92 | 103 | }
|
93 | 104 |
|
94 | 105 | private partial void SetupNamespaces();
|
@@ -133,48 +144,115 @@ private ValueTask<TResponse> DoRequestCoreAsync<TRequest, TResponse, TRequestPar
|
133 | 144 | where TResponse : TransportResponse, new()
|
134 | 145 | where TRequestParameters : RequestParameters, new()
|
135 | 146 | {
|
136 |
| - if (_productCheckStatus == ProductCheckStatus.Failed) |
137 |
| - throw new UnsupportedProductException(UnsupportedProductException.InvalidProductError); |
| 147 | + // The product check modifies request parameters and therefore must not be executed concurrently. |
| 148 | + // We use a lockless CAS approach to make sure that only a single product check request is executed at a time. |
| 149 | + // We do not guarantee that the product check is always performed on the first request. |
138 | 150 |
|
139 |
| - var (requestModified, hadRequestConfig, originalHeaders) = AttachProductCheckHeaderIfRequired<TRequest, TRequestParameters>(request); |
140 |
| - var (resolvedUrl, urlTemplate, resolvedRouteValues, postData) = PrepareRequest<TRequest, TRequestParameters>(request, forceConfiguration); |
141 |
| - var openTelemetryData = PrepareOpenTelemetryData<TRequest, TRequestParameters>(request, resolvedRouteValues); |
| 151 | + var productCheckStatus = Interlocked.CompareExchange( |
| 152 | + ref _productCheckStatus, |
| 153 | + (int)ProductCheckStatus.InProgress, |
| 154 | + (int)ProductCheckStatus.NotChecked |
| 155 | + ); |
142 | 156 |
|
143 |
| - if (_productCheckStatus == ProductCheckStatus.Succeeded && !requestModified) |
| 157 | + return productCheckStatus switch |
144 | 158 | {
|
145 |
| - if (isAsync) |
146 |
| - return new ValueTask<TResponse>(_transport.RequestAsync<TResponse>(request.HttpMethod, resolvedUrl, postData, request.RequestParameters, in openTelemetryData, cancellationToken)); |
147 |
| - else |
148 |
| - return new ValueTask<TResponse>(_transport.Request<TResponse>(request.HttpMethod, resolvedUrl, postData, request.RequestParameters, in openTelemetryData)); |
| 159 | + (int)ProductCheckStatus.NotChecked => SendRequestWithProductCheck(), |
| 160 | + (int)ProductCheckStatus.InProgress or |
| 161 | + (int)ProductCheckStatus.Succeeded => SendRequest(), |
| 162 | + (int)ProductCheckStatus.Failed => throw new UnsupportedProductException(UnsupportedProductException.InvalidProductError), |
| 163 | + _ => throw new InvalidOperationException("unreachable") |
| 164 | + }; |
| 165 | + |
| 166 | + ValueTask<TResponse> SendRequest() |
| 167 | + { |
| 168 | + var (resolvedUrl, _, resolvedRouteValues, postData) = PrepareRequest<TRequest, TRequestParameters>(request, forceConfiguration); |
| 169 | + var openTelemetryData = PrepareOpenTelemetryData<TRequest, TRequestParameters>(request, resolvedRouteValues); |
| 170 | + |
| 171 | + return isAsync |
| 172 | + ? new ValueTask<TResponse>(_transport |
| 173 | + .RequestAsync<TResponse>(request.HttpMethod, resolvedUrl, postData, request.RequestParameters, in openTelemetryData, cancellationToken)) |
| 174 | + : new ValueTask<TResponse>(_transport |
| 175 | + .Request<TResponse>(request.HttpMethod, resolvedUrl, postData, request.RequestParameters, in openTelemetryData)); |
149 | 176 | }
|
150 | 177 |
|
151 |
| - return SendRequest(isAsync); |
| 178 | + async ValueTask<TResponse> SendRequestWithProductCheck() |
| 179 | + { |
| 180 | + try |
| 181 | + { |
| 182 | + return await SendRequestWithProductCheckCore().ConfigureAwait(false); |
| 183 | + } |
| 184 | + catch |
| 185 | + { |
| 186 | + // Re-try product check on next request. |
| 187 | + |
| 188 | + // 32-bit read/write operations are atomic and due to the initial memory barrier, we can be sure that |
| 189 | + // no other thread executes the product check at the same time. Locked access is not required here. |
| 190 | + if (_productCheckStatus is (int)ProductCheckStatus.InProgress) |
| 191 | + _productCheckStatus = (int)ProductCheckStatus.NotChecked; |
| 192 | + |
| 193 | + throw; |
| 194 | + } |
| 195 | + } |
152 | 196 |
|
153 |
| - async ValueTask<TResponse> SendRequest(bool isAsync) |
| 197 | + async ValueTask<TResponse> SendRequestWithProductCheckCore() |
154 | 198 | {
|
| 199 | + // Attach product check header |
| 200 | + |
| 201 | + var hadRequestConfig = false; |
| 202 | + HeadersList? originalHeaders = null; |
| 203 | + |
| 204 | + if (request.RequestParameters.RequestConfiguration is null) |
| 205 | + request.RequestParameters.RequestConfiguration = new RequestConfiguration(); |
| 206 | + else |
| 207 | + { |
| 208 | + originalHeaders = request.RequestParameters.RequestConfiguration.ResponseHeadersToParse; |
| 209 | + hadRequestConfig = true; |
| 210 | + } |
| 211 | + |
| 212 | + request.RequestParameters.RequestConfiguration.ResponseHeadersToParse = request.RequestParameters.RequestConfiguration.ResponseHeadersToParse.Count == 0 |
| 213 | + ? new HeadersList("x-elastic-product") |
| 214 | + : new HeadersList(request.RequestParameters.RequestConfiguration.ResponseHeadersToParse, "x-elastic-product"); |
| 215 | + |
| 216 | + // Send request |
| 217 | + |
| 218 | + var (resolvedUrl, _, resolvedRouteValues, postData) = PrepareRequest<TRequest, TRequestParameters>(request, forceConfiguration); |
| 219 | + var openTelemetryData = PrepareOpenTelemetryData<TRequest, TRequestParameters>(request, resolvedRouteValues); |
| 220 | + |
155 | 221 | TResponse response;
|
156 | 222 |
|
157 | 223 | if (isAsync)
|
158 |
| - response = await _transport.RequestAsync<TResponse>(request.HttpMethod, resolvedUrl, postData, request.RequestParameters, in openTelemetryData, cancellationToken).ConfigureAwait(false); |
| 224 | + { |
| 225 | + response = await _transport |
| 226 | + .RequestAsync<TResponse>(request.HttpMethod, resolvedUrl, postData, request.RequestParameters, in openTelemetryData, cancellationToken) |
| 227 | + .ConfigureAwait(false); |
| 228 | + } |
159 | 229 | else
|
160 |
| - response = _transport.Request<TResponse>(request.HttpMethod, resolvedUrl, postData, request.RequestParameters, in openTelemetryData); |
| 230 | + { |
| 231 | + response = _transport |
| 232 | + .Request<TResponse>(request.HttpMethod, resolvedUrl, postData, request.RequestParameters, in openTelemetryData); |
| 233 | + } |
| 234 | + |
| 235 | + // Evaluate product check result |
| 236 | + |
| 237 | + var productCheckSucceeded = response.ApiCallDetails.TryGetHeader("x-elastic-product", out var values) && |
| 238 | + values.FirstOrDefault(x => x.Equals("Elasticsearch", StringComparison.Ordinal)) is not null; |
161 | 239 |
|
162 |
| - PostRequestProductCheck<TRequest, TResponse>(request, response); |
| 240 | + _productCheckStatus = productCheckSucceeded |
| 241 | + ? (int)ProductCheckStatus.Succeeded |
| 242 | + : (int)ProductCheckStatus.Failed; |
163 | 243 |
|
164 |
| - if (_productCheckStatus == ProductCheckStatus.Failed) |
| 244 | + if (_productCheckStatus == (int)ProductCheckStatus.Failed) |
165 | 245 | throw new UnsupportedProductException(UnsupportedProductException.InvalidProductError);
|
166 | 246 |
|
167 |
| - if (request.RequestParameters.RequestConfiguration is not null) |
168 |
| - { |
169 |
| - if (!hadRequestConfig) |
170 |
| - { |
171 |
| - request.RequestParameters.RequestConfiguration = null; |
172 |
| - } |
173 |
| - else if (originalHeaders.HasValue && originalHeaders.Value.Count > 0) |
174 |
| - { |
175 |
| - request.RequestParameters.RequestConfiguration.ResponseHeadersToParse = originalHeaders.Value; |
176 |
| - } |
177 |
| - } |
| 247 | + if (request.RequestParameters.RequestConfiguration is null) |
| 248 | + return response; |
| 249 | + |
| 250 | + // Reset request configuration |
| 251 | + |
| 252 | + if (!hadRequestConfig) |
| 253 | + request.RequestParameters.RequestConfiguration = null; |
| 254 | + else if (originalHeaders is { Count: > 0 }) |
| 255 | + request.RequestParameters.RequestConfiguration.ResponseHeadersToParse = originalHeaders.Value; |
178 | 256 |
|
179 | 257 | return response;
|
180 | 258 | }
|
@@ -215,42 +293,6 @@ private static OpenTelemetryData PrepareOpenTelemetryData<TRequest, TRequestPara
|
215 | 293 | return openTelemetryData;
|
216 | 294 | }
|
217 | 295 |
|
218 |
| - private (bool requestModified, bool hadRequestConfig, HeadersList? originalHeaders) AttachProductCheckHeaderIfRequired<TRequest, TRequestParameters>(TRequest request) |
219 |
| - where TRequest : Request<TRequestParameters> |
220 |
| - where TRequestParameters : RequestParameters, new() |
221 |
| - { |
222 |
| - var requestModified = false; |
223 |
| - var hadRequestConfig = false; |
224 |
| - HeadersList? originalHeaders = null; |
225 |
| - |
226 |
| - // If we have not yet checked the product name, add the product header to the list of headers to parse. |
227 |
| - if (_productCheckStatus == ProductCheckStatus.NotChecked) |
228 |
| - { |
229 |
| - requestModified = true; |
230 |
| - |
231 |
| - if (request.RequestParameters.RequestConfiguration is null) |
232 |
| - { |
233 |
| - request.RequestParameters.RequestConfiguration = new RequestConfiguration(); |
234 |
| - } |
235 |
| - else |
236 |
| - { |
237 |
| - originalHeaders = request.RequestParameters.RequestConfiguration.ResponseHeadersToParse; |
238 |
| - hadRequestConfig = true; |
239 |
| - } |
240 |
| - |
241 |
| - if (request.RequestParameters.RequestConfiguration.ResponseHeadersToParse.Count == 0) |
242 |
| - { |
243 |
| - request.RequestParameters.RequestConfiguration.ResponseHeadersToParse = new HeadersList("x-elastic-product"); |
244 |
| - } |
245 |
| - else |
246 |
| - { |
247 |
| - request.RequestParameters.RequestConfiguration.ResponseHeadersToParse = new HeadersList(request.RequestParameters.RequestConfiguration.ResponseHeadersToParse, "x-elastic-product"); |
248 |
| - } |
249 |
| - } |
250 |
| - |
251 |
| - return (requestModified, hadRequestConfig, originalHeaders); |
252 |
| - } |
253 |
| - |
254 | 296 | private (string resolvedUrl, string urlTemplate, Dictionary<string, string>? resolvedRouteValues, PostData data) PrepareRequest<TRequest, TRequestParameters>(TRequest request,
|
255 | 297 | Action<IRequestConfiguration>? forceConfiguration)
|
256 | 298 | where TRequest : Request<TRequestParameters>
|
@@ -278,21 +320,6 @@ private static OpenTelemetryData PrepareOpenTelemetryData<TRequest, TRequestPara
|
278 | 320 | return (resolvedUrl, urlTemplate, routeValues, postData);
|
279 | 321 | }
|
280 | 322 |
|
281 |
| - private void PostRequestProductCheck<TRequest, TResponse>(TRequest request, TResponse response) |
282 |
| - where TRequest : Request |
283 |
| - where TResponse : TransportResponse, new() |
284 |
| - { |
285 |
| - if (response.ApiCallDetails.HttpStatusCode.HasValue && response.ApiCallDetails.HttpStatusCode.Value >= 200 && response.ApiCallDetails.HttpStatusCode.Value <= 299 && _productCheckStatus == ProductCheckStatus.NotChecked) |
286 |
| - { |
287 |
| - if (!response.ApiCallDetails.TryGetHeader("x-elastic-product", out var values) || !values.Single().Equals("Elasticsearch", StringComparison.Ordinal)) |
288 |
| - { |
289 |
| - _productCheckStatus = ProductCheckStatus.Failed; |
290 |
| - } |
291 |
| - |
292 |
| - _productCheckStatus = ProductCheckStatus.Succeeded; |
293 |
| - } |
294 |
| - } |
295 |
| - |
296 | 323 | private static void ForceConfiguration<TRequestParameters>(Request<TRequestParameters> request, Action<IRequestConfiguration> forceConfiguration)
|
297 | 324 | where TRequestParameters : RequestParameters, new()
|
298 | 325 | {
|
|
0 commit comments