Skip to content

Commit b96f8c0

Browse files
authored
[DBMON-6547] Add support for surfacing Mysql blocking queries from metadata locks (DataDog#23505)
* Add support for surfacing blocking queries from metadata locks * Add changelog * Fix query to handle NULL schemas * ensure consumer is enabled
1 parent fccf488 commit b96f8c0

3 files changed

Lines changed: 128 additions & 5 deletions

File tree

mysql/changelog.d/23505.added

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add metadata lock blocking detection to activity query for MySQL 8.0+.

mysql/datadog_checks/mysql/activity.py

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,22 +89,53 @@
8989
"""
9090

9191
BLOCKING_COLUMNS_MYSQL8 = """\
92-
,blocking_thread.thread_id AS blocking_thread_id,
93-
blocking_thread.processlist_id AS blocking_processlist_id
92+
,COALESCE(blocking_thread.thread_id, mdl_blocking_thread.thread_id) AS blocking_thread_id,
93+
COALESCE(blocking_thread.processlist_id, mdl_blocking_thread.processlist_id) AS blocking_processlist_id,
94+
mdl_waiting.OBJECT_TYPE AS mdl_object_type,
95+
mdl_waiting.OBJECT_SCHEMA AS mdl_object_schema,
96+
mdl_waiting.OBJECT_NAME AS mdl_object_name,
97+
mdl_waiting.LOCK_TYPE AS mdl_waiting_lock_type,
98+
mdl_blocking.LOCK_TYPE AS mdl_blocking_lock_type
9499
"""
95100

96101
BLOCKING_JOINS_MYSQL8 = """\
97-
LEFT JOIN performance_schema.data_lock_waits AS lock_waits ON thread_a.thread_id = lock_waits.requesting_thread_id
98-
LEFT JOIN performance_schema.threads AS blocking_thread ON lock_waits.blocking_thread_id = blocking_thread.thread_id
102+
LEFT JOIN performance_schema.data_lock_waits AS lock_waits
103+
ON thread_a.thread_id = lock_waits.requesting_thread_id
104+
LEFT JOIN performance_schema.threads AS blocking_thread
105+
ON lock_waits.blocking_thread_id = blocking_thread.thread_id
106+
LEFT JOIN performance_schema.metadata_locks AS mdl_waiting
107+
ON thread_a.thread_id = mdl_waiting.OWNER_THREAD_ID
108+
AND mdl_waiting.LOCK_STATUS = 'PENDING'
109+
LEFT JOIN performance_schema.metadata_locks AS mdl_blocking
110+
ON mdl_waiting.OBJECT_TYPE = mdl_blocking.OBJECT_TYPE
111+
AND mdl_waiting.OBJECT_SCHEMA <=> mdl_blocking.OBJECT_SCHEMA
112+
AND mdl_waiting.OBJECT_NAME <=> mdl_blocking.OBJECT_NAME
113+
AND mdl_blocking.LOCK_STATUS = 'GRANTED'
114+
AND mdl_waiting.OWNER_THREAD_ID != mdl_blocking.OWNER_THREAD_ID
115+
LEFT JOIN performance_schema.threads AS mdl_blocking_thread
116+
ON mdl_blocking.OWNER_THREAD_ID = mdl_blocking_thread.thread_id
99117
"""
100118

101119
IDLE_BLOCKERS_SUBQUERY_MYSQL8 = """\
102120
OR
103-
-- Include idle sessions that are blocking others
121+
-- Include idle sessions that are blocking others via InnoDB data locks
104122
thread_a.thread_id IN (
105123
SELECT blocking_thread_id
106124
FROM performance_schema.data_lock_waits
107125
)
126+
OR
127+
-- Include idle sessions that are blocking others via metadata locks
128+
thread_a.thread_id IN (
129+
SELECT mdl_granted.OWNER_THREAD_ID
130+
FROM performance_schema.metadata_locks AS mdl_pending
131+
JOIN performance_schema.metadata_locks AS mdl_granted
132+
ON mdl_pending.OBJECT_TYPE = mdl_granted.OBJECT_TYPE
133+
AND mdl_pending.OBJECT_SCHEMA <=> mdl_granted.OBJECT_SCHEMA
134+
AND mdl_pending.OBJECT_NAME <=> mdl_granted.OBJECT_NAME
135+
AND mdl_granted.LOCK_STATUS = 'GRANTED'
136+
AND mdl_pending.LOCK_STATUS = 'PENDING'
137+
AND mdl_pending.OWNER_THREAD_ID != mdl_granted.OWNER_THREAD_ID
138+
)
108139
"""
109140

110141
BLOCKING_COLUMNS_MYSQL7 = """\

mysql/tests/test_query_activity.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,97 @@ def run_second_deadlock_query(conn, event1, event2):
667667
)
668668

669669

670+
@pytest.mark.skipif(
671+
MYSQL_FLAVOR == 'mariadb' or MYSQL_VERSION_PARSED < parse_version('8.0'),
672+
reason='MDL blocking detection requires MySQL 8.0+ with metadata_locks instrument enabled by default',
673+
)
674+
@pytest.mark.integration
675+
@pytest.mark.usefixtures('dd_environment')
676+
def test_mdl_blocking_activity(aggregator, dbm_instance, dd_run_check, root_conn):
677+
"""Verify that metadata lock contention appears in the activity payload with blocking relationships."""
678+
config = deepcopy(dbm_instance)
679+
config['query_activity']['collect_blocking_queries'] = True
680+
# Disable all metrics that could query information_schema or touch user table
681+
# metadata — those queries acquire SHARED_READ metadata locks which would get
682+
# queued behind the ALTER TABLE's pending EXCLUSIVE lock, deadlocking the check.
683+
config['options'] = {
684+
'extra_performance_metrics': False,
685+
'replication': False,
686+
'extra_status_metrics': False,
687+
'extra_innodb_metrics': False,
688+
'schema_size_metrics': False,
689+
'table_size_metrics': False,
690+
'system_table_size_metrics': False,
691+
'table_rows_stats_metrics': False,
692+
}
693+
config['index_metrics'] = {'enabled': False}
694+
config['only_custom_queries'] = True
695+
check = MySql(CHECK_NAME, {}, instances=[config])
696+
697+
MDL_TABLE = 'testdb.mdl_test_table'
698+
699+
with closing(root_conn.cursor()) as cur:
700+
# Ensure the consumer is enabled — other tests in this session may disable it.
701+
cur.execute("UPDATE performance_schema.setup_consumers SET enabled='YES' WHERE name = 'events_waits_current'")
702+
cur.execute('DROP TABLE IF EXISTS {}'.format(MDL_TABLE))
703+
cur.execute('CREATE TABLE {} (id INT PRIMARY KEY, val VARCHAR(50))'.format(MDL_TABLE))
704+
cur.execute('INSERT INTO {} VALUES (1, %s)'.format(MDL_TABLE), ('hello',))
705+
# PyMySQL defaults to autocommit=False, so the DDL/DML above opened an
706+
# implicit transaction that holds a SHARED_WRITE metadata lock on the table.
707+
# Commit to release it before we set up the intentional MDL contention.
708+
root_conn.commit()
709+
710+
root_password = 'mypass' if MYSQL_FLAVOR == 'percona' or MYSQL_REPLICATION in ('group', 'hybrid') else None
711+
holder_conn = pymysql.connect(host=HOST, port=PORT, user='root', password=root_password)
712+
ddl_conn = pymysql.connect(host=HOST, port=PORT, user='root', password=root_password)
713+
ddl_ready = Event()
714+
715+
def _hold_shared_mdl(conn):
716+
conn.begin()
717+
conn.cursor().execute('SELECT * FROM {}'.format(MDL_TABLE))
718+
719+
def _run_ddl(conn, ready_event):
720+
ready_event.set()
721+
try:
722+
conn.cursor().execute('ALTER TABLE {} ADD COLUMN new_col INT'.format(MDL_TABLE))
723+
except Exception:
724+
pass
725+
726+
executor = ThreadPoolExecutor(2)
727+
try:
728+
executor.submit(_hold_shared_mdl, holder_conn)
729+
time.sleep(0.2)
730+
executor.submit(_run_ddl, ddl_conn, ddl_ready)
731+
ddl_ready.wait(timeout=5)
732+
time.sleep(0.5)
733+
734+
dd_run_check(check)
735+
736+
dbm_activity = aggregator.get_event_platform_events("dbm-activity")
737+
assert dbm_activity, "should have collected at least one activity payload"
738+
739+
all_rows = [row for event in dbm_activity for row in event['mysql_activity']]
740+
mdl_blocked_rows = [r for r in all_rows if r.get('mdl_object_name') == 'mdl_test_table']
741+
assert mdl_blocked_rows, "should have at least one activity row with MDL blocking context; all rows: {}".format(
742+
[(r.get('sql_text', '?')[:60], r.get('mdl_object_name')) for r in all_rows]
743+
)
744+
745+
mdl_row = mdl_blocked_rows[0]
746+
assert mdl_row['mdl_object_type'] == 'TABLE'
747+
assert mdl_row['mdl_object_schema'] == 'testdb'
748+
assert mdl_row.get('blocking_thread_id'), "MDL blocked row should have a blocking_thread_id"
749+
finally:
750+
holder_conn.commit()
751+
holder_conn.close()
752+
# Wait for the ALTER TABLE to complete now that the MDL holder released.
753+
# Must shutdown before closing ddl_conn — closing a socket that another
754+
# thread is recv()ing on deadlocks on macOS.
755+
executor.shutdown(wait=True)
756+
ddl_conn.close()
757+
with closing(root_conn.cursor()) as cur:
758+
cur.execute('DROP TABLE IF EXISTS {}'.format(MDL_TABLE))
759+
760+
670761
def _get_conn_for_user(user, _autocommit=False):
671762
return pymysql.connect(host=HOST, port=PORT, user=user, password=user, autocommit=_autocommit)
672763

0 commit comments

Comments
 (0)