Repository: adamchainz/django-mysql

Issue:
Use metadata_lock_info data in Lock class
The MariaDB plugin [metadata_lock_info](https://mariadb.com/kb/en/mariadb/information-schema-metadata_lock_info-table/) allows you to query all the held locks. This could be useful as a "Who holds what locks that like..." method, e.g.:

``` python
held = Lock.held_with_prefix('API.')  # A dict of lock name to connection (thread) IDs
connection_ids = held.values()
# Kill all those connection ids...
```



Discussion:

Resolving patch:
diff --git a/django_mysql/locks.py b/django_mysql/locks.py
--- a/django_mysql/locks.py
+++ b/django_mysql/locks.py
@@ -16,11 +16,21 @@ def __init__(self, name, acquire_timeout=10.0, using=None):
 
         # For multi-database servers, we prefix the name of the lock wth
         # the database, to protect against concurrent apps with the same locks
-        self.name = '.'.join((
-            connections[self.db].settings_dict['NAME'],
+        self.name = self.make_name(self.db, name)
+
+    @classmethod
+    def make_name(cls, db, name):
+        return '.'.join((
+            connections[db].settings_dict['NAME'],
             name
         ))
 
+    @classmethod
+    def unmake_name(cls, db, name):
+        # Cut off the 'dbname.' prefix
+        db_name = connections[db].settings_dict['NAME']
+        return name[len(db_name) + 1:]
+
     def get_cursor(self):
         return connections[self.db].cursor()
 
@@ -54,3 +64,22 @@ def holding_connection_id(self):
         with self.get_cursor() as cursor:
             cursor.execute("SELECT IS_USED_LOCK(%s)", (self.name,))
             return cursor.fetchone()[0]
+
+    @classmethod
+    def held_with_prefix(cls, prefix, using=DEFAULT_DB_ALIAS):
+        # Use the METADATA_LOCK_INFO table from the MariaDB plugin to show
+        # which locks of a given prefix are held
+        prefix = cls.make_name(using, prefix)
+
+        with connections[using].cursor() as cursor:
+            cursor.execute(
+                """SELECT TABLE_SCHEMA, THREAD_ID
+                   FROM INFORMATION_SCHEMA.METADATA_LOCK_INFO
+                   WHERE TABLE_SCHEMA LIKE %s AND
+                         LOCK_TYPE = 'User Lock'""",
+                (prefix + '%',)
+            )
+            return {
+                cls.unmake_name(using, row[0]): row[1]
+                for row in cursor.fetchall()
+            }


Test patch:
diff --git a/tests/django_mysql_tests/test_locks.py b/tests/django_mysql_tests/test_locks.py
--- a/tests/django_mysql_tests/test_locks.py
+++ b/tests/django_mysql_tests/test_locks.py
@@ -11,6 +11,29 @@
 
 class LockTests(TestCase):
 
+    @classmethod
+    def setUpClass(cls):
+        super(LockTests, cls).setUpClass()
+
+        cls.supports_lock_info = (
+            connection.is_mariadb and connection.mysql_version >= (10, 0, 7)
+        )
+        if cls.supports_lock_info:
+            with connection.cursor() as cursor:
+                cursor.execute(
+                    """SELECT COUNT(*) FROM INFORMATION_SCHEMA.PLUGINS
+                       WHERE PLUGIN_NAME = 'metadata_lock_info'""")
+                cls.lock_info_preinstalled = (cursor.fetchone()[0] > 0)
+                if not cls.lock_info_preinstalled:
+                    cursor.execute("INSTALL SONAME 'metadata_lock_info'")
+
+    @classmethod
+    def tearDownClass(cls):
+        if cls.supports_lock_info and not cls.lock_info_preinstalled:
+            with connection.cursor() as cursor:
+                cursor.execute("UNINSTALL SONAME 'metadata_lock_info'")
+        super(LockTests, cls).tearDownClass()
+
     def test_simple(self):
         mylock = Lock("mylock")
         self.assertFalse(mylock.is_held())
@@ -156,3 +179,30 @@ def test_multi_connection(self):
             # Different connections = can hold > 1!
             self.assertTrue(lock_a.is_held())
             self.assertTrue(lock_b.is_held())
+
+    def test_held_with_prefix(self):
+        if not self.supports_lock_info:
+            self.skipTest(
+                "Only MariaDB 10.0.7+ has the metadata_lock_info plugin on "
+                "which held_with_prefix relies"
+            )
+
+        self.assertEqual(Lock.held_with_prefix(''), {})
+        self.assertEqual(Lock.held_with_prefix('mylock'), {})
+
+        with Lock('mylock-alpha') as lock:
+            self.assertEqual(
+                Lock.held_with_prefix(''),
+                {'mylock-alpha': lock.holding_connection_id()}
+            )
+            self.assertEqual(
+                Lock.held_with_prefix('mylock'),
+                {'mylock-alpha': lock.holding_connection_id()}
+            )
+            self.assertEqual(
+                Lock.held_with_prefix('mylock-beta'),
+                {}
+            )
+
+        self.assertEqual(Lock.held_with_prefix(''), {})
+        self.assertEqual(Lock.held_with_prefix('mylock'), {})