Skip to content

Commit de5e955

Browse files
committed
Implement gevent middleware.
1 parent 1fe3f6d commit de5e955

File tree

7 files changed

+118
-9
lines changed

7 files changed

+118
-9
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ python:
77
- 3.5
88
- pypy
99
install:
10-
- pip install pytest-cov coveralls flake8 import-order
10+
- pip install pytest-cov coveralls flake8 import-order gevent>=1.1b5
1111
- pip install pytest>=2.7.3 --upgrade
1212
- pip install -e .
1313
script:

graphql/core/execution/executor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ def run_resolve_fn(self, resolve_fn, source, args, info):
287287
try:
288288
for middleware in self.execution_middlewares:
289289
if hasattr(middleware, 'run_resolve_fn'):
290-
curried_resolve_fn = functools.partial(middleware.run_resolve_fn, curried_resolve_fn)
290+
curried_resolve_fn = functools.partial(middleware.run_resolve_fn, curried_resolve_fn, resolve_fn)
291291

292292
return curried_resolve_fn()
293293
except Exception as e:

graphql/core/execution/middlewares/asyncio.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def handle_future_result(future):
1616

1717

1818
class AsyncioExecutionMiddleware(object):
19-
def run_resolve_fn(self, resolver):
19+
def run_resolve_fn(self, resolver, original_resolver):
2020
result = resolver()
2121
if isinstance(result, Future) or iscoroutine(result):
2222
future = ensure_future(result)
@@ -32,7 +32,5 @@ def execution_result(self, executor):
3232
assert isinstance(result, Deferred), 'Another middleware has converted the execution result ' \
3333
'away from a Deferred.'
3434

35-
result.add_callback(future.set_result)
36-
result.add_errback(future.set_exception)
37-
35+
result.add_callbacks(future.set_result, future.set_exception)
3836
return future
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from __future__ import absolute_import
2+
3+
from gevent import get_hub, spawn
4+
from gevent.event import AsyncResult
5+
from graphql.core.defer import Deferred, DeferredException
6+
7+
8+
def _run_resolver_in_greenlet(d, resolver):
9+
try:
10+
result = resolver()
11+
get_hub().loop.run_callback(d.callback, result)
12+
except:
13+
e = DeferredException()
14+
get_hub().loop.run_callback(d.errback, e)
15+
16+
17+
def run_in_greenlet(f):
18+
"""
19+
Marks a resolver to run inside a greenlet.
20+
21+
@run_in_greenlet
22+
def resolve_something(context, _*):
23+
gevent.sleep(1)
24+
return 5
25+
26+
"""
27+
f._run_in_greenlet = True
28+
return f
29+
30+
31+
class GeventExecutionMiddleware(object):
32+
def run_resolve_fn(self, resolver, original_resolver):
33+
if hasattr(original_resolver, '_run_in_greenlet'):
34+
d = Deferred()
35+
spawn(_run_resolver_in_greenlet, d, resolver)
36+
return d
37+
38+
return resolver()
39+
40+
def execution_result(self, executor):
41+
result = AsyncResult()
42+
deferred = executor()
43+
assert isinstance(deferred, Deferred), 'Another middleware has converted the execution result ' \
44+
'away from a Deferred.'
45+
46+
deferred.add_callbacks(result.set, lambda e: result.set_exception(e.value, (e.type, e.value, e.traceback)))
47+
return result.get()

graphql/core/execution/middlewares/sync.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44

55
class SynchronousExecutionMiddleware(object):
6-
def run_resolve_fn(self, resolver):
6+
def run_resolve_fn(self, resolver, original_resolver):
77
result = resolver()
88
if isinstance(result, Deferred):
99
raise GraphQLError('You cannot return a Deferred from a resolver when using SynchronousExecutionMiddleware')

tests/core_execution/test_gevent.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# flake8: noqa
2+
3+
from graphql.core.execution import Executor
4+
from graphql.core.execution.middlewares.gevent import GeventExecutionMiddleware, run_in_greenlet
5+
from graphql.core.language.location import SourceLocation
6+
from graphql.core.type import (
7+
GraphQLSchema,
8+
GraphQLObjectType,
9+
GraphQLField,
10+
GraphQLString
11+
)
12+
13+
import gevent
14+
15+
16+
def test_gevent_executor():
17+
doc = 'query Example { a, b }'
18+
19+
@run_in_greenlet
20+
def resolver(context, *_):
21+
gevent.sleep(0.001)
22+
return 'hey'
23+
24+
@run_in_greenlet
25+
def resolver_2(context, *_):
26+
gevent.sleep(0.003)
27+
return 'hey2'
28+
29+
Type = GraphQLObjectType('Type', {
30+
'a': GraphQLField(GraphQLString, resolver=resolver),
31+
'b': GraphQLField(GraphQLString, resolver=resolver_2)
32+
})
33+
34+
executor = Executor(GraphQLSchema(Type), [GeventExecutionMiddleware()])
35+
result = executor.execute(doc)
36+
assert not result.errors
37+
assert result.data == {'a': 'hey', 'b': 'hey2'}
38+
39+
40+
def test_gevent_executor_with_error():
41+
doc = 'query Example { a, b }'
42+
43+
@run_in_greenlet
44+
def resolver(context, *_):
45+
gevent.sleep(0.001)
46+
return 'hey'
47+
48+
@run_in_greenlet
49+
def resolver_2(context, *_):
50+
gevent.sleep(0.003)
51+
raise Exception('resolver_2 failed!')
52+
53+
Type = GraphQLObjectType('Type', {
54+
'a': GraphQLField(GraphQLString, resolver=resolver),
55+
'b': GraphQLField(GraphQLString, resolver=resolver_2)
56+
})
57+
58+
executor = Executor(GraphQLSchema(Type), [GeventExecutionMiddleware()])
59+
result = executor.execute(doc)
60+
assert result.errors == [{'locations': [SourceLocation(1, 20)], 'message': 'resolver_2 failed!'}]
61+
assert result.data == {'a': 'hey', 'b': None}

tox.ini

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ envlist = flake8,import-order,py27,py33,py34,py35,pypy,docs
44
[testenv]
55
deps =
66
pytest>=2.7.2
7+
gevent==1.1b5
78
commands =
8-
py{27,33,34}: py.test tests
9+
py{27,33,34,py}: py.test tests
910
py35: py.test tests tests_py35
1011

1112

@@ -15,7 +16,9 @@ commands = flake8
1516

1617
[testenv:import-order]
1718
basepython=python3.5
18-
deps = import-order
19+
deps =
20+
import-order
21+
gevent==1.1b5
1922
commands = import-order graphql
2023

2124
[testenv:docs]

0 commit comments

Comments
 (0)