summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBrant Knudson <bknudson@us.ibm.com>2013-05-18 13:52:49 -0500
committerGerrit Code Review <review@openstack.org>2013-07-03 22:01:15 +0000
commiteb930fd2f8d34ed8c5a8701934a314ee459c428b (patch)
treed97fbf81711f92efe5a9f236a67c74de8e02b6c9
parent62d948a66b27ad2622a324bd9a070346f7b607d2 (diff)
Add callbacks for set_global_engine
This adds functionality where a class can monitor for the global engine changing. This is useful for a class that caches the global engine and wants to know when its cached global engine isn't valid anymore. Part of fix for bug 1179259 Change-Id: I5736a05308c63de9fccb8af7720ddd70530f4270
-rw-r--r--keystone/common/sql/core.py44
-rw-r--r--tests/test_sql_core.py124
2 files changed, 167 insertions, 1 deletions
diff --git a/keystone/common/sql/core.py b/keystone/common/sql/core.py
index d75c73a8..f3a3deeb 100644
--- a/keystone/common/sql/core.py
+++ b/keystone/common/sql/core.py
@@ -32,10 +32,12 @@ from keystone import exception
from keystone.openstack.common import jsonutils
+LOG = logging.getLogger(__name__)
CONF = config.CONF
-# maintain a single engine reference for sqlite in-memory
+# maintain a single engine reference for sqlalchemy engine
GLOBAL_ENGINE = None
+GLOBAL_ENGINE_CALLBACKS = set()
ModelBase = declarative.declarative_base()
@@ -95,9 +97,49 @@ ModelBase.__init__ = initialize_decorator(ModelBase.__init__)
def set_global_engine(engine):
+ """Set the global engine.
+
+ This sets the current global engine, which is returned by
+ Base.get_engine(allow_global_engine=True).
+
+ When the global engine is changed, all of the callbacks registered via
+ register_global_engine_callback since the last time set_global_engine was
+ changed are called. The callback functions are invoked with no arguments.
+
+ """
+
global GLOBAL_ENGINE
+ global GLOBAL_ENGINE_CALLBACKS
+
+ if engine is GLOBAL_ENGINE:
+ # It's the same engine so nothing to do.
+ return
+
GLOBAL_ENGINE = engine
+ cbs = GLOBAL_ENGINE_CALLBACKS
+ GLOBAL_ENGINE_CALLBACKS = set()
+ for cb in cbs:
+ try:
+ cb()
+ except Exception:
+ LOG.exception(_("Global engine callback raised."))
+ # Just logging the exception so can process other callbacks.
+
+
+def register_global_engine_callback(cb_fn):
+ """Register a function to be called when the global engine is set.
+
+ Note that the callback will be called only once or not at all, so to get
+ called each time the global engine is changed the function must be
+ re-registered.
+
+ """
+
+ global GLOBAL_ENGINE_CALLBACKS
+
+ GLOBAL_ENGINE_CALLBACKS.add(cb_fn)
+
# Special Fields
class JsonBlob(sql_types.TypeDecorator):
diff --git a/tests/test_sql_core.py b/tests/test_sql_core.py
index d8f2a4f7..8be80e30 100644
--- a/tests/test_sql_core.py
+++ b/tests/test_sql_core.py
@@ -17,6 +17,130 @@ from keystone.common import sql
from keystone import test
+class CallbackMonitor:
+ def __init__(self, expect_called=True, raise_=False):
+ self.expect_called = expect_called
+ self.called = False
+ self._complete = False
+ self._raise = raise_
+
+ def call_this(self):
+ if self._complete:
+ return
+
+ if not self.expect_called:
+ raise Exception("Did not expect callback.")
+
+ if self.called:
+ raise Exception("Callback already called.")
+
+ self.called = True
+
+ if self._raise:
+ raise Exception("When called, raises.")
+
+ def check(self):
+ if self.expect_called:
+ if not self.called:
+ raise Exception("Expected function to be called.")
+ self._complete = True
+
+
+class TestGlobalEngine(test.TestCase):
+
+ def tearDown(self):
+ sql.set_global_engine(None)
+ super(TestGlobalEngine, self).tearDown()
+
+ def test_notify_on_set(self):
+ # If call sql.set_global_engine(), notify callbacks get called.
+
+ cb_mon = CallbackMonitor()
+
+ sql.register_global_engine_callback(cb_mon.call_this)
+ fake_engine = object()
+ sql.set_global_engine(fake_engine)
+
+ cb_mon.check()
+
+ def test_multi_notify(self):
+ # You can also set multiple notify callbacks and they each get called.
+
+ cb_mon1 = CallbackMonitor()
+ cb_mon2 = CallbackMonitor()
+
+ sql.register_global_engine_callback(cb_mon1.call_this)
+ sql.register_global_engine_callback(cb_mon2.call_this)
+
+ fake_engine = object()
+ sql.set_global_engine(fake_engine)
+
+ cb_mon1.check()
+ cb_mon2.check()
+
+ def test_notify_once(self):
+ # After a callback is called, it's not called again if set global
+ # engine again.
+
+ cb_mon = CallbackMonitor()
+
+ sql.register_global_engine_callback(cb_mon.call_this)
+ fake_engine = object()
+ sql.set_global_engine(fake_engine)
+
+ fake_engine = object()
+ # Note that cb_mon.call_this would raise if it's called again.
+ sql.set_global_engine(fake_engine)
+
+ cb_mon.check()
+
+ def test_set_same_engine(self):
+ # If you set the global engine to the same engine, callbacks don't get
+ # called.
+
+ fake_engine = object()
+
+ sql.set_global_engine(fake_engine)
+
+ cb_mon = CallbackMonitor(expect_called=False)
+ sql.register_global_engine_callback(cb_mon.call_this)
+
+ # Note that cb_mon.call_this would raise if it's called.
+ sql.set_global_engine(fake_engine)
+
+ cb_mon.check()
+
+ def test_notify_register_same(self):
+ # If you register the same callback twice, only gets called once.
+ cb_mon = CallbackMonitor()
+
+ sql.register_global_engine_callback(cb_mon.call_this)
+ sql.register_global_engine_callback(cb_mon.call_this)
+
+ fake_engine = object()
+ # Note that cb_mon.call_this would raise if it's called twice.
+ sql.set_global_engine(fake_engine)
+
+ cb_mon.check()
+
+ def test_callback_throws(self):
+ # If a callback function raises,
+ # a) the caller doesn't know about it,
+ # b) other callbacks are still called
+
+ cb_mon1 = CallbackMonitor(raise_=True)
+ cb_mon2 = CallbackMonitor()
+
+ sql.register_global_engine_callback(cb_mon1.call_this)
+ sql.register_global_engine_callback(cb_mon2.call_this)
+
+ fake_engine = object()
+ sql.set_global_engine(fake_engine)
+
+ cb_mon1.check()
+ cb_mon2.check()
+
+
class TestBase(test.TestCase):
def tearDown(self):