fix(2meet-data-optimizer): take the index lock on the primary under read/write splitting
- when $wpdb has send_reads_to_masters() (HyperDB, LudicrousDB), it is called before SELECT GET_LOCK, which a replica would take without locking out the ALTER on the primary - Verify: GroupCompositeIndexTest (call order through a wpdb proxy), integration 583, unit 701, phpcs, stan - PR: PR-108
This commit is contained in:
@@ -231,6 +231,11 @@ final class TMDO_Schema_Manager {
|
|||||||
// processes both "acquire" an option lock; GET_LOCK is atomic and is
|
// processes both "acquire" an option lock; GET_LOCK is atomic and is
|
||||||
// released by MySQL if this process dies.
|
// released by MySQL if this process dies.
|
||||||
$lock = self::index_lock_name();
|
$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 ) ) ) {
|
if ( '1' !== (string) $wpdb->get_var( $wpdb->prepare( 'SELECT GET_LOCK(MD5(CONCAT(DATABASE(), %s)), 0)', $lock ) ) ) {
|
||||||
return; // Another process is adding indexes.
|
return; // Another process is adding indexes.
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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' );
|
$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<string, string[]> index name → ordered columns */
|
/** @return array<string, string[]> index name → ordered columns */
|
||||||
private function indexes(): array {
|
private function indexes(): array {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
|
|||||||
Reference in New Issue
Block a user