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
349 lines
8.8 KiB
PHP
349 lines
8.8 KiB
PHP
<?php
|
|
/**
|
|
* Feature flags manager for WPDO module lifecycle states.
|
|
*
|
|
* @package WP_Data_Optimizer
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Feature flags for each WPDO module.
|
|
*
|
|
* Option key: wpdo_features (serialized array)
|
|
*
|
|
* 7-state lifecycle per module (extends HPCT's 4-state model):
|
|
* idle → dual_write → backfill → verify → cutover → cleanup → complete
|
|
*
|
|
* Any state can roll back to idle.
|
|
*/
|
|
class TMDO_Feature_Flags {
|
|
|
|
/** Option name storing all module states. */
|
|
private const OPTION_KEY = 'wpdo_features';
|
|
|
|
/**
|
|
* All known HPCT-inherited modules.
|
|
* These modules operate on existing hpct_* tables.
|
|
*/
|
|
public const HPCT_MODULES = array(
|
|
'reviews',
|
|
'messages',
|
|
'favorites',
|
|
'memberships',
|
|
'statistics',
|
|
'requests',
|
|
'listing_meta',
|
|
'wc_orders',
|
|
'latepoint',
|
|
);
|
|
|
|
/**
|
|
* Zone-based modules (new in WPDO).
|
|
* These modules operate on wpdo_* tables.
|
|
*/
|
|
public const ZONE_MODULES = array(
|
|
'hot_hp_listing',
|
|
'hot_hp_vendor',
|
|
'warm',
|
|
'cold_hp_listing',
|
|
'cold_hp_vendor',
|
|
'archive',
|
|
);
|
|
|
|
/** Valid lifecycle states. */
|
|
public const VALID_STATES = array(
|
|
'idle',
|
|
'dual_write',
|
|
'backfill',
|
|
'verify',
|
|
'cutover',
|
|
'cleanup',
|
|
'complete',
|
|
);
|
|
|
|
/**
|
|
* States in which interceptors should capture writes (dual-write active).
|
|
*/
|
|
public const WRITE_ACTIVE_STATES = array(
|
|
'dual_write',
|
|
'backfill',
|
|
'verify',
|
|
'cutover',
|
|
);
|
|
|
|
/**
|
|
* States in which reads come from the custom/zone table.
|
|
*/
|
|
public const READ_CUSTOM_STATES = array(
|
|
'cutover',
|
|
'cleanup',
|
|
'complete',
|
|
);
|
|
|
|
/**
|
|
* States in which query interceptors are active.
|
|
*/
|
|
public const QUERY_ACTIVE_STATES = array(
|
|
'cutover',
|
|
'cleanup',
|
|
'complete',
|
|
);
|
|
|
|
/**
|
|
* Get the state of a single module.
|
|
*
|
|
* @param string $module Module name.
|
|
* @return string One of VALID_STATES, defaults to 'idle'.
|
|
*/
|
|
public static function get( string $module ): string {
|
|
$flags = self::all();
|
|
return $flags[ $module ] ?? 'idle';
|
|
}
|
|
|
|
/**
|
|
* Set the state of a module.
|
|
*
|
|
* V2.2.0 M2: gated by TMDO_FSM_Guard — invalid transitions are blocked
|
|
* (returns false), destructive transitions automatically snapshot first.
|
|
* Filter `wpdo/fsm_guard/bypass` allows CLI/tests to override.
|
|
*
|
|
* @param string $module Module name.
|
|
* @param string $state One of VALID_STATES.
|
|
* @return bool|WP_Error true on success, WP_Error on guard rejection,
|
|
* false on invalid state name.
|
|
*/
|
|
public static function set( string $module, string $state ) {
|
|
if ( ! in_array( $state, self::VALID_STATES, true ) ) {
|
|
return false;
|
|
}
|
|
|
|
// FSM Guard: validate transition + auto-snapshot if destructive.
|
|
// Defensive: only invoke when class is loaded AND not explicitly
|
|
// disabled (raw-PHP unit harness sets TMDO_FSM_GUARD_DISABLED).
|
|
// Dedicated FSMGuardTest unsets the constant before its own assertions.
|
|
$guard_active = class_exists( 'TMDO_FSM_Guard' )
|
|
&& ! ( defined( 'TMDO_FSM_GUARD_DISABLED' ) && TMDO_FSM_GUARD_DISABLED );
|
|
if ( $guard_active ) {
|
|
$check = TMDO_FSM_Guard::before_transition( $module, $state );
|
|
if ( is_wp_error( $check ) ) {
|
|
return $check;
|
|
}
|
|
}
|
|
|
|
$flags = self::all();
|
|
$flags[ $module ] = $state;
|
|
update_option( self::OPTION_KEY, $flags );
|
|
self::$cache = null; // Invalidate request-level cache.
|
|
|
|
// Record entry timestamp (M2) for downstream verify-gate / wash-period checks.
|
|
if ( $guard_active ) {
|
|
TMDO_FSM_Guard::record_entry( $module, $state );
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// ── shadow_read_only sub-flag (PR-4) ────────────────────────────────────
|
|
// Orthogonal to the main 7-state FSM. When enabled for a module that is in
|
|
// the `verify` state, every read fans out to BOTH postmeta and the zone
|
|
// table; divergence is logged to wp_wpdo_shadow_diffs without affecting
|
|
// the served value. Lets ops bake confidence before cutover.
|
|
|
|
/** Option key storing per-module shadow_read_only flags. */
|
|
private const SHADOW_OPTION_KEY = 'wpdo_features_shadow';
|
|
|
|
/**
|
|
* Request-level cache for shadow flags.
|
|
*
|
|
* @var array<string,bool>|null
|
|
*/
|
|
private static ?array $shadow_cache = null;
|
|
|
|
/**
|
|
* Enable shadow_read_only for a module (only meaningful in the verify state).
|
|
*
|
|
* @param string $module Module identifier.
|
|
* @return bool True on success.
|
|
*/
|
|
public static function enable_shadow_read( string $module ): bool {
|
|
$flags = self::all_shadow();
|
|
$flags[ $module ] = true;
|
|
update_option( self::SHADOW_OPTION_KEY, $flags, false );
|
|
self::$shadow_cache = null;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Disable shadow_read_only for a module.
|
|
*
|
|
* @param string $module Module identifier.
|
|
* @return bool
|
|
*/
|
|
public static function disable_shadow_read( string $module ): bool {
|
|
$flags = self::all_shadow();
|
|
unset( $flags[ $module ] );
|
|
update_option( self::SHADOW_OPTION_KEY, $flags, false );
|
|
self::$shadow_cache = null;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Whether shadow_read_only is currently active for a module.
|
|
*
|
|
* Returns true only when:
|
|
* - Module is in the `verify` state, AND
|
|
* - Shadow flag has been explicitly enabled.
|
|
*
|
|
* @param string $module Module identifier.
|
|
* @return bool
|
|
*/
|
|
public static function is_shadow_read_active( string $module ): bool {
|
|
if ( 'verify' !== self::get( $module ) ) {
|
|
return false;
|
|
}
|
|
$flags = self::all_shadow();
|
|
return ! empty( $flags[ $module ] );
|
|
}
|
|
|
|
/**
|
|
* Return all shadow flags.
|
|
*
|
|
* @return array<string,bool>
|
|
*/
|
|
public static function all_shadow(): array {
|
|
if ( null !== self::$shadow_cache ) {
|
|
return self::$shadow_cache;
|
|
}
|
|
$saved = get_option( self::SHADOW_OPTION_KEY, array() );
|
|
if ( ! is_array( $saved ) ) {
|
|
$saved = array();
|
|
}
|
|
self::$shadow_cache = $saved;
|
|
return $saved;
|
|
}
|
|
|
|
/**
|
|
* Check if a module is fully complete (reads and writes on custom table only).
|
|
*
|
|
* @param string $module Module identifier.
|
|
* @return bool True if module state is 'complete'.
|
|
*/
|
|
public static function is_complete( string $module ): bool {
|
|
return 'complete' === self::get( $module );
|
|
}
|
|
|
|
/**
|
|
* Check if dual-write is active for a module.
|
|
*
|
|
* @param string $module Module identifier.
|
|
* @return bool True if module is in a write-active state.
|
|
*/
|
|
public static function is_write_active( string $module ): bool {
|
|
return in_array( self::get( $module ), self::WRITE_ACTIVE_STATES, true );
|
|
}
|
|
|
|
/**
|
|
* Check if reads should come from the custom/zone table.
|
|
*
|
|
* @param string $module Module identifier.
|
|
* @return bool True if module is in a read-custom state.
|
|
*/
|
|
public static function is_read_custom( string $module ): bool {
|
|
return in_array( self::get( $module ), self::READ_CUSTOM_STATES, true );
|
|
}
|
|
|
|
/**
|
|
* Check if query interceptors should be active.
|
|
*
|
|
* @param string $module Module identifier.
|
|
* @return bool True if module is in a query-active state.
|
|
*/
|
|
public static function is_query_active( string $module ): bool {
|
|
return in_array( self::get( $module ), self::QUERY_ACTIVE_STATES, true );
|
|
}
|
|
|
|
/**
|
|
* Reset a module to idle (rollback).
|
|
*
|
|
* @param string $module Module identifier.
|
|
* @return void
|
|
*/
|
|
public static function reset( string $module ): void {
|
|
self::set( $module, 'idle' );
|
|
self::$cache = null; // Invalidate request-level cache.
|
|
}
|
|
|
|
/**
|
|
* Request-level cache for all() to avoid repeated get_option() calls.
|
|
*
|
|
* @var array|null
|
|
*/
|
|
private static ?array $cache = null;
|
|
|
|
/**
|
|
* Return all module states.
|
|
*
|
|
* @return array<string, string>
|
|
*/
|
|
public static function all(): array {
|
|
if ( null !== self::$cache ) {
|
|
return self::$cache;
|
|
}
|
|
|
|
$saved = get_option( self::OPTION_KEY, array() );
|
|
if ( ! is_array( $saved ) ) {
|
|
$saved = array();
|
|
}
|
|
|
|
$all_modules = array_merge( self::HPCT_MODULES, self::ZONE_MODULES );
|
|
$defaults = array_fill_keys( $all_modules, 'idle' );
|
|
|
|
self::$cache = array_merge( $defaults, $saved );
|
|
return self::$cache;
|
|
}
|
|
|
|
/**
|
|
* Return only HPCT-inherited module states.
|
|
*
|
|
* @return array<string, string>
|
|
*/
|
|
public static function hpct_modules(): array {
|
|
$all = self::all();
|
|
return array_intersect_key( $all, array_flip( self::HPCT_MODULES ) );
|
|
}
|
|
|
|
/**
|
|
* Return only zone module states.
|
|
*
|
|
* @return array<string, string>
|
|
*/
|
|
public static function zone_modules(): array {
|
|
$all = self::all();
|
|
return array_intersect_key( $all, array_flip( self::ZONE_MODULES ) );
|
|
}
|
|
|
|
/**
|
|
* Map an HPCT feature flag status to a WPDO state.
|
|
*
|
|
* Used during HPCT import to translate HPCT's 4-state model
|
|
* to WPDO's 7-state model.
|
|
*
|
|
* @param string $hpct_status HPCT status (disabled/migrating/verified/enabled).
|
|
* @return string WPDO state.
|
|
*/
|
|
public static function map_hpct_status( string $hpct_status ): string {
|
|
return match ( $hpct_status ) {
|
|
'disabled' => 'idle',
|
|
'migrating' => 'backfill',
|
|
'verified' => 'cutover',
|
|
'enabled' => 'complete',
|
|
default => 'idle',
|
|
};
|
|
}
|
|
}
|