summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBrant Knudson <bknudson@us.ibm.com>2014-03-05 16:45:36 -0600
committerBrant Knudson <bknudson@us.ibm.com>2014-03-05 16:45:36 -0600
commit33c0ed2e9d400dc0a0cb7bba8928ec3883ef1e15 (patch)
tree99c898fd9a57296e99abcf40f7f974c79d98efc6
parentf05a88361451daf70eca30c9b84ed03b694b4481 (diff)
downloadoslo-db-33c0ed2e9d400dc0a0cb7bba8928ec3883ef1e15.tar.gz
Fix excessive logging from db.sqlalchemy.session
A message about the MySQL server mode was logged at the info level every time a connection was checked out. This happens often, and logging every time is unnecessary. Here's an example of the message when using the default configuration: MySQL server mode set to STRICT_TRANS_TABLES,STRICT_ALL_TABLES,... The fix is to send the message once per engine creation. Also, this message is now logged the debug level since there's a warning logged if the mode isn't a "safe" one already. Closes-Bug: #1288443 Co-Authored-By: Ben Nemec <bnemec@redhat.com> Change-Id: I13b24d04c28e69a8534aaa8d3a4463ca1d65570f
-rw-r--r--openstack/common/db/sqlalchemy/session.py54
-rw-r--r--tests/unit/db/sqlalchemy/test_sqlalchemy.py198
2 files changed, 170 insertions, 82 deletions
diff --git a/openstack/common/db/sqlalchemy/session.py b/openstack/common/db/sqlalchemy/session.py
index 802ea5f..98939b3 100644
--- a/openstack/common/db/sqlalchemy/session.py
+++ b/openstack/common/db/sqlalchemy/session.py
@@ -291,7 +291,7 @@ from sqlalchemy.pool import NullPool, StaticPool
from sqlalchemy.sql.expression import literal_column
from openstack.common.db import exception
-from openstack.common.gettextutils import _LE, _LW, _LI
+from openstack.common.gettextutils import _LE, _LW
from openstack.common import timeutils
@@ -520,30 +520,54 @@ def _set_session_sql_mode(dbapi_con, connection_rec,
Note: passing in '' (empty string) for sql_mode clears
the SQL mode for the session, overriding a potentially set
- server default. Passing in None (the default) makes this
- a no-op, meaning if a server-side SQL mode is set, it still applies.
+ server default.
"""
+
cursor = dbapi_con.cursor()
- if sql_mode is not None:
- cursor.execute("SET SESSION sql_mode = %s", [sql_mode])
+ cursor.execute("SET SESSION sql_mode = %s", [sql_mode])
+
+
+def _mysql_get_effective_sql_mode(engine):
+ """Returns the effective SQL mode for connections from the engine pool.
- # Check against the real effective SQL mode. Even when unset by
+ Returns ``None`` if the mode isn't available, otherwise returns the mode.
+
+ """
+ # Get the real effective SQL mode. Even when unset by
# our own config, the server may still be operating in a specific
- # SQL mode as set by the server configuration
- cursor.execute("SHOW VARIABLES LIKE 'sql_mode'")
- row = cursor.fetchone()
+ # SQL mode as set by the server configuration.
+ # Also note that the checkout listener will be called on execute to
+ # set the mode if it's registered.
+ row = engine.execute("SHOW VARIABLES LIKE 'sql_mode'").fetchone()
if row is None:
+ return
+ return row[1]
+
+
+def _mysql_check_effective_sql_mode(engine):
+ """Logs a message based on the effective SQL mode for MySQL connections."""
+ realmode = _mysql_get_effective_sql_mode(engine)
+
+ if realmode is None:
LOG.warning(_LW('Unable to detect effective SQL mode'))
return
- realmode = row[1]
- LOG.info(_LI('MySQL server mode set to %s') % realmode)
+
+ LOG.debug('MySQL server mode set to %s', realmode)
# 'TRADITIONAL' mode enables several other modes, so
# we need a substring match here
if not ('TRADITIONAL' in realmode.upper() or
'STRICT_ALL_TABLES' in realmode.upper()):
LOG.warning(_LW("MySQL SQL mode is '%s', "
- "consider enabling TRADITIONAL or STRICT_ALL_TABLES")
- % realmode)
+ "consider enabling TRADITIONAL or STRICT_ALL_TABLES"),
+ realmode)
+
+
+def _mysql_set_mode_callback(engine, sql_mode):
+ if sql_mode is not None:
+ mode_callback = functools.partial(_set_session_sql_mode,
+ sql_mode=sql_mode)
+ sqlalchemy.event.listen(engine, 'checkout', mode_callback)
+ _mysql_check_effective_sql_mode(engine)
def _is_db_connection_error(args):
@@ -619,9 +643,7 @@ def create_engine(sql_connection, sqlite_fk=False, mysql_sql_mode=None,
sqlalchemy.event.listen(engine, 'checkout', ping_callback)
if engine.name == 'mysql':
if mysql_sql_mode:
- mode_callback = functools.partial(_set_session_sql_mode,
- sql_mode=mysql_sql_mode)
- sqlalchemy.event.listen(engine, 'checkout', mode_callback)
+ _mysql_set_mode_callback(engine, mysql_sql_mode)
elif 'sqlite' in connection_dict.drivername:
if not sqlite_synchronous:
sqlalchemy.event.listen(engine, 'connect',
diff --git a/tests/unit/db/sqlalchemy/test_sqlalchemy.py b/tests/unit/db/sqlalchemy/test_sqlalchemy.py
index f7de80c..fbe8c69 100644
--- a/tests/unit/db/sqlalchemy/test_sqlalchemy.py
+++ b/tests/unit/db/sqlalchemy/test_sqlalchemy.py
@@ -17,6 +17,8 @@
"""Unit tests for SQLAlchemy specific code."""
+import logging
+
import _mysql_exceptions
import mock
import sqlalchemy
@@ -32,7 +34,6 @@ from openstack.common.db.sqlalchemy import session
from openstack.common.db.sqlalchemy import test_base
from openstack.common import log
from openstack.common import test
-from openstack.common.gettextutils import _LW, _LI
from tests.unit import test_log
@@ -379,72 +380,137 @@ class EngineFacadeTestCase(test.BaseTestCase):
expire_on_commit=True)
-class SetSQLModeTestCase(test_log.LogTestBase):
- def setUp(self):
- super(SetSQLModeTestCase, self).setUp()
- self.dbapi_mock = mock.Mock()
- self.cursor = mock.Mock()
- self.dbapi_mock.cursor.return_value = self.cursor
- # Add fake logger so we can verify log messages
- self.logger = log.getLogger('openstack.common.db.sqlalchemy.session')
- self._add_handler_with_cleanup(self.logger)
-
- def _assert_calls(self, test_mode, recommended):
- self.cursor.execute.assert_any_call("SET SESSION sql_mode = %s",
- [test_mode])
- self.cursor.execute.assert_called_with(
- "SHOW VARIABLES LIKE 'sql_mode'")
- self.assertIn(_LI('MySQL server mode set to %s') % test_mode,
+class MysqlSetCallbackTest(test_log.LogTestBase):
+
+ class FakeCursor(object):
+ def __init__(self, execs):
+ self._execs = execs
+
+ def execute(self, sql, arg):
+ self._execs.append(sql % arg)
+
+ class FakeDbapiCon(object):
+ def __init__(self, execs):
+ self._execs = execs
+
+ def cursor(self):
+ return MysqlSetCallbackTest.FakeCursor(self._execs)
+
+ class FakeResultSet(object):
+ def __init__(self, realmode):
+ self._realmode = realmode
+
+ def fetchone(self):
+ return ['ignored', self._realmode]
+
+ class FakeEngine(object):
+ def __init__(self, realmode=None):
+ self._cbs = {}
+ self._execs = []
+ self._realmode = realmode
+
+ def set_callback(self, name, cb):
+ self._cbs[name] = cb
+
+ def execute(self, sql):
+ cb = self._cbs.get('checkout', lambda *x, **y: None)
+ dbapi_con = MysqlSetCallbackTest.FakeDbapiCon(self._execs)
+ connection_rec = None # Not used.
+ connection_proxy = None # Not used.
+ cb(dbapi_con, connection_rec, connection_proxy)
+ self._execs.append(sql)
+ return MysqlSetCallbackTest.FakeResultSet(self._realmode)
+
+ def stub_listen(engine, name, cb):
+ engine.set_callback(name, cb)
+
+ @mock.patch.object(sqlalchemy.event, 'listen', side_effect=stub_listen)
+ def _call_set_callback(self, listen_mock, sql_mode=None, realmode=None):
+ engine = self.FakeEngine(realmode=realmode)
+
+ logger = log.getLogger('openstack.common.db.sqlalchemy.session')
+ self._set_log_level_with_cleanup(logger, logging.DEBUG)
+ self._add_handler_with_cleanup(logger)
+
+ session._mysql_set_mode_callback(engine, sql_mode=sql_mode)
+ return engine
+
+ def test_set_mode_traditional(self):
+ # If _mysql_set_mode_callback is called with an sql_mode, then the SQL
+ # mode is set on the connection.
+
+ engine = self._call_set_callback(sql_mode='TRADITIONAL')
+
+ exp_calls = [
+ "SET SESSION sql_mode = ['TRADITIONAL']",
+ "SHOW VARIABLES LIKE 'sql_mode'"
+ ]
+ self.assertEqual(exp_calls, engine._execs)
+
+ def test_set_mode_ansi(self):
+ # If _mysql_set_mode_callback is called with an sql_mode, then the SQL
+ # mode is set on the connection.
+
+ engine = self._call_set_callback(sql_mode='ANSI')
+
+ exp_calls = [
+ "SET SESSION sql_mode = ['ANSI']",
+ "SHOW VARIABLES LIKE 'sql_mode'"
+ ]
+ self.assertEqual(exp_calls, engine._execs)
+
+ def test_set_mode_no_mode(self):
+ # If _mysql_set_mode_callback is called with sql_mode=None, then
+ # the SQL mode is NOT set on the connection.
+
+ engine = self._call_set_callback()
+
+ exp_calls = [
+ "SHOW VARIABLES LIKE 'sql_mode'"
+ ]
+ self.assertEqual(exp_calls, engine._execs)
+
+ def test_fail_detect_mode(self):
+ # If "SHOW VARIABLES LIKE 'sql_mode'" results in no row, then
+ # we get a log indicating can't detect the mode.
+
+ self._call_set_callback()
+
+ self.assertIn('Unable to detect effective SQL mode',
self.stream.getvalue())
- if not recommended:
- self.assertIn(_LW("MySQL SQL mode is '%s', "
- "consider enabling TRADITIONAL or "
- "STRICT_ALL_TABLES")
- % test_mode,
- self.stream.getvalue())
-
- def _set_cursor_retval(self, test_mode):
- retval = mock.MagicMock()
- retval.__getitem__.return_value = test_mode
- self.cursor.fetchone.return_value = retval
-
- def test_set_mode_not_recommended(self):
- test_mode = 'foo'
- self._set_cursor_retval(test_mode)
- session._set_session_sql_mode(self.dbapi_mock, None, None,
- sql_mode=test_mode)
- self._assert_calls(test_mode, recommended=False)
-
- def test_set_mode_none(self):
- test_mode = None
- self._set_cursor_retval('')
- session._set_session_sql_mode(self.dbapi_mock, None, None,
- sql_mode=test_mode)
- self.cursor.execute.assert_called_with(
- "SHOW VARIABLES LIKE 'sql_mode'")
- self.assertIn(_LW("MySQL SQL mode is '', "
- "consider enabling TRADITIONAL or "
- "STRICT_ALL_TABLES"),
+
+ def test_logs_real_mode(self):
+ # If "SHOW VARIABLES LIKE 'sql_mode'" results in a value, then
+ # we get a log with the value.
+
+ self._call_set_callback(realmode='SOMETHING')
+
+ self.assertIn('MySQL server mode set to SOMETHING',
self.stream.getvalue())
- def test_set_mode_traditional(self):
- test_mode = 'traditional'
- self._set_cursor_retval(test_mode)
- session._set_session_sql_mode(self.dbapi_mock, None, None,
- sql_mode=test_mode)
- self._assert_calls(test_mode, True)
-
- def test_set_mode_strict_all_tables(self):
- test_mode = 'STRICT_ALL_TABLES'
- self._set_cursor_retval(test_mode)
- session._set_session_sql_mode(self.dbapi_mock, None, None,
- sql_mode=test_mode)
- self._assert_calls(test_mode, True)
-
- def test_read_mode_fail(self):
- test_mode = None
- self.cursor.fetchone.return_value = None
- session._set_session_sql_mode(self.dbapi_mock, None, None,
- sql_mode=test_mode)
- self.assertIn(_LW('Unable to detect effective SQL mode'),
+ def test_warning_when_not_traditional(self):
+ # If "SHOW VARIABLES LIKE 'sql_mode'" results in a value that doesn't
+ # include 'TRADITIONAL', then a warning is logged.
+
+ self._call_set_callback(realmode='NOT_TRADIT')
+
+ self.assertIn("consider enabling TRADITIONAL or STRICT_ALL_TABLES",
self.stream.getvalue())
+
+ def test_no_warning_when_traditional(self):
+ # If "SHOW VARIABLES LIKE 'sql_mode'" results in a value that includes
+ # 'TRADITIONAL', then no warning is logged.
+
+ self._call_set_callback(realmode='TRADITIONAL')
+
+ self.assertNotIn("consider enabling TRADITIONAL or STRICT_ALL_TABLES",
+ self.stream.getvalue())
+
+ def test_no_warning_when_strict_all_tables(self):
+ # If "SHOW VARIABLES LIKE 'sql_mode'" results in a value that includes
+ # 'STRICT_ALL_TABLES', then no warning is logged.
+
+ self._call_set_callback(realmode='STRICT_ALL_TABLES')
+
+ self.assertNotIn("consider enabling TRADITIONAL or STRICT_ALL_TABLES",
+ self.stream.getvalue())