Files
2meet-data-optimizer/includes/engine/class-tmdo-auto-promoter.php
T
wpdev 9d1a7c5591 feat(2meet-data-optimizer): throttle shadow_read diffs per key and promote on clean drift checks
- The shadow diff logger records at most 20 rows per entity type + key per hour across all objects, on top of the existing per-object 5-minute window: a key stale on every object (WooCommerce total_sales, written by direct SQL) no longer logs a row per product read.
- The auto-promoter's third condition is now TMDO_Drift_Check::clean_since(): every backfillable group of the entity checked within 2 days (sampled or full) with no drift; no record, no promotion. The shadow diff count no longer gates it (the filter's 4th argument is passed as 0). The Promotion Guard still refuses HivePress / WooCommerce sites.
- Reason classification was dropped (decision 2026-09-27): it needs a schema change and drift-check already reports miss / diff / extra per key.
- Verify: ShadowDiffThrottleTest, AutoPromoterDriftTest (RED before); dev36 simulated user shadow_read: clean_since yes, promotion still refused by the guard, mode unchanged.
- PR: PR-075
2026-09-27 00:48:18 +08:00

139 lines
5.1 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* TMDO_Auto_Promoter — shadow_read 穩定期後自動升級到 aeav_only。
*
* 僅針對 `shadow_read` 做自動升級(升級到 aeav_only),其他 mode 不自動動。
* 判斷條件(皆滿足才觸發):
* 1. 當前 mode = shadow_read
* 2. 進入 shadow_read 距今 ≥ `min_days`
* 3. 該 entity 每個群組最近一次 drift-check(2 天內,抽樣或全量)皆無漂移
* (1.2.0 起取代 shadow_diffs 筆數;無紀錄不升級)
*
* 這符合「穩定期沒問題就進 production」的安全遷移原則。
* 若 diff 出現,規則永不觸發;人工介入即可。
*
* 可由 option 或 filter 調整:
* option `wpdo_auto_promote_enabled` — 全域開關(bool,預設 false)
* option `wpdo_auto_promote_min_days` — 停留天數門檻(int,預設 7)
* filter `wpdo_auto_promote_should_run` — per-entity 覆寫(bool default)
*
* @package WP_Data_Optimizer
* @since 1.5.0
*/
declare(strict_types=1);
// phpcs:disable Squiz.Commenting,Generic.Commenting,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,Generic.CodeAnalysis.UnusedFunctionParameter,Generic.CodeAnalysis.EmptyStatement,Squiz.PHP.DisallowMultipleAssignments,Squiz.PHP.DisallowSizeFunctionsInLoops,WordPress.WP.I18n.MissingTranslatorsComment,WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents,Squiz.PHP.CommentedOutCode,Universal.NamingConventions.NoReservedKeywordParameterNames,WordPress.PHP.YodaConditions,Squiz.Commenting.InlineComment.InvalidEndChar -- PR-1 ported from UAE; cleanup PR scheduled.
defined( 'ABSPATH' ) || exit;
final class TMDO_Auto_Promoter {
public const OPT_ENABLED = 'wpdo_auto_promote_enabled';
public const OPT_MIN_DAYS = 'wpdo_auto_promote_min_days';
public const DEFAULT_MIN_DAYS = 7;
public const CRON_HOOK = 'wpdo_auto_promote_check';
/** A drift check older than this (seconds) does not count for promotion. */
public const DRIFT_MAX_AGE = 2 * DAY_IN_SECONDS;
public static function init(): void {
add_action( self::CRON_HOOK, array( self::class, 'check_all' ) );
add_action( 'init', array( self::class, 'maybe_schedule' ), 30 );
}
/**
* 首次啟用時排入 daily cron。Option 關閉時會停掉,開啟時重排。
*/
public static function maybe_schedule(): void {
$enabled = (bool) get_option( self::OPT_ENABLED, false );
$scheduled = wp_next_scheduled( self::CRON_HOOK );
if ( $enabled && ! $scheduled ) {
wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', self::CRON_HOOK );
} elseif ( ! $enabled && $scheduled ) {
wp_unschedule_event( $scheduled, self::CRON_HOOK );
}
}
/**
* 遍歷所有 entity,符合條件者升級為 aeav_only。
*
* @return array<string, string> entity_type → 結果('promoted'|'not_ready'|'no_diff_yet'|'disabled_globally')
*/
public static function check_all(): array {
$results = array();
if ( ! (bool) get_option( self::OPT_ENABLED, false ) ) {
foreach ( array( 'post', 'user', 'term', 'comment' ) as $type ) {
$results[ $type ] = 'disabled_globally';
}
return $results;
}
$min_days = (int) get_option( self::OPT_MIN_DAYS, self::DEFAULT_MIN_DAYS );
$entered_all = get_option( TMDO_Mode_Manager::OPT_ENTERED_AT, array() );
foreach ( array( 'post', 'user', 'term', 'comment' ) as $type ) {
$results[ $type ] = self::check_entity( $type, $min_days, $entered_all );
}
return $results;
}
/**
* 評估單一 entity 是否可升級。
*
* @return string 'promoted' | 'not_shadow_read' | 'not_ready' | 'drift_not_clean' | 'vetoed' | 'error'
*/
public static function check_entity( string $type, int $min_days, array $entered_all ): string {
$current = TMDO_Mode_Manager::get( $type );
if ( $current !== TMDO_Mode_Manager::MODE_SHADOW_READ ) {
return 'not_shadow_read';
}
$entered_at = isset( $entered_all[ $type ] ) ? (int) $entered_all[ $type ] : 0;
if ( $entered_at === 0 || ( time() - $entered_at ) < $min_days * DAY_IN_SECONDS ) {
return 'not_ready';
}
if ( ! TMDO_Drift_Check::clean_since( $type, self::DRIFT_MAX_AGE ) ) {
return 'drift_not_clean';
}
// 讓外部 filter 最後一次 veto(例如:還在外部驗證期間)。第 4 參數原為
// shadow diff 筆數;1.2.0 起改看 drift,固定傳 0。
$should = apply_filters( 'wpdo_auto_promote_should_run', true, $type, $entered_at, 0 );
if ( ! $should ) {
return 'vetoed';
}
$result = TMDO_Mode_Manager::set( $type, TMDO_Mode_Manager::MODE_AEAV_ONLY );
if ( is_wp_error( $result ) ) {
TMDO_Logger::error(
'auto_promoter',
'auto_promote_failed',
$result->get_error_message(),
array( 'entity_type' => $type )
);
return 'error';
}
TMDO_Logger::info(
'auto_promote_success',
array(
'entity_type' => $type,
'entered_at' => gmdate( 'c', $entered_at ),
'days_elapsed' => (int) ( ( time() - $entered_at ) / DAY_IN_SECONDS ),
'min_days' => $min_days,
)
);
do_action( 'wpdo_auto_promoted', $type, $entered_at, $min_days );
return 'promoted';
}
}