504 lines
15 KiB
PHP
504 lines
15 KiB
PHP
<?php
|
|
/**
|
|
* Simply Schedule Appointments Availability Cache.
|
|
*
|
|
* @since 4.0.1
|
|
* @package Simply_Schedule_Appointments
|
|
*/
|
|
use League\Period\Period;
|
|
|
|
/**
|
|
* Simply Schedule Appointments Availability Cache.
|
|
*
|
|
* @since 4.0.1
|
|
*/
|
|
class SSA_Availability_Cache {
|
|
/**
|
|
* Parent plugin class.
|
|
*
|
|
* @since 4.0.1
|
|
*
|
|
* @var Simply_Schedule_Appointments
|
|
*/
|
|
protected $plugin = null;
|
|
|
|
const CACHE_MODE_DISABLED = 0;
|
|
const CACHE_MODE_ENABLED = 10;
|
|
|
|
/**
|
|
* Constructor.
|
|
*
|
|
* @since 4.0.1
|
|
*
|
|
* @param Simply_Schedule_Appointments $plugin Main plugin object.
|
|
*/
|
|
public function __construct( $plugin ) {
|
|
$this->plugin = $plugin;
|
|
$this->hooks();
|
|
}
|
|
|
|
/**
|
|
* Initiate our hooks.
|
|
*
|
|
* @since 4.0.1
|
|
*/
|
|
public function hooks() {
|
|
// add_action( 'ssa_populate_cache', array( $this, 'async_process_populate_cache' ), 10, 2 );
|
|
}
|
|
|
|
public function generate_cache_key( $args ) {
|
|
$args = $this->get_args( $args );
|
|
$args['start_date'] = '1970-01-01';
|
|
$args['end_date'] = '1970-01-01';
|
|
$availability_id = $this->plugin->availability_model->insert( $args );
|
|
|
|
$obj_cache_key = 'args:'.$args['cache_args_hash'].'/latest_cache_key';
|
|
ssa_cache_set( $obj_cache_key, $availability_id );
|
|
|
|
return $availability_id;
|
|
}
|
|
|
|
public function get_cache_args_hash( $args ) {
|
|
return ssa_int_hash( json_encode( $args ) );
|
|
}
|
|
|
|
public function get_args( $args ) {
|
|
$args = shortcode_atts( array(
|
|
'appointment_type_id' => 0,
|
|
'appointment_id' => 0,
|
|
'staff_id' => 0,
|
|
'resource_group_id' => 0,
|
|
'type' => '',
|
|
'subtype' => '',
|
|
'skip_appointment_id' => '',
|
|
|
|
'cache_key' => '',
|
|
'cache_args_hash' => '',
|
|
'cache_force' => false,
|
|
'cache_level_read' => 1,
|
|
'cache_level_write' => 1,
|
|
), $args );
|
|
|
|
$args_to_hash = $args;
|
|
$args_to_hash['cache_key'] = '';
|
|
$args_to_hash['cache_args_hash'] = '';
|
|
$args_to_hash['cache_level_read'] = '';
|
|
$args_to_hash['cache_level_write'] = '';
|
|
$args_to_hash['cache_force'] = false;
|
|
|
|
$args['cache_args_hash'] = $this->get_cache_args_hash( $args_to_hash );
|
|
|
|
return $args;
|
|
}
|
|
|
|
public function get_cache_mode() {
|
|
$developer_settings = $this->plugin->developer_settings->get();
|
|
if ( ! empty( $developer_settings['disable_availability_caching'] ) ) {
|
|
return self::CACHE_MODE_DISABLED;
|
|
}
|
|
|
|
return self::CACHE_MODE_ENABLED;
|
|
}
|
|
|
|
public function is_cache_mode( $mode ) {
|
|
return $mode === $this->get_cache_mode();
|
|
}
|
|
|
|
public function is_enabled() {
|
|
return ! $this->is_cache_mode( self::CACHE_MODE_DISABLED );
|
|
}
|
|
|
|
public function get_latest_cache_key( $args ) {
|
|
$args = $this->get_args( $args );
|
|
|
|
$obj_cache_key = 'args:'.$args['cache_args_hash'].'/latest_cache_key';
|
|
|
|
$latest_cache_key = ssa_cache_get( $obj_cache_key );
|
|
if ( false !== $latest_cache_key ) {
|
|
return $latest_cache_key;
|
|
}
|
|
|
|
global $wpdb;
|
|
$sql = 'SELECT cache_key FROM '.$this->plugin->availability_model->get_table_name().' WHERE cache_args_hash=%d ORDER BY cache_key DESC LIMIT 1';
|
|
$sql = $wpdb->prepare(
|
|
$sql,
|
|
$args['cache_args_hash']
|
|
);
|
|
$latest_cache_key = $wpdb->get_row( $sql, ARRAY_A );
|
|
$latest_cache_key = $latest_cache_key['cache_key'];
|
|
ssa_cache_set( $obj_cache_key, $latest_cache_key );
|
|
|
|
return $latest_cache_key;
|
|
}
|
|
|
|
public function query( SSA_Appointment_Type_Object $appointment_type, Period $query_period, $args ) {
|
|
if ( ! $this->is_enabled() ) {
|
|
return;
|
|
}
|
|
if ( empty( $args['cache_level_read'] ) || $args['cache_level_read'] > 2 ) {
|
|
return;
|
|
}
|
|
|
|
$appointment_type_id = ( empty( $appointment_type ) ) ? 0 : $appointment_type->id;
|
|
if ( ! empty( $appointment_type_id ) || empty( $args['appointment_type_id'] ) ) {
|
|
$args['appointment_type_id'] = $appointment_type_id;
|
|
}
|
|
$args = $this->get_args( $args );
|
|
|
|
$query_args = array(
|
|
'number' => -1,
|
|
'cache_args_hash' => $args['cache_args_hash'],
|
|
'intersects_period' => $query_period,
|
|
'order' => 'ASC',
|
|
'orderby' => 'start_date',
|
|
);
|
|
// $latest_cache_key = $this->get_latest_cache_key( $args );
|
|
// if ( ! empty( $latest_cache_key ) ) {
|
|
// $query_args['cache_key'] = $latest_cache_key;
|
|
// }
|
|
|
|
$availability_rows = $this->plugin->availability_model->query( $query_args );
|
|
|
|
$schedule = new SSA_Availability_Schedule();
|
|
$availability_blocks = array();
|
|
foreach ($availability_rows as $availability_row) {
|
|
if ( $availability_row['start_date'] > $availability_row['end_date'] ) {
|
|
ssa_debug_log( 'availability-cache query()', 100 );
|
|
ssa_debug_log( 'Invalid Period returned in query', 100 );
|
|
$this->plugin->availability_model->truncate();
|
|
return;
|
|
}
|
|
unset( $availability_row['id'] );
|
|
$start_date = $availability_row['start_date'];
|
|
$availability_row = shortcode_atts( array(
|
|
'capacity_available' => '',
|
|
'capacity_reserved' => '',
|
|
'buffer_available' => '',
|
|
'buffer_reserved' => '',
|
|
'period' => new Period(
|
|
$availability_row['start_date'],
|
|
$availability_row['end_date']
|
|
),
|
|
), $availability_row );
|
|
$availability_block = SSA_Availability_Block_Factory::create( $availability_row );
|
|
$availability_blocks[$start_date] = $availability_block;
|
|
}
|
|
|
|
$schedule = $schedule->set_blocks( $availability_blocks, true ); // added for performance October 19, 2023
|
|
// $schedule = $schedule->pushmerge( $availability_blocks ); // removed October 19, 2023
|
|
|
|
if ( $schedule->is_empty() ) {
|
|
return;
|
|
}
|
|
$boundaries = $schedule->boundaries();
|
|
if ( empty( $boundaries ) || ! $boundaries instanceof Period ) {
|
|
return;
|
|
}
|
|
|
|
if ( ! $boundaries->contains( $query_period ) ) {
|
|
return;
|
|
}
|
|
|
|
if ( ! $schedule->is_continuous() ) {
|
|
return;
|
|
}
|
|
|
|
return $schedule;
|
|
}
|
|
|
|
private function insert( SSA_Availability_Block $block, $args = array() ) {
|
|
if ( ! $this->is_enabled() ) {
|
|
return;
|
|
}
|
|
// $args = $this->get_args( $args ); // <--- not needed if insert() remains a private function
|
|
|
|
$args = array_merge( $args, array(
|
|
'start_date' => $block->get_period()->getStartDate()->format( 'Y-m-d H:i:s' ),
|
|
'end_date' => $block->get_period()->getEndDate()->format( 'Y-m-d H:i:s' ),
|
|
'capacity_reserved' => $block->capacity_reserved,
|
|
'capacity_available' => $block->capacity_available,
|
|
) );
|
|
|
|
$availability_id = $this->plugin->availability_model->db_insert( $args );
|
|
}
|
|
|
|
public function insert_schedule( SSA_Availability_Schedule $schedule, $args = array() ) {
|
|
if ( ! $this->is_enabled() ) {
|
|
return;
|
|
}
|
|
if ( empty( $args['cache_level_write'] ) || $args['cache_level_write'] > 2 ) {
|
|
return;
|
|
}
|
|
$args = $this->get_args( $args );
|
|
|
|
unset( $args['cache_level_read'] );
|
|
unset( $args['cache_level_write'] );
|
|
$args['cache_key'] = $this->generate_cache_key( $args );
|
|
|
|
$availability_rows = array();
|
|
foreach ($schedule->get_blocks() as $block) {
|
|
$availability_rows[] = array_merge( $args, array(
|
|
'start_date' => $block->get_period()->getStartDate()->format( 'Y-m-d H:i:s' ),
|
|
'end_date' => $block->get_period()->getEndDate()->format( 'Y-m-d H:i:s' ),
|
|
'capacity_reserved' => $block->capacity_reserved,
|
|
'capacity_available' => $block->capacity_available,
|
|
'buffer_reserved' => $block->buffer_reserved,
|
|
'buffer_available' => $block->buffer_available,
|
|
) );
|
|
}
|
|
$this->plugin->availability_model->db_bulk_insert( $availability_rows );
|
|
|
|
$this->delete_schedule( $schedule, $args['cache_args_hash'], $args['cache_key'] );
|
|
}
|
|
|
|
public function delete_schedule( SSA_Availability_Schedule $schedule, $cache_args_hash, $below_this_cache_key = null ) {
|
|
$boundaries = $schedule->boundaries();
|
|
if ( empty( $boundaries ) ) {
|
|
return;
|
|
}
|
|
global $wpdb;
|
|
$start_date_string = $boundaries->getStartDate()->format( 'Y-m-d H:i:s' );
|
|
$end_date_string = $boundaries->getEndDate()->format( 'Y-m-d H:i:s' );
|
|
|
|
$sql = 'DELETE FROM '.$this->plugin->availability_model->get_table_name()." WHERE cache_args_hash=%d AND (
|
|
(end_date > '{$start_date_string}' AND end_date < '{$end_date_string}' )
|
|
OR
|
|
(start_date < '{$end_date_string}' AND start_date > '{$start_date_string}' )
|
|
OR
|
|
(start_date < '{$start_date_string}' AND end_date > '{$end_date_string}' )
|
|
)"; // same as intersects_period, except we use < rather than <= (so we don't delete the neighboring availability). This will delete any availability cache that starts in the period OR ends in the period OR contains the entire period
|
|
|
|
$sql = $wpdb->prepare(
|
|
$sql, array(
|
|
$cache_args_hash,
|
|
)
|
|
);
|
|
if ( empty( $below_this_cache_key ) ) {
|
|
$this->wpdb_query_while_preventing_deadlock($sql);
|
|
return;
|
|
}
|
|
|
|
$sql .= $wpdb->prepare( ' AND cache_key < %d', $below_this_cache_key );
|
|
$this->wpdb_query_while_preventing_deadlock($sql);
|
|
|
|
$sql = 'DELETE FROM '.$this->plugin->availability_model->get_table_name().' WHERE id=%d';
|
|
$sql = $wpdb->prepare(
|
|
$sql,
|
|
$below_this_cache_key
|
|
);
|
|
$this->wpdb_query_while_preventing_deadlock( $sql );
|
|
}
|
|
|
|
private function wpdb_query_while_preventing_deadlock( $sql ) {
|
|
global $wpdb;
|
|
// This query can potentially be deadlocked if there is
|
|
// high concurrency, in which case DB will abort the query which has done less work to resolve deadlock.
|
|
// We will try up to 3 times before giving up.
|
|
for ($count = 0; $count < 10; $count++) {
|
|
$result = $wpdb->query( $sql ); // WPCS: unprepared SQL ok.
|
|
if ( false !== $result ) {
|
|
if( $count > 0 ) {
|
|
ssa_debug_log("Deadlock successfully resolved after trying " . ( $count + 1 ) . " times");
|
|
}
|
|
break;
|
|
}
|
|
// sleep 0, 1 or 2 seconds randomly
|
|
sleep( rand( 0, 2 ) );
|
|
}
|
|
if ( false === $result ) {
|
|
ssa_debug_log("Deadlock could not be resolved after trying 10 times. Query: " . $sql);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
public static function object_cache_get( $key, $group = '', $force = false, &$found = null ) {
|
|
if ( ! ssa()->availability_cache->is_enabled() ) {
|
|
return false;
|
|
}
|
|
|
|
$group .= SSA_Availability_Cache_Invalidation::get_cache_group();
|
|
$key = 'ssa/' . $group . '/' . $key;
|
|
|
|
return get_transient( $key );
|
|
}
|
|
|
|
public static function object_cache_set( $key, $data, $group = '', $expire = 0 ) {
|
|
if ( ! ssa()->availability_cache->is_enabled() ) {
|
|
return false;
|
|
}
|
|
|
|
$group .= SSA_Availability_Cache_Invalidation::get_cache_group();
|
|
$key = 'ssa/' . $group . '/' . $key;
|
|
if ( empty( $expire ) ) {
|
|
$expire = WEEK_IN_SECONDS;
|
|
}
|
|
|
|
set_transient( $key, $data, $expire );
|
|
return true;
|
|
}
|
|
|
|
public function remember_recent( $key, $value, $number_to_remember = 10 ) {
|
|
$key = 'ssa/recent_'.$key;
|
|
$recent_values = get_transient( $key );
|
|
if ( empty( $recent_values ) ) {
|
|
$recent_values = array();
|
|
}
|
|
$recent_values[] = $value;
|
|
if ( count( $recent_values ) > $number_to_remember ) {
|
|
array_shift( $recent_values );
|
|
}
|
|
|
|
set_transient( $key, $recent_values, MONTH_IN_SECONDS );
|
|
}
|
|
|
|
public function async_process_populate_cache( $payload, $async_action ) {
|
|
// this won't be called until re-enabled in hooks()
|
|
$start = microtime(true);
|
|
$this->populate_cache();
|
|
$end = microtime(true);
|
|
ssa_complete_action( $async_action['id'], 'Execution time: '.( $end - $start ) . 's' );
|
|
}
|
|
|
|
public function populate_cache() {
|
|
return; // disable
|
|
|
|
if ( ! $this->is_enabled() ) {
|
|
return;
|
|
}
|
|
|
|
$developer_settings = $this->plugin->developer_settings->get();
|
|
if ( empty( $developer_settings['populate_cache'] ) ) {
|
|
return;
|
|
}
|
|
|
|
$recent_availability_query_args = get_transient( 'ssa/recent_availability_query_args' );
|
|
if ( empty( $recent_availability_query_args ) ) {
|
|
return;
|
|
}
|
|
$recent_availability_query_args = array_reverse( $recent_availability_query_args ); // so start with the most recent queries
|
|
$hashes_to_process = array();
|
|
$availability_query_args_to_process = array();
|
|
foreach ($recent_availability_query_args as $value) {
|
|
if ( in_array( $value['query_hash'], $hashes_to_process ) ) {
|
|
continue;
|
|
}
|
|
$query_hash = $value['query_hash'];
|
|
$hashes_to_process[] = $query_hash;
|
|
set_transient( 'ssa/cache/lock_'.$query_hash, true, 30 );
|
|
unset( $value['query_hash'] );
|
|
$availability_query_args_to_process[$query_hash] = $value;
|
|
if ( count( $availability_query_args_to_process ) >= 5 ) {
|
|
break;
|
|
}
|
|
}
|
|
set_transient( 'ssa/cache/lock_global', false, 0 ); // once we've locked individual queries, unlock the global
|
|
|
|
|
|
foreach ($availability_query_args_to_process as $query_hash => $value) {
|
|
$appointment_type = new SSA_Appointment_Type_Object( $value['appointment_type_id'] );
|
|
$availability_query = new SSA_Availability_Query(
|
|
$appointment_type,
|
|
$value['period'],
|
|
$value['args']
|
|
);
|
|
$bookable_start_datetime_strings = $availability_query->get_bookable_appointment_start_datetime_strings();
|
|
set_transient( 'ssa/cache/lock_'.$query_hash, false, 5 );
|
|
}
|
|
}
|
|
|
|
public static function delete_expired_transients( $force_db = false ) {
|
|
global $wpdb;
|
|
|
|
if ( ! $force_db && wp_using_ext_object_cache() ) {
|
|
return;
|
|
}
|
|
|
|
$wpdb->query(
|
|
$wpdb->prepare(
|
|
"DELETE a, b FROM {$wpdb->options} a, {$wpdb->options} b
|
|
WHERE a.option_name LIKE %s
|
|
AND a.option_name NOT LIKE %s
|
|
AND b.option_name = CONCAT( '_transient_timeout_', SUBSTRING( a.option_name, 12 ) )
|
|
AND b.option_value < %d",
|
|
$wpdb->esc_like( '_transient_ssa/' ) . '%',
|
|
$wpdb->esc_like( '_transient_timeout_' ) . '%',
|
|
time()
|
|
)
|
|
);
|
|
|
|
if ( ! is_multisite() ) {
|
|
// Single site stores site transients in the options table.
|
|
$wpdb->query(
|
|
$wpdb->prepare(
|
|
"DELETE a, b FROM {$wpdb->options} a, {$wpdb->options} b
|
|
WHERE a.option_name LIKE %s
|
|
AND a.option_name NOT LIKE %s
|
|
AND b.option_name = CONCAT( '_site_transient_timeout_', SUBSTRING( a.option_name, 17 ) )
|
|
AND b.option_value < %d",
|
|
$wpdb->esc_like( '_site_transient_ssa/' ) . '%',
|
|
$wpdb->esc_like( '_site_transient_timeout_' ) . '%',
|
|
time()
|
|
)
|
|
);
|
|
} elseif ( is_multisite() && is_main_site() && is_main_network() ) {
|
|
// Multisite stores site transients in the sitemeta table.
|
|
$wpdb->query(
|
|
$wpdb->prepare(
|
|
"DELETE a, b FROM {$wpdb->sitemeta} a, {$wpdb->sitemeta} b
|
|
WHERE a.meta_key LIKE %s
|
|
AND a.meta_key NOT LIKE %s
|
|
AND b.meta_key = CONCAT( '_site_transient_timeout_', SUBSTRING( a.meta_key, 17 ) )
|
|
AND b.meta_value < %d",
|
|
$wpdb->esc_like( '_site_transient_ssa/' ) . '%',
|
|
$wpdb->esc_like( '_site_transient_timeout_' ) . '%',
|
|
time()
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
public static function delete_all_transients( $force_db = false ) {
|
|
global $wpdb;
|
|
|
|
if ( ! $force_db && wp_using_ext_object_cache() ) {
|
|
return;
|
|
}
|
|
|
|
$wpdb->query(
|
|
$wpdb->prepare(
|
|
"DELETE a FROM {$wpdb->options} a
|
|
WHERE (a.option_name LIKE %s
|
|
OR a.option_name LIKE %s)",
|
|
$wpdb->esc_like( '_transient_ssa/v' ) . '%',
|
|
$wpdb->esc_like( '_transient_timeout_ssa/v' ) . '%'
|
|
)
|
|
);
|
|
|
|
if ( ! is_multisite() ) {
|
|
// Single site stores site transients in the options table.
|
|
$wpdb->query(
|
|
$wpdb->prepare(
|
|
"DELETE a FROM {$wpdb->options} a
|
|
WHERE a.option_name LIKE %s
|
|
OR a.option_name LIKE %s",
|
|
$wpdb->esc_like( '_site_transient_ssa/v' ) . '%',
|
|
$wpdb->esc_like( '_site_transient_timeout_ssa/v' ) . '%'
|
|
)
|
|
);
|
|
} elseif ( is_multisite() && is_main_site() && is_main_network() ) {
|
|
// Multisite stores site transients in the sitemeta table.
|
|
$wpdb->query(
|
|
$wpdb->prepare(
|
|
"DELETE a FROM {$wpdb->sitemeta} a
|
|
WHERE a.meta_key LIKE %s
|
|
OR a.meta_key LIKE %s",
|
|
$wpdb->esc_like( '_site_transient_ssa/v' ) . '%',
|
|
$wpdb->esc_like( '_site_transient_timeout_ssa/v' ) . '%'
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
}
|