diff --git a/includes/engine/class-tmdo-schema-manager.php b/includes/engine/class-tmdo-schema-manager.php index 527c9da..710f06b 100644 --- a/includes/engine/class-tmdo-schema-manager.php +++ b/includes/engine/class-tmdo-schema-manager.php @@ -231,6 +231,11 @@ final class TMDO_Schema_Manager { // processes both "acquire" an option lock; GET_LOCK is atomic and is // released by MySQL if this process dies. $lock = self::index_lock_name(); + if ( method_exists( $wpdb, 'send_reads_to_masters' ) ) { + // HyperDB / LudicrousDB would send the SELECT GET_LOCK to a replica, + // where it locks nothing against the ALTER on the primary. + $wpdb->send_reads_to_masters(); + } if ( '1' !== (string) $wpdb->get_var( $wpdb->prepare( 'SELECT GET_LOCK(MD5(CONCAT(DATABASE(), %s)), 0)', $lock ) ) ) { return; // Another process is adding indexes. } diff --git a/tests/integration/GroupCompositeIndexTest.php b/tests/integration/GroupCompositeIndexTest.php index e8d6e3e..4f335da 100644 --- a/tests/integration/GroupCompositeIndexTest.php +++ b/tests/integration/GroupCompositeIndexTest.php @@ -134,6 +134,45 @@ class GroupCompositeIndexTest extends TestCase { $this->assertSame( '1', (string) $wpdb->get_var( $wpdb->prepare( 'SELECT IS_FREE_LOCK(MD5(CONCAT(DATABASE(), %s)))', TMDO_Schema_Manager::index_lock_name() ) ), 'lock released' ); } + /** + * Under read/write splitting (HyperDB, LudicrousDB) a SELECT may go to a + * replica, where GET_LOCK would lock nothing: reads go to the primary first. + */ + public function test_the_lock_is_taken_on_the_primary_under_read_write_splitting(): void { + global $wpdb; + $real = $wpdb; + $wpdb = new class( $real ) { + /** @var string[] */ + public array $calls = array(); + + public function __construct( private object $db ) {} + + public function send_reads_to_masters(): void { + $this->calls[] = 'send_reads_to_masters'; + } + + public function __call( string $name, array $args ) { + if ( 'get_var' === $name && str_contains( (string) $args[0], 'GET_LOCK' ) ) { + $this->calls[] = 'GET_LOCK'; + } + return $this->db->$name( ...$args ); + } + + public function __get( string $name ) { + return $this->db->$name; + } + }; + try { + TMDO_Schema_Manager::ensure_group_indexes( 'post', 'ci_core' ); + $calls = $wpdb->calls; + } finally { + $wpdb = $real; + } + + $this->assertSame( array( 'send_reads_to_masters', 'GET_LOCK' ), $calls ); + $this->assertArrayHasKey( 'cidx_flag_price', $this->indexes() ); + } + /** @return array index name → ordered columns */ private function indexes(): array { global $wpdb;