@@ -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+
670761def _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