Commit f42a35a0 authored by Jason Madden's avatar Jason Madden

Enable the dnspython resolver to work in non-monkey-patched processes

Do this by importing dns specially with sys.modules containing the
gevent modules. This is the technique eventlet uses (but we don't
leave the patched modules sitting around in sys.modules after we're
done; eventlet may have good reason to do that, but this is not a
public API here and is specialized just for dns).
parent a616644a
...@@ -67,7 +67,7 @@ alltest: basictest ...@@ -67,7 +67,7 @@ alltest: basictest
cd src/greentest && GEVENT_RESOLVER=ares GEVENTARES_SERVERS=8.8.8.8 ${PYTHON} testrunner.py --config known_failures.py --ignore tests_that_dont_use_resolver.txt --quiet cd src/greentest && GEVENT_RESOLVER=ares GEVENTARES_SERVERS=8.8.8.8 ${PYTHON} testrunner.py --config known_failures.py --ignore tests_that_dont_use_resolver.txt --quiet
${PYTHON} scripts/travis.py fold_end ares ${PYTHON} scripts/travis.py fold_end ares
${PYTHON} scripts/travis.py fold_start dnspython "Running dnspython tests" ${PYTHON} scripts/travis.py fold_start dnspython "Running dnspython tests"
cd src/greentest && GEVENT_RESOLVER=dnspython ${PYTHON} testrunner.py --config known_failures.py --ignore tests_that_dont_use_resolver.txt,tests_that_dont_monkeypatch.txt --quiet cd src/greentest && GEVENT_RESOLVER=dnspython ${PYTHON} testrunner.py --config known_failures.py --ignore tests_that_dont_use_resolver.txt --quiet
${PYTHON} scripts/travis.py fold_end dnspython ${PYTHON} scripts/travis.py fold_end dnspython
# In the past, we included all test files that had a reference to 'subprocess'' somewhere in their # In the past, we included all test files that had a reference to 'subprocess'' somewhere in their
# text. The monkey-patched stdlib tests were specifically included here. # text. The monkey-patched stdlib tests were specifically included here.
......
# Copyright 2018 gevent. See LICENSE for details.
# Portions of the following are inspired by code from eventlet. I
# believe they are distinct enough that no eventlet copyright would
# apply (they are not a copy or substantial portion of the eventlot
# code).
# Added in gevent 1.3a2. Not public in that release.
from __future__ import absolute_import, print_function
import imp
import importlib
import sys
from gevent._compat import PY3
from gevent._compat import iteritems
from gevent.builtins import __import__ as _import
MAPPING = {
'gevent.local': '_threading_local',
'gevent.socket': 'socket',
'gevent.select': 'select',
'gevent.ssl': 'ssl',
'gevent.thread': '_thread' if PY3 else 'thread',
'gevent.subprocess': 'subprocess',
'gevent.os': 'os',
'gevent.threading': 'threading',
'gevent.builtins': 'builtins' if PY3 else '__builtin__',
'gevent.signal': 'signal',
'gevent.time': 'time',
'gevent.queue': 'queue' if PY3 else 'Queue',
}
_PATCH_PREFIX = '__g_patched_module_'
class _SysModulesPatcher(object):
def __init__(self, importing):
self._saved = {}
self.importing = importing
self.green_modules = {
stdlib_name: importlib.import_module(gevent_name)
for gevent_name, stdlib_name
in iteritems(MAPPING)
}
self.orig_imported = frozenset(sys.modules)
def _save(self):
for modname in self.green_modules:
self._saved[modname] = sys.modules.get(modname, None)
self._saved[self.importing] = sys.modules.get(self.importing, None)
# Anything we've already patched regains its original name during this
# process
for mod_name, mod in iteritems(sys.modules):
if mod_name.startswith(_PATCH_PREFIX):
orig_mod_name = mod_name[len(_PATCH_PREFIX):]
self._saved[mod_name] = sys.modules.get(orig_mod_name, None)
self.green_modules[orig_mod_name] = mod
def _replace(self):
# Cover the target modules so that when you import the module it
# sees only the patched versions
for name, mod in iteritems(self.green_modules):
sys.modules[name] = mod
def _restore(self):
for modname, mod in iteritems(self._saved):
if mod is not None:
sys.modules[modname] = mod
else:
try:
del sys.modules[modname]
except KeyError:
pass
# Anything from the same package tree we imported this time
# needs to be saved so we can restore it later, and so it doesn't
# leak into the namespace.
pkg_prefix = self.importing.split('.', 1)[0]
for modname, mod in list(iteritems(sys.modules)):
if (modname not in self.orig_imported
and modname != self.importing
and not modname.startswith(_PATCH_PREFIX)
and modname.startswith(pkg_prefix)):
sys.modules[_PATCH_PREFIX + modname] = mod
del sys.modules[modname]
def __exit__(self, t, v, tb):
try:
self._restore()
finally:
imp.release_lock()
def __enter__(self):
imp.acquire_lock()
self._save()
self._replace()
def import_patched(module_name):
"""
Import *module_name* with gevent monkey-patches active,
and return the greened module.
Any sub-modules that were imported by the package are also
saved.
"""
patched_name = _PATCH_PREFIX + module_name
if patched_name in sys.modules:
return sys.modules[patched_name]
# Save the current module state, and restore on exit,
# capturing desirable changes in the modules package.
with _SysModulesPatcher(module_name):
sys.modules.pop(module_name, None)
module = _import(module_name, {}, {}, module_name.split('.')[:-1])
sys.modules[patched_name] = module
return module
...@@ -14,15 +14,15 @@ from gevent.lock import RLock ...@@ -14,15 +14,15 @@ from gevent.lock import RLock
# So we test for the old, deprecated version first # So we test for the old, deprecated version first
try: # Py2 try: # Py2
import __builtin__ as builtins import __builtin__ as __gbuiltins__
_allowed_module_name_types = (basestring,) # pylint:disable=undefined-variable _allowed_module_name_types = (basestring,) # pylint:disable=undefined-variable
__target__ = '__builtin__' __target__ = '__builtin__'
except ImportError: except ImportError:
import builtins # pylint: disable=import-error import builtins as __gbuiltins__ # pylint: disable=import-error
_allowed_module_name_types = (str,) _allowed_module_name_types = (str,)
__target__ = 'builtins' __target__ = 'builtins'
_import = builtins.__import__ _import = __gbuiltins__.__import__
# We need to protect imports both across threads and across greenlets. # We need to protect imports both across threads and across greenlets.
# And the order matters. Note that under 3.4, the global import lock # And the order matters. Note that under 3.4, the global import lock
...@@ -120,6 +120,13 @@ def _lock_imports(): ...@@ -120,6 +120,13 @@ def _lock_imports():
if sys.version_info[:2] >= (3, 3): if sys.version_info[:2] >= (3, 3):
__implements__ = [] __implements__ = []
__import__ = _import
else: else:
__implements__ = ['__import__'] __implements__ = ['__import__']
__all__ = __implements__ __all__ = __implements__
from gevent._util import copy_globals
__imports__ = copy_globals(__gbuiltins__, globals(),
names_to_ignore=__implements__)
...@@ -42,7 +42,10 @@ from gevent.hub import get_hub, Waiter, getcurrent ...@@ -42,7 +42,10 @@ from gevent.hub import get_hub, Waiter, getcurrent
from gevent.hub import InvalidSwitchError from gevent.hub import InvalidSwitchError
__all__ = ['Empty', 'Full', 'Queue', 'PriorityQueue', 'LifoQueue', 'JoinableQueue', 'Channel'] __implements__ = ['Queue', 'PriorityQueue', 'LifoQueue']
__extensions__ = ['JoinableQueue', 'Channel']
__imports__ = ['Empty', 'Full']
__all__ = __implements__ + __extensions__ + __imports__
def _safe_remove(deq, item): def _safe_remove(deq, item):
......
# Copyright (c) 2018 gevent contributors. See LICENSE for details. # Copyright (c) 2018 gevent contributors. See LICENSE for details.
import _socket import _socket
from _socket import AI_NUMERICHOST from _socket import AI_NUMERICHOST
from _socket import error from _socket import error
...@@ -9,14 +10,41 @@ import socket ...@@ -9,14 +10,41 @@ import socket
from . import AbstractResolver from . import AbstractResolver
from dns import resolver from gevent._patcher import import_patched
import dns
__all__ = [ __all__ = [
'Resolver', 'Resolver',
] ]
# Import the DNS packages to use the gevent modules,
# even if the system is not monkey-patched.
dns = import_patched('dns')
for pkg in ('dns',
'dns.rdtypes',
'dns.rdtypes.IN',
'dns.rdtypes.ANY'):
mod = import_patched(pkg)
for name in mod.__all__:
setattr(mod, name, import_patched(pkg + '.' + name))
def _dns_import_patched(name):
assert name.startswith('dns')
import_patched(name)
return dns
# This module tries to dynamically import classes
# using __import__, and it's important that they match
# the ones we just created, otherwise exceptions won't be caught
# as expected. It uses a one-arg __import__ statement and then
# tries to walk down the sub-modules using getattr, so we can't
# directly use import_patched as-is.
dns.rdata.__import__ = _dns_import_patched
resolver = dns.resolver
# This is a copy of resolver._getaddrinfo with the crucial change that it # This is a copy of resolver._getaddrinfo with the crucial change that it
# doesn't have a bare except:, because that breaks Timeout and KeyboardInterrupt # doesn't have a bare except:, because that breaks Timeout and KeyboardInterrupt
# See https://github.com/rthalley/dnspython/pull/300 # See https://github.com/rthalley/dnspython/pull/300
...@@ -123,6 +151,8 @@ def _getaddrinfo(host=None, service=None, family=socket.AF_UNSPEC, socktype=0, ...@@ -123,6 +151,8 @@ def _getaddrinfo(host=None, service=None, family=socket.AF_UNSPEC, socktype=0,
return tuples return tuples
resolver._getaddrinfo = _getaddrinfo
class Resolver(AbstractResolver): class Resolver(AbstractResolver):
""" """
An *experimental* resolver that uses `dnspython`_. An *experimental* resolver that uses `dnspython`_.
...@@ -136,17 +166,15 @@ class Resolver(AbstractResolver): ...@@ -136,17 +166,15 @@ class Resolver(AbstractResolver):
resolver can resolve Unicode host names that the system resolver resolver can resolve Unicode host names that the system resolver
cannot. cannot.
This uses thread locks and sockets, so it only functions if the .. note::
system has been monkey-patched. Otherwise it will raise a
``ValueError``.
This uses dnspython's default resolver object. This object has This **does not** use dnspython's default resolver object, or share any
several useful attributes that can be used to adjust the behaviour classes with ``import dns``. A separate copy of the objects is imported to
of the DNS system; in particular, the ``cache`` attribute could be be able to function in a non monkey-patched process. The documentation for the resolver
set to an instance of :class:`dns.resolver.Cache` or object still applies.
:class:`dns.resolver.LRUCache` (by default a ``LRUCache`` is
used), and ``nameservers`` controls which nameservers to talk to, The resolver that we use is available as the :attr:`resolver` attribute
and ``lifetime`` configures a timeout for each individual query. of this object (typically ``gevent.get_hub().resolver.resolver``).
.. caution:: .. caution::
...@@ -169,15 +197,25 @@ class Resolver(AbstractResolver): ...@@ -169,15 +197,25 @@ class Resolver(AbstractResolver):
""" """
def __init__(self, hub=None): # pylint: disable=unused-argument def __init__(self, hub=None): # pylint: disable=unused-argument
from gevent import monkey
if not all(monkey.is_module_patched(m) for m in ['threading', 'socket', 'select']):
raise ValueError("Can only be used when monkey-patched")
if resolver._resolver is None: if resolver._resolver is None:
resolver._resolver = resolver.get_default_resolver() resolver._resolver = resolver.get_default_resolver()
# Add a default cache # Add a default cache
resolver._resolver.cache = resolver.LRUCache() resolver._resolver.cache = resolver.LRUCache()
if resolver._getaddrinfo is not _getaddrinfo:
resolver._getaddrinfo = _getaddrinfo @property
def resolver(self):
"""
The dnspython resolver object we use.
This object has several useful attributes that can be used to
adjust the behaviour of the DNS system:
* ``cache`` is a :class:`dns.resolver.LRUCache`. Its maximum size
can be configured by calling :meth:`resolver.cache.set_max_size`
* ``nameservers`` controls which nameservers to talk to
* ``lifetime`` configures a timeout for each individual query.
"""
return resolver._resolver
def close(self): def close(self):
pass pass
......
...@@ -7,21 +7,7 @@ import types ...@@ -7,21 +7,7 @@ import types
from greentest.modules import walk_modules from greentest.modules import walk_modules
from greentest.sysinfo import PLATFORM_SPECIFIC_SUFFIXES from greentest.sysinfo import PLATFORM_SPECIFIC_SUFFIXES
from gevent._patcher import MAPPING
MAPPING = {
'gevent.local': '_threading_local',
'gevent.socket': 'socket',
'gevent.select': 'select',
'gevent.ssl': 'ssl',
'gevent.thread': '_thread' if six.PY3 else 'thread',
'gevent.subprocess': 'subprocess',
'gevent.os': 'os',
'gevent.threading': 'threading',
'gevent.builtins': 'builtins' if six.PY3 else '__builtin__',
'gevent.signal': 'signal',
'gevent.time': 'time',
}
class ANY(object): class ANY(object):
def __contains__(self, item): def __contains__(self, item):
...@@ -53,6 +39,7 @@ NO_ALL = [ ...@@ -53,6 +39,7 @@ NO_ALL = [
'gevent._fileobjectposix', 'gevent._fileobjectposix',
'gevent._tblib', 'gevent._tblib',
'gevent._corecffi', 'gevent._corecffi',
'gevent._patcher',
] ]
# A list of modules that may contain things that aren't actually, technically, # A list of modules that may contain things that aren't actually, technically,
...@@ -90,7 +77,8 @@ class Test(unittest.TestCase): ...@@ -90,7 +77,8 @@ class Test(unittest.TestCase):
def check_implements_presence_justified(self): def check_implements_presence_justified(self):
"Check that __implements__ is present only if the module is modeled after a module from stdlib (like gevent.socket)." "Check that __implements__ is present only if the module is modeled after a module from stdlib (like gevent.socket)."
if self.__implements__ is not None and self.stdlib_module is None: if self.__implements__ is not None and self.stdlib_module is None:
raise AssertionError('%r has __implements__ but no stdlib counterpart' % self.modname) raise AssertionError('%r has __implements__ but no stdlib counterpart (%s)'
% (self.modname, self.stdlib_name))
def set_stdlib_all(self): def set_stdlib_all(self):
self.assertIsNotNone(self.stdlib_module) self.assertIsNotNone(self.stdlib_module)
......
...@@ -4,10 +4,6 @@ ...@@ -4,10 +4,6 @@
import gevent import gevent
from gevent import monkey from gevent import monkey
if ['gevent.resolver.dnspython.Resolver'] == gevent.get_hub().resolver_class:
# dnspython requires monkey-patching
monkey.patch_all()
import os import os
import re import re
import greentest import greentest
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment