9d1a7c5591
- 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
139 lines
5.1 KiB
PHP
139 lines
5.1 KiB
PHP
<?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';
|
||
}
|
||
}
|