diff --git a/.zuul.yaml b/.zuul.yaml
index f5cced5..d653cf9 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -1,27 +1,21 @@
 - project:
     templates:
       - check-requirements
-      - lib-forward-testing
       - lib-forward-testing-python3
       - openstack-cover-jobs
       - openstack-lower-constraints-jobs
-      - openstack-python-jobs
-      - openstack-python3-train-jobs
+      - openstack-python3-ussuri-jobs
       - periodic-stable-jobs
       - publish-openstack-docs-pti
       - release-notes-jobs-python3
     check:
       jobs:
-        - openstack-tox-functional
         - openstack-tox-functional-py36
         - tempest-smoke-py3-osprofiler-redis
         - tempest-smoke-py3-osprofiler-sqlalchemy
     gate:
       jobs:
-        - openstack-tox-functional
         - openstack-tox-functional-py36
-        - tempest-smoke-py3-osprofiler-redis
-        - tempest-smoke-py3-osprofiler-sqlalchemy
 
 - job:
     name: tempest-smoke-py3-osprofiler-redis
diff --git a/debian/changelog b/debian/changelog
index f090d57..02f6bb4 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+python-osprofiler (3.0.0-1) UNRELEASED; urgency=medium
+
+  * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Fri, 27 Mar 2020 06:08:19 +0000
+
 python-osprofiler (2.8.2-3) unstable; urgency=medium
 
   * Removed Python 2 autopkgtest (Closes: #937988).
diff --git a/lower-constraints.txt b/lower-constraints.txt
index 481b456..ffd70a2 100644
--- a/lower-constraints.txt
+++ b/lower-constraints.txt
@@ -2,7 +2,6 @@ coverage===4.0
 ddt===1.0.1
 dulwich===0.15.0
 elasticsearch===2.0.0
-futures===3.0.0
 jaeger-client==3.8.0
 mock===2.0.0
 netaddr===0.7.18
diff --git a/osprofiler/drivers/elasticsearch_driver.py b/osprofiler/drivers/elasticsearch_driver.py
index 85bab74..c24eb60 100644
--- a/osprofiler/drivers/elasticsearch_driver.py
+++ b/osprofiler/drivers/elasticsearch_driver.py
@@ -36,9 +36,9 @@ class ElasticsearchDriver(base.Driver):
             from elasticsearch import Elasticsearch
         except ImportError:
             raise exc.CommandError(
-                "To use this command, you should install "
-                "'elasticsearch' manually. Use command:\n "
-                "'pip install elasticsearch'.")
+                "To use OSProfiler with ElasticSearch driver, "
+                "please install `elasticsearch` library. "
+                "To install with pip:\n `pip install elasticsearch`.")
 
         client_url = parser.urlunparse(parser.urlparse(self.connection_str)
                                        ._replace(scheme="http"))
diff --git a/osprofiler/drivers/jaeger.py b/osprofiler/drivers/jaeger.py
index 669aa13..82b7a23 100644
--- a/osprofiler/drivers/jaeger.py
+++ b/osprofiler/drivers/jaeger.py
@@ -40,8 +40,8 @@ class Jaeger(base.Driver):
         except ImportError:
             raise exc.CommandError(
                 "To use OSProfiler with Uber Jaeger tracer, "
-                "you have to install `jaeger-client` manually. "
-                "Install with pip:\n `pip install jaeger-client`."
+                "please install `jaeger-client` library. "
+                "To install with pip:\n `pip install jaeger-client`."
             )
 
         parsed_url = parser.urlparse(connection_str)
diff --git a/osprofiler/drivers/mongodb.py b/osprofiler/drivers/mongodb.py
index 7713baf..86119e4 100644
--- a/osprofiler/drivers/mongodb.py
+++ b/osprofiler/drivers/mongodb.py
@@ -28,9 +28,9 @@ class MongoDB(base.Driver):
             from pymongo import MongoClient
         except ImportError:
             raise exc.CommandError(
-                "To use this command, you should install "
-                "'pymongo' manually. Use command:\n "
-                "'pip install pymongo'.")
+                "To use OSProfiler with MongoDB driver, "
+                "please install `pymongo` library. "
+                "To install with pip:\n `pip install pymongo`.")
 
         client = MongoClient(self.connection_str, connect=False)
         self.db = client[db_name]
diff --git a/osprofiler/drivers/redis_driver.py b/osprofiler/drivers/redis_driver.py
index b8101de..c54b37b 100644
--- a/osprofiler/drivers/redis_driver.py
+++ b/osprofiler/drivers/redis_driver.py
@@ -40,9 +40,9 @@ class Redis(base.Driver):
             from redis import StrictRedis
         except ImportError:
             raise exc.CommandError(
-                "To use this command, you should install "
-                "'redis' manually. Use command:\n "
-                "'pip install redis'.")
+                "To use OSProfiler with Redis driver, "
+                "please install `redis` library. "
+                "To install with pip:\n `pip install redis`.")
 
         # only connection over network is supported with schema
         # redis://[:password]@host[:port][/db]
diff --git a/osprofiler/notifier.py b/osprofiler/notifier.py
index 3bd4412..8b909f0 100644
--- a/osprofiler/notifier.py
+++ b/osprofiler/notifier.py
@@ -13,16 +13,21 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+import logging
+
 from osprofiler.drivers import base
 
 
+LOG = logging.getLogger(__name__)
+
+
 def _noop_notifier(info, context=None):
     """Do nothing on notify()."""
 
 
 # NOTE(boris-42): By default we are using noop notifier.
 __notifier = _noop_notifier
-__driver_cache = {}
+__notifier_cache = {}  # map: connection-string -> notifier
 
 
 def notify(info):
@@ -54,14 +59,24 @@ def create(connection_string, *args, **kwargs):
 
     :param connection_string: connection string which specifies the storage
                               driver for notifier
-    :param *args: args that will be passed to the driver's __init__ method
-    :param **kwargs: kwargs that will be passed to the driver's __init__ method
+    :param args: args that will be passed to the driver's __init__ method
+    :param kwargs: kwargs that will be passed to the driver's __init__ method
     :returns: Callable notifier method
-    :raises TypeError: In case of invalid name of plugin raises TypeError
     """
-    global __driver_cache
-    if connection_string not in __driver_cache:
-        __driver_cache[connection_string] = base.get_driver(connection_string,
-                                                            *args,
-                                                            **kwargs).notify
-    return __driver_cache[connection_string]
+    global __notifier_cache
+    if connection_string not in __notifier_cache:
+        try:
+            driver = base.get_driver(connection_string, *args, **kwargs)
+            __notifier_cache[connection_string] = driver.notify
+            LOG.info("osprofiler is enabled with connection string: %s",
+                     connection_string)
+        except Exception:
+            LOG.exception("Could not initialize driver for connection string "
+                          "%s, osprofiler is disabled", connection_string)
+            __notifier_cache[connection_string] = _noop_notifier
+
+    return __notifier_cache[connection_string]
+
+
+def clear_notifier_cache():
+    __notifier_cache.clear()
diff --git a/osprofiler/tests/unit/test_notifier.py b/osprofiler/tests/unit/test_notifier.py
index 7cccdad..8e658fe 100644
--- a/osprofiler/tests/unit/test_notifier.py
+++ b/osprofiler/tests/unit/test_notifier.py
@@ -23,6 +23,7 @@ class NotifierTestCase(test.TestCase):
 
     def tearDown(self):
         notifier.set(notifier._noop_notifier)  # restore defaults
+        notifier.clear_notifier_cache()
         super(NotifierTestCase, self).tearDown()
 
     def test_set(self):
@@ -49,3 +50,11 @@ class NotifierTestCase(test.TestCase):
         result = notifier.create("test", 10, b=20)
         mock_factory.assert_called_once_with("test", 10, b=20)
         self.assertEqual(mock_factory.return_value.notify, result)
+
+    @mock.patch("osprofiler.notifier.base.get_driver")
+    def test_create_driver_init_failure(self, mock_get_driver):
+        mock_get_driver.side_effect = Exception()
+
+        result = notifier.create("test", 10, b=20)
+        mock_get_driver.assert_called_once_with("test", 10, b=20)
+        self.assertEqual(notifier._noop_notifier, result)
diff --git a/releasenotes/notes/drop-python-2-7-73d3113c69d724d6.yaml b/releasenotes/notes/drop-python-2-7-73d3113c69d724d6.yaml
new file mode 100644
index 0000000..0554c61
--- /dev/null
+++ b/releasenotes/notes/drop-python-2-7-73d3113c69d724d6.yaml
@@ -0,0 +1,5 @@
+---
+upgrade:
+  - |
+    Python 2.7 support has been dropped. The minimum version of Python now
+    supported by osprofiler is Python 3.6.
diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst
index 72511c5..3c4f35d 100644
--- a/releasenotes/source/index.rst
+++ b/releasenotes/source/index.rst
@@ -6,6 +6,7 @@
     :maxdepth: 1
 
     unreleased
+    train
     stein
     rocky
     queens
diff --git a/releasenotes/source/train.rst b/releasenotes/source/train.rst
new file mode 100644
index 0000000..5839003
--- /dev/null
+++ b/releasenotes/source/train.rst
@@ -0,0 +1,6 @@
+==========================
+Train Series Release Notes
+==========================
+
+.. release-notes::
+   :branch: stable/train
diff --git a/setup.cfg b/setup.cfg
index 6030a25..22b42ae 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -6,6 +6,7 @@ description-file =
 author = OpenStack
 author-email = openstack-discuss@lists.openstack.org
 home-page = https://docs.openstack.org/osprofiler/latest/
+python-requires = >=3.6
 classifier =
     Environment :: OpenStack
     Intended Audience :: Developers
@@ -13,30 +14,20 @@ classifier =
     License :: OSI Approved :: Apache Software License
     Operating System :: POSIX :: Linux
     Programming Language :: Python
-    Programming Language :: Python :: 2
-    Programming Language :: Python :: 2.7
     Programming Language :: Python :: 3
     Programming Language :: Python :: 3.6
     Programming Language :: Python :: 3.7
+    Programming Language :: Python :: 3 :: Only
+    Programming Language :: Python :: Implementation :: CPython
 
 [files]
 packages =
     osprofiler
 
-[global]
-setup-hooks =
-    pbr.hooks.setup_hook
-
 [extras]
 oslo_config =
   oslo.config>=5.2.0 # Apache-2.0
 
-[build_sphinx]
-all_files = 1
-build-dir = doc/build
-source-dir = doc/source
-warning-is-error = 1
-
 [entry_points]
 oslo.config.opts =
     osprofiler = osprofiler.opts:list_opts
@@ -44,6 +35,3 @@ console_scripts =
     osprofiler = osprofiler.cmd.shell:main
 paste.filter_factory =
     osprofiler = osprofiler.web:WsgiMiddleware.factory
-
-[wheel]
-universal = 1
diff --git a/setup.py b/setup.py
index ddd1771..a7c1710 100644
--- a/setup.py
+++ b/setup.py
@@ -14,14 +14,6 @@
 # THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT
 import setuptools
 
-# In python < 2.7.4, a lazy loading of package `pbr` will break
-# setuptools if some other modules registered functions in `atexit`.
-# solution from: http://bugs.python.org/issue15881#msg170215
-try:
-    import multiprocessing  # noqa
-except ImportError:
-    pass
-
 setuptools.setup(
-    setup_requires=['pbr>=1.8'],
+    setup_requires=['pbr>=2.0'],
     pbr=True)
diff --git a/test-requirements.txt b/test-requirements.txt
index 9153e9e..33a3fb3 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -1,4 +1,4 @@
-hacking>=0.12.0,!=0.13.0,<0.14  # Apache-2.0
+hacking>=2.0,<=2.1  # Apache-2.0
 
 coverage>=4.0 # Apache-2.0
 ddt>=1.0.1  # MIT
@@ -25,4 +25,3 @@ reno>=2.5.0 # Apache-2.0
 
 # For Jaeger Tracing
 jaeger-client>=3.8.0 # Apache-2.0
-futures>=3.0.0;python_version=='2.7' or python_version=='2.6' # PSF
diff --git a/tox.ini b/tox.ini
index 8601286..aee1fcc 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,9 +1,12 @@
 [tox]
-minversion = 2.0
-skipsdist = True
-envlist = py27,py37,pep8
+minversion = 3.1.0
+# Needed to create ChangeLog for docs building
+skipsdist = False
+envlist = py37,pep8
+ignore_basepython_conflict = True
 
 [testenv]
+basepython = python3
 setenv = VIRTUAL_ENV={envdir}
          LANG=en_US.UTF-8
          LANGUAGE=en_US:en
@@ -12,13 +15,11 @@ deps =
   -c{env:UPPER_CONSTRAINTS_FILE:https://opendev.org/openstack/requirements/raw/branch/master/upper-constraints.txt}
   -r{toxinidir}/requirements.txt
   -r{toxinidir}/test-requirements.txt
-install_command = pip install -U {opts} {packages}
 usedevelop = True
 commands = stestr run --slowest {posargs}
 distribute = false
 
 [testenv:functional]
-basepython = python2.7
 setenv = {[testenv]setenv}
          OS_TEST_PATH=./osprofiler/tests/functional
 deps =
@@ -28,10 +29,10 @@ deps =
 [testenv:functional-py36]
 basepython = python3.6
 setenv = {[testenv:functional]setenv}
-deps = {[testenv:functional]deps}
+deps =
+  {[testenv:functional]deps}
 
 [testenv:pep8]
-basepython = python3
 commands =
   flake8
   # Run security linter
@@ -39,11 +40,9 @@ commands =
 distribute = false
 
 [testenv:venv]
-basepython = python3
 commands = {posargs}
 
 [testenv:cover]
-basepython = python3
 setenv =
     PYTHON=coverage run --source osprofiler --parallel-mode
 commands =
@@ -53,11 +52,10 @@ commands =
     coverage xml -o cover/coverage.xml
 
 [testenv:docs]
-basepython = python3
-commands = python setup.py build_sphinx
+commands =
+  sphinx-build -W --keep-going -b html -d doc/build/doctrees doc/source doc/build/html
 
 [testenv:bandit]
-basepython = python3
 commands = bandit -r osprofiler -n5
 
 [flake8]
@@ -69,11 +67,9 @@ exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,tools,setup.py,build,releasen
 local-check-factory = osprofiler.hacking.checks.factory
 
 [testenv:releasenotes]
-basepython = python3
 commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html
 
 [testenv:lower-constraints]
-basepython = python3
 deps =
   -c{toxinidir}/lower-constraints.txt
   -r{toxinidir}/test-requirements.txt