public async Task <IQueryResult <T> > QueryAsync <T>(string statement, QueryOptions options) { if (string.IsNullOrEmpty(options.CurrentContextId)) { options.ClientContextId(Guid.NewGuid().ToString()); } using var rootSpan = _tracer.RootSpan(RequestTracing.ServiceIdentifier.Query, OperationNames.N1qlQuery) .WithTag(CouchbaseTags.OperationId, options.CurrentContextId !) .WithTag(CouchbaseTags.OpenTracingTags.DbStatement, statement) .WithLocalAddress(); // does this query use a prepared plan? if (options.IsAdHoc) { // don't use prepared plan, execute query directly options.Statement(statement); return(await ExecuteQuery <T>(options, options.Serializer ?? _serializer, rootSpan).ConfigureAwait(false)); } // try find cached query plan if (_queryCache.TryGetValue(statement, out var queryPlan)) { // if an upgrade has happened, don't use query plans that have an encoded plan if (!EnhancedPreparedStatementsEnabled || string.IsNullOrWhiteSpace(queryPlan.EncodedPlan)) { using var prepareAndExecuteSpan = _tracer.InternalSpan(OperationNames.PrepareAndExecute, rootSpan); // plan is valid, execute query with it options.Prepared(queryPlan, statement); return(await ExecuteQuery <T>(options, options.Serializer ?? _serializer, rootSpan).ConfigureAwait(false)); } // entry is stale, remove from cache _queryCache.TryRemove(statement, out _); } // create prepared statement var prepareStatement = statement; if (!prepareStatement.StartsWith("PREPARE ", StringComparison.InvariantCultureIgnoreCase)) { prepareStatement = $"PREPARE {statement}"; } // set prepared statement options.Statement(prepareStatement); // server supports combined prepare & execute if (EnhancedPreparedStatementsEnabled) { _logger.LogDebug("Using enhanced prepared statement behavior for request {currentContextId}", options.CurrentContextId); // execute combined prepare & execute query options.AutoExecute(true); var result = await ExecuteQuery <T>(options, options.Serializer ?? _serializer, rootSpan).ConfigureAwait(false); // add/replace query plan name in query cache if (result is StreamingQueryResult <T> streamingResult) // NOTE: hack to not make 'PreparedPlanName' property public { var plan = new QueryPlan { Name = streamingResult.PreparedPlanName, Text = statement }; _queryCache.AddOrUpdate(statement, plan, (k, p) => plan); } return(result); } _logger.LogDebug("Using legacy prepared statement behavior for request {currentContextId}", options.CurrentContextId); // older style, prepare then execute var preparedResult = await ExecuteQuery <QueryPlan>(options, _queryPlanSerializer, rootSpan).ConfigureAwait(false); queryPlan = await preparedResult.FirstAsync().ConfigureAwait(false); // add plan to cache and execute _queryCache.TryAdd(statement, queryPlan); options.Prepared(queryPlan, statement); // execute query using plan return(await ExecuteQuery <T>(options, options.Serializer ?? _serializer, rootSpan).ConfigureAwait(false)); }
private async Task <IQueryResult <T> > ExecuteQuery <T>(QueryOptions options, ITypeSerializer serializer, IInternalSpan span) { var currentContextId = options.CurrentContextId ?? DefaultClientContextId; QueryErrorContext ErrorContextFactory(QueryResultBase <T> failedQueryResult, HttpStatusCode statusCode) { // We use a local function to capture context like options and currentContextId return(new QueryErrorContext { ClientContextId = options.CurrentContextId, Parameters = options.GetAllParametersAsJson(), Statement = options.ToString(), Message = GetErrorMessage(failedQueryResult, currentContextId, statusCode), Errors = failedQueryResult.Errors, HttpStatus = statusCode, QueryStatus = failedQueryResult.MetaData?.Status ?? QueryStatus.Fatal }); } // try get Query node var queryUri = _serviceUriProvider.GetRandomQueryUri(); span.WithRemoteAddress(queryUri); using var encodingSpan = span.StartPayloadEncoding(); var body = options.GetFormValuesAsJson(); encodingSpan.Dispose(); _logger.LogDebug("Sending query {contextId} to node {endpoint}.", options.CurrentContextId, queryUri); QueryResultBase <T> queryResult; using var content = new StringContent(body, System.Text.Encoding.UTF8, MediaType.Json); try { using var dispatchSpan = span.StartDispatch(); var response = await HttpClient.PostAsync(queryUri, content, options.Token).ConfigureAwait(false); dispatchSpan.Dispose(); var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); if (serializer is IStreamingTypeDeserializer streamingDeserializer) { queryResult = new StreamingQueryResult <T>(stream, streamingDeserializer, ErrorContextFactory); } else { queryResult = new BlockQueryResult <T>(stream, serializer); } queryResult.HttpStatusCode = response.StatusCode; queryResult.Success = response.StatusCode == HttpStatusCode.OK; //read the header and stop when we reach the queried rows await queryResult.InitializeAsync(options.Token).ConfigureAwait(false); if (response.StatusCode != HttpStatusCode.OK || queryResult.MetaData?.Status != QueryStatus.Success) { _logger.LogDebug("Request {currentContextId} has failed because {status}.", currentContextId, queryResult.MetaData?.Status); if (queryResult.ShouldRetry(EnhancedPreparedStatementsEnabled)) { if (queryResult.Errors.Any(x => x.Code == 4040 && EnhancedPreparedStatementsEnabled)) { //clear the cache of stale query plan var statement = options.StatementValue ?? string.Empty; if (_queryCache.TryRemove(statement, out var queryPlan)) { _logger.LogDebug("Query plan is stale for {currentContextId}. Purging plan {queryPlanName}.", currentContextId, queryPlan.Name); } ; } _logger.LogDebug("Request {currentContextId} is being retried.", currentContextId); return(queryResult); } var context = ErrorContextFactory(queryResult, response.StatusCode); if (queryResult.MetaData?.Status == QueryStatus.Timeout) { if (options.IsReadOnly) { throw new AmbiguousTimeoutException { Context = context }; } throw new UnambiguousTimeoutException { Context = context }; } queryResult.ThrowExceptionOnError(context); } } catch (OperationCanceledException e) { var context = new QueryErrorContext { ClientContextId = options.CurrentContextId, Parameters = options.GetAllParametersAsJson(), Statement = options.ToString(), HttpStatus = HttpStatusCode.RequestTimeout, QueryStatus = QueryStatus.Fatal }; _logger.LogDebug(LoggingEvents.QueryEvent, e, "Request timeout."); if (options.IsReadOnly) { throw new UnambiguousTimeoutException("The query was timed out via the Token.", e) { Context = context }; } throw new AmbiguousTimeoutException("The query was timed out via the Token.", e) { Context = context }; } catch (HttpRequestException e) { _logger.LogDebug(LoggingEvents.QueryEvent, e, "Request canceled"); var context = new QueryErrorContext { ClientContextId = options.CurrentContextId, Parameters = options.GetAllParametersAsJson(), Statement = options.ToString(), HttpStatus = HttpStatusCode.RequestTimeout, QueryStatus = QueryStatus.Fatal }; throw new RequestCanceledException("The query was canceled.", e) { Context = context }; } _logger.LogDebug($"Request {options.CurrentContextId} has succeeded."); return(queryResult); }