'Batch Geocoding', 'csv_import' => 'CSV Import', 'profile_migration' => 'Profile Migration', 'cache_warming' => 'Cache Warming' ]; /** * Initialize hooks */ public static function init() { // Register cron hook add_action('hvac_process_background_jobs', [__CLASS__, 'process_jobs']); // Schedule recurring job processing if not already scheduled if (!wp_next_scheduled('hvac_process_background_jobs')) { wp_schedule_event(time(), 'every_minute', 'hvac_process_background_jobs'); } // Add custom cron interval add_filter('cron_schedules', [__CLASS__, 'add_cron_intervals']); // AJAX handlers for job management add_action('wp_ajax_hvac_get_job_status', [__CLASS__, 'ajax_get_job_status']); add_action('wp_ajax_hvac_cancel_job', [__CLASS__, 'ajax_cancel_job']); } /** * Add custom cron intervals * * @param array $schedules Existing cron schedules * @return array Modified schedules */ public static function add_cron_intervals($schedules) { $schedules['every_minute'] = [ 'interval' => 60, 'display' => 'Every Minute' ]; $schedules['every_five_minutes'] = [ 'interval' => 300, 'display' => 'Every 5 Minutes' ]; return $schedules; } /** * Queue a background job * * @param string $type Job type * @param array $data Job data * @param int $priority Job priority (lower = higher priority) * @return string Job ID */ public static function queue_job($type, $data = [], $priority = 10) { if (!isset(self::JOB_TYPES[$type])) { throw new InvalidArgumentException("Invalid job type: $type"); } $job_id = uniqid('job_'); $job = [ 'id' => $job_id, 'type' => $type, 'data' => $data, 'priority' => $priority, 'status' => 'queued', 'created_at' => time(), 'attempts' => 0, 'max_attempts' => 3 ]; // Get current queue $queue = get_option(self::QUEUE_OPTION, []); // Add job to queue $queue[] = $job; // Sort by priority usort($queue, function($a, $b) { return $a['priority'] <=> $b['priority']; }); // Save queue update_option(self::QUEUE_OPTION, $queue); // Store job status self::update_job_status($job_id, 'queued', 'Job queued for processing'); HVAC_Logger::info("Background job queued: {$type} (ID: {$job_id})", 'Background Jobs'); return $job_id; } /** * Process background jobs */ public static function process_jobs() { $queue = get_option(self::QUEUE_OPTION, []); if (empty($queue)) { return; } $processed = 0; $remaining_jobs = []; foreach ($queue as $job) { if ($processed >= self::BATCH_SIZE) { $remaining_jobs[] = $job; continue; } // Skip jobs that have exceeded max attempts if ($job['attempts'] >= $job['max_attempts']) { self::update_job_status($job['id'], 'failed', 'Maximum attempts exceeded'); continue; } // Process job $result = self::process_job($job); if ($result['success']) { self::update_job_status($job['id'], 'completed', $result['message']); $processed++; } else { // Increment attempts and re-queue if not at max attempts $job['attempts']++; if ($job['attempts'] < $job['max_attempts']) { $remaining_jobs[] = $job; self::update_job_status($job['id'], 'retrying', "Attempt {$job['attempts']}/{$job['max_attempts']}: {$result['message']}"); } else { self::update_job_status($job['id'], 'failed', "Final attempt failed: {$result['message']}"); } } } // Update queue with remaining jobs update_option(self::QUEUE_OPTION, $remaining_jobs); if ($processed > 0) { HVAC_Logger::info("Processed {$processed} background jobs", 'Background Jobs'); } } /** * Process a single job * * @param array $job Job data * @return array Result with success boolean and message */ private static function process_job($job) { try { self::update_job_status($job['id'], 'processing', 'Job started'); switch ($job['type']) { case 'geocoding_batch': return self::process_geocoding_batch($job); case 'csv_import': return self::process_csv_import($job); case 'profile_migration': return self::process_profile_migration($job); case 'cache_warming': return self::process_cache_warming($job); default: return [ 'success' => false, 'message' => "Unknown job type: {$job['type']}" ]; } } catch (Exception $e) { HVAC_Logger::error("Background job error (ID: {$job['id']}): " . $e->getMessage(), 'Background Jobs'); return [ 'success' => false, 'message' => $e->getMessage() ]; } } /** * Process geocoding batch job * * @param array $job Job data * @return array Result */ private static function process_geocoding_batch($job) { if (!class_exists('HVAC_Geocoding_Service')) { return [ 'success' => false, 'message' => 'Geocoding service not available' ]; } $user_ids = $job['data']['user_ids'] ?? []; $processed = 0; foreach (array_slice($user_ids, 0, 10) as $user_id) { // Process 10 at a time $result = HVAC_Geocoding_Service::geocode_user($user_id); if ($result) { $processed++; } } return [ 'success' => true, 'message' => "Processed geocoding for {$processed} users" ]; } /** * Process CSV import job * * @param array $job Job data * @return array Result */ private static function process_csv_import($job) { $file_path = $job['data']['file_path'] ?? ''; $import_type = $job['data']['import_type'] ?? ''; if (!file_exists($file_path)) { return [ 'success' => false, 'message' => 'Import file not found' ]; } // Process CSV in chunks $processed = 0; $handle = fopen($file_path, 'r'); if ($handle === false) { return [ 'success' => false, 'message' => 'Could not open import file' ]; } // Skip header row fgetcsv($handle); // Process up to 50 rows while (($data = fgetcsv($handle)) !== false && $processed < 50) { // Process row based on import type if ($import_type === 'trainers') { // Process trainer import $processed++; } } fclose($handle); return [ 'success' => true, 'message' => "Processed {$processed} CSV rows" ]; } /** * Process profile migration job * * @param array $job Job data * @return array Result */ private static function process_profile_migration($job) { if (!class_exists('HVAC_Trainer_Profile_Migration')) { return [ 'success' => false, 'message' => 'Migration class not available' ]; } $batch_size = $job['data']['batch_size'] ?? 20; $offset = $job['data']['offset'] ?? 0; // Process batch of users $users = get_users([ 'role__in' => ['hvac_trainer', 'hvac_master_trainer'], 'number' => $batch_size, 'offset' => $offset ]); $processed = 0; foreach ($users as $user) { // Process user migration $processed++; } return [ 'success' => true, 'message' => "Migrated {$processed} user profiles" ]; } /** * Process cache warming job * * @param array $job Job data * @return array Result */ private static function process_cache_warming($job) { if (!class_exists('HVAC_Master_Dashboard_Data')) { return [ 'success' => false, 'message' => 'Dashboard data class not available' ]; } $dashboard_data = new HVAC_Master_Dashboard_Data(); // Warm up key caches $dashboard_data->get_total_events_count(); $dashboard_data->get_upcoming_events_count(); $dashboard_data->get_past_events_count(); $dashboard_data->get_total_tickets_sold(); $dashboard_data->get_total_revenue(); return [ 'success' => true, 'message' => 'Cache warmed successfully' ]; } /** * Update job status * * @param string $job_id Job ID * @param string $status Status * @param string $message Status message */ private static function update_job_status($job_id, $status, $message = '') { $status_data = [ 'status' => $status, 'message' => $message, 'updated_at' => time() ]; update_option(self::STATUS_PREFIX . $job_id, $status_data); } /** * Get job status * * @param string $job_id Job ID * @return array|false Job status or false if not found */ public static function get_job_status($job_id) { return get_option(self::STATUS_PREFIX . $job_id, false); } /** * AJAX handler for getting job status */ public static function ajax_get_job_status() { check_ajax_referer('hvac_ajax_nonce', 'nonce'); if (!current_user_can('hvac_master_trainer')) { wp_send_json_error('Insufficient permissions'); } $job_id = sanitize_text_field($_POST['job_id']); $status = self::get_job_status($job_id); if ($status === false) { wp_send_json_error('Job not found'); } wp_send_json_success($status); } /** * AJAX handler for canceling jobs */ public static function ajax_cancel_job() { check_ajax_referer('hvac_ajax_nonce', 'nonce'); if (!current_user_can('hvac_master_trainer')) { wp_send_json_error('Insufficient permissions'); } $job_id = sanitize_text_field($_POST['job_id']); // Remove from queue $queue = get_option(self::QUEUE_OPTION, []); $queue = array_filter($queue, function($job) use ($job_id) { return $job['id'] !== $job_id; }); update_option(self::QUEUE_OPTION, array_values($queue)); // Update status self::update_job_status($job_id, 'cancelled', 'Job cancelled by user'); wp_send_json_success('Job cancelled'); } /** * Get queue statistics * * @return array Queue stats */ public static function get_queue_stats() { $queue = get_option(self::QUEUE_OPTION, []); $stats = [ 'total' => count($queue), 'by_status' => [], 'by_type' => [] ]; foreach ($queue as $job) { $status = $job['status'] ?? 'unknown'; $type = $job['type'] ?? 'unknown'; $stats['by_status'][$status] = ($stats['by_status'][$status] ?? 0) + 1; $stats['by_type'][$type] = ($stats['by_type'][$type] ?? 0) + 1; } return $stats; } }