76c01e44df
對齊 A v3.2.0。型別強制會把隱式轉換變成 TypeError,所以一次全檔加入 並跑完整測試(unit 451 / integration 398 全綠,無迴歸)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
332 lines
9.3 KiB
PHP
332 lines
9.3 KiB
PHP
<?php
|
|
/**
|
|
* Migration Engine for 7-state module lifecycle control.
|
|
*
|
|
* @package WP_Data_Optimizer
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Migration Engine — 7-state lifecycle controller.
|
|
*
|
|
* Manages the full migration lifecycle for any module:
|
|
* idle → dual_write → backfill → verify → cutover → cleanup → complete
|
|
*
|
|
* Provides:
|
|
* - State transitions with validation
|
|
* - Orchestration of backfill + verify + cutover + cleanup steps
|
|
* - Rollback to idle from any state
|
|
* - CLI and Admin integration points
|
|
*
|
|
* Each module has a corresponding TMDO_Migration_Base subclass that handles
|
|
* the actual data migration logic (batch processing, counting, verifying).
|
|
*/
|
|
class TMDO_Migration_Engine {
|
|
|
|
/**
|
|
* Valid state transitions.
|
|
* Key = current state, value = array of allowed next states.
|
|
* 'idle' is always allowed from any state (rollback).
|
|
*/
|
|
private const TRANSITIONS = array(
|
|
'idle' => array( 'dual_write' ),
|
|
'dual_write' => array( 'backfill', 'idle' ),
|
|
'backfill' => array( 'verify', 'dual_write', 'idle' ),
|
|
'verify' => array( 'cutover', 'dual_write', 'idle' ),
|
|
'cutover' => array( 'cleanup', 'idle' ),
|
|
'cleanup' => array( 'complete', 'idle' ),
|
|
'complete' => array( 'idle' ),
|
|
);
|
|
|
|
/**
|
|
* Registry of migration class instances, keyed by module name.
|
|
*
|
|
* @var array<string, TMDO_Migration_Base>
|
|
*/
|
|
private static array $migrations = array();
|
|
|
|
/**
|
|
* Register a migration class for a module.
|
|
*
|
|
* @param string $module Module identifier.
|
|
* @param TMDO_Migration_Base $migration Migration instance.
|
|
* @return void
|
|
*/
|
|
public static function register( string $module, TMDO_Migration_Base $migration ): void {
|
|
self::$migrations[ $module ] = $migration;
|
|
}
|
|
|
|
/**
|
|
* Get the migration instance for a module.
|
|
*
|
|
* @param string $module Module identifier.
|
|
* @return TMDO_Migration_Base|null Migration instance, or null if not found.
|
|
*/
|
|
public static function get_migration( string $module ): ?TMDO_Migration_Base {
|
|
return self::$migrations[ $module ] ?? null;
|
|
}
|
|
|
|
/**
|
|
* Get all registered migration instances.
|
|
*
|
|
* @return array<string, TMDO_Migration_Base>
|
|
*/
|
|
public static function all(): array {
|
|
return self::$migrations;
|
|
}
|
|
|
|
// ── State transitions ─────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Check if a state transition is valid.
|
|
*
|
|
* @param string $module Module identifier.
|
|
* @param string $target_state Target state to transition to.
|
|
* @return bool True if the transition is allowed.
|
|
*/
|
|
public static function can_transition( string $module, string $target_state ): bool {
|
|
$current = TMDO_Feature_Flags::get( $module );
|
|
|
|
// Rollback to idle is always allowed.
|
|
if ( 'idle' === $target_state ) {
|
|
return true;
|
|
}
|
|
|
|
$allowed = self::TRANSITIONS[ $current ] ?? array();
|
|
return in_array( $target_state, $allowed, true );
|
|
}
|
|
|
|
/**
|
|
* Transition a module to a new state.
|
|
*
|
|
* @param string $module Module identifier.
|
|
* @param string $target_state Target state to transition to.
|
|
* @return bool True on success, false if transition is invalid.
|
|
*/
|
|
public static function transition( string $module, string $target_state ): bool {
|
|
if ( ! self::can_transition( $module, $target_state ) ) {
|
|
return false;
|
|
}
|
|
|
|
return TMDO_Feature_Flags::set( $module, $target_state );
|
|
}
|
|
|
|
// ── High-level operations ─────────────────────────────────────────────
|
|
|
|
/**
|
|
* Start or resume migration for a module.
|
|
*
|
|
* Flow: idle → dual_write → backfill (run batches)
|
|
*
|
|
* @param string $module Module name.
|
|
* @param callable|null $progress_callback Called after each batch.
|
|
* @return array{status: string, message: string}
|
|
*/
|
|
public static function migrate( string $module, ?callable $progress_callback = null ): array {
|
|
$migration = self::get_migration( $module );
|
|
if ( ! $migration ) {
|
|
return array(
|
|
'status' => 'error',
|
|
'message' => "No migration registered for module: {$module}",
|
|
);
|
|
}
|
|
|
|
$current = TMDO_Feature_Flags::get( $module );
|
|
|
|
// Already in backfill — resume.
|
|
if ( 'backfill' === $current ) {
|
|
$completed = $migration->run( true, $progress_callback );
|
|
return array(
|
|
'status' => $completed ? 'verify' : 'backfill',
|
|
'message' => $completed ? 'Backfill complete. Ready to verify.' : 'Backfill timed out. Resume to continue.',
|
|
);
|
|
}
|
|
|
|
// Already past backfill.
|
|
if ( in_array( $current, array( 'verify', 'cutover', 'cleanup', 'complete' ), true ) ) {
|
|
return array(
|
|
'status' => $current,
|
|
'message' => "Module is already in state: {$current}.",
|
|
);
|
|
}
|
|
|
|
// Start fresh: idle or dual_write → backfill.
|
|
if ( ! self::transition( $module, 'dual_write' ) && 'dual_write' !== $current ) {
|
|
return array(
|
|
'status' => 'error',
|
|
'message' => "Cannot start migration from state: {$current}",
|
|
);
|
|
}
|
|
|
|
$completed = $migration->run( false, $progress_callback );
|
|
|
|
return array(
|
|
'status' => $completed ? 'verify' : 'backfill',
|
|
'message' => $completed ? 'Backfill complete. Ready to verify.' : 'Backfill timed out. Resume to continue.',
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Verify data consistency for a module.
|
|
*
|
|
* @param string $module Module identifier.
|
|
* @return array{status: string, message: string, verified: bool} Verification result.
|
|
*/
|
|
public static function verify( string $module ): array {
|
|
$migration = self::get_migration( $module );
|
|
if ( ! $migration ) {
|
|
return array(
|
|
'status' => 'error',
|
|
'message' => "No migration registered for module: {$module}",
|
|
'verified' => false,
|
|
);
|
|
}
|
|
|
|
$current = TMDO_Feature_Flags::get( $module );
|
|
if ( 'verify' !== $current ) {
|
|
return array(
|
|
'status' => 'error',
|
|
'message' => "Module must be in 'verify' state. Current: {$current}",
|
|
'verified' => false,
|
|
);
|
|
}
|
|
|
|
$ok = $migration->verify_counts();
|
|
|
|
if ( ! $ok ) {
|
|
// Verification failed — allow retry via dual_write → backfill.
|
|
self::transition( $module, 'dual_write' );
|
|
return array(
|
|
'status' => 'dual_write',
|
|
'message' => 'Verification failed. Rolled back to dual_write for retry.',
|
|
'verified' => false,
|
|
);
|
|
}
|
|
|
|
return array(
|
|
'status' => 'verify',
|
|
'message' => 'Verification passed. Ready for cutover.',
|
|
'verified' => true,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Cutover: switch reads to the custom table.
|
|
*
|
|
* @param string $module Module identifier.
|
|
* @return array{status: string, message: string} Operation result.
|
|
*/
|
|
public static function cutover( string $module ): array {
|
|
$current = TMDO_Feature_Flags::get( $module );
|
|
|
|
if ( 'verify' !== $current ) {
|
|
return array(
|
|
'status' => 'error',
|
|
'message' => "Module must be in 'verify' state. Current: {$current}",
|
|
);
|
|
}
|
|
|
|
self::transition( $module, 'cutover' );
|
|
|
|
return array(
|
|
'status' => 'cutover',
|
|
'message' => 'Cutover complete. Reads now come from the custom table. Run cleanup when ready.',
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Rollback: return to idle state from any state.
|
|
*
|
|
* @param string $module Module identifier.
|
|
* @return array{status: string, message: string} Operation result.
|
|
*/
|
|
public static function rollback( string $module ): array {
|
|
$current = TMDO_Feature_Flags::get( $module );
|
|
|
|
if ( 'idle' === $current ) {
|
|
return array(
|
|
'status' => 'idle',
|
|
'message' => 'Module is already idle.',
|
|
);
|
|
}
|
|
|
|
TMDO_Feature_Flags::reset( $module );
|
|
|
|
return array(
|
|
'status' => 'idle',
|
|
'message' => "Module rolled back from '{$current}' to idle.",
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Cleanup: stop writing to native postmeta.
|
|
*
|
|
* @param string $module Module identifier.
|
|
* @return array{status: string, message: string} Operation result.
|
|
*/
|
|
public static function cleanup( string $module ): array {
|
|
$current = TMDO_Feature_Flags::get( $module );
|
|
|
|
if ( 'cutover' !== $current ) {
|
|
return array(
|
|
'status' => 'error',
|
|
'message' => "Module must be in 'cutover' state. Current: {$current}",
|
|
);
|
|
}
|
|
|
|
self::transition( $module, 'cleanup' );
|
|
|
|
return array(
|
|
'status' => 'cleanup',
|
|
'message' => 'Cleanup started. Native postmeta writes are stopped. Run enable to complete.',
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Enable: mark migration fully complete.
|
|
*
|
|
* @param string $module Module identifier.
|
|
* @return array{status: string, message: string} Operation result.
|
|
*/
|
|
public static function enable( string $module ): array {
|
|
$current = TMDO_Feature_Flags::get( $module );
|
|
|
|
if ( 'cleanup' !== $current ) {
|
|
return array(
|
|
'status' => 'error',
|
|
'message' => "Module must be in 'cleanup' state. Current: {$current}",
|
|
);
|
|
}
|
|
|
|
self::transition( $module, 'complete' );
|
|
|
|
return array(
|
|
'status' => 'complete',
|
|
'message' => 'Module fully enabled. All reads and writes use the custom table.',
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get comprehensive status for a module.
|
|
*
|
|
* @param string $module Module identifier.
|
|
* @return array{module: string, state: string, zone: string, record: ?array} Module status.
|
|
*/
|
|
public static function status( string $module ): array {
|
|
$migration = self::get_migration( $module );
|
|
$record = $migration ? $migration->get_record() : null;
|
|
|
|
return array(
|
|
'module' => $module,
|
|
'state' => TMDO_Feature_Flags::get( $module ),
|
|
'zone' => $migration ? $migration->get_zone() : '',
|
|
'record' => $record,
|
|
);
|
|
}
|
|
}
|