Hollis Creek Family Dental · Repair report

Exhibit C-2 Evidence: commit 3652956

Committed · 3652956fa6e41393f1a11c609cbf04c6eb9a00f6

Every request loaded megabytes of dead cache

The repository itself is private; this page carries the commit as it is recorded there.

Commit message

Fix C-2: remove ~3.5 MB of autoloaded options left by a dead feed plugin

Root cause: HC Social Feed stored every response of its feed service as a
new option with autoload 'yes'. The service closed in 2023 and the widget
has rendered nothing since, but the cached responses (~3.5 MB) were still
loaded from the database and unserialized on every single request.

Fix: remove the dead plugin, and add the repair migrations mu-plugin
(`wp hollis migrate`, run by deploy.sh) with a first step that deletes the
plugin's orphaned options. Steps check state first, so a second run is a
no-op.

Proof: tests/phpunit/repair/AutoloadTest.php (autoload under Site Health's
800 KB limit after the migration).

Exhibit: C-2

The change

Text files only, as recorded in git. Lines starting with + were added, lines with − removed.

Files changed

---
 site/wp-content/mu-plugins/hollis-migrations.php          | 69 +++++++++++++++++++++++++++++++++++
 site/wp-content/plugins/hc-social-feed/hc-social-feed.php | 41 ---------------------
 tests/phpunit/repair/AutoloadTest.php                     | 50 +++++++++++++++++++++++++
 3 files changed, 119 insertions(+), 41 deletions(-)
diff --git a/site/wp-content/mu-plugins/hollis-migrations.php b/site/wp-content/mu-plugins/hollis-migrations.php
new file mode 100644
index 0000000..6a79548
--- /dev/null
+++ b/site/wp-content/mu-plugins/hollis-migrations.php
@@ -0,0 +1,69 @@
+<?php
+/**
+ * Plugin Name: Hollis Creek repair migrations
+ * Description: Database changes that belong to the repair. Each step checks the current state first, so
+ *              running them again changes nothing. Run by deploy.sh: `wp hollis migrate`.
+ * Version:     1.0.0
+ * Author:      Hollis Creek Family Dental (fictional demo)
+ *
+ * @package Hollis\Migrations
+ */
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * The steps, in order. Each returns a one-line description of what it did.
+ *
+ * @return array<string, callable>
+ */
+function hollis_migrations_steps() {
+	return apply_filters(
+		'hollis_migrations_steps',
+		array(
+			'remove-social-feed-caches' => 'hollis_migration_remove_social_feed_caches',
+		)
+	);
+}
+
+/**
+ * Runs every step.
+ *
+ * @return array<string, string> Step name => result.
+ */
+function hollis_migrations_run() {
+	$results = array();
+	foreach ( hollis_migrations_steps() as $name => $step ) {
+		$results[ $name ] = (string) call_user_func( $step );
+	}
+	update_option( 'hollis_migrations_last_run', array( gmdate( 'c' ), $results ), false );
+	return $results;
+}
+
+/**
+ * C-2: HC Social Feed is removed; its cached responses (autoloaded, ~3.5 MB) go with it.
+ *
+ * @return string
+ */
+function hollis_migration_remove_social_feed_caches() {
+	global $wpdb;
+	$like  = $wpdb->esc_like( 'hcsf_' ) . '%';
+	$names = $wpdb->get_col( $wpdb->prepare( "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s", $like ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
+	$bytes = 0;
+	foreach ( $names as $name ) {
+		$bytes += strlen( maybe_serialize( get_option( $name ) ) );
+		delete_option( $name );
+	}
+	return sprintf( 'deleted %d social feed options (%s)', count( $names ), size_format( $bytes, 1 ) );
+}
+
+if ( defined( 'WP_CLI' ) && WP_CLI ) {
+	WP_CLI::add_command(
+		'hollis migrate',
+		static function () {
+			foreach ( hollis_migrations_run() as $name => $result ) {
+				WP_CLI::log( "$name: $result" );
+			}
+			WP_CLI::success( 'Repair migrations are up to date.' );
+		}
+	);
+}
diff --git a/site/wp-content/plugins/hc-social-feed/hc-social-feed.php b/site/wp-content/plugins/hc-social-feed/hc-social-feed.php
deleted file mode 100644
index a2c4ab9..0000000
--- a/site/wp-content/plugins/hc-social-feed/hc-social-feed.php
+++ /dev/null
@@ -1,41 +0,0 @@
-<?php
-/**
- * Plugin Name: HC Social Feed
- * Description: Shows the practice's latest social posts in the footer. Caches each feed response in an option.
- * Version:     1.1.0
- * Author:      Previous web developer (fictional)
- *
- * The feed service it reads was shut down in 2023; the widget has rendered nothing since, but every cached
- * response it ever stored is still an autoloaded option.
- *
- * @package HC\SocialFeed
- */
-
-defined( 'ABSPATH' ) || exit;
-
-/**
- * Stores a feed response. Called by the hourly refresh, which stopped when the service closed.
- *
- * @param string $url  Feed URL.
- * @param array  $data Response.
- */
-function hc_social_feed_store( $url, $data ) {
-	$key = 'hcsf_cache_' . md5( $url . gmdate( 'YmdH' ) );
-	add_option( $key, $data, '', 'yes' );
-}
-
-/**
- * Footer widget: the latest cached posts.
- */
-function hc_social_feed_render() {
-	$items = get_option( 'hcsf_latest', array() );
-	if ( empty( $items['posts'] ) ) {
-		return;
-	}
-	echo '<ul class="social-feed">';
-	foreach ( array_slice( $items['posts'], 0, 3 ) as $post ) {
-		echo '<li>' . esc_html( $post['text'] ) . '</li>';
-	}
-	echo '</ul>';
-}
-add_action( 'clinicpress_footer_column', 'hc_social_feed_render', 20 );
diff --git a/tests/phpunit/repair/AutoloadTest.php b/tests/phpunit/repair/AutoloadTest.php
new file mode 100644
index 0000000..4762ddf
--- /dev/null
+++ b/tests/phpunit/repair/AutoloadTest.php
@@ -0,0 +1,50 @@
+<?php
+/**
+ * Exhibit C-2: what every request loads from wp_options stays small.
+ *
+ * @package Hollis\Tests
+ */
+
+/**
+ * Autoloaded options.
+ */
+class AutoloadTest extends WP_UnitTestCase {
+
+	/**
+	 * Bytes of autoloaded option values, the way WordPress loads them on every request.
+	 *
+	 * @return int
+	 */
+	private function autoload_bytes() {
+		global $wpdb;
+		$in = "'" . implode( "','", wp_autoload_values_to_autoload() ) . "'";
+		return (int) $wpdb->get_var( "SELECT SUM(LENGTH(option_value)) FROM {$wpdb->options} WHERE autoload IN ($in)" ); // phpcs:ignore
+	}
+
+	public function test_migration_removes_the_dead_feed_caches_and_autoload_drops_under_site_health_limit() {
+		// What the old plugin left behind: ten cached responses of ~300 KB, autoloaded.
+		for ( $i = 0; $i < 10; $i++ ) {
+			add_option( 'hcsf_cache_' . md5( (string) $i ), array( 'posts' => array_fill( 0, 800, str_repeat( 'smile creek ', 30 ) ) ), '', 'yes' );
+		}
+		add_option( 'hcsf_latest', array( 'posts' => array() ), '', 'yes' );
+		$this->assertGreaterThan( 2 * MB_IN_BYTES, $this->autoload_bytes() );
+
+		$results = hollis_migrations_run();
+
+		$this->assertStringContainsString( 'deleted 11 social feed options', $results['remove-social-feed-caches'] );
+		// Site Health warns above 800 KB of autoloaded options.
+		$this->assertLessThan( 800 * KB_IN_BYTES, $this->autoload_bytes() );
+		wp_cache_delete( 'alloptions', 'options' );
+		$this->assertSame( array(), preg_grep( '/^hcsf_/', array_keys( wp_load_alloptions() ) ) );
+	}
+
+	public function test_running_again_changes_nothing() {
+		hollis_migrations_run();
+		$again = hollis_migrations_run();
+		$this->assertStringContainsString( 'deleted 0 social feed options', $again['remove-social-feed-caches'] );
+	}
+
+	public function test_the_dead_plugin_is_gone_from_the_code() {
+		$this->assertDirectoryDoesNotExist( HOLLIS_CODE . '/plugins/hc-social-feed' );
+	}
+}

The proving test

tests/phpunit/repair/AutoloadTest.php

tests/phpunit/repair/AutoloadTest.php, as committed.

<?php
/**
 * Exhibit C-2: what every request loads from wp_options stays small.
 *
 * @package Hollis\Tests
 */

/**
 * Autoloaded options.
 */
class AutoloadTest extends WP_UnitTestCase {

	/**
	 * Bytes of autoloaded option values, the way WordPress loads them on every request.
	 *
	 * @return int
	 */
	private function autoload_bytes() {
		global $wpdb;
		$in = "'" . implode( "','", wp_autoload_values_to_autoload() ) . "'";
		return (int) $wpdb->get_var( "SELECT SUM(LENGTH(option_value)) FROM {$wpdb->options} WHERE autoload IN ($in)" ); // phpcs:ignore
	}

	public function test_migration_removes_the_dead_feed_caches_and_autoload_drops_under_site_health_limit() {
		// What the old plugin left behind: ten cached responses of ~300 KB, autoloaded.
		for ( $i = 0; $i < 10; $i++ ) {
			add_option( 'hcsf_cache_' . md5( (string) $i ), array( 'posts' => array_fill( 0, 800, str_repeat( 'smile creek ', 30 ) ) ), '', 'yes' );
		}
		add_option( 'hcsf_latest', array( 'posts' => array() ), '', 'yes' );
		$this->assertGreaterThan( 2 * MB_IN_BYTES, $this->autoload_bytes() );

		$results = hollis_migrations_run();

		$this->assertStringContainsString( 'deleted 11 social feed options', $results['remove-social-feed-caches'] );
		// Site Health warns above 800 KB of autoloaded options.
		$this->assertLessThan( 800 * KB_IN_BYTES, $this->autoload_bytes() );
		wp_cache_delete( 'alloptions', 'options' );
		$this->assertSame( array(), preg_grep( '/^hcsf_/', array_keys( wp_load_alloptions() ) ) );
	}

	public function test_running_again_changes_nothing() {
		hollis_migrations_run();
		$again = hollis_migrations_run();
		$this->assertStringContainsString( 'deleted 0 social feed options', $again['remove-social-feed-caches'] );
	}

	public function test_the_dead_plugin_is_gone_from_the_code() {
		$this->assertDirectoryDoesNotExist( HOLLIS_CODE . '/plugins/hc-social-feed' );
	}
}

Back to Exhibit C-2