0) ? $existing : $pre; } public static function dedup_pre_schedule_single($pre, $timestamp, $hook, $args = array(), $group = '') { if ($hook !== self::HOOK_TICK || $group !== 'aieo') { return $pre; } $existing = self::find_existing_tick_action(); return ($existing > 0) ? $existing : $pre; } private static function find_existing_tick_action() { global $wpdb; $lock_key = 'aieo_recompute_tick_sched'; $got = (int) $wpdb->get_var($wpdb->prepare('SELECT GET_LOCK(%s, 0)', $lock_key)); if ($got !== 1) { return 1; } try { $existing = (int) $wpdb->get_var($wpdb->prepare( "SELECT a.action_id FROM {$wpdb->prefix}actionscheduler_actions a LEFT JOIN {$wpdb->prefix}actionscheduler_groups g ON g.group_id = a.group_id WHERE a.hook = %s AND a.status IN ('pending', 'in-progress') AND g.slug = %s ORDER BY a.action_id ASC LIMIT 1", self::HOOK_TICK, 'aieo' )); return $existing; } finally { $wpdb->query($wpdb->prepare('SELECT RELEASE_LOCK(%s)', $lock_key)); } } public static function enqueue($surface, $entity_id, $audience_key = '') { global $wpdb; if (!self::table_exists()) { return; } $entity_id = (int) $entity_id; if ($entity_id <= 0 || !is_string($surface) || $surface === '') { return; } $audience_key = is_string($audience_key) ? $audience_key : ''; $table = self::table_name(); $max = (int) self::MAX_ATTEMPTS; $wpdb->query($wpdb->prepare( "INSERT INTO {$table} (surface, entity_id, audience_key, enqueued_at, attempts) VALUES (%s, %d, %s, %s, 0) ON DUPLICATE KEY UPDATE last_error = IF(attempts >= {$max}, NULL, last_error), enqueued_at = IF(attempts >= {$max}, VALUES(enqueued_at), enqueued_at), attempts = IF(attempts >= {$max}, 0, attempts)", $surface, $entity_id, $audience_key, current_time('mysql', true) )); } public static function tick() { global $wpdb; if (!self::table_exists()) { return; } $table = self::table_name(); $tick_started = microtime(true); $rows = $wpdb->get_results($wpdb->prepare( "SELECT * FROM {$table} WHERE attempts < %d ORDER BY enqueued_at ASC LIMIT %d", self::MAX_ATTEMPTS, self::BATCH_SIZE ), ARRAY_A); if (empty($rows)) { return; } foreach ($rows as $row) { if ((microtime(true) - $tick_started) > 20) { break; } $id = (int) $row['id']; $surface = (string) $row['surface']; $entity = (int) $row['entity_id']; $audience = (string) $row['audience_key']; $error = null; try { self::process_one($surface, $entity, $audience); } catch (\Throwable $e) { $error = $e->getMessage(); } if ($error === null) { $wpdb->delete($table, array('id' => $id), array('%d')); } else { $wpdb->update( $table, array('attempts' => (int) $row['attempts'] + 1, 'last_error' => $error), array('id' => $id), array('%d', '%s'), array('%d') ); } } } private static function process_one($surface, $entity_id, $audience_key) { switch ($surface) { case self::SURFACE_PRICES: if (!class_exists('AIEO_Effective_Prices')) { return; } if ($audience_key !== '') { AIEO_Effective_Prices::recompute($entity_id, $audience_key); } else { AIEO_Effective_Prices::recompute_all_audiences($entity_id); } return; case self::SURFACE_RECOS: if (class_exists('AIEO_Effective_Recos')) { if ($audience_key !== '') { AIEO_Effective_Recos::recompute($entity_id, $audience_key); } else { AIEO_Effective_Recos::recompute_all_audiences($entity_id); } } return; case self::SURFACE_ELIGIBILITY: if (class_exists('AIEO_Effective_Eligibility')) { if ($audience_key !== '') { AIEO_Effective_Eligibility::recompute($entity_id, $audience_key); } else { AIEO_Effective_Eligibility::recompute_all_audiences($entity_id); } } return; case self::SURFACE_SEM_REEXTRACT: if (class_exists('AIEO_Semantic_Index') && method_exists('AIEO_Semantic_Index', 'index_object') && (!method_exists('AIEO_Semantic_Index', 'enabled') || AIEO_Semantic_Index::enabled())) { $type = '' !== $audience_key ? $audience_key : 'product'; $stored = class_exists('AIEO_Semantic_Index_Store') ? AIEO_Semantic_Index_Store::get_object($type, $entity_id) : null; AIEO_Semantic_Index::index_object($type, $entity_id, $stored ? $stored['content_hash'] : null); } return; case self::SURFACE_SEM_GRAPH: if (class_exists('AIEO_Semantic_Graph_Cache')) { AIEO_Semantic_Graph_Cache::build( '' !== $audience_key ? $audience_key : 'product', $entity_id ); } return; } } public static function on_product_save($product_id) { $product_id = (int) $product_id; if ($product_id <= 0) { return; } self::enqueue(self::SURFACE_PRICES, $product_id, ''); self::enqueue(self::SURFACE_RECOS, $product_id, ''); self::enqueue(self::SURFACE_ELIGIBILITY, $product_id, ''); } public static function on_price_meta_invalidated($product_ids) { foreach ((array) $product_ids as $pid) { $pid = (int) $pid; if ($pid > 0) { self::enqueue(self::SURFACE_PRICES, $pid, ''); } } } public static function on_price_meta_storm() { self::bulk_enqueue_products_for_surface(self::SURFACE_PRICES); } private static function surfaces_for_domain($domain) { switch ($domain) { case 'prices': case 'loyalty': return array(self::SURFACE_PRICES); case 'gifts': case 'needs': case 'shipping': return array(self::SURFACE_PRICES, self::SURFACE_ELIGIBILITY); case 'brands': return array(self::SURFACE_PRICES, self::SURFACE_ELIGIBILITY); case 'complementary': return array(self::SURFACE_ELIGIBILITY); default: return array(); } } private static function bulk_enqueue_products_for_surface($surface) { global $wpdb; if (!self::table_exists()) { return 0; } $table = self::table_name(); $now = current_time('mysql', true); $sql = $wpdb->prepare( "INSERT IGNORE INTO {$table} (surface, entity_id, audience_key, enqueued_at, attempts) SELECT %s, ID, '', %s, 0 FROM {$wpdb->posts} WHERE post_type = 'product' AND post_status = 'publish'", $surface, $now ); return (int) $wpdb->query($sql); } public static function on_rule_change($domain) { $domain = (string) $domain; $surfaces = self::surfaces_for_domain($domain); if (empty($surfaces)) { return; } update_option('aieo_effective_prices_sweep_needed', time(), 'no'); foreach ($surfaces as $surface) { self::bulk_enqueue_products_for_surface($surface); } } public static function depth() { if (!self::table_exists()) { return 0; } global $wpdb; return (int) $wpdb->get_var('SELECT COUNT(*) FROM ' . self::table_name()); } public static function depth_by_surface() { if (!self::table_exists()) { return array(); } global $wpdb; $rows = $wpdb->get_results('SELECT surface, COUNT(*) AS n FROM ' . self::table_name() . ' GROUP BY surface', ARRAY_A); $out = array(); if (is_array($rows)) { foreach ($rows as $r) { $out[(string) $r['surface']] = (int) $r['n']; } } return $out; } public static function table_name() { global $wpdb; return $wpdb->prefix . self::TABLE_NAME; } public static function table_exists() { static $cached = null; if ($cached !== null) { return $cached; } global $wpdb; $found = (int) $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s", self::table_name() )); return $cached = ($found > 0); } public static function maybe_install() { $current = (int) get_option(self::SCHEMA_OPTION, 0); if ($current >= self::SCHEMA_VERSION) { if (!is_admin() && !wp_doing_cron()) { return; } if (self::table_exists()) { return; } } if (!AIEO_Schema_Lock::acquire(self::SCHEMA_OPTION)) { return; } global $wpdb; $table = self::table_name(); $charset = aieo_get_charset_collate(); $sql = "CREATE TABLE {$table} ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, surface VARCHAR(32) NOT NULL, entity_id BIGINT(20) UNSIGNED NOT NULL, audience_key VARCHAR(64) NOT NULL DEFAULT '', enqueued_at DATETIME NOT NULL, attempts SMALLINT UNSIGNED NOT NULL DEFAULT 0, last_error TEXT, PRIMARY KEY (id), UNIQUE KEY uq_pending (surface, entity_id, audience_key), KEY ix_enqueued (enqueued_at) ) {$charset};"; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; dbDelta($sql); update_option(self::SCHEMA_OPTION, self::SCHEMA_VERSION, 'no'); } } add_filter('cron_schedules', function ($schedules) { if (!isset($schedules['aieo_recompute_minute'])) { $schedules['aieo_recompute_minute'] = array( 'interval' => 60, 'display' => 'AIEO recompute (every minute)', ); } return $schedules; });