PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.3-beta
16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 All 507 releases
jetpack / jetpack_vendor / automattic / jetpack-stats / src / class-transient-cleanup.php

class-transient-cleanup.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-beta, at jetpack_vendor/automattic/jetpack-stats/src/class-transient-cleanup.php

245 lines 7.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Stats Transient Cleanup
4 *
5 * Handles cleanup of expired stats cache transients to prevent database bloat
6 * on sites without a persistent object cache.
7 *
8 * Adapted from Brute Force Protection transient cleanup.
9 *
10 * @package automattic/jetpack-stats
11 */
12
13 namespace Automattic\Jetpack\Stats;
14
15 /**
16 * Transient_Cleanup class.
17 *
18 * Provides a scheduled cron job to clean up expired stats transients.
19 * WordPress transient garbage collection is "lazy" - expired transients are only
20 * deleted when accessed via get_transient(). Stats transients use dynamic cache keys
21 * based on query parameters, so expired entries are rarely accessed again and
22 * accumulate indefinitely in wp_options on sites without an external object cache.
23 *
24 * @since 0.18.0
25 */
26 class Transient_Cleanup {
27
28 /**
29 * Cron hook name.
30 */
31 const CRON_HOOK = 'jetpack_stats_transient_cleanup';
32
33 /**
34 * Batch size for transient cleanup.
35 *
36 * Used as the SQL LIMIT per prefix and as the threshold for stopping iteration.
37 * Actual deletions per run may slightly exceed this when processing multiple prefixes.
38 */
39 const BATCH_SIZE = 5000;
40
41 /**
42 * Cron interval in seconds (8 hours).
43 */
44 const CRON_INTERVAL = 28800;
45
46 /**
47 * Initialize the transient cleanup.
48 *
49 * @return void
50 */
51 public static function init() {
52 // Register the cron hook.
53 add_action( self::CRON_HOOK, array( static::class, 'run_cleanup' ) );
54
55 // Register custom cron schedule.
56 // phpcs:ignore WordPress.WP.CronInterval.ChangeDetected -- 8 hours is intentional for batched transient cleanup.
57 add_filter( 'cron_schedules', array( static::class, 'add_cron_schedule' ) );
58
59 // Schedule the cron job if not already scheduled.
60 add_action( 'admin_init', array( static::class, 'schedule_cleanup' ) );
61 }
62
63 /**
64 * Add custom cron schedule for 8-hour intervals.
65 *
66 * @param array $schedules Existing cron schedules.
67 * @return array Modified cron schedules.
68 */
69 public static function add_cron_schedule( $schedules ) {
70 if ( ! isset( $schedules['jetpack_stats_eight_hours'] ) ) {
71 $schedules['jetpack_stats_eight_hours'] = array(
72 'interval' => self::CRON_INTERVAL,
73 'display' => __( 'Every Eight Hours', 'jetpack-stats-pkg' ),
74 );
75 }
76 return $schedules;
77 }
78
79 /**
80 * Schedule the cleanup cron job.
81 *
82 * Skips scheduling on sites with persistent object cache since transients
83 * auto-expire there and cleanup is unnecessary.
84 *
85 * @return void
86 */
87 public static function schedule_cleanup() {
88 // Skip scheduling on sites with persistent object cache - not needed there.
89 if ( wp_using_ext_object_cache() ) {
90 // Unschedule if it was previously scheduled (e.g., object cache was added later).
91 if ( wp_next_scheduled( self::CRON_HOOK ) ) {
92 wp_clear_scheduled_hook( self::CRON_HOOK );
93 }
94 return;
95 }
96
97 if ( ! wp_next_scheduled( self::CRON_HOOK ) ) {
98 wp_schedule_event( time() + self::CRON_INTERVAL, 'jetpack_stats_eight_hours', self::CRON_HOOK );
99 }
100 }
101
102 /**
103 * Unschedule the cleanup cron job.
104 * Hooked to 'jetpack_deactivate_module_stats' in Main::__construct().
105 *
106 * @return void
107 */
108 public static function unschedule_cleanup() {
109 wp_clear_scheduled_hook( self::CRON_HOOK );
110 }
111
112 /**
113 * Get the list of transient prefixes to clean up.
114 *
115 * @return array List of transient prefixes.
116 */
117 public static function get_transient_prefixes() {
118 $prefixes = array(
119 WPCOM_Stats::STATS_CACHE_TRANSIENT_PREFIX, // jetpack_restapi_stats_cache_
120 );
121
122 /**
123 * Filter the list of transient prefixes to clean up.
124 *
125 * @since 0.18.0
126 *
127 * @param array $prefixes List of transient prefixes.
128 */
129 $filtered = apply_filters( 'jetpack_stats_transient_cleanup_prefixes', $prefixes );
130
131 // Normalize filtered value: ensure array, filter to non-empty strings, dedupe.
132 if ( ! is_array( $filtered ) ) {
133 $filtered = $prefixes;
134 }
135
136 $filtered = array_filter(
137 $filtered,
138 function ( $prefix ) {
139 return is_string( $prefix ) && '' !== $prefix;
140 }
141 );
142
143 return array_unique( $filtered );
144 }
145
146 /**
147 * Run the transient cleanup.
148 *
149 * @return int|false Number of deleted transient options (two entries per transient), or false if skipped.
150 */
151 public static function run_cleanup() {
152 /**
153 * Filter to disable transient cleanup.
154 *
155 * @since 0.18.0
156 *
157 * @param bool $disabled Whether to disable transient cleanup. Default false.
158 */
159 if ( apply_filters( 'jetpack_stats_transient_cleanup_disabled', false ) ) {
160 return false;
161 }
162
163 // Skip if using external object cache - transients auto-expire there.
164 if ( wp_using_ext_object_cache() ) {
165 return false;
166 }
167
168 $total_deleted = 0;
169 $prefixes = self::get_transient_prefixes();
170
171 foreach ( $prefixes as $prefix ) {
172 $deleted = self::purge_expired_transients( $prefix );
173 $total_deleted += $deleted;
174
175 // Stop processing additional prefixes once we've reached the batch threshold.
176 // Note: total may exceed BATCH_SIZE since each prefix can delete up to BATCH_SIZE.
177 if ( $total_deleted >= self::BATCH_SIZE ) {
178 break;
179 }
180 }
181
182 return $total_deleted;
183 }
184
185 /**
186 * Purge expired transients for a specific prefix.
187 *
188 * @param string $prefix The transient prefix to clean up.
189 * @return int Number of deleted transient options, two entries for each transient.
190 */
191 private static function purge_expired_transients( $prefix ) {
192 global $wpdb;
193
194 $now = time();
195 $timeout_prefix = '_transient_timeout_' . $prefix;
196 $like_pattern = $wpdb->esc_like( $timeout_prefix ) . '%';
197
198 // Find expired transients by querying timeout entries.
199 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
200 $transients = $wpdb->get_col(
201 $wpdb->prepare(
202 "SELECT SUBSTRING(option_name, %d) AS transient_name FROM {$wpdb->options} WHERE option_name LIKE %s AND option_value < %d LIMIT %d",
203 strlen( $timeout_prefix ) + 1,
204 $like_pattern,
205 $now,
206 self::BATCH_SIZE
207 )
208 );
209
210 if ( empty( $transients ) ) {
211 return 0;
212 }
213
214 // Build list of option names to delete (both transient and timeout entries).
215 $option_names = array();
216 foreach ( $transients as $transient ) {
217 $option_names[] = '_transient_' . $prefix . $transient;
218 $option_names[] = '_transient_timeout_' . $prefix . $transient;
219 }
220
221 // Delete in chunks to avoid excessively long SQL queries.
222 // Each option name can be ~80 chars, so 50 items ≈ 4KB per query.
223 $chunks = array_chunk( $option_names, 50 );
224 $total_deleted = 0;
225
226 foreach ( $chunks as $chunk ) {
227 $placeholders = implode( ', ', array_fill( 0, count( $chunk ), '%s' ) );
228
229 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
230 $result = $wpdb->query(
231 $wpdb->prepare(
232 "DELETE FROM {$wpdb->options} WHERE option_name IN ($placeholders)", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $placeholders is a list of %s.
233 $chunk
234 )
235 );
236
237 if ( false !== $result ) {
238 $total_deleted += $result;
239 }
240 }
241
242 return $total_deleted;
243 }
244 }
245