Exemple #1
0
        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));
        }
Exemple #2
0
        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);
        }