chore: initial snapshot of 2meet-data-optimizer v0.1.0
Baseline before backporting wp-data-optimizer v3.0.1-v3.4.6. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
This commit is contained in:
@@ -0,0 +1,329 @@
|
||||
<?php
|
||||
/**
|
||||
* TMDO_Entity_Health — Entity Bridge 健康狀態聚合器
|
||||
*
|
||||
* 收集每個 entity type (user/term/comment) 的即時健康快照:
|
||||
* - 模式與已停留天數
|
||||
* - 每個群組的覆蓋率(flat table rows vs EAV rows)
|
||||
* - Shadow diff 計數
|
||||
* - 遷移進度(斷點位置、已搬移筆數)
|
||||
* - Auto-promote 資格評估
|
||||
* - 操作建議
|
||||
*
|
||||
* @package WP_Data_Optimizer
|
||||
* @since 2.6.6
|
||||
*/
|
||||
|
||||
// phpcs:disable Squiz.Commenting,Generic.Commenting,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,Generic.CodeAnalysis.UnusedFunctionParameter,Generic.CodeAnalysis.EmptyStatement,Squiz.PHP.DisallowMultipleAssignments,Squiz.PHP.DisallowSizeFunctionsInLoops,WordPress.WP.I18n.MissingTranslatorsComment,WordPress.PHP.NoSilencedErrors,Universal.NamingConventions.NoReservedKeywordParameterNames,WordPress.PHP.YodaConditions -- inherits engine coding standard.
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
final class TMDO_Entity_Health {
|
||||
|
||||
/** Entity types managed by this class (post is handled by Feature_Flags FSM). */
|
||||
public const MANAGED_TYPES = array( 'user', 'term', 'comment' );
|
||||
|
||||
/**
|
||||
* 取得全部 entity 的健康快照(批次,供 REST polling 用)。
|
||||
*
|
||||
* @return array<string, array>
|
||||
*/
|
||||
public static function get_all(): array {
|
||||
$result = array();
|
||||
foreach ( self::MANAGED_TYPES as $type ) {
|
||||
$result[ $type ] = self::get_one( $type );
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 取得單一 entity 的健康快照。
|
||||
*/
|
||||
public static function get_one( string $entity_type ): array {
|
||||
$mode = class_exists( 'TMDO_Mode_Manager' ) ? TMDO_Mode_Manager::get( $entity_type ) : 'disabled';
|
||||
$entered_all = get_option( TMDO_Mode_Manager::OPT_ENTERED_AT, array() );
|
||||
$entered_at = isset( $entered_all[ $entity_type ] ) ? (int) $entered_all[ $entity_type ] : 0;
|
||||
$mode_days = $entered_at > 0 ? (int) floor( ( time() - $entered_at ) / DAY_IN_SECONDS ) : 0;
|
||||
|
||||
$groups = self::get_groups_health( $entity_type );
|
||||
|
||||
$shadow_diffs = 0;
|
||||
if ( class_exists( 'TMDO_Shadow_Diff_Logger' ) ) {
|
||||
$diff_counts = TMDO_Shadow_Diff_Logger::count_by_entity();
|
||||
$shadow_diffs = (int) ( $diff_counts[ $entity_type ] ?? 0 );
|
||||
}
|
||||
|
||||
$auto_promote = self::get_auto_promote_status( $entity_type, $mode, $entered_at, $shadow_diffs );
|
||||
$backfill_active = self::is_backfill_active( $entity_type );
|
||||
$native_counts = self::get_native_counts( $entity_type );
|
||||
|
||||
return array(
|
||||
'entity_type' => $entity_type,
|
||||
'mode' => $mode,
|
||||
'entered_at' => $entered_at,
|
||||
'mode_days' => $mode_days,
|
||||
'groups' => $groups,
|
||||
'shadow_diffs' => $shadow_diffs,
|
||||
'auto_promote' => $auto_promote,
|
||||
'backfill_active' => $backfill_active,
|
||||
'native_entity_count' => $native_counts['entity_count'],
|
||||
'native_meta_count' => $native_counts['meta_count'],
|
||||
'native_entity_table' => $native_counts['entity_table'],
|
||||
'native_meta_table' => $native_counts['meta_table'],
|
||||
'recommendation' => self::get_recommendation( $entity_type, $mode, $groups, $shadow_diffs ),
|
||||
'next_mode' => self::get_next_mode( $mode ),
|
||||
'prev_mode' => self::get_prev_mode( $mode ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取得 native EAV 表的總筆數(entity 本體 + meta)。
|
||||
*/
|
||||
private static function get_native_counts( string $entity_type ): array {
|
||||
global $wpdb;
|
||||
|
||||
$adapter = class_exists( 'TMDO_Entity_Registry' ) ? TMDO_Entity_Registry::get_adapter( $entity_type ) : null;
|
||||
|
||||
if ( ! $adapter ) {
|
||||
return array(
|
||||
'entity_table' => '',
|
||||
'meta_table' => '',
|
||||
'entity_count' => 0,
|
||||
'meta_count' => 0,
|
||||
);
|
||||
}
|
||||
|
||||
$meta_table = $adapter->get_native_meta_table();
|
||||
$entity_table = self::get_native_entity_table( $entity_type );
|
||||
|
||||
$entity_count = $entity_table ? (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$entity_table}`" ) : 0;
|
||||
$meta_count = $meta_table ? (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$meta_table}`" ) : 0;
|
||||
|
||||
return array(
|
||||
'entity_table' => $entity_table,
|
||||
'meta_table' => $meta_table,
|
||||
'entity_count' => $entity_count,
|
||||
'meta_count' => $meta_count,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取得 entity 本體資料表名稱(非 meta 表)。
|
||||
*/
|
||||
private static function get_native_entity_table( string $entity_type ): string {
|
||||
global $wpdb;
|
||||
$map = array(
|
||||
'user' => $wpdb->users,
|
||||
'term' => $wpdb->terms,
|
||||
'comment' => $wpdb->comments,
|
||||
'post' => $wpdb->posts,
|
||||
);
|
||||
return $map[ $entity_type ] ?? '';
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────
|
||||
// 群組覆蓋率
|
||||
// ─────────────────────────────────────────────────────────
|
||||
|
||||
private static function get_groups_health( string $entity_type ): array {
|
||||
if ( ! class_exists( 'TMDO_Entity_Registry' ) || ! class_exists( 'TMDO_Schema_Manager' ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
|
||||
$groups = TMDO_Entity_Registry::get_groups_for_type( $entity_type );
|
||||
$adapter = TMDO_Entity_Registry::get_adapter( $entity_type );
|
||||
$result = array();
|
||||
|
||||
foreach ( $groups as $group_name ) {
|
||||
$fields = TMDO_Entity_Registry::get_group_fields( $entity_type, $group_name );
|
||||
$table = TMDO_Schema_Manager::get_table_name( $entity_type, $group_name );
|
||||
$table_exists = TMDO_Schema_Manager::table_exists( $table );
|
||||
|
||||
$flat_rows = 0;
|
||||
$eav_rows = 0;
|
||||
|
||||
if ( $table_exists && $adapter ) {
|
||||
$flat_rows = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$table}`" );
|
||||
|
||||
$managed_keys = array_column( $fields, 'key' );
|
||||
if ( ! empty( $managed_keys ) ) {
|
||||
$meta_table = $adapter->get_native_meta_table();
|
||||
$id_col = $adapter->get_entity_id_column();
|
||||
$phs = implode( ',', array_fill( 0, count( $managed_keys ), '%s' ) );
|
||||
|
||||
$eav_rows = (int) $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(DISTINCT `{$id_col}`) FROM `{$meta_table}` WHERE `meta_key` IN ({$phs})",
|
||||
...$managed_keys
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$coverage_pct = 0.0;
|
||||
if ( $eav_rows > 0 ) {
|
||||
$coverage_pct = round( min( $flat_rows / $eav_rows, 1.0 ) * 100, 1 );
|
||||
} elseif ( $flat_rows > 0 ) {
|
||||
$coverage_pct = 100.0;
|
||||
}
|
||||
|
||||
$migration_info = self::get_migration_status( $entity_type, $group_name );
|
||||
|
||||
$result[] = array(
|
||||
'name' => $group_name,
|
||||
'fields_count' => count( $fields ),
|
||||
'table' => $table,
|
||||
'table_exists' => $table_exists,
|
||||
'flat_rows' => $flat_rows,
|
||||
'eav_rows' => $eav_rows,
|
||||
'coverage_pct' => $coverage_pct,
|
||||
'migration_status' => $migration_info['status'],
|
||||
'last_id' => $migration_info['last_id'],
|
||||
'total_migrated' => $migration_info['total_migrated'],
|
||||
'started_at' => $migration_info['started_at'],
|
||||
'completed_at' => $migration_info['completed_at'],
|
||||
);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private static function get_migration_status( string $entity_type, string $group_name ): array {
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . TMDO_TABLE_PREFIX . 'migration_status';
|
||||
|
||||
$row = $wpdb->get_row(
|
||||
$wpdb->prepare(
|
||||
"SELECT status, last_id, total_migrated, started_at, completed_at FROM `{$table}` WHERE entity_type = %s AND group_name = %s",
|
||||
$entity_type,
|
||||
$group_name
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
return array(
|
||||
'status' => $row['status'] ?? 'not_started',
|
||||
'last_id' => (int) ( $row['last_id'] ?? 0 ),
|
||||
'total_migrated' => (int) ( $row['total_migrated'] ?? 0 ),
|
||||
'started_at' => $row['started_at'] ?? null,
|
||||
'completed_at' => $row['completed_at'] ?? null,
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────
|
||||
// Auto-promote 資格評估
|
||||
// ─────────────────────────────────────────────────────────
|
||||
|
||||
private static function get_auto_promote_status(
|
||||
string $entity_type,
|
||||
string $mode,
|
||||
int $entered_at,
|
||||
int $shadow_diffs
|
||||
): array {
|
||||
$enabled = (bool) get_option( TMDO_Auto_Promoter::OPT_ENABLED, false );
|
||||
$min_days = (int) get_option( TMDO_Auto_Promoter::OPT_MIN_DAYS, TMDO_Auto_Promoter::DEFAULT_MIN_DAYS );
|
||||
$days_elapsed = $entered_at > 0 ? (int) floor( ( time() - $entered_at ) / DAY_IN_SECONDS ) : 0;
|
||||
|
||||
if ( $mode !== TMDO_Mode_Manager::MODE_SHADOW_READ ) {
|
||||
return array(
|
||||
'enabled' => $enabled,
|
||||
'min_days' => $min_days,
|
||||
'eligible' => false,
|
||||
'reason' => 'not_shadow_read',
|
||||
);
|
||||
}
|
||||
|
||||
if ( $days_elapsed < $min_days ) {
|
||||
return array(
|
||||
'enabled' => $enabled,
|
||||
'min_days' => $min_days,
|
||||
'days_elapsed' => $days_elapsed,
|
||||
'eligible' => false,
|
||||
'reason' => "need_{$min_days}_days",
|
||||
);
|
||||
}
|
||||
|
||||
if ( $shadow_diffs > 0 ) {
|
||||
return array(
|
||||
'enabled' => $enabled,
|
||||
'min_days' => $min_days,
|
||||
'days_elapsed' => $days_elapsed,
|
||||
'eligible' => false,
|
||||
'reason' => "has_{$shadow_diffs}_diffs",
|
||||
);
|
||||
}
|
||||
|
||||
return array(
|
||||
'enabled' => $enabled,
|
||||
'min_days' => $min_days,
|
||||
'days_elapsed' => $days_elapsed,
|
||||
'eligible' => true,
|
||||
'reason' => 'ready',
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────
|
||||
// 建議與模式轉換
|
||||
// ─────────────────────────────────────────────────────────
|
||||
|
||||
private static function get_recommendation(
|
||||
string $entity_type,
|
||||
string $mode,
|
||||
array $groups,
|
||||
int $shadow_diffs
|
||||
): string {
|
||||
switch ( $mode ) {
|
||||
case TMDO_Mode_Manager::MODE_DISABLED:
|
||||
return '啟用 Hook Bus 並切換到 dual_write,讓系統開始對 flat table 雙寫。';
|
||||
|
||||
case TMDO_Mode_Manager::MODE_DUAL_WRITE:
|
||||
foreach ( $groups as $g ) {
|
||||
if ( $g['coverage_pct'] < 99.0 ) {
|
||||
return '執行 Backfill 遷移歷史資料,待所有群組覆蓋率達到 100% 後再升級到 shadow_read。';
|
||||
}
|
||||
}
|
||||
return '所有群組覆蓋率已達 100%,可以升級到 shadow_read 進行驗證期觀察。';
|
||||
|
||||
case TMDO_Mode_Manager::MODE_SHADOW_READ:
|
||||
if ( $shadow_diffs > 0 ) {
|
||||
return "發現 {$shadow_diffs} 筆 shadow diff,請先調查差異原因(Settings → Shadow Diffs)後再考慮升級。";
|
||||
}
|
||||
$min_days = (int) get_option( TMDO_Auto_Promoter::OPT_MIN_DAYS, TMDO_Auto_Promoter::DEFAULT_MIN_DAYS );
|
||||
$entered_all = get_option( TMDO_Mode_Manager::OPT_ENTERED_AT, array() );
|
||||
$entered_at = isset( $entered_all[ $entity_type ] ) ? (int) $entered_all[ $entity_type ] : 0;
|
||||
$days = $entered_at > 0 ? (int) floor( ( time() - $entered_at ) / DAY_IN_SECONDS ) : 0;
|
||||
if ( $days < $min_days ) {
|
||||
return "繼續觀察中(已 {$days}/{$min_days} 天)。無 diff 且穩定後可升級到 aeav_only。";
|
||||
}
|
||||
return '已達穩定期,可以升級到 aeav_only(最終生產模式)。';
|
||||
|
||||
case TMDO_Mode_Manager::MODE_AEAV_ONLY:
|
||||
return '遷移完成。所有讀寫均走 flat table,效能最佳。';
|
||||
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
private static function get_next_mode( string $mode ): ?string {
|
||||
$idx = array_search( $mode, TMDO_Mode_Manager::ALL_MODES, true );
|
||||
if ( $idx === false || $idx >= count( TMDO_Mode_Manager::ALL_MODES ) - 1 ) {
|
||||
return null;
|
||||
}
|
||||
return TMDO_Mode_Manager::ALL_MODES[ $idx + 1 ];
|
||||
}
|
||||
|
||||
private static function get_prev_mode( string $mode ): ?string {
|
||||
$idx = array_search( $mode, TMDO_Mode_Manager::ALL_MODES, true );
|
||||
if ( $idx === false || $idx <= 0 ) {
|
||||
return null;
|
||||
}
|
||||
return TMDO_Mode_Manager::ALL_MODES[ $idx - 1 ];
|
||||
}
|
||||
|
||||
private static function is_backfill_active( string $entity_type ): bool {
|
||||
return (bool) wp_next_scheduled( 'wpdo_entity_backfill_batch', array( $entity_type ) );
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user