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,346 @@
|
||||
<?php
|
||||
/**
|
||||
* Feature flags manager for WPDO module lifecycle states.
|
||||
*
|
||||
* @package WP_Data_Optimizer
|
||||
*/
|
||||
|
||||
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',
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user