feat(2meet-data-optimizer): backfill writes made while the Hook Bus was off

- When an entity goes from disabled to dual_write / shadow_read, or the Hook Bus flag is switched on, a single WP-Cron event (tmdo_drift_resync, one per entity, 60 s later) runs drift-check --fix for the entities that keep native meta, so values written while flat was bypassed land in flat. aeav_only is never touched.
- A full fix instead of the planned post_modified_gmt increment (decision 2026-09-27): meta-only writes such as WooCommerce's direct stock SQL do not bump post_modified. No wpdo_bridge_off_since.
- The resync hook is cleared on deactivate / uninstall (literal name: uninstall.php does not load TMDO_Drift_Check).
- Verify: DriftCheckTest resync cases (RED before); dev36 end to end with real WP-Cron: write while user disabled → miss 1 → bridge-mode user dual_write schedules tmdo_drift_resync [user] → run → drift 0.
- PR: PR-074
This commit is contained in:
2026-09-27 00:41:43 +08:00
parent 1b21ee9add
commit a26500206a
4 changed files with 198 additions and 0 deletions
@@ -45,6 +45,18 @@ final class TMDO_Drift_Check {
*/
public const CRON_SAMPLE = 500;
/**
* Single WP-Cron event rebuilding an entity's flat tables from native after
* writes bypassed them. Argument: the entity type.
*/
public const RESYNC_HOOK = 'tmdo_drift_resync';
/**
* Seconds before the scheduled rebuild, so the switch that triggered it
* finishes first.
*/
private const RESYNC_DELAY = 60;
/**
* MySQL regex for a plain decimal number, used to skip rows that obviously match.
* Exact matches compare BINARY: meta and flat tables may differ in collation,
@@ -122,6 +134,118 @@ final class TMDO_Drift_Check {
}
}
/**
* Entities to rebuild after a mode change: one whose flat tables were not
* written (disabled) is written next to native again, and values written
* meanwhile are missing from flat.
*
* @param string $entity_type Entity type.
* @param string $new_mode New mode.
* @param string $old_mode Previous mode.
* @return string[]
*/
public static function resync_after_mode_change( string $entity_type, string $new_mode, string $old_mode ): array {
$keeps_native = array( TMDO_Mode_Manager::MODE_DUAL_WRITE, TMDO_Mode_Manager::MODE_SHADOW_READ );
return TMDO_Mode_Manager::MODE_DISABLED === $old_mode && in_array( $new_mode, $keeps_native, true ) ? array( $entity_type ) : array();
}
/**
* Entities to rebuild after the Hook Bus flag changes: no write reached
* flat while it was off. Every entity written to both native and flat
* (aeav_only keeps flat as the source of truth).
*
* @param mixed $old_value Previous flag value.
* @param mixed $value New flag value.
* @return string[]
*/
public static function resync_after_hook_bus_change( $old_value, $value ): array {
if ( self::flag_on( $old_value ) || ! self::flag_on( $value ) ) {
return array();
}
$entities = array();
foreach ( TMDO_Entity_Registry::VALID_ENTITY_TYPES as $entity_type ) {
if ( TMDO_Mode_Manager::writes_to_flat( $entity_type ) && TMDO_Mode_Manager::writes_to_eav( $entity_type ) ) {
$entities[] = $entity_type;
}
}
return $entities;
}
/**
* `wpdo_bridge_mode_changed` handler.
*
* @param string $entity_type Entity type.
* @param string $new_mode New mode.
* @param string $old_mode Previous mode.
*/
public static function on_mode_changed( string $entity_type, string $new_mode, string $old_mode ): void {
self::schedule_resync( self::resync_after_mode_change( $entity_type, $new_mode, $old_mode ) );
}
/**
* `update_option_wpdo_hook_bus_enabled` handler.
*
* @param mixed $old_value Previous flag value.
* @param mixed $value New flag value.
*/
public static function on_hook_bus_toggled( $old_value, $value ): void {
self::schedule_resync( self::resync_after_hook_bus_change( $old_value, $value ) );
}
/**
* `add_option_wpdo_hook_bus_enabled` handler: the flag is stored for the first time.
*
* @param string $option Option name.
* @param mixed $value Flag value.
*/
public static function on_hook_bus_added( $option, $value ): void {
unset( $option );
self::schedule_resync( self::resync_after_hook_bus_change( '0', $value ) );
}
/**
* RESYNC_HOOK handler. An entity that has meanwhile moved to aeav_only is
* left alone (fix() refuses).
*
* @param string $entity_type Entity type.
*/
public static function run_scheduled_fix( string $entity_type ): void {
try {
$result = self::fix( $entity_type );
TMDO_Logger::info( 'drift_resync', array( 'entity' => $entity_type ) + $result['fixed'] );
} catch ( InvalidArgumentException | RuntimeException $e ) {
TMDO_Logger::info(
'drift_resync_skipped',
array(
'entity' => $entity_type,
'reason' => $e->getMessage(),
)
);
}
}
/**
* Schedule one rebuild per entity type (not twice while one is pending).
*
* @param string[] $entities Entity types.
*/
private static function schedule_resync( array $entities ): void {
foreach ( $entities as $entity_type ) {
if ( ! wp_next_scheduled( self::RESYNC_HOOK, array( $entity_type ) ) ) {
wp_schedule_single_event( time() + self::RESYNC_DELAY, self::RESYNC_HOOK, array( $entity_type ) );
}
}
}
/**
* Whether a stored Hook Bus flag value means "on" (as TMDO_Hook_Bus_Bridge reads it).
*
* @param mixed $value Flag value.
*/
private static function flag_on( $value ): bool {
return '1' === (string) $value || true === $value;
}
/**
* Rebuild drifted flat rows from native meta, then check again.
*