Changeset 7467 for trunk/src/includes/core/abstraction.php
- Timestamp:
- 09/10/2026 12:35:43 AM (2 weeks ago)
- File:
-
- 1 edited
-
trunk/src/includes/core/abstraction.php (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/includes/core/abstraction.php
r7447 r7467 147 147 function bbp_db() { 148 148 return bbp_get_global_object( 'wpdb', 'WPDB' ); 149 } 150 151 /** 152 * Atomically bump a numeric metadata value using compare-and-swap retries. 153 * 154 * Count updates normally require reading a value, changing it in PHP, and 155 * writing it back. Two requests can read the same value and overwrite one 156 * another's changes. This function avoids that lost update by making the write 157 * conditional on the value that was read. If another request changes the value 158 * first, the condition matches no rows, so the current value is read directly 159 * from the database and the calculation is retried. 160 * 161 * The first read uses the WordPress metadata API and its cache. Missing 162 * metadata is added through add_metadata() so its standard lifecycle continues 163 * to run. Existing metadata uses a conditional database update while preserving 164 * the standard update metadata short-circuit filter and before/after actions. 165 * Before actions run for every conditional attempt, while after actions run 166 * only after a successful write. Metadata caches are cleared between attempts 167 * and after successful writes. Values are sanitized through sanitize_meta(), 168 * cast to integers, and prevented from falling below zero. 169 * 170 * Retries are bounded and filterable. This function does not lock rows or hold 171 * a database transaction open, and returns false when there is no change, a 172 * database operation fails, or all attempts lose to concurrent writes. It is 173 * intended for uniquely keyed numeric count metadata; WordPress metadata tables 174 * do not enforce uniqueness during simultaneous first-time inserts. 175 * 176 * @since 2.6.16 177 * 178 * @see https://bbpress.trac.wordpress.org/ticket/3678 179 * 180 * @param string $meta_type Type of object metadata is for. 181 * @param int $object_id ID of the object metadata is for. 182 * @param string $meta_key Metadata key. 183 * @param int $difference Amount to add to the stored value. 184 * @param int $default Existing value to use when metadata is missing. 185 * @return bool True on success, false on failure or no change. 186 */ 187 function bbp_bump_count_meta( $meta_type = '', $object_id = 0, $meta_key = '', $difference = 1, $default = 0 ) { 188 189 $object_id = (int) $object_id; 190 $difference = (int) $difference; 191 $default = (int) $default; 192 193 // Bail if required values are missing 194 if ( empty( $object_id ) || empty( $meta_key ) || empty( $difference ) ) { 195 return false; 196 } 197 198 /** 199 * Short-circuits bumping numeric metadata. 200 * 201 * Returning a non-null value prevents the normal metadata update. 202 * 203 * @since 2.6.16 204 * 205 * @param null|bool $check Whether to short-circuit the metadata update. 206 * @param string $meta_type Type of object metadata is for. 207 * @param int $object_id ID of the object metadata is for. 208 * @param string $meta_key Metadata key. 209 * @param int $difference Amount to add to the stored value. 210 * @param int $default Existing value to use when metadata is missing. 211 */ 212 $check = apply_filters( 'bbp_pre_bump_count_meta', null, $meta_type, $object_id, $meta_key, $difference, $default ); 213 if ( null !== $check ) { 214 return (bool) $check; 215 } 216 217 /** 218 * Filters the metadata types that support atomic count updates. 219 * 220 * @since 2.6.16 221 * 222 * @param array $meta_types Supported metadata types. 223 * @param string $meta_type Requested metadata type. 224 * @param int $object_id ID of the object metadata is for. 225 * @param string $meta_key Metadata key. 226 */ 227 $meta_types = (array) apply_filters( 'bbp_bump_count_meta_types', array( 'post', 'user', 'term', 'comment' ), $meta_type, $object_id, $meta_key ); 228 229 // Bail if the metadata type is unsupported 230 if ( ! in_array( $meta_type, $meta_types, true ) ) { 231 return false; 232 } 233 234 $bbp_db = bbp_db(); 235 $table_name = sanitize_key( $meta_type . 'meta' ); 236 $table = isset( $bbp_db->{$table_name} ) ? $bbp_db->{$table_name} : ''; 237 $column = sanitize_key( $meta_type . '_id' ); 238 $id_column = ( 'user' === $meta_type ) ? 'umeta_id' : 'meta_id'; 239 240 // Bail if the metadata table does not exist 241 if ( empty( $table ) ) { 242 return false; 243 } 244 245 /** 246 * Filters the maximum number of conditional metadata write attempts. 247 * 248 * @since 2.6.16 249 * 250 * @param int $max_attempts Maximum number of attempts. 251 * @param string $meta_type Type of object metadata is for. 252 * @param int $object_id ID of the object metadata is for. 253 * @param string $meta_key Metadata key. 254 * @param int $difference Amount to add to the stored value. 255 * @param int $default Existing value to use when metadata is missing. 256 */ 257 $max_attempts = (int) apply_filters( 'bbp_bump_count_meta_max_attempts', 5, $meta_type, $object_id, $meta_key, $difference, $default ); 258 $max_attempts = max( 1, $max_attempts ); 259 $checked = false; 260 $subtype = get_object_subtype( $meta_type, $object_id ); 261 $count_query = $bbp_db->prepare( "SELECT meta_value FROM {$table} WHERE meta_key = %s AND {$column} = %d LIMIT 1", $meta_key, $object_id ); 262 263 // Retry when another request updates the same value first 264 for ( $attempt = 0; $attempt < $max_attempts; $attempt++ ) { 265 if ( empty( $attempt ) ) { 266 $exists = metadata_exists( $meta_type, $object_id, $meta_key ); 267 $count = $exists 268 ? (int) get_metadata( $meta_type, $object_id, $meta_key, true ) 269 : $default; 270 } else { 271 $stored = $bbp_db->get_var( $count_query ); 272 273 // Bail on a database error 274 if ( ! empty( $bbp_db->last_error ) ) { 275 return false; 276 } 277 278 $exists = null !== $stored; 279 $count = $exists ? (int) $stored : $default; 280 } 281 282 $new_count = sanitize_meta( $meta_key, bbp_number_not_negative( $count + $difference ), $meta_type, $subtype ); 283 $new_count = (int) $new_count; 284 285 // Allow metadata updates to be short-circuited as usual 286 if ( ! $checked ) { 287 $checked = true; 288 $check = apply_filters( "update_{$meta_type}_metadata", null, $object_id, $meta_key, $new_count, '' ); 289 290 if ( null !== $check ) { 291 return (bool) $check; 292 } 293 } 294 295 // Bail if the count is already at its lower bound 296 if ( $new_count === $count ) { 297 if ( ! empty( $attempt ) ) { 298 return false; 299 } 300 301 $stored = $bbp_db->get_var( $count_query ); 302 303 // Bail on a database error 304 if ( ! empty( $bbp_db->last_error ) ) { 305 return false; 306 } 307 308 $current_exists = null !== $stored; 309 $current_count = $current_exists ? (int) $stored : $default; 310 311 if ( ( $current_exists === $exists ) && ( $current_count === $count ) ) { 312 return false; 313 } 314 315 wp_cache_delete( $object_id, $meta_type . '_meta' ); 316 continue; 317 } 318 319 // Add missing metadata using the standard WordPress lifecycle 320 if ( ! $exists ) { 321 if ( ! empty( add_metadata( $meta_type, $object_id, $meta_key, $new_count, true ) ) ) { 322 return true; 323 } 324 325 $stored = $bbp_db->get_var( $count_query ); 326 327 // Bail if the add failed without a concurrent insert or on a database error 328 if ( ! empty( $bbp_db->last_error ) || ( null === $stored ) ) { 329 return false; 330 } 331 332 wp_cache_delete( $object_id, $meta_type . '_meta' ); 333 continue; 334 } 335 336 $meta_ids = $bbp_db->get_col( $bbp_db->prepare( "SELECT {$id_column} FROM {$table} WHERE meta_key = %s AND {$column} = %d", $meta_key, $object_id ) ); 337 338 // Bail on a database error 339 if ( ! empty( $bbp_db->last_error ) ) { 340 return false; 341 } 342 343 // Retry if metadata was removed after the cached existence check 344 if ( empty( $meta_ids ) ) { 345 wp_cache_delete( $object_id, $meta_type . '_meta' ); 346 continue; 347 } 348 349 // Run the standard actions immediately before the conditional update 350 foreach ( $meta_ids as $meta_id ) { 351 do_action( "update_{$meta_type}_meta", $meta_id, $object_id, $meta_key, $new_count ); 352 353 if ( 'post' === $meta_type ) { 354 do_action( 'update_postmeta', $meta_id, $object_id, $meta_key, $new_count ); 355 } 356 } 357 358 // Compare count metadata numerically to normalize stored numeric strings 359 $updated = $bbp_db->update( 360 $table, 361 array( 'meta_value' => $new_count ), 362 array( 363 $column => $object_id, 364 'meta_key' => $meta_key, 365 'meta_value' => $count 366 ), 367 array( '%d' ), 368 array( '%d', '%s', '%d' ) 369 ); 370 371 // Bail on a database error 372 if ( false === $updated ) { 373 return false; 374 } 375 376 wp_cache_delete( $object_id, $meta_type . '_meta' ); 377 378 // Retry when another request updated the count first 379 if ( empty( $updated ) ) { 380 continue; 381 } 382 383 // Run the standard actions immediately after the conditional update 384 foreach ( $meta_ids as $meta_id ) { 385 do_action( "updated_{$meta_type}_meta", $meta_id, $object_id, $meta_key, $new_count ); 386 387 if ( 'post' === $meta_type ) { 388 do_action( 'updated_postmeta', $meta_id, $object_id, $meta_key, $new_count ); 389 } 390 } 391 392 return true; 393 } 394 395 return false; 149 396 } 150 397
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)