Compare commits

..

3 Commits

Author SHA1 Message Date
wpdev e4e49e70ff test(2meet-data-optimizer): live tools no longer flush the object cache
- mut.php called wp_cache_flush() after each write and qbench.php
  wp_cache_flush_group() before each run; with Redis Object Cache's selective
  flush both are one server-side Lua scan over the site's keys, which blocked
  the shared Redis for 5–20 s on dev36 (3 M keys) and broke other sites'
  requests (BUSY / read errors)
- mut.php clears the touched post or user caches plus the runtime cache;
  qbench.php bumps the routed count version (posts' last_changed before 1.3.0)
- tests/ is not packaged; plugin code does not flush
2026-09-27 19:25:56 +08:00
wpdev f929165b7b Merge pull request 'hardening 2.3: 2meet-data-optimizer 1.3.0 (flat-table DDL outside web requests, routed count cache, audit fixes)' (#7) from hardening/2.3 into main
Tests / PHP Lint (push) Successful in 11s
Anti-EAV Lint + Quality Gate / anti-eav-lint (push) Successful in 14s
Tests / Unit Tests (push) Successful in 43s
Tests / PHPStan (push) Failing after 14m14s
Tests / PHPCS (push) Failing after 14m16s
Tests / Integration Tests (push) Failing after 15m11s
2026-09-27 18:19:17 +08:00
wpdev 947e130386 docs(2meet-data-optimizer): add PR-116 and PR-117 to the 1.3.0 release notes
Anti-EAV Lint + Quality Gate / anti-eav-lint (pull_request) Successful in 24s
Tests / PHP Lint (pull_request) Successful in 40s
Tests / Unit Tests (pull_request) Successful in 41s
Tests / Integration Tests (pull_request) Successful in 1m23s
Tests / PHPCS (pull_request) Successful in 29s
Tests / PHPStan (pull_request) Successful in 1m14s
- routed count cache: placeholder escape and count version group
- PR: PR-101 (follow-up)
2026-09-27 17:58:07 +08:00
4 changed files with 19 additions and 7 deletions
+7 -1
View File
@@ -22,7 +22,13 @@ Versioning follows [Semantic Versioning](https://semver.org/).
索引。filter:`tmdo_schema_ddl_allowed`。dev36 90 萬列加兩個索引約 4.7 s。(PR-090a) 索引。filter:`tmdo_schema_ddl_allowed`。dev36 90 萬列加兩個索引約 4.7 s。(PR-090a)
- **路由搜尋的 found rows**:帶 `post__not_in` 的查詢(HivePress 每頁送一組隨機排除清單) - **路由搜尋的 found rows**:帶 `post__not_in` 的查詢(HivePress 每頁送一組隨機排除清單)
改為「不含排除條件的 COUNT(快取)」減去「排除清單中符合條件的篇數(主鍵查詢)」,結果 改為「不含排除條件的 COUNT(快取)」減去「排除清單中符合條件的篇數(主鍵查詢)」,結果
精確,快取不再被打散。dev36 關鍵字 + 價格:首次 5.25 s、之後 3.2 s(原生 3.35 s)。(PR-091) 精確,快取不再被打散。(PR-091)快取 key 另外去掉 wpdb 替 `%` 產生的 placeholder escape
(每個請求不同,關鍵字搜尋原本永遠不命中,也就是 2.2 驗收「關鍵字 + 篩選 routed 較慢」的
原因)(PR-116);計數改跟隨自有的 `tmdo_posts` last_changed(文章儲存/刪除、經 Hook Bus
的扁平寫入)與 terms 的 last_changed,不再跟隨 posts 的 last_changed——任何 post meta 寫入
都會推動它,dev36 的 Blocksy 佈景在爬蟲瀏覽每篇 listing 時都寫一筆 meta,快取幾秒就失效
(PR-117)。dev36 關鍵字 + 價格 routed 9.8 s → 首次後約 5.5 s(原生 5.0–6.2 s,剩餘為
WordPress 核心的 LIKE 全表掃描)。
- `POST /wpdo/v1/listings/{id}/view` 被限流擋下時,`wpdo_rl_stats` 改為 1/10 抽樣寫入 - `POST /wpdo/v1/listings/{id}/view` 被限流擋下時,`wpdo_rl_stats` 改為 1/10 抽樣寫入
(每次 +10),匿名請求不再 1:1 放大成 option 寫入。(PR-094) (每次 +10),匿名請求不再 1:1 放大成 option 寫入。(PR-094)
- `TMDO_Logger::info()` 只在 `WP_DEBUG_LOG` 時寫 error_log(filter `tmdo_log_info`); - `TMDO_Logger::info()` 只在 `WP_DEBUG_LOG` 時寫 error_log(filter `tmdo_log_info`);
+1 -1
View File
@@ -109,7 +109,7 @@ The Classifier tab analyzes your existing `wp_postmeta` data (access frequency,
= 1.3.0 = = 1.3.0 =
* Declared flat-table indexes are added only from WP-CLI or WP-Cron, one process at a time (MySQL GET_LOCK); web requests schedule `tmdo_ensure_group_indexes`, and doctor lists indexes not added yet * Declared flat-table indexes are added only from WP-CLI or WP-Cron, one process at a time (MySQL GET_LOCK); web requests schedule `tmdo_ensure_group_indexes`, and doctor lists indexes not added yet
* Routed searches with post__not_in count from a cached base count minus the excluded posts (HivePress keyword + price search no longer slower than native) * Routed searches with post__not_in count from a cached base count minus the excluded posts; the cache key ignores wpdb's per-request placeholder escape and follows post saves and managed-key writes only, not every post meta write (HivePress keyword + price search no longer slower than native)
* Rate-limited view requests sample their statistics (1 in 10) instead of writing an option each time * Rate-limited view requests sample their statistics (1 in 10) instead of writing an option each time
* Logger info / notice lines only with WP_DEBUG_LOG (filter `tmdo_log_info`) * Logger info / notice lines only with WP_DEBUG_LOG (filter `tmdo_log_info`)
* The public listings REST output withholds email, phone, address and IP fields even when declared public * The public listings REST output withholds email, phone, address and IP fields even when declared public
+5 -4
View File
@@ -7,7 +7,8 @@ $mode = TMDO_Mode_Manager::get( 'post' ) . '/' . TMDO_Mode_Manager::get( 'user'
function nat( $t, $id, $k ) { global $wpdb; $mt = $t === 'user' ? $wpdb->usermeta : $wpdb->postmeta; $c = $t === 'user' ? 'user_id' : 'post_id'; $v = $wpdb->get_col( $wpdb->prepare( "SELECT meta_value FROM $mt WHERE $c=%d AND meta_key=%s", $id, $k ) ); return $v ? implode( '|', $v ) : '∅'; } function nat( $t, $id, $k ) { global $wpdb; $mt = $t === 'user' ? $wpdb->usermeta : $wpdb->postmeta; $c = $t === 'user' ? 'user_id' : 'post_id'; $v = $wpdb->get_col( $wpdb->prepare( "SELECT meta_value FROM $mt WHERE $c=%d AND meta_key=%s", $id, $k ) ); return $v ? implode( '|', $v ) : '∅'; }
function flat( $t, $g, $id, $k ) { global $wpdb; $c = $t === 'user' ? 'user_id' : 'post_id'; $v = $wpdb->get_row( $wpdb->prepare( "SELECT `$k` v FROM {$wpdb->prefix}wpdo_{$t}_{$g} WHERE $c=%d", $id ) ); return $v ? ( $v->v ?? 'NULL' ) : 'NO-ROW'; } function flat( $t, $g, $id, $k ) { global $wpdb; $c = $t === 'user' ? 'user_id' : 'post_id'; $v = $wpdb->get_row( $wpdb->prepare( "SELECT `$k` v FROM {$wpdb->prefix}wpdo_{$t}_{$g} WHERE $c=%d", $id ) ); return $v ? ( $v->v ?? 'NULL' ) : 'NO-ROW'; }
function row( $name, $vals ) { printf( "%-44s %s\n", $name, implode( ' ', array_map( fn( $k, $v ) => "$k=" . ( is_scalar( $v ) || is_null( $v ) ? var_export( $v, true ) : json_encode( $v ) ), array_keys( $vals ), $vals ) ) ); } function row( $name, $vals ) { printf( "%-44s %s\n", $name, implode( ' ', array_map( fn( $k, $v ) => "$k=" . ( is_scalar( $v ) || is_null( $v ) ? var_export( $v, true ) : json_encode( $v ) ), array_keys( $vals ), $vals ) ) ); }
function clear_caches( $id ) { wp_cache_flush(); clean_post_cache( $id ); } // Never wp_cache_flush(): with Redis Object Cache selective flush it is one server-side scan that blocks Redis for every site.
function clear_caches( $id ) { clean_post_cache( $id ); wp_cache_flush_runtime(); }
echo "== mode post/user = $mode\n"; echo "== mode post/user = $mode\n";
// T1: HivePress model update of a listing price. // T1: HivePress model update of a listing price.
@@ -42,12 +43,12 @@ row( "T5 delete_post_meta hp_featured", array( 'native' => nat( 'post', $lid, 'h
// T6: user first_name via wp_update_user. // T6: user first_name via wp_update_user.
$uid = (int) $wpdb->get_var( "SELECT ID FROM {$wpdb->users} WHERE user_login LIKE 's36v_B1p1_%' ORDER BY ID LIMIT 1" ); $uid = (int) $wpdb->get_var( "SELECT ID FROM {$wpdb->users} WHERE user_login LIKE 's36v_B1p1_%' ORDER BY ID LIMIT 1" );
$fn = 'Fn' . mt_rand( 100, 999 ); wp_update_user( array( 'ID' => $uid, 'first_name' => $fn ) ); wp_cache_flush(); $fn = 'Fn' . mt_rand( 100, 999 ); wp_update_user( array( 'ID' => $uid, 'first_name' => $fn ) ); clean_user_cache( $uid ); wp_cache_flush_runtime();
$u = get_userdata( $uid ); $u = get_userdata( $uid );
row( "T6 user $uid first_name:=$fn", array( 'native' => nat( 'user', $uid, 'first_name' ), 'flat' => flat( 'user', 'core_profile', $uid, 'first_name' ), 'get_meta' => get_user_meta( $uid, 'first_name', true ), 'WP_User' => $u->first_name, 'wc_customer' => ( new WC_Customer( $uid ) )->get_first_name() ) ); row( "T6 user $uid first_name:=$fn", array( 'native' => nat( 'user', $uid, 'first_name' ), 'flat' => flat( 'user', 'core_profile', $uid, 'first_name' ), 'get_meta' => get_user_meta( $uid, 'first_name', true ), 'WP_User' => $u->first_name, 'wc_customer' => ( new WC_Customer( $uid ) )->get_first_name() ) );
// T7: WC customer billing city via CRUD. // T7: WC customer billing city via CRUD.
$c = new WC_Customer( $uid ); $city = 'City' . mt_rand( 10, 99 ); $c->set_billing_city( $city ); $c->save(); wp_cache_flush(); $c = new WC_Customer( $uid ); $city = 'City' . mt_rand( 10, 99 ); $c->set_billing_city( $city ); $c->save(); clean_user_cache( $uid ); wp_cache_flush_runtime();
row( "T7 WC_Customer billing_city:=$city", array( 'native' => nat( 'user', $uid, 'billing_city' ), 'flat' => flat( 'user', 'commerce', $uid, 'billing_city' ), 'get_meta' => get_user_meta( $uid, 'billing_city', true ), 'wc_customer' => ( new WC_Customer( $uid ) )->get_billing_city() ) ); row( "T7 WC_Customer billing_city:=$city", array( 'native' => nat( 'user', $uid, 'billing_city' ), 'flat' => flat( 'user', 'commerce', $uid, 'billing_city' ), 'get_meta' => get_user_meta( $uid, 'billing_city', true ), 'wc_customer' => ( new WC_Customer( $uid ) )->get_billing_city() ) );
// T8: delete a listing → flat row? // T8: delete a listing → flat row?
@@ -57,5 +58,5 @@ row( "T8 wp_delete_post $d", array( 'posts' => (int) $wpdb->get_var( "SELECT COU
// T9: HP review → listing rating recompute. // T9: HP review → listing rating recompute.
$before = get_post_meta( $lid, 'hp_rating_count', true ); $before = get_post_meta( $lid, 'hp_rating_count', true );
$rv = ( new Models\Review() )->fill( array( 'text' => 'mut', 'rating' => 5, 'approved' => 1, 'author' => $uid, 'author__display_name' => 'x', 'author__email' => 'x@example.test', 'listing' => $lid ) ); $rv->save(); wp_cache_flush(); $rv = ( new Models\Review() )->fill( array( 'text' => 'mut', 'rating' => 5, 'approved' => 1, 'author' => $uid, 'author__display_name' => 'x', 'author__email' => 'x@example.test', 'listing' => $lid ) ); $rv->save(); clean_post_cache( $lid ); wp_cache_flush_runtime();
row( "T9 add review (count was $before)", array( 'native' => nat( 'post', $lid, 'hp_rating_count' ), 'flat' => flat( 'post', 'hp_listing_core', $lid, 'hp_rating_count' ), 'hp_model' => Models\Listing::query()->get_by_id( $lid )->get_rating_count() ) ); row( "T9 add review (count was $before)", array( 'native' => nat( 'post', $lid, 'hp_rating_count' ), 'flat' => flat( 'post', 'hp_listing_core', $lid, 'hp_rating_count' ), 'hp_model' => Models\Listing::query()->get_by_id( $lid )->get_rating_count() ) );
+6 -1
View File
@@ -40,7 +40,12 @@ foreach ( $cases as $name => $qa ) {
$err = ''; $err = '';
$sql = ''; $sql = '';
for ( $i = 0; $i < $iters; $i++ ) { for ( $i = 0; $i < $iters; $i++ ) {
wp_cache_flush_group( 'tmdo_found_rows' ); // Fresh routed counts without wp_cache_flush_group() (a Redis-blocking server-side scan).
if ( method_exists( 'TMDO_Post_Query_Router', 'bump_count_version' ) ) {
TMDO_Post_Query_Router::bump_count_version();
} else {
wp_cache_set_last_changed( 'posts' ); // Core < 1.3.0 keyed the counts on posts' last_changed.
}
$wpdb->last_error = ''; $wpdb->last_error = '';
$t = microtime( true ); $t = microtime( true );
$q = new WP_Query( $qa + array( 'fields' => 'ids', 'cache_results' => false, 'suppress_filters' => false ) ); $q = new WP_Query( $qa + array( 'fields' => 'ids', 'cache_results' => false, 'suppress_filters' => false ) );