| @@ -46,8 +46,15 @@ | ||
| 46 | 46 | */ |
| 47 | 47 | const COPIES_SWEEP_BATCH = 50; |
| 48 | 48 | |
| 49 | 49 | /** |
| 50 | + * Lock that keeps two requests from walking the network at once | |
| 51 | + * | |
| 52 | + * @since 2.11.8 | |
| 53 | + */ | |
| 54 | + const COPIES_SWEEP_LOCK = 'vigilante_config_copies_sweep_lock'; | |
| 55 | + | |
| 56 | + /** | |
| 50 | 57 | * Legacy on-disk backup directory (kept only to clean it up on upgrade). |
| 51 | 58 | * |
| 52 | 59 | * @var string |
| 53 | 60 | */ |
| @@ -137,8 +144,9 @@ | ||
| 137 | 144 | } |
| 138 | 145 | |
| 139 | 146 | if ( $state < 1 ) { |
| 140 | 147 | self::purge_stored_copies(); |
| 148 | + update_option( self::COPIES_PURGED_OPTION, 1, true ); | |
| 141 | 149 | } |
| 142 | 150 | |
| 143 | 151 | if ( ! is_multisite() ) { |
| 144 | 152 | update_option( self::COPIES_PURGED_OPTION, 2, true ); |
| @@ -144,34 +152,74 @@ | ||
| 144 | 152 | update_option( self::COPIES_PURGED_OPTION, 2, true ); |
| 145 | 153 | return; |
| 146 | 154 | } |
| 147 | 155 | |
| 148 | - $sweep = get_site_option( self::COPIES_SWEEP_OPTION, 0 ); | |
| 156 | + if ( 'done' !== get_site_option( self::COPIES_SWEEP_OPTION, 0 ) ) { | |
| 157 | + /* | |
| 158 | + * One request walks at a time. Right after an update every request | |
| 159 | + * gets here, and until 2.11.8 each of them repeated the same batch of | |
| 160 | + * sites. The lock lives in the options table of the main site, which | |
| 161 | + * every site of the network reaches the same way. | |
| 162 | + */ | |
| 163 | + switch_to_blog( get_main_site_id() ); | |
| 164 | + $locked = Vigilante_Settings::acquire_option_lock( self::COPIES_SWEEP_LOCK, MINUTE_IN_SECONDS ); | |
| 165 | + restore_current_blog(); | |
| 149 | 166 | |
| 150 | - if ( 'done' !== $sweep ) { | |
| 151 | - global $wpdb; | |
| 167 | + if ( ! $locked ) { | |
| 168 | + return; | |
| 169 | + } | |
| 152 | 170 | |
| 153 | - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- the walk needs the sites after the last one it cleaned, in id order, and get_sites() cannot ask for ids greater than a value. | |
| 154 | - $site_ids = $wpdb->get_col( | |
| 155 | - $wpdb->prepare( | |
| 156 | - "SELECT blog_id FROM {$wpdb->blogs} WHERE site_id = %d AND blog_id > %d ORDER BY blog_id ASC LIMIT %d", | |
| 157 | - get_current_network_id(), | |
| 158 | - (int) $sweep, | |
| 159 | - self::COPIES_SWEEP_BATCH | |
| 160 | - ) | |
| 161 | - ); | |
| 162 | - | |
| 163 | - foreach ( $site_ids as $site_id ) { | |
| 164 | - switch_to_blog( (int) $site_id ); | |
| 165 | - self::purge_stored_copies(); | |
| 171 | + try { | |
| 172 | + self::sweep_next_batch(); | |
| 173 | + } finally { | |
| 174 | + switch_to_blog( get_main_site_id() ); | |
| 175 | + Vigilante_Settings::release_option_lock( self::COPIES_SWEEP_LOCK ); | |
| 166 | 176 | restore_current_blog(); |
| 167 | 177 | } |
| 178 | + } | |
| 168 | 179 | |
| 169 | - $sweep = count( $site_ids ) < self::COPIES_SWEEP_BATCH ? 'done' : (int) end( $site_ids ); | |
| 170 | - update_site_option( self::COPIES_SWEEP_OPTION, $sweep ); | |
| 180 | + if ( 'done' === get_site_option( self::COPIES_SWEEP_OPTION, 0 ) ) { | |
| 181 | + update_option( self::COPIES_PURGED_OPTION, 2, true ); | |
| 171 | 182 | } |
| 183 | + } | |
| 172 | 184 | |
| 173 | - update_option( self::COPIES_PURGED_OPTION, 'done' === $sweep ? 2 : 1, true ); | |
| 185 | + /** | |
| 186 | + * Clean the next batch of sites of the network, with the walk lock held | |
| 187 | + * | |
| 188 | + * @since 2.11.8 | |
| 189 | + */ | |
| 190 | + private static function sweep_next_batch() { | |
| 191 | + global $wpdb; | |
| 192 | + | |
| 193 | + // Read again inside the lock: the request that held it before may have | |
| 194 | + // moved the walk on. | |
| 195 | + // The core caches network options under "$network_id:$option", and absent | |
| 196 | + // ones in "$network_id:notoptions" (wp-includes/option.php:2091 and :2065). | |
| 197 | + wp_cache_delete( get_current_network_id() . ':' . self::COPIES_SWEEP_OPTION, 'site-options' ); | |
| 198 | + wp_cache_delete( get_current_network_id() . ':notoptions', 'site-options' ); | |
| 199 | + $sweep = get_site_option( self::COPIES_SWEEP_OPTION, 0 ); | |
| 200 | + | |
| 201 | + if ( 'done' === $sweep ) { | |
| 202 | + return; | |
| 203 | + } | |
| 204 | + | |
| 205 | + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- the walk needs the sites after the last one it cleaned, in id order, and get_sites() cannot ask for ids greater than a value. | |
| 206 | + $site_ids = $wpdb->get_col( | |
| 207 | + $wpdb->prepare( | |
| 208 | + "SELECT blog_id FROM {$wpdb->blogs} WHERE site_id = %d AND blog_id > %d ORDER BY blog_id ASC LIMIT %d", | |
| 209 | + get_current_network_id(), | |
| 210 | + (int) $sweep, | |
| 211 | + self::COPIES_SWEEP_BATCH | |
| 212 | + ) | |
| 213 | + ); | |
| 214 | + | |
| 215 | + foreach ( $site_ids as $site_id ) { | |
| 216 | + switch_to_blog( (int) $site_id ); | |
| 217 | + self::purge_stored_copies(); | |
| 218 | + restore_current_blog(); | |
| 219 | + } | |
| 220 | + | |
| 221 | + update_site_option( self::COPIES_SWEEP_OPTION, count( $site_ids ) < self::COPIES_SWEEP_BATCH ? 'done' : (int) end( $site_ids ) ); | |
| 174 | 222 | } |
| 175 | 223 | |
| 176 | 224 | /** |
| 177 | 225 | * Get the legacy on-disk backup directory path. |