$schedule ), true ); self::schedule( true ); return $schedule; } public static function schedule( $again = false ) { $choice = self::schedule_choice(); $next = wp_next_scheduled( self::HOOK ); if ( $next && ! $again && 'off' !== $choice ) { return; } wp_clear_scheduled_hook( self::HOOK ); if ( 'off' === $choice ) { return; } $schedules = wp_get_schedules(); $recur = isset( $schedules[ $choice ] ) ? $choice : 'weekly'; $hour = class_exists( 'AIEO_Data_Retention' ) ? ( AIEO_Data_Retention::settings()['hour'] + 1 ) % 24 : 5; $at = strtotime( gmdate( 'Y-m-d' ) . sprintf( ' %02d:30:00 UTC', $hour ) ); if ( $at <= time() ) { $at += DAY_IN_SECONDS; } wp_schedule_event( $at, $recur, self::HOOK ); } public static function unavailable() { global $wpdb; $fpt = strtoupper( (string) $wpdb->get_var( 'SELECT @@innodb_file_per_table' ) ); if ( '1' !== $fpt && 'ON' !== $fpt ) { return __( 'This database keeps its tables in one shared file, so rebuilding a table gives no space back to the disk.', 'ai-eshop-optimizer' ); } return ''; } public static function candidates() { global $wpdb; if ( '' !== self::unavailable() ) { return array(); } $rows = $wpdb->get_results( $wpdb->prepare( "SELECT TABLE_NAME AS t, TABLE_ROWS AS r, DATA_LENGTH AS d, INDEX_LENGTH AS i, DATA_FREE AS f, ENGINE AS e FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_TYPE = 'BASE TABLE' AND TABLE_NAME LIKE %s", $wpdb->esc_like( $wpdb->prefix . 'aieo_' ) . '%' ), ARRAY_A ); return self::pick( (array) $rows, $wpdb->prefix ); } public static function pick( array $rows, $prefix ) { $skip = self::SKIP; if ( class_exists( 'AIEO_Analytics_Collation' ) ) { $skip = array_merge( $skip, AIEO_Analytics_Collation::built_tables() ); } $out = array(); foreach ( $rows as $row ) { $table = (string) ( $row['t'] ?? '' ); $short = substr( $table, strlen( $prefix ) ); if ( 0 !== strpos( $table, $prefix . 'aieo_' ) || in_array( $short, $skip, true ) || 'InnoDB' !== (string) ( $row['e'] ?? '' ) || ! preg_match( '/^[A-Za-z0-9_]+$/', $table ) ) { continue; } $size = (float) ( $row['d'] ?? 0 ) + (float) ( $row['i'] ?? 0 ); $free = (float) ( $row['f'] ?? 0 ); if ( $free < self::MIN_FREE_MB * 1048576 || $free < self::MIN_FREE_SHARE * ( $size + $free ) ) { continue; } $out[] = array( 'table' => $table, 'rows' => (int) ( $row['r'] ?? 0 ), 'data_mb' => round( (float) ( $row['d'] ?? 0 ) / 1048576, 1 ), 'index_mb' => round( (float) ( $row['i'] ?? 0 ) / 1048576, 1 ), 'free_mb' => round( $free / 1048576, 1 ), ); } usort( $out, static function ( $a, $b ) { return $b['free_mb'] <=> $a['free_mb']; } ); return $out; } public static function step( array $tried = array() ) { $tried = array_flip( array_map( 'strval', $tried ) ); $list = array_values( array_filter( self::candidates(), static function ( $c ) use ( $tried ) { return ! isset( $tried[ $c['table'] ] ); } ) ); if ( ! $list ) { return array( 'table' => '', 'freed_mb' => 0.0, 'error' => '', 'left' => 0 ); } $done = self::rebuild( $list[0] ); $done['left'] = count( $list ) - 1; self::remember( $done ); return $done; } public static function run( $budget = 600.0 ) { $t0 = microtime( true ); $tried = array(); while ( microtime( true ) - $t0 < $budget ) { $list = array_values( array_filter( self::candidates(), static function ( $c ) use ( $tried ) { return ! isset( $tried[ $c['table'] ] ); } ) ); if ( ! $list ) { break; } $done = self::rebuild( $list[0] ); $tried[ $done['table'] ] = true; self::remember( $done ); } } private static function rebuild( array $c ) { global $wpdb; $table = (string) $c['table']; $need = ( (float) $c['data_mb'] + (float) $c['index_mb'] ) * 1048576 * 1.2; $room = self::disk_room(); if ( null !== $room && $room < $need ) { return array( 'table' => $table, 'freed_mb' => 0.0, 'error' => __( 'not enough free disk for the copy', 'ai-eshop-optimizer' ) ); } $wpdb->query( $wpdb->prepare( 'SET SESSION lock_wait_timeout = %d', self::LOCK_WAIT ) ); $ok = $wpdb->query( "ALTER TABLE `{$table}` ENGINE=InnoDB, ALGORITHM=INPLACE, LOCK=NONE" ); $err = false === $ok ? (string) $wpdb->last_error : ''; $after = (float) $wpdb->get_var( $wpdb->prepare( 'SELECT DATA_FREE FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s', $table ) ); return array( 'table' => $table, 'freed_mb' => '' === $err ? max( 0.0, round( (float) $c['free_mb'] - $after / 1048576, 1 ) ) : 0.0, 'error' => $err, ); } private static function disk_room() { global $wpdb; $dir = (string) $wpdb->get_var( 'SELECT @@datadir' ); if ( '' === $dir || ! function_exists( 'disk_free_space' ) || ! @is_dir( $dir ) ) { return null; } $free = @disk_free_space( $dir ); return false === $free ? null : (float) $free; } private static function remember( array $done ) { $state = (array) get_option( self::STATE, array() ); $list = isset( $state['tables'] ) && is_array( $state['tables'] ) ? $state['tables'] : array(); array_unshift( $list, array( 'table' => $done['table'], 'freed_mb' => $done['freed_mb'], 'error' => $done['error'], 'at' => gmdate( 'Y-m-d H:i:s' ) ) ); update_option( self::STATE, array( 'at' => gmdate( 'Y-m-d H:i:s' ), 'tables' => array_slice( $list, 0, 20 ) ), false ); } }