Skip to content

Commit

Permalink
Merge pull request #75 from evo-company/cache-graphql-ast-parsing
Browse files Browse the repository at this point in the history
add lru_cache for parsing graphql ast
  • Loading branch information
kindermax committed Aug 24, 2022
2 parents a736c97 + 33d7317 commit 6dde4b4
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 6 deletions.
1 change: 1 addition & 0 deletions docs/changelog/changes_07.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Changes in 0.7
])
- Added mypy and typings to codebase
- Added checks for unhashable link results and extend errors. This must improve developer experience.
- Added caching for parsing graphql query. It is optional and can be enabled by calling :py:func:`hiku.readers.graphql.setup_query_cache`.

Backward-incompatible changes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down
40 changes: 40 additions & 0 deletions docs/graphql.rst
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,43 @@ execution result into JSON, it should be denormalized, to replace references
return jsonify(result)
.. _graphql-core: https://github.com/graphql-python/graphql-core


Query parsing cache
~~~~~~~~~~~~~~~~~~~

Hiku uses ``graphql-core`` library to parse queries. It is possible to enable
cache for parsed queries. This is useful when you have a lot of queries, and you
want to parse them only once.

Current implementation uses ``functools.lru_cache``.

Note than for cache to be effective, you need to separate query and variables, otherwise
cache will be useless.

Query with inlined variables is bad for caching.

.. code-block:: python
query User {
user(id: 1) {
name
photo(size: 50)
}
}
Query with separated variables is good for caching.

.. code-block:: python
query User($id: ID!, $photoSize: Int) {
user(id: $id) {
name
photo(size: $photoSize)
}
}
{
"id": 1,
"photoSize": 50
}
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ User's Guide
graphql
protobuf
federation
telemetry
reference/index
changelog/index
development
53 changes: 53 additions & 0 deletions docs/telemetry.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
Telemetry
=========

Hiku exposes several prometheus metrics.

Graph execution time metrics
~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Shows the time spent in the graph execution for each Field or Link.

You can enable graph execution time metrics like this:

.. code-block:: python
from hiku.telemetry import GraphMetrics
from hiku.graph import Graph, Field, Root
GRAPH = Graph([
Root([
Field('value', String, value_func),
]),
])
metrics = GraphMetrics('mobile', metric=mobile_graph_metrics)
GRAPH = metrics.visit(GRAPH)
Where:
- *mobile* - is a graph label (in case you have multiple graphs in your app)
- *metric* - is your custom Summary metric. If not provided, the default Summary('graph_field_time') is used

Default metric:

.. code-block:: python
Summary(
'graph_field_time',
'Graph field time (seconds)',
['graph', 'node', 'field'],
)
Query cache metrics
~~~~~~~~~~~~~~~~~~~

It is possible to enable query cache. That means that the same query will be parsed only once.

When query cache is enabled, the following metrics are exposed:

.. code-block:: python
Gauge('hiku_query_cache_hits', 'Query cache hits')
Gauge('hiku_query_cache_misses', 'Query cache misses')
6 changes: 4 additions & 2 deletions examples/graphql_federation.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
Link,
Graph,
)
from hiku.readers.graphql import setup_query_cache
from hiku.types import (
Integer,
TypeRef,
Expand Down Expand Up @@ -140,8 +141,9 @@ def handle_graphql():


def main():
logging.basicConfig()
app.run(host='0.0.0.0', port=5000)
logging.basicConfig(level=logging.DEBUG)
setup_query_cache(size=128)
app.run(host='0.0.0.0', port=5000, debug=True)


if __name__ == '__main__':
Expand Down
44 changes: 41 additions & 3 deletions hiku/readers/graphql.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import enum

from typing import (
Optional,
Dict,
Expand All @@ -8,12 +9,49 @@
cast,
Any,
Set,
Callable,
)
from functools import lru_cache

from graphql.language import ast
from graphql.language.parser import parse

from ..query import Node, Field, Link, merge
from ..telemetry.prometheus import (
QUERY_CACHE_HITS,
QUERY_CACHE_MISSES,
)


def parse_query(src: str) -> ast.DocumentNode:
"""Parses a query into GraphQL ast
:param str src: GraphQL query string
:return: :py:class:`ast.DocumentNode`
"""
return parse(src)


def wrap_metrics(cached_parser: Callable) -> Callable:
def wrapper(*args: Any, **kwargs: Any) -> ast.DocumentNode:
ast = cached_parser(*args, **kwargs)
info = cached_parser.cache_info() # type: ignore
QUERY_CACHE_HITS.set(info.hits)
QUERY_CACHE_MISSES.set(info.misses)
return ast
return wrapper


def setup_query_cache(
size: int = 128,
) -> None:
"""Sets up lru cache for the ast parsing.
:param int size: Maximum size of the cache
"""
global parse_query
parse_query = lru_cache(maxsize=size)(parse_query)
parse_query = wrap_metrics(parse_query)


class NodeVisitor:
Expand Down Expand Up @@ -356,7 +394,7 @@ def visit_operation_definition(
def read(
src: str,
variables: Optional[Dict] = None,
operation_name: Optional[str] = None
operation_name: Optional[str] = None,
) -> Node:
"""Reads a query from the GraphQL document
Expand All @@ -372,7 +410,7 @@ def read(
:param str operation_name: Name of the operation to execute
:return: :py:class:`hiku.query.Node`, ready to execute query object
"""
doc = parse(src)
doc = parse_query(src)
op = OperationGetter.get(doc, operation_name=operation_name)
if op.operation is not ast.OperationType.QUERY:
raise TypeError('Only "query" operations are supported, '
Expand Down Expand Up @@ -428,7 +466,7 @@ def read_operation(
:param str operation_name: Name of the operation to execute
:return: :py:class:`Operation`
"""
doc = parse(src)
doc = parse_query(src)
op = OperationGetter.get(doc, operation_name=operation_name)
query = GraphQLTransformer.transform(doc, op, variables)
type_ = cast(Optional[OperationType], (
Expand Down
6 changes: 5 additions & 1 deletion hiku/telemetry/prometheus.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@
from abc import abstractmethod
from functools import partial

from prometheus_client import Summary
from prometheus_client import Summary, Gauge

from ..graph import GraphTransformer
from ..engine import pass_context, _do_pass_context
from ..sources.graph import CheckedExpr


QUERY_CACHE_HITS = Gauge('hiku_query_cache_hits', 'Query cache hits')
QUERY_CACHE_MISSES = Gauge('hiku_query_cache_misses', 'Query cache misses')


_METRIC = None


Expand Down

0 comments on commit 6dde4b4

Please sign in to comment.