get_results( $wpdb->prepare( 'SELECT option_name, option_value FROM %i WHERE option_name LIKE %s AND CAST(option_value AS UNSIGNED) < %d LIMIT 500', $wpdb->options, $wpdb->esc_like( 'accessible_forms_lock_' ) . '%', time() ), ARRAY_A ); foreach ( $rows as $row ) { self::release( array( $row['option_name'], $row['option_value'] ) ); } if ( count( $rows ) === 500 && ! wp_next_scheduled( 'accessible_forms_purge_batch' ) ) { wp_schedule_single_event( time() + 30, 'accessible_forms_purge_batch' ); } } public static function acquire( $key, $seconds = 30 ) { global $wpdb; $name = 'accessible_forms_lock_' . hash( 'sha256', $key ); $owner = time() + $seconds . ':' . bin2hex( random_bytes( 16 ) ); // Option/object caches are not a concurrency boundary. A long request may // retain a lease already released by another PHP process. The unique index // and conditional takeover below arbitrate against current database state. $inserted = $wpdb->query( $wpdb->prepare( 'INSERT IGNORE INTO %i (option_name, option_value, autoload) VALUES (%s, %s, %s)', $wpdb->options, $name, $owner, 'no' ) ); if ( false === $inserted ) { return false; } $claimed = 1 === $inserted || 1 === $wpdb->query( $wpdb->prepare( "UPDATE %i SET option_value = %s WHERE option_name = %s AND CAST(SUBSTRING_INDEX(option_value, ':', 1) AS UNSIGNED) < %d", $wpdb->options, $owner, $name, time() ) ); wp_cache_delete( $name, 'options' ); wp_cache_delete( 'notoptions', 'options' ); return $claimed ? array( $name, $owner ) : false; } public static function release( $lease ) { if ( ! is_array( $lease ) ) { return; } global $wpdb; $wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE option_name = %s AND option_value = %s', $wpdb->options, $lease[0], $lease[1] ) ); wp_cache_delete( $lease[0], 'options' ); wp_cache_delete( 'notoptions', 'options' ); } public static function limit( $purpose, $limit, $seconds ) { $key = 'accessible_forms_limit_' . hash( 'sha256', $purpose . ':' . (int) floor( time() / $seconds ) ); $lock = self::acquire( $key, 5 ); if ( ! $lock ) { return false; } try { // A different worker may have updated this counter before releasing its lease. wp_cache_delete( $key, 'transient' ); wp_cache_delete( '_transient_' . $key, 'options' ); wp_cache_delete( '_transient_timeout_' . $key, 'options' ); $count = (int) get_transient( $key ); if ( $count >= $limit ) { return false; } return set_transient( $key, $count + 1, $seconds ) && (int) get_transient( $key ) === $count + 1; } finally { self::release( $lock ); } } }