From b515b8441eb6dbc2e5d05bd5cb46a2436aba96e9 Mon Sep 17 00:00:00 2001 From: wpdev Date: Sun, 27 Sep 2026 09:00:03 +0800 Subject: [PATCH 1/6] fix(2meet-data-optimizer-hivepress-addon): keep HivePress transients in the object cache when there is one - With a persistent object cache (wp_using_ext_object_cache(), Redis on the HivePress sites) the transient filter stores HivePress's per-post transient pairs in the tmdo_hp_transient cache group instead of wp_options, where they piled up (1.15 M rows on dev36). Reads fall back to wp_options for values written before; deletes clear both. Without an object cache nothing changes. - Verify: HivepressTransientFilterTest object-cache cases (RED before); dev36 with Redis: update/get/delete_post_meta on an HP transient key leaves 0 rows in wp_options and postmeta. - PR: PR-081 --- .../class-tmdo-hivepress-transient-filter.php | 28 +++++++++--- .../HivepressTransientFilterTest.php | 44 ++++++++++++++++++- tests/integration/bootstrap.php | 7 +++ 3 files changed, 72 insertions(+), 7 deletions(-) diff --git a/includes/class-tmdo-hivepress-transient-filter.php b/includes/class-tmdo-hivepress-transient-filter.php index 92c9fec..1deadff 100644 --- a/includes/class-tmdo-hivepress-transient-filter.php +++ b/includes/class-tmdo-hivepress-transient-filter.php @@ -77,6 +77,9 @@ final class TMDO_Hivepress_Transient_Filter { /** Namespace prefix for translated wp_options entries. */ public const TRANSLATED_NAMESPACE = 'wpdo_hp_pm_'; + /** Object cache group used instead of wp_options when a persistent cache is present. */ + public const CACHE_GROUP = 'tmdo_hp_transient'; + /** * Register the four metadata filters on `init`. Idempotent — safe to call * multiple times. @@ -164,7 +167,11 @@ final class TMDO_Hivepress_Transient_Filter { // HivePress cache layer reads the timeout *first*, then decides whether // to read the value. We must preserve raw stored values until HivePress // itself orders deletion. - $value = get_option( $option_name, null ); + $value = wp_using_ext_object_cache() ? wp_cache_get( $option_name, self::CACHE_GROUP ) : false; + if ( false === $value ) { + // wp_options: where values went before an object cache was used. + $value = get_option( $option_name, null ); + } if ( null === $value ) { // Cache miss in our store → fall through to normal postmeta path @@ -239,16 +246,23 @@ final class TMDO_Hivepress_Transient_Filter { $translated = self::translate_key( (int) $object_id, (string) $meta_key ); $option_name = ( $is_timeout ? '_transient_timeout_' : '_transient_' ) . $translated; + if ( wp_using_ext_object_cache() ) { + wp_cache_delete( $option_name, self::CACHE_GROUP ); + } delete_option( $option_name ); return true; } /** - * Internal: persist value or timeout to wp_options. + * Internal: persist value or timeout to the object cache when a persistent + * one is in use (Redis: a TTL cache belongs there, not in wp_options), else + * to wp_options. * - * Both halves of the transient pair are written with `autoload=no` (they - * are read on-demand only — never via wp_load_alloptions() — so eagerly - * loading them on every page would defeat the point of the move). + * In wp_options both halves of the transient pair are written with + * `autoload=no` (they are read on-demand only — never via + * wp_load_alloptions() — so eagerly loading them on every page would defeat + * the point of the move). HivePress checks the timeout itself, so the + * cache entries get no TTL of their own. * * @param int $post_id Post ID. * @param string $meta_key Original `_transient_*` meta_key. @@ -260,6 +274,10 @@ final class TMDO_Hivepress_Transient_Filter { $translated = self::translate_key( $post_id, $meta_key ); $option_name = ( $is_timeout ? '_transient_timeout_' : '_transient_' ) . $translated; + if ( wp_using_ext_object_cache() ) { + wp_cache_set( $option_name, $meta_value, self::CACHE_GROUP ); + return; + } update_option( $option_name, $meta_value, false ); } diff --git a/tests/integration/HivepressTransientFilterTest.php b/tests/integration/HivepressTransientFilterTest.php index 827d907..41e2f9a 100644 --- a/tests/integration/HivepressTransientFilterTest.php +++ b/tests/integration/HivepressTransientFilterTest.php @@ -44,8 +44,10 @@ class HivepressTransientFilterTest extends TestCase { global $wpdb; $wpdb->query( 'TRUNCATE TABLE `' . self::POSTMETA . '`' ); - // Reset wp_options state for filter tests - $GLOBALS['_wp_options'] = array(); + // Reset wp_options / object cache state for filter tests + $GLOBALS['_wp_options'] = array(); + $GLOBALS['_wp_cache'] = array(); + $GLOBALS['_wp_using_ext_object_cache'] = false; // Default the toggle to enabled update_option( WPDO_Hivepress_Transient_Filter::OPT_ENABLED, '1' ); @@ -195,6 +197,44 @@ class HivepressTransientFilterTest extends TestCase { $this->assertSame( array( array( 'serialized', 'data' ) ), $value ); } + // ── External object cache (2.2 PR-081) ────────────────────────────────── + + public function test_with_an_object_cache_writes_skip_wp_options(): void { + $GLOBALS['_wp_using_ext_object_cache'] = true; + $key = '_transient_hp_models/cat/v1'; + $translated = WPDO_Hivepress_Transient_Filter::translate_key( 100, $key ); + + $this->assertTrue( WPDO_Hivepress_Transient_Filter::on_update( null, 100, $key, 'val', '' ) ); + WPDO_Hivepress_Transient_Filter::on_update( null, 100, '_transient_timeout_hp_models/cat/v1', '99999999', '' ); + + $this->assertFalse( get_option( '_transient_' . $translated ), 'no wp_options row' ); + $this->assertFalse( get_option( '_transient_timeout_' . $translated ), 'no wp_options row' ); + $this->assertSame( array( 'val' ), WPDO_Hivepress_Transient_Filter::on_read( null, 100, $key, true ) ); + $this->assertSame( array( '99999999' ), WPDO_Hivepress_Transient_Filter::on_read( null, 100, '_transient_timeout_hp_models/cat/v1', true ) ); + } + + public function test_with_an_object_cache_a_value_left_in_wp_options_still_reads(): void { + $key = '_transient_hp_models/cat/v1'; + $translated = WPDO_Hivepress_Transient_Filter::translate_key( 100, $key ); + update_option( '_transient_' . $translated, 'from-before' ); + $GLOBALS['_wp_using_ext_object_cache'] = true; + + $this->assertSame( array( 'from-before' ), WPDO_Hivepress_Transient_Filter::on_read( null, 100, $key, true ) ); + } + + public function test_with_an_object_cache_delete_clears_both_stores(): void { + $key = '_transient_hp_models/cat/v1'; + $translated = WPDO_Hivepress_Transient_Filter::translate_key( 100, $key ); + update_option( '_transient_' . $translated, 'from-before' ); + $GLOBALS['_wp_using_ext_object_cache'] = true; + WPDO_Hivepress_Transient_Filter::on_update( null, 100, $key, 'val', '' ); + + WPDO_Hivepress_Transient_Filter::on_delete( null, 100, $key, '', false ); + + $this->assertNull( WPDO_Hivepress_Transient_Filter::on_read( null, 100, $key, true ) ); + $this->assertFalse( get_option( '_transient_' . $translated ) ); + } + // ── count/purge legacy ────────────────────────────────────────────────── public function test_count_legacy_postmeta_rows_returns_zero_when_clean(): void { diff --git a/tests/integration/bootstrap.php b/tests/integration/bootstrap.php index b34bf9d..c5be687 100644 --- a/tests/integration/bootstrap.php +++ b/tests/integration/bootstrap.php @@ -23,6 +23,13 @@ if ( ! file_exists( $tmdo_core_bootstrap ) ) { } require_once $tmdo_core_bootstrap; +// External object cache switch for the transient filter tests (Redis on real sites). +if ( ! function_exists( 'wp_using_ext_object_cache' ) ) { + function wp_using_ext_object_cache( $using = null ): bool { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found + return ! empty( $GLOBALS['_wp_using_ext_object_cache'] ); + } +} + define( 'TMDO_HIVEPRESS_PATH', dirname( __DIR__, 2 ) . '/' ); define( 'TMDO_HIVEPRESS_URL', 'http://localhost/wp-content/plugins/2meet-data-optimizer-hivepress-addon/' ); define( 'TMDO_HIVEPRESS_FILE', TMDO_HIVEPRESS_PATH . '2meet-data-optimizer-hivepress-addon.php' ); -- 2.43.0 From bab91ef6b999881baa74bbe99ebbb695742908f5 Mon Sep 17 00:00:00 2001 From: wpdev Date: Sun, 27 Sep 2026 09:15:20 +0800 Subject: [PATCH 2/6] docs(2meet-data-optimizer-hivepress-addon): document registered keys HivePress never writes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docs/REGISTERED-KEYS.md lists the six registered keys that are aliases of core columns in the HivePress models (listing hp_vendor → post_parent, hp_status → post_status; request hp_status, hp_user → post_author; vendor hp_user; review hp_rating → comment_karma), with the dev36 evidence (0 meta rows each) and why the schema stays (plan D03). README links it. - PR: PR-082 --- README.md | 2 ++ docs/REGISTERED-KEYS.md | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 docs/REGISTERED-KEYS.md diff --git a/README.md b/README.md index 0cce256..6b31bdd 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,8 @@ reviews / messages / memberships / requests / listing-meta --- +Registered keys HivePress keeps in core columns instead of meta (`hp_vendor`, `hp_status`, …): see [docs/REGISTERED-KEYS.md](docs/REGISTERED-KEYS.md). + ## Requirements - WordPress ≥ 6.0 diff --git a/docs/REGISTERED-KEYS.md b/docs/REGISTERED-KEYS.md new file mode 100644 index 0000000..c02c94c --- /dev/null +++ b/docs/REGISTERED-KEYS.md @@ -0,0 +1,38 @@ +# Registered keys HivePress never writes + +Some keys the AddOn registers in its flat groups are **aliases of core columns** in the +HivePress models: HivePress reads and writes the column, never a meta row. Their flat +columns therefore stay `NULL`, and a meta query on them finds nothing — natively and +routed alike. + +| Entity / group | Key | HivePress model field | Stored in | +|---|---|---|---| +| post / `hp_listing_core` | `hp_vendor` | Listing `vendor` | `wp_posts.post_parent` | +| post / `hp_listing_core` | `hp_status` | Listing `status` | `wp_posts.post_status` | +| post / `hp_request_core` | `hp_status` | Request `status` | `wp_posts.post_status` | +| post / `hp_request_core` | `hp_user` | Request `user` | `wp_posts.post_author` | +| post / `hp_vendor_core` | `hp_user` | Vendor `user` | `wp_posts.post_author` | +| comment / `hp_review` | `hp_rating` | Review `rating` (hivepress-reviews) | `wp_comments.comment_karma` | + +Sources: `'_alias' => …` in `hivepress/includes/models/class-listing.php`, +`class-vendor.php`, `hivepress-requests/includes/models/class-request.php` and +`hivepress-reviews/includes/models/class-review.php`. + +## Checked on dev36 (2026-09-27, 101,175 listings) + +- Native meta rows: `hp_vendor` 0, `hp_status` 0, `hp_user` 0, comment `hp_rating` 0. +- Instead: 101,175 / 101,175 listings have `post_parent > 0`; listing statuses are + `publish` 91,804 / `draft` 9,371; 5,244 of 5,324 `hp_review` comments carry + `comment_karma`. + +## What this means + +- Filters and sorts HivePress builds on these fields use the core columns, never + `meta_query`, so the router has nothing to route for them. +- Drift checks report nothing for them: native and flat are both empty. +- **No schema change** (plan decision D03, 2.2): the columns cost only NULLs, and + dropping them would need a flat-table migration on every site. + +Not in this list: `hp_expired_time` and `hp_featured_time` are real meta fields of the +Listing model (no alias); they are empty on dev36 only because its seeded listings +never expire or get featured. -- 2.43.0 From 99f4653b2505d8f0e69ae874939328c74a614a31 Mon Sep 17 00:00:00 2001 From: wpdev Date: Sun, 27 Sep 2026 09:19:09 +0800 Subject: [PATCH 3/6] docs(2meet-data-optimizer-hivepress-addon): map HivePress extension meta keys and query shapes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docs/HIVEPRESS-COVERAGE.md: read-only inventory on dev36 of the hp_* meta each post type holds (registered or not, queried or not), where the extensions keep their data (comments, taxonomies, user meta) and the query shapes a front-end crawl produced. - Conclusion for plan D04: attribute auto-registration is not needed now — every meta key HivePress queries is registered, select attributes are taxonomies, and the unregistered keys written (hp_due_date, hp_availability, hp_warranty) are never queried; revisit for sites that add filterable or sortable number / date attributes. README links it. - PR: PR-083 --- README.md | 1 + docs/HIVEPRESS-COVERAGE.md | 61 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 docs/HIVEPRESS-COVERAGE.md diff --git a/README.md b/README.md index 6b31bdd..2a5f5de 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ reviews / messages / memberships / requests / listing-meta --- Registered keys HivePress keeps in core columns instead of meta (`hp_vendor`, `hp_status`, …): see [docs/REGISTERED-KEYS.md](docs/REGISTERED-KEYS.md). +What the HivePress extensions write and how they query it (dev36 inventory, D04 decision): see [docs/HIVEPRESS-COVERAGE.md](docs/HIVEPRESS-COVERAGE.md). ## Requirements diff --git a/docs/HIVEPRESS-COVERAGE.md b/docs/HIVEPRESS-COVERAGE.md new file mode 100644 index 0000000..bf7d8ee --- /dev/null +++ b/docs/HIVEPRESS-COVERAGE.md @@ -0,0 +1,61 @@ +# HivePress coverage: what the extensions write and how HivePress queries it + +Read-only inventory on **dev36** (2026-09-27): HivePress 1.7 with blocks, bookings, +favorites, marketplace, memberships, messages, requests, reviews, seo, social-links and +tags; 101,175 listings, 1,499 vendors, 709 requests. The data is seeded, so features the +seed never used show no rows — noted per line. Account pages (favorites, messages, +memberships) were not crawled (login). + +## Meta keys written (rows, `hp_*`) + +| Post type | Key | Rows | Registered group | Queried? | +|---|---|---|---|---| +| hp_listing | `hp_hourly_rate` | 101,157 | `hp_listing_core` | yes — filter BETWEEN, sort | +| hp_listing | `hp_price` | 100,437 | `hp_listing_core` | yes — filter BETWEEN, sort | +| hp_listing | `hp_rating`, `hp_rating_count` | 53,299 | `hp_listing_core` | `hp_rating` sort | +| hp_listing | `hp_verified` | 31,985 | `hp_listing_core` | no | +| hp_listing | `hp_featured` | 5,131 | `hp_listing_core` | yes — featured block (`= CHAR`, `meta_key` + `rand`) | +| hp_listing | `hp_warranty` | 100,401 | — | no (seed data: values are not warranty term ids; see below) | +| hp_vendor | `hp_rating`, `hp_hourly_rate`, `hp_rating_count`, `hp_verified` | 761–1,499 | `hp_vendor_core` | `hp_hourly_rate` filter, `hp_rating` sort | +| hp_vendor | `hp_availability` | 4 | — | no | +| hp_request | `hp_budget` | 709 | `hp_request_core` | yes — filter BETWEEN, `meta_value_num` sort | +| hp_request | `hp_due_date` | 709 | — | no | +| hp_booking | `hp_start_time`, `hp_end_time` | 1 | `hp_booking_core` | no (1 booking) | +| `*_attribute` | `hp_edit_field_*`, `hp_filterable`, … | 3 each | — | attribute settings, read by ID | + +User meta and comment meta: no `hp_*` rows. Term meta: `hp_sort_order` 170, `hp_icon` +153 (`hp_taxonomy`). + +## Where the extensions keep their data + +- **Reviews** — `hp_review` comments (5,324); rating in `comment_karma` (see + [REGISTERED-KEYS.md](REGISTERED-KEYS.md)). +- **Requests / offers** — `hp_request` posts; offers are `hp_offer` comments (48), no meta. +- **Messages** — `hp_message` comments (0 on dev36). +- **Favorites** — 4 user meta rows (no `hp_` prefix); nothing to flatten. +- **Bookings** — `hp_booking` posts (1); **memberships** — no plans on dev36. +- **Tags / categories / select attributes** — taxonomies (`hp_listing_tags` 2,160, + `hp_listing_category` 101,175, `hp_listing_warranty` 720 relationships). A select + filter (`warranty=28`) becomes `tax_query hp_listing_warranty IN`, never a meta query. +- **SEO, social links** — no rows on dev36. + +## Query shapes seen (front-end crawl, 18 URLs) + +- Search filters on number attributes: `hp_price` / `hp_hourly_rate` / `hp_budget` + `BETWEEN DECIMAL` — routed. +- Sort: the named shape `(NOT EXISTS OR EXISTS)` on `hp_price`, `hp_hourly_rate`, + `hp_rating` with `orderby=__order` — routed, flat-first since core 1.2.0; + requests sort by `meta_key=hp_budget` + `meta_value_num` (a secondary query here) — + a shape the router handles. +- Featured block: `meta_key=hp_featured` + `orderby=rand`, and `hp_featured = CHAR`. +- Every front-end search carries WooCommerce's `product_visibility NOT IN` tax query. +- `HivePress\Components\Attribute::set_search_query` runs its own `get_posts()` inside + `pre_get_posts` (suppress_filters): not routable (~100 ms on dev36). + +## D04 (auto-register HivePress attributes): not now + +Every meta key HivePress queries on dev36 is already registered; select attributes are +taxonomies; the unregistered keys that are written (`hp_due_date`, `hp_availability`, +`hp_warranty`) are never queried. Auto-registration would only pay off on a site whose +admin adds **filterable or sortable number / date attributes** — revisit then (a doctor +check listing such attributes without a registered key would flag it). -- 2.43.0 From ef7e005583a44ede8789b14fd305180aedc76360 Mon Sep 17 00:00:00 2001 From: wpdev Date: Sun, 27 Sep 2026 09:20:27 +0800 Subject: [PATCH 4/6] chore(2meet-data-optimizer-hivepress-addon): escape the admin status cells - The detected / bound status cells of the HivePress admin table echo their literal marks through esc_html() (2.1 audit, Info 1, AddOn part); the output is unchanged. - PR: PR-084 --- admin/class-tmdo-admin-hivepress.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/admin/class-tmdo-admin-hivepress.php b/admin/class-tmdo-admin-hivepress.php index ebe1c68..c1638a4 100644 --- a/admin/class-tmdo-admin-hivepress.php +++ b/admin/class-tmdo-admin-hivepress.php @@ -85,9 +85,9 @@ if ( ! class_exists( 'TMDO_Admin_HivePress' ) ) { - + - + -- 2.43.0 From c6faaebbc124bece083ec923b1d40a84201ba8f0 Mon Sep 17 00:00:00 2001 From: wpdev Date: Sun, 27 Sep 2026 09:54:03 +0800 Subject: [PATCH 5/6] docs(2meet-data-optimizer-hivepress-addon): 0.3.0 release notes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Bump to 0.3.0; CHANGELOG entry for PR-081–084, with the post-upgrade cleanup-hp-transients step - Verify: composer test, integration - PR: PR-087 --- 2meet-data-optimizer-hivepress-addon.php | 4 +-- CHANGELOG.md | 33 ++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/2meet-data-optimizer-hivepress-addon.php b/2meet-data-optimizer-hivepress-addon.php index 5ef54fb..35cc1c6 100644 --- a/2meet-data-optimizer-hivepress-addon.php +++ b/2meet-data-optimizer-hivepress-addon.php @@ -3,7 +3,7 @@ * Plugin Name: 2meet Data Optimizer — Hivepress AddOn * Plugin URI: https://2meet.io/2meet-data-optimizer/hivepress * Description: HivePress + 12 擴充整合:13 adapter (core/reviews/bookings/messages/memberships/requests/favorites/statistics/tags/seo/social-links/blocks/marketplace) + 7 HPCT interceptor + 5 query interceptor + admin tab + CLI namespace - * Version: 0.2.0 + * Version: 0.3.0 * Requires at least: 6.0 * Tested up to: 6.9.4 * Requires PHP: 8.1 @@ -24,7 +24,7 @@ if ( ! defined( 'ABSPATH' ) ) { exit; } -define( 'TMDO_HIVEPRESS_VERSION', '0.2.0' ); +define( 'TMDO_HIVEPRESS_VERSION', '0.3.0' ); define( 'TMDO_HIVEPRESS_PATH', plugin_dir_path( __FILE__ ) ); define( 'TMDO_HIVEPRESS_URL', plugin_dir_url( __FILE__ ) ); define( 'TMDO_HIVEPRESS_FILE', __FILE__ ); diff --git a/CHANGELOG.md b/CHANGELOG.md index 03aae0e..acf5429 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,39 @@ Versioning follows [Semantic Versioning](https://semver.org/). --- +## [0.3.0] — 2026-09-27 + +Released with 2meet-data-optimizer 1.2.0 (hardening 2.2). + +### Changed + +- With a persistent object cache (`wp_using_ext_object_cache()`), the + transient filter keeps HivePress's per-post transient pairs in the + `tmdo_hp_transient` cache group instead of `wp_options`, where they piled + up (1.15 M pairs on dev36). Reads fall back to `wp_options` for values + written before; deletes clear both. Without an object cache nothing changes. + After upgrading, remove the rows left behind with core's + `wp tmdo cleanup-hp-transients --options --confirm`. + +### Fixed + +- The detected / bound status cells of the HivePress admin table go through + `esc_html()` (2.1 audit); output unchanged. + +### Docs + +- `docs/REGISTERED-KEYS.md`: the six registered keys HivePress never writes + as meta, because its models alias them to core columns (listing + `hp_vendor` → `post_parent`, `hp_status` → `post_status`; request + `hp_status`, `hp_user`; vendor `hp_user`; review `hp_rating` → + `comment_karma`). The schema stays; their flat columns are always NULL. +- `docs/HIVEPRESS-COVERAGE.md`: dev36 inventory of the meta each HivePress + post type holds, where the extensions keep their data and the query shapes + the front end produces. Attribute auto-registration is not needed now: + every meta key HivePress queries is registered. + +--- + ## [0.2.0] — 2026-09-26 Released with 2meet-data-optimizer 1.1.0 (hardening 2.1, search-index mode). -- 2.43.0 From 788ddccfb32ec71660fcbc10dbb48832ead741cb Mon Sep 17 00:00:00 2001 From: wpdev Date: Sun, 27 Sep 2026 10:32:12 +0800 Subject: [PATCH 6/6] fix(2meet-data-optimizer-hivepress-addon): with an object cache, keep HivePress transients out of wp_options entirely MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - On a cache miss the filter fell back to get_option(): with the values now written to the cache, the option is absent, so WordPress added its name to `notoptions` for good (dev36: +1,000 entries for 1,000 posts, one array loaded on every miss); a hit kept the old row in the options cache group with no expiry, which slowed the selective Redis flush (8.9 s, BUSY). The cache is now the only store for reads and deletes; old rows wait for cleanup-hp-transients --options. Without an object cache nothing changes. - Verify: HivepressTransientFilterTest (2 RED before); dev36 notoptions 210 → 210 over 1,000 posts (was 128 → 1,128) - PR: PR-089c --- CHANGELOG.md | 10 ++++++---- includes/class-tmdo-hivepress-transient-filter.php | 12 +++++++++--- tests/integration/HivepressTransientFilterTest.php | 10 ++++++---- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index acf5429..f57af6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,10 +20,12 @@ Released with 2meet-data-optimizer 1.2.0 (hardening 2.2). - With a persistent object cache (`wp_using_ext_object_cache()`), the transient filter keeps HivePress's per-post transient pairs in the `tmdo_hp_transient` cache group instead of `wp_options`, where they piled - up (1.15 M pairs on dev36). Reads fall back to `wp_options` for values - written before; deletes clear both. Without an object cache nothing changes. - After upgrading, remove the rows left behind with core's - `wp tmdo cleanup-hp-transients --options --confirm`. + up (1.15 M pairs on dev36). The cache is then the only store: rows written + before are neither read nor deleted (HivePress rebuilds those caches once), + because a `get_option()` miss adds the name to WordPress's `notoptions` for + good and a hit keeps the old row in the `options` cache group with no expiry. + Without an object cache nothing changes. After upgrading, remove the rows + left behind with core's `wp tmdo cleanup-hp-transients --options --confirm`. ### Fixed diff --git a/includes/class-tmdo-hivepress-transient-filter.php b/includes/class-tmdo-hivepress-transient-filter.php index 1deadff..8adb2d5 100644 --- a/includes/class-tmdo-hivepress-transient-filter.php +++ b/includes/class-tmdo-hivepress-transient-filter.php @@ -167,9 +167,14 @@ final class TMDO_Hivepress_Transient_Filter { // HivePress cache layer reads the timeout *first*, then decides whether // to read the value. We must preserve raw stored values until HivePress // itself orders deletion. - $value = wp_using_ext_object_cache() ? wp_cache_get( $option_name, self::CACHE_GROUP ) : false; - if ( false === $value ) { - // wp_options: where values went before an object cache was used. + if ( wp_using_ext_object_cache() ) { + // The object cache is the only store. Rows left in wp_options from + // before are not read (`wp tmdo cleanup-hp-transients --options` + // removes them): a get_option() miss would add the name to + // `notoptions` for good, as later writes never reach wp_options. + $value = wp_cache_get( $option_name, self::CACHE_GROUP ); + $value = false === $value ? null : $value; + } else { $value = get_option( $option_name, null ); } @@ -248,6 +253,7 @@ final class TMDO_Hivepress_Transient_Filter { if ( wp_using_ext_object_cache() ) { wp_cache_delete( $option_name, self::CACHE_GROUP ); + return true; } delete_option( $option_name ); return true; diff --git a/tests/integration/HivepressTransientFilterTest.php b/tests/integration/HivepressTransientFilterTest.php index 41e2f9a..3547f20 100644 --- a/tests/integration/HivepressTransientFilterTest.php +++ b/tests/integration/HivepressTransientFilterTest.php @@ -213,16 +213,18 @@ class HivepressTransientFilterTest extends TestCase { $this->assertSame( array( '99999999' ), WPDO_Hivepress_Transient_Filter::on_read( null, 100, '_transient_timeout_hp_models/cat/v1', true ) ); } - public function test_with_an_object_cache_a_value_left_in_wp_options_still_reads(): void { + public function test_with_an_object_cache_a_value_left_in_wp_options_is_not_read(): void { + // A miss must not fall back to get_option(): the option is absent after + // cleanup and WordPress would add its name to `notoptions` for good. $key = '_transient_hp_models/cat/v1'; $translated = WPDO_Hivepress_Transient_Filter::translate_key( 100, $key ); update_option( '_transient_' . $translated, 'from-before' ); $GLOBALS['_wp_using_ext_object_cache'] = true; - $this->assertSame( array( 'from-before' ), WPDO_Hivepress_Transient_Filter::on_read( null, 100, $key, true ) ); + $this->assertNull( WPDO_Hivepress_Transient_Filter::on_read( null, 100, $key, true ) ); } - public function test_with_an_object_cache_delete_clears_both_stores(): void { + public function test_with_an_object_cache_delete_leaves_wp_options_alone(): void { $key = '_transient_hp_models/cat/v1'; $translated = WPDO_Hivepress_Transient_Filter::translate_key( 100, $key ); update_option( '_transient_' . $translated, 'from-before' ); @@ -232,7 +234,7 @@ class HivepressTransientFilterTest extends TestCase { WPDO_Hivepress_Transient_Filter::on_delete( null, 100, $key, '', false ); $this->assertNull( WPDO_Hivepress_Transient_Filter::on_read( null, 100, $key, true ) ); - $this->assertFalse( get_option( '_transient_' . $translated ) ); + $this->assertSame( 'from-before', get_option( '_transient_' . $translated ), 'left for cleanup-hp-transients --options' ); } // ── count/purge legacy ────────────────────────────────────────────────── -- 2.43.0