Files
2meet-data-optimizer/includes/migration/class-tmdo-migration-engine.php
T
wpdev d36bb954d1 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
2026-07-31 05:06:36 +08:00

330 lines
9.3 KiB
PHP

<?php
/**
* Migration Engine for 7-state module lifecycle control.
*
* @package WP_Data_Optimizer
*/
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,
);
}
}