Class: ElasticGraph::GraphQL::QueryExecutor

Inherits:
Object
  • Object
show all
Defined in:
lib/elastic_graph/graphql/query_executor.rb

Overview

Responsible for executing queries.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(schema:, monotonic_clock:, logger:, slow_query_threshold_ms:, datastore_search_router:) ⇒ QueryExecutor

Returns a new instance of QueryExecutor.



21
22
23
24
25
26
27
# File 'lib/elastic_graph/graphql/query_executor.rb', line 21

def initialize(schema:, monotonic_clock:, logger:, slow_query_threshold_ms:, datastore_search_router:)
  @schema = schema
  @monotonic_clock = monotonic_clock
  @logger = logger
  @slow_query_threshold_ms = slow_query_threshold_ms
  @datastore_search_router = datastore_search_router
end

Instance Attribute Details

#schemaObject (readonly)

Returns the value of attribute schema.



19
20
21
# File 'lib/elastic_graph/graphql/query_executor.rb', line 19

def schema
  @schema
end

Instance Method Details

#execute(query_string, client: Client::ANONYMOUS, variables: {}, timeout_in_ms: nil, operation_name: nil, context: {}, start_time_in_ms: @monotonic_clock.now_in_ms) ⇒ Object

Executes the given ‘query_string` using the provided `variables`.

‘timeout_in_ms` can be provided to limit how long the query runs for. If the timeout is exceeded, `RequestExceededDeadlineError` will be raised. Note that `timeout_in_ms` does not provide an absolute guarantee that the query will take no longer than the provided value; it is only used to halt datastore queries. In process computation can make the total query time exceeded the specified timeout.

‘context` is merged into the context hash passed to the resolvers.



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/elastic_graph/graphql/query_executor.rb', line 38

def execute(
  query_string,
  client: Client::ANONYMOUS,
  variables: {},
  timeout_in_ms: nil,
  operation_name: nil,
  context: {},
  start_time_in_ms: @monotonic_clock.now_in_ms
)
  # Before executing the query, prune any null-valued variable fields. This means we
  # treat `foo: null` the same as if `foo` was unmentioned. With certain clients (e.g.
  # code-gen'd clients in a statically typed language), it is non-trivial to avoid
  # mentioning variable fields they aren't using. It makes it easier to evolve the
  # schema if we ignore null-valued fields rather than potentially returning an error
  # due to a null-valued field referencing an undefined schema element.
  variables = ElasticGraph::Support::HashUtil.recursively_prune_nils_from(variables)

  query_tracker = QueryDetailsTracker.empty

  query, result = build_and_execute_query(
    query_string: query_string,
    variables: variables,
    operation_name: operation_name,
    client: client,
    context: context.merge({
      monotonic_clock_deadline: timeout_in_ms&.+(start_time_in_ms),
      elastic_graph_schema: @schema,
      schema_element_names: @schema.element_names,
      elastic_graph_query_tracker: query_tracker,
      datastore_search_router: @datastore_search_router
    }.compact)
  )

  unless result.to_h.fetch("errors", []).empty?
    @logger.error "      Query \#{query.operation_name}[1] for client \#{client.description} resulted in errors[2].\n\n      [1] \#{full_description_of(query)}\n\n      [2] \#{::JSON.pretty_generate(result.to_h.fetch(\"errors\"))}\n    EOS\n  end\n\n  unless query_tracker.hidden_types.empty?\n    @logger.warn \"\#{query_tracker.hidden_types.size} GraphQL types were hidden from the schema due to their backing indices being inaccessible: \#{query_tracker.hidden_types.sort.join(\", \")}\"\n  end\n\n  duration = @monotonic_clock.now_in_ms - start_time_in_ms\n\n  # Note: I also wanted to log the sanitized query if `result` has `errors`, but `GraphQL::Query#sanitized_query`\n  # returns `nil` on an invalid query, and I don't want to risk leaking PII by logging the raw query string, so\n  # we don't log any form of the query in that case.\n  if duration > @slow_query_threshold_ms\n    @logger.warn \"Query \#{query.operation_name} for client \#{client.description} with shard routing values \" \\\n      \"\#{query_tracker.shard_routing_values.sort.inspect} and search index expressions \#{query_tracker.search_index_expressions.sort.inspect} took longer \" \\\n      \"(\#{duration} ms) than the configured slow query threshold (\#{@slow_query_threshold_ms} ms). \" \\\n      \"Sanitized query:\\n\\n\#{query.sanitized_query_string}\"\n  end\n\n  unless client == Client::ELASTICGRAPH_INTERNAL\n    @logger.info({\n      \"message_type\" => \"ElasticGraphQueryExecutorQueryDuration\",\n      \"client\" => client.name,\n      \"query_fingerprint\" => fingerprint_for(query),\n      \"query_name\" => query.operation_name,\n      \"duration_ms\" => duration,\n      # Here we log how long the datastore queries took according to what the datastore itself reported.\n      \"datastore_server_duration_ms\" => query_tracker.datastore_query_server_duration_ms,\n      # Here we log an estimate for how much overhead ElasticGraph added on top of how long the datastore took.\n      # This is based on the duration, excluding how long the datastore calls took from the client side\n      # (e.g. accounting for network latency, serialization time, etc)\n      \"elasticgraph_overhead_ms\" => duration - query_tracker.datastore_query_client_duration_ms,\n      # According to https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/FilterAndPatternSyntax.html#metric-filters-extract-json,\n      # > Value nodes can be strings or numbers...If a property selector points to an array or object, the metric filter won't match the log format.\n      # So, to allow flexibility to deal with cloud watch metric filters, we coerce these values to a string here.\n      \"unique_shard_routing_values\" => query_tracker.shard_routing_values.sort.join(\", \"),\n      # We also include the count of shard routing values, to make it easier to search logs\n      # for the case of no shard routing values.\n      \"unique_shard_routing_value_count\" => query_tracker.shard_routing_values.count,\n      \"unique_search_index_expressions\" => query_tracker.search_index_expressions.sort.join(\", \"),\n      # Indicates how many requests we sent to the datastore to satisfy the GraphQL query.\n      \"datastore_request_count\" => query_tracker.query_counts_per_datastore_request.size,\n      # Indicates how many individual datastore queries there were. One datastore request\n      # can contain many queries (since we use `msearch`), so these counts can be different.\n      \"datastore_query_count\" => query_tracker.query_counts_per_datastore_request.sum,\n      \"over_slow_threshold\" => (duration > @slow_query_threshold_ms).to_s,\n      \"slo_result\" => slo_result_for(query, duration)\n    })\n  end\n\n  result\nend\n"