diff --git a/admin/class-admin-interface.php b/admin/class-admin-interface.php deleted file mode 100644 index a4fa45a..0000000 --- a/admin/class-admin-interface.php +++ /dev/null @@ -1,1327 +0,0 @@ -queue_manager = $queue_manager; - $this->job_processor = $job_processor; - } - - /** - * Initialize admin interface. - * - * @since 1.0.0 - */ - public function init() { - add_action( 'admin_menu', array( $this, 'add_admin_menu' ) ); - add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_scripts' ) ); - add_action( 'wp_ajax_redis_queue_trigger_worker', array( $this, 'ajax_trigger_worker' ) ); - add_action( 'wp_ajax_redis_queue_get_stats', array( $this, 'ajax_get_stats' ) ); - add_action( 'wp_ajax_redis_queue_clear_queue', array( $this, 'ajax_clear_queue' ) ); - add_action( 'wp_ajax_redis_queue_create_test_job', array( $this, 'ajax_create_test_job' ) ); - add_action( 'wp_ajax_redis_queue_diagnostics', array( $this, 'ajax_diagnostics' ) ); - add_action( 'wp_ajax_redis_queue_debug_test', array( $this, 'ajax_debug_test' ) ); - add_action( 'wp_ajax_redis_queue_reset_stuck_jobs', array( $this, 'ajax_reset_stuck_jobs' ) ); - add_action( 'wp_ajax_redis_queue_purge_jobs', array( $this, 'ajax_purge_jobs' ) ); - } - - /** - * Add admin menu. - * - * @since 1.0.0 - */ - public function add_admin_menu() { - add_menu_page( - __( 'Redis Queue', 'redis-queue-demo' ), - __( 'Redis Queue', 'redis-queue-demo' ), - 'manage_options', - 'redis-queue-demo', - array( $this, 'render_dashboard_page' ), - 'dashicons-database-view', - 30 - ); - - add_submenu_page( - 'redis-queue-demo', - __( 'Dashboard', 'redis-queue-demo' ), - __( 'Dashboard', 'redis-queue-demo' ), - 'manage_options', - 'redis-queue-demo', - array( $this, 'render_dashboard_page' ) - ); - - add_submenu_page( - 'redis-queue-demo', - __( 'Jobs', 'redis-queue-demo' ), - __( 'Jobs', 'redis-queue-demo' ), - 'manage_options', - 'redis-queue-jobs', - array( $this, 'render_jobs_page' ) - ); - - add_submenu_page( - 'redis-queue-demo', - __( 'Test Jobs', 'redis-queue-demo' ), - __( 'Test Jobs', 'redis-queue-demo' ), - 'manage_options', - 'redis-queue-test', - array( $this, 'render_test_page' ) - ); - - add_submenu_page( - 'redis-queue-demo', - __( 'Settings', 'redis-queue-demo' ), - __( 'Settings', 'redis-queue-demo' ), - 'manage_options', - 'redis-queue-settings', - array( $this, 'render_settings_page' ) - ); - } - - /** - * Enqueue admin scripts and styles. - * - * @since 1.0.0 - * @param string $hook_suffix Current admin page hook suffix. - */ - public function enqueue_admin_scripts( $hook_suffix ) { - // Only enqueue on our admin pages. - if ( strpos( $hook_suffix, 'redis-queue' ) === false ) { - return; - } - - wp_enqueue_script( - 'redis-queue-admin', - plugin_dir_url( __FILE__ ) . '../assets/admin.js', - array( 'jquery' ), - REDIS_QUEUE_DEMO_VERSION, - true - ); - - wp_localize_script( - 'redis-queue-admin', - 'redisQueueAdmin', - array( - 'ajaxUrl' => admin_url( 'admin-ajax.php' ), - 'nonce' => wp_create_nonce( 'redis_queue_admin' ), - 'restNonce' => wp_create_nonce( 'wp_rest' ), - 'restUrl' => rest_url( 'redis-queue/v1/' ), - 'strings' => array( - 'processing' => __( 'Processing...', 'redis-queue-demo' ), - 'success' => __( 'Success!', 'redis-queue-demo' ), - 'error' => __( 'Error occurred', 'redis-queue-demo' ), - 'confirmClear' => __( 'Are you sure you want to clear this queue?', 'redis-queue-demo' ), - 'workerTriggered' => __( 'Worker triggered successfully', 'redis-queue-demo' ), - 'queueCleared' => __( 'Queue cleared successfully', 'redis-queue-demo' ), - ), - ) - ); - - wp_enqueue_style( - 'redis-queue-admin', - plugin_dir_url( __FILE__ ) . '../assets/admin.css', - array(), - REDIS_QUEUE_DEMO_VERSION - ); - } - - /** - * Render dashboard page. - * - * @since 1.0.0 - */ - public function render_dashboard_page() { - $stats = $this->queue_manager->get_queue_stats(); - $flat_stats = $this->flatten_stats( $stats ); - $health = $this->get_system_health(); - ?> -
-

- - -
-

- - -

-
-
- - - - -
-
- - - - -
-
-
- - -
-
-

-
-
-
-

-
-
-
-
-

-
-
-
-
-

-
-
-
- - -
-

-
- - - - - -
-
- -
-
-
-
- - -
-

-
- -
-
-
- prefix . 'redis_queue_jobs'; - $offset = ( $current_page - 1 ) * $per_page; - - // Build query. - $where_conditions = array(); - $prepare_values = array(); - - if ( $status_filter ) { - $where_conditions[] = 'status = %s'; - $prepare_values[] = $status_filter; - } - - $where_clause = ''; - if ( ! empty( $where_conditions ) ) { - $where_clause = 'WHERE ' . implode( ' AND ', $where_conditions ); - } - - // Get total count. - $count_query = "SELECT COUNT(*) FROM {$table_name} {$where_clause}"; - if ( ! empty( $prepare_values ) ) { - $count_query = $wpdb->prepare( $count_query, ...$prepare_values ); - } - $total_jobs = (int) $wpdb->get_var( $count_query ); - - // Get jobs. - $jobs_query = "SELECT * FROM {$table_name} {$where_clause} ORDER BY created_at DESC LIMIT %d OFFSET %d"; - $prepare_values[] = $per_page; - $prepare_values[] = $offset; - - $jobs = $wpdb->get_results( - $wpdb->prepare( $jobs_query, ...$prepare_values ), - ARRAY_A - ); - - $total_pages = ceil( $total_jobs / $per_page ); - ?> -
-

-
- - - - - - -
-
- - -
-
- - - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - | - - - -
- - - 1 ) : ?> -
- add_query_arg( 'paged', '%#%' ), - 'format' => '', - 'prev_text' => __( '« Previous', 'redis-queue-demo' ), - 'next_text' => __( 'Next »', 'redis-queue-demo' ), - 'total' => $total_pages, - 'current' => $current_page, - ) - ); - ?> -
- -
- -
-

-

-

- - -
- -
-

-
- - - - - - - - - - - - - - - - - -
- - -
-
-
-
- -
-
- - -
-

-
- - - - - - - - - -
- - -
- - -

- -

-
- -
-
- - -
-

-
- - - - - - - - - - - - - -
- - -
-
- -
-
-
- - -
- save_settings(); - } elseif ( isset( $_POST[ 'generate_api_token' ] ) && wp_verify_nonce( $_POST[ '_wpnonce' ], 'redis_queue_settings' ) ) { - // Generate token then save other settings. - $_POST[ '__generate_api_token' ] = 1; // Internal flag consumed in save_settings(). - $this->save_settings(); - } elseif ( isset( $_POST[ 'clear_api_token' ] ) && wp_verify_nonce( $_POST[ '_wpnonce' ], 'redis_queue_settings' ) ) { - $_POST[ '__clear_api_token' ] = 1; // Internal flag. - $this->save_settings(); - } - - $options = get_option( 'redis_queue_settings', array() ); - $defaults = array( - 'redis_host' => '127.0.0.1', - 'redis_port' => 6379, - 'redis_database' => 0, - 'redis_password' => '', - 'worker_timeout' => 30, - 'max_retries' => 3, - 'retry_delay' => 60, - 'batch_size' => 10, - 'api_token' => '', - 'api_token_scope' => 'worker', - 'rate_limit_per_minute' => 60, - 'enable_request_logging' => 0, - 'log_rotate_size_kb' => 256, - 'log_max_files' => 5, - ); - $options = wp_parse_args( $options, $defaults ); - ?> -
-

- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -

- -

-
- -

-

-
- -

- -

-
- -

- -

-
- -

- -

-
- -

- -

-
- -

- -

-
- -

- -

-
- -
- - - - -

- " or "X-Redis-Queue-Token: ". Possession grants the same access as an admin for these endpoints; keep it secret.', 'redis-queue-demo' ); ?> -

-

- -

-
- -

- -

-
- -

- -

-
- -

- -

-
- - -

- -

-
- - -
- - -
-

- -
-
-
- sanitize_text_field( $_POST[ 'redis_host' ] ), - 'redis_port' => intval( $_POST[ 'redis_port' ] ), - 'redis_database' => intval( $_POST[ 'redis_database' ] ), - 'redis_password' => sanitize_text_field( $_POST[ 'redis_password' ] ), - 'worker_timeout' => intval( $_POST[ 'worker_timeout' ] ), - 'max_retries' => intval( $_POST[ 'max_retries' ] ), - 'retry_delay' => intval( $_POST[ 'retry_delay' ] ), - 'batch_size' => intval( $_POST[ 'batch_size' ] ), - 'api_token' => isset( $existing[ 'api_token' ] ) ? $existing[ 'api_token' ] : '', // default preserve existing - 'api_token_scope' => isset( $_POST[ 'api_token_scope' ] ) && in_array( $_POST[ 'api_token_scope' ], array( 'worker', 'full' ), true ) ? $_POST[ 'api_token_scope' ] : ( isset( $existing[ 'api_token_scope' ] ) ? $existing[ 'api_token_scope' ] : 'worker' ), - 'rate_limit_per_minute' => isset( $_POST[ 'rate_limit_per_minute' ] ) ? max( 1, intval( $_POST[ 'rate_limit_per_minute' ] ) ) : ( isset( $existing[ 'rate_limit_per_minute' ] ) ? intval( $existing[ 'rate_limit_per_minute' ] ) : 60 ), - 'enable_request_logging' => isset( $_POST[ 'enable_request_logging' ] ) ? 1 : 0, - 'log_rotate_size_kb' => isset( $_POST[ 'log_rotate_size_kb' ] ) ? max( 8, intval( $_POST[ 'log_rotate_size_kb' ] ) ) : ( isset( $existing[ 'log_rotate_size_kb' ] ) ? intval( $existing[ 'log_rotate_size_kb' ] ) : 256 ), - 'log_max_files' => isset( $_POST[ 'log_max_files' ] ) ? max( 1, intval( $_POST[ 'log_max_files' ] ) ) : ( isset( $existing[ 'log_max_files' ] ) ? intval( $existing[ 'log_max_files' ] ) : 5 ), - ); - - // Handle token clear. - if ( isset( $_POST[ '__clear_api_token' ] ) || isset( $_POST[ 'clear_api_token' ] ) ) { - $settings[ 'api_token' ] = ''; - } - - // Handle token generation. - if ( isset( $_POST[ '__generate_api_token' ] ) || isset( $_POST[ 'generate_api_token' ] ) ) { - try { - $settings[ 'api_token' ] = bin2hex( random_bytes( 32 ) ); // 64 hex chars ~256 bits. - } catch (Exception $e) { - // Fallback if random_bytes unavailable. - $settings[ 'api_token' ] = wp_generate_password( 64, false, false ); - } - } - - update_option( 'redis_queue_settings', $settings ); - add_action( 'admin_notices', array( $this, 'settings_saved_notice' ) ); - } - - /** - * Display settings saved notice. - * - * @since 1.0.0 - */ - public function settings_saved_notice() { - ?> -
-

-
- queue_manager || ! $this->job_processor ) { - wp_send_json_error( 'Queue system not initialized' ); - return; - } - - // Check if Redis connection is available - if ( ! $this->queue_manager->is_connected() ) { - wp_send_json_error( 'Redis connection not available' ); - return; - } - - // Use the helper function to process jobs safely if available - if ( function_exists( 'redis_queue_process_jobs' ) ) { - $results = redis_queue_process_jobs( array( 'default', 'email', 'media', 'api' ), 10 ); - } else { - // Fallback to direct instantiation - if ( ! class_exists( 'Sync_Worker' ) ) { - wp_send_json_error( 'Sync_Worker class not available' ); - return; - } - $sync_worker = new Sync_Worker( $this->queue_manager, $this->job_processor ); - $results = $sync_worker->process_jobs( array( 'default', 'email', 'media', 'api' ), 10 ); - } - - // Validate results - if ( $results === null ) { - wp_send_json_error( 'Worker returned null results' ); - return; - } - - if ( ! is_array( $results ) ) { - wp_send_json_error( 'Worker returned invalid results format' ); - return; - } - - wp_send_json_success( $results ); - } catch (Exception $e) { - $error_message = 'Worker error: '; - if ( $e && method_exists( $e, 'getMessage' ) ) { - $error_message .= $e->getMessage(); - } else { - $error_message .= 'Unknown exception occurred'; - } - wp_send_json_error( $error_message ); - } catch (Error $e) { - $error_message = 'Fatal error: '; - if ( $e && method_exists( $e, 'getMessage' ) ) { - $error_message .= $e->getMessage(); - } else { - $error_message .= 'Unknown fatal error occurred'; - } - wp_send_json_error( $error_message ); - } catch (Throwable $e) { - $error_message = 'Unexpected error: '; - if ( $e && method_exists( $e, 'getMessage' ) ) { - $error_message .= $e->getMessage(); - } else { - $error_message .= 'Unknown throwable occurred'; - } - wp_send_json_error( $error_message ); - } - } - - /** - * AJAX handler for getting stats. - * - * @since 1.0.0 - */ - public function ajax_get_stats() { - check_ajax_referer( 'redis_queue_admin', 'nonce' ); - - if ( ! current_user_can( 'manage_options' ) ) { - wp_die( -1 ); - } - - $stats = $this->queue_manager->get_queue_stats(); - $flat_stats = $this->flatten_stats( $stats ); - wp_send_json_success( $flat_stats ); - } - - /** - * AJAX handler for clearing queue. - * - * @since 1.0.0 - */ - public function ajax_clear_queue() { - check_ajax_referer( 'redis_queue_admin', 'nonce' ); - - if ( ! current_user_can( 'manage_options' ) ) { - wp_die( -1 ); - } - - $queue_name = sanitize_text_field( $_POST[ 'queue' ] ?? 'default' ); - $result = $this->queue_manager->clear_queue( $queue_name ); - - if ( $result ) { - wp_send_json_success( array( 'message' => 'Queue cleared successfully' ) ); - } else { - wp_send_json_error( 'Failed to clear queue' ); - } - } - - /** - * AJAX handler for creating test jobs. - * - * @since 1.0.0 - */ - public function ajax_create_test_job() { - check_ajax_referer( 'redis_queue_admin', 'nonce' ); - - if ( ! current_user_can( 'manage_options' ) ) { - wp_die( -1 ); - } - - $job_type = sanitize_text_field( $_POST[ 'job_type' ] ?? '' ); - $payload = $_POST[ 'payload' ] ?? array(); - - // Sanitize payload data - $payload = array_map( 'sanitize_text_field', $payload ); - - try { - // Use the main plugin instance to create the job - $plugin = redis_queue_demo(); - $job_id = $plugin->enqueue_job( $job_type, $payload, array( 'priority' => 10 ) ); - - if ( $job_id ) { - wp_send_json_success( array( - 'job_id' => $job_id, - 'message' => 'Job created and enqueued successfully.', - ) ); - } else { - wp_send_json_error( 'Failed to enqueue job.' ); - } - } catch (Exception $e) { - wp_send_json_error( 'Job creation failed: ' . $e->getMessage() ); - } - } - - /** - * AJAX handler for diagnostics. - * - * @since 1.0.0 - */ - public function ajax_diagnostics() { - check_ajax_referer( 'redis_queue_admin', 'nonce' ); - - if ( ! current_user_can( 'manage_options' ) ) { - wp_die( -1 ); - } - - try { - $diagnostics = $this->queue_manager->diagnostic(); - wp_send_json_success( $diagnostics ); - } catch (Exception $e) { - wp_send_json_error( 'Diagnostic failed: ' . ( $e ? $e->getMessage() : 'Unknown error' ) ); - } - } - - /** - * AJAX handler for comprehensive debug test. - * - * @since 1.0.0 - */ - public function ajax_debug_test() { - check_ajax_referer( 'redis_queue_admin', 'nonce' ); - - if ( ! current_user_can( 'manage_options' ) ) { - wp_die( -1 ); - } - - $debug_results = array(); - - // Test 1: Plugin initialization - $plugin = redis_queue_demo(); - $debug_results[ '1. Plugin Initialization' ] = array( - 'Queue Manager' => $plugin->queue_manager ? 'OK' : 'FAILED', - 'Job Processor' => $plugin->job_processor ? 'OK' : 'FAILED', - ); - - // Test 2: Redis connection - $debug_results[ '2. Redis Connection' ] = array( - 'Connected' => $plugin->queue_manager->is_connected() ? 'YES' : 'NO', - ); - - // Test 3: Redis diagnostics - $diagnostics = $plugin->queue_manager->diagnostic(); - $debug_results[ '3. Redis Diagnostics' ] = array( - 'Test Write' => isset( $diagnostics[ 'test_write' ] ) && $diagnostics[ 'test_write' ] ? 'OK' : 'FAILED', - 'Test Read' => isset( $diagnostics[ 'test_read' ] ) && $diagnostics[ 'test_read' ] ? 'OK' : 'FAILED', - 'Queue Prefix' => $diagnostics[ 'queue_prefix' ] ?? 'unknown', - 'Redis Keys Found' => isset( $diagnostics[ 'redis_keys' ] ) ? count( $diagnostics[ 'redis_keys' ] ) : 0, - 'Keys' => isset( $diagnostics[ 'redis_keys' ] ) ? implode( ', ', $diagnostics[ 'redis_keys' ] ) : 'none', - ); - - // Test 4: Job creation and processing - $debug_results[ '4. Job Creation Test' ] = array(); - $job_id = $plugin->enqueue_job( 'email', array( - 'type' => 'single', - 'to' => 'test@example.com', - 'subject' => 'Debug Test Email ' . date( 'H:i:s' ), - 'message' => 'This is a debug test email', - ) ); - - if ( $job_id ) { - $debug_results[ '4. Job Creation Test' ][ 'Job Created' ] = 'YES (ID: ' . $job_id . ')'; - - // Check Redis keys after creation - $diagnostics_after = $plugin->queue_manager->diagnostic(); - $debug_results[ '4. Job Creation Test' ][ 'Redis Keys After Creation' ] = count( $diagnostics_after[ 'redis_keys' ] ); - $debug_results[ '4. Job Creation Test' ][ 'Keys' ] = implode( ', ', $diagnostics_after[ 'redis_keys' ] ); - - // Try to dequeue - $dequeued = $plugin->queue_manager->dequeue( array( 'email' ) ); - if ( $dequeued ) { - $debug_results[ '4. Job Creation Test' ][ 'Job Dequeued' ] = 'YES'; - $debug_results[ '4. Job Creation Test' ][ 'Dequeued Job ID' ] = $dequeued[ 'job_id' ] ?? 'unknown'; - $debug_results[ '4. Job Creation Test' ][ 'Dequeued Job Type' ] = $dequeued[ 'job_type' ] ?? 'unknown'; - $debug_results[ '4. Job Creation Test' ][ 'Payload Keys' ] = isset( $dequeued[ 'payload' ] ) ? implode( ', ', array_keys( $dequeued[ 'payload' ] ) ) : 'none'; - - // Attempt to process the dequeued job immediately so it doesn't remain stuck in 'processing' - try { - $job_result = $plugin->job_processor->process_job( $dequeued ); - $debug_results[ '4. Job Creation Test' ][ 'Job Processed' ] = 'YES'; - $debug_results[ '4. Job Creation Test' ][ 'Job Successful' ] = $job_result->is_successful() ? 'YES' : 'NO'; - - // PHPMailer diagnostics (after wp_mail invocation inside job) - global $phpmailer; - if ( isset( $phpmailer ) && is_object( $phpmailer ) ) { - $debug_results[ '4. Job Creation Test' ][ 'PHPMailer Host' ] = $phpmailer->Host ?? '(unset)'; - $debug_results[ '4. Job Creation Test' ][ 'PHPMailer Port' ] = $phpmailer->Port ?? '(unset)'; - $debug_results[ '4. Job Creation Test' ][ 'PHPMailer SMTPSecure' ] = $phpmailer->SMTPSecure ?? '(unset)'; - $debug_results[ '4. Job Creation Test' ][ 'PHPMailer SMTPAuth' ] = isset( $phpmailer->SMTPAuth ) ? ( $phpmailer->SMTPAuth ? 'true' : 'false' ) : '(unset)'; - $debug_results[ '4. Job Creation Test' ][ 'PHPMailer ErrorInfo' ] = ! empty( $phpmailer->ErrorInfo ) ? $phpmailer->ErrorInfo : '(none)'; - $debug_results[ '4. Job Creation Test' ][ 'PHPMailer From' ] = $phpmailer->From ?? '(unset)'; - $debug_results[ '4. Job Creation Test' ][ 'PHPMailer FromName' ] = $phpmailer->FromName ?? '(unset)'; - } - - if ( $job_result->is_successful() ) { - $data = $job_result->get_data(); - $debug_results[ '4. Job Creation Test' ][ 'Result Data' ] = is_scalar( $data ) ? (string) $data : wp_json_encode( $data ); - } else { - $debug_results[ '4. Job Creation Test' ][ 'Error Message' ] = $job_result->get_error_message(); - $debug_results[ '4. Job Creation Test' ][ 'Error Code' ] = $job_result->get_error_code(); - // Include metadata (e.g., phpmailer_error) for deeper insight - $metadata = $job_result->get_metadata(); - if ( ! empty( $metadata ) ) { - $debug_results[ '4. Job Creation Test' ][ 'Job Result Metadata' ] = wp_json_encode( $metadata ); - } - } - } catch (Throwable $e) { - $debug_results[ '4. Job Creation Test' ][ 'Job Processed' ] = 'NO (Exception)'; - $debug_results[ '4. Job Creation Test' ][ 'Processing Exception' ] = $e->getMessage(); - $debug_results[ '4. Job Creation Test' ][ 'Processing Exception File' ] = $e->getFile() . ':' . $e->getLine(); - } - } else { - $debug_results[ '4. Job Creation Test' ][ 'Job Dequeued' ] = 'NO'; - } - } else { - $debug_results[ '4. Job Creation Test' ][ 'Job Created' ] = 'NO'; - } - - // Test 5: Database check - global $wpdb; - $table_name = $wpdb->prefix . 'redis_queue_jobs'; - $debug_results[ '5. Database Check' ] = array( - 'Table Exists' => $wpdb->get_var( "SHOW TABLES LIKE '$table_name'" ) == $table_name ? 'YES' : 'NO', - 'Job Count' => (int) $wpdb->get_var( "SELECT COUNT(*) FROM $table_name" ), - ); - - // Get recent jobs - $recent_jobs = $wpdb->get_results( "SELECT job_id, job_type, status, created_at FROM $table_name ORDER BY created_at DESC LIMIT 5", ARRAY_A ); - $debug_results[ '5. Database Check' ][ 'Recent Jobs' ] = array(); - foreach ( $recent_jobs as $job ) { - $debug_results[ '5. Database Check' ][ 'Recent Jobs' ][] = $job[ 'job_id' ] . ' (' . $job[ 'job_type' ] . ') - ' . $job[ 'status' ] . ' - ' . $job[ 'created_at' ]; - } - - wp_send_json_success( $debug_results ); - } - - /** - * AJAX handler to reset stuck jobs. - * - * @since 1.0.0 - */ - public function ajax_reset_stuck_jobs() { - check_ajax_referer( 'redis_queue_admin', 'nonce' ); - - if ( ! current_user_can( 'manage_options' ) ) { - wp_die( -1 ); - } - - try { - $reset_count = $this->queue_manager->reset_stuck_jobs( 30 ); - wp_send_json_success( array( - 'message' => sprintf( 'Reset %d stuck jobs back to queued status.', $reset_count ), - 'count' => $reset_count, - ) ); - } catch (Exception $e) { - wp_send_json_error( 'Failed to reset stuck jobs: ' . ( $e ? $e->getMessage() : 'Unknown error' ) ); - } - } - - /** - * Get system health status. - * - * @since 1.0.0 - * @return array Health status. - */ - private function get_system_health() { - return array( - 'redis' => $this->queue_manager->is_connected(), - 'database' => $this->check_database_health(), - 'overall' => $this->queue_manager->is_connected() && $this->check_database_health(), - ); - } - - /** - * Check database health. - * - * @since 1.0.0 - * @return bool True if database is healthy. - */ - private function check_database_health() { - global $wpdb; - - $table_name = $wpdb->prefix . 'redis_queue_jobs'; - $table_exists = $wpdb->get_var( - $wpdb->prepare( - "SHOW TABLES LIKE %s", - $table_name - ) - ); - - return ! empty( $table_exists ); - } - - /** - * Flatten raw stats (which include per-queue arrays and a 'database' summary) into - * a simple associative array with top-level queued / processing / completed / failed - * counts expected by the dashboard UI and JS refresh logic. - * - * @since 1.0.0 - * @param array $stats Raw stats from Redis_Queue_Manager::get_queue_stats(). - * @return array Flattened stats. - */ - private function flatten_stats( $stats ) { - $flat = array( - 'queued' => 0, - 'processing' => 0, - 'completed' => 0, - 'failed' => 0, - 'total' => 0, - ); - - // Preferred source: database summary if present. - if ( isset( $stats[ 'database' ] ) && is_array( $stats[ 'database' ] ) ) { - $flat[ 'queued' ] = (int) ( $stats[ 'database' ][ 'queued' ] ?? 0 ); - $flat[ 'processing' ] = (int) ( $stats[ 'database' ][ 'processing' ] ?? 0 ); - $flat[ 'completed' ] = (int) ( $stats[ 'database' ][ 'completed' ] ?? 0 ); - $flat[ 'failed' ] = (int) ( $stats[ 'database' ][ 'failed' ] ?? 0 ); - $flat[ 'total' ] = (int) ( $stats[ 'database' ][ 'total' ] ?? ( $flat[ 'queued' ] + $flat[ 'processing' ] + $flat[ 'completed' ] + $flat[ 'failed' ] ) ); - return $flat; - } - - // Fallback: derive from per-queue job 'pending' counts if no database summary. - foreach ( $stats as $queue => $data ) { - if ( ! is_array( $data ) ) { - continue; - } - // We only know 'pending' (queued) from Redis side; other states live in DB. - if ( isset( $data[ 'pending' ] ) ) { - $flat[ 'queued' ] += (int) $data[ 'pending' ]; - } - } - $flat[ 'total' ] = $flat[ 'queued' ]; - return $flat; - } - - /** - * AJAX handler for purging jobs. - * - * Supported scopes: - * - completed : delete completed jobs - * - failed : delete failed jobs - * - older : delete jobs older than N days (default 7) - * - all : delete ALL jobs (dangerous) - * - * @since 1.0.0 - */ - public function ajax_purge_jobs() { - check_ajax_referer( 'redis_queue_admin', 'nonce' ); - - if ( ! current_user_can( 'manage_options' ) ) { - wp_die( -1 ); - } - - $scope = sanitize_text_field( $_POST[ 'scope' ] ?? '' ); - $days_arg = isset( $_POST[ 'days' ] ) ? intval( $_POST[ 'days' ] ) : 7; - $days = $days_arg > 0 ? $days_arg : 7; - - if ( ! in_array( $scope, array( 'completed', 'failed', 'older', 'all' ), true ) ) { - wp_send_json_error( __( 'Invalid purge scope.', 'redis-queue-demo' ) ); - } - - global $wpdb; - $table = $wpdb->prefix . 'redis_queue_jobs'; - $where = ''; - $args = array(); - - switch ( $scope ) { - case 'completed': - $where = "WHERE status = 'completed'"; - break; - case 'failed': - $where = "WHERE status = 'failed'"; - break; - case 'older': - $cutoff = gmdate( 'Y-m-d H:i:s', time() - ( $days * DAY_IN_SECONDS ) ); - $where = 'WHERE created_at < %s'; - $args[] = $cutoff; - break; - case 'all': - // No where clause (will truncate all rows). - break; - } - - // Count first for reporting. - $count_sql = "SELECT COUNT(*) FROM $table " . $where; - if ( $args ) { - $count = (int) $wpdb->get_var( $wpdb->prepare( $count_sql, ...$args ) ); - } else { - $count = (int) $wpdb->get_var( $count_sql ); - } - - if ( 0 === $count ) { - wp_send_json_success( array( - 'message' => __( 'No matching jobs to purge.', 'redis-queue-demo' ), - 'count' => 0, - ) ); - } - - // Perform deletion. - $delete_sql = "DELETE FROM $table " . $where; - $deleted = 0; - if ( $args ) { - $prepared = $wpdb->prepare( $delete_sql, ...$args ); - $wpdb->query( $prepared ); - $deleted = $wpdb->rows_affected; - } else { - $wpdb->query( $delete_sql ); - $deleted = $wpdb->rows_affected; - } - - $message = ( 'older' === $scope ) - ? sprintf( __( 'Purged %d jobs older than %d days.', 'redis-queue-demo' ), $deleted, $days ) - : sprintf( __( 'Purged %d jobs (scope: %s).', 'redis-queue-demo' ), $deleted, $scope ); - - wp_send_json_success( array( - 'message' => $message, - 'count' => $deleted, - 'scope' => $scope, - 'days' => $days, - ) ); - } -} \ No newline at end of file diff --git a/api/class-rest-controller.php b/api/class-rest-controller.php deleted file mode 100644 index c7aa0fa..0000000 --- a/api/class-rest-controller.php +++ /dev/null @@ -1,923 +0,0 @@ -queue_manager = $queue_manager; - $this->job_processor = $job_processor; - $this->sync_worker = new Sync_Worker( $queue_manager, $job_processor ); - - // Attach logging filters lazily after WordPress REST server is initialized. - add_filter( 'rest_post_dispatch', function ( $response, $server, $request ) { - return $this->maybe_log_request( $response, $request ); - }, 10, 3 ); - } - - /** - * Register REST API routes. - * - * @since 1.0.0 - */ - public function register_routes() { - // Jobs endpoints. - register_rest_route( - self::NAMESPACE , - '/jobs', - array( - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_jobs' ), - 'permission_callback' => array( $this, 'check_permissions' ), - 'args' => $this->get_collection_params(), - ), - array( - 'methods' => WP_REST_Server::CREATABLE, - 'callback' => array( $this, 'create_job' ), - 'permission_callback' => array( $this, 'check_permissions' ), - 'args' => $this->get_create_job_params(), - ), - ) - ); - - register_rest_route( - self::NAMESPACE , - '/jobs/(?P[a-zA-Z0-9_-]+)', - array( - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_job' ), - 'permission_callback' => array( $this, 'check_permissions' ), - ), - array( - 'methods' => WP_REST_Server::DELETABLE, - 'callback' => array( $this, 'delete_job' ), - 'permission_callback' => array( $this, 'check_permissions' ), - ), - ) - ); - - // Workers endpoints. - register_rest_route( - self::NAMESPACE , - '/workers/trigger', - array( - 'methods' => WP_REST_Server::CREATABLE, - 'callback' => array( $this, 'trigger_worker' ), - 'permission_callback' => array( $this, 'check_permissions' ), - 'args' => $this->get_trigger_worker_params(), - ) - ); - - register_rest_route( - self::NAMESPACE , - '/workers/status', - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_worker_status' ), - 'permission_callback' => array( $this, 'check_permissions' ), - ) - ); - - // Queue statistics endpoints. - register_rest_route( - self::NAMESPACE , - '/stats', - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_stats' ), - 'permission_callback' => array( $this, 'check_permissions' ), - ) - ); - - register_rest_route( - self::NAMESPACE , - '/health', - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_health' ), - 'permission_callback' => array( $this, 'check_permissions' ), - ) - ); - - // Queue management endpoints. - register_rest_route( - self::NAMESPACE , - '/queues/(?P[a-zA-Z0-9_-]+)/clear', - array( - 'methods' => WP_REST_Server::CREATABLE, - 'callback' => array( $this, 'clear_queue' ), - 'permission_callback' => array( $this, 'check_admin_permissions' ), - ) - ); - } - - /** - * Get jobs from the queue. - * - * @since 1.0.0 - * @param WP_REST_Request $request Request object. - * @return WP_REST_Response|WP_Error Response object. - */ - public function get_jobs( $request ) { - global $wpdb; - - $per_page = $request->get_param( 'per_page' ) ?: 10; - $page = $request->get_param( 'page' ) ?: 1; - $status = $request->get_param( 'status' ); - $queue = $request->get_param( 'queue' ); - - $offset = ( $page - 1 ) * $per_page; - $table_name = $wpdb->prefix . 'redis_queue_jobs'; - - // Build query. - $where_conditions = array(); - $prepare_values = array(); - - if ( $status ) { - $where_conditions[] = 'status = %s'; - $prepare_values[] = $status; - } - - if ( $queue ) { - $where_conditions[] = 'queue_name = %s'; - $prepare_values[] = $queue; - } - - $where_clause = ''; - if ( ! empty( $where_conditions ) ) { - $where_clause = 'WHERE ' . implode( ' AND ', $where_conditions ); - } - - // Get total count. - $count_query = "SELECT COUNT(*) FROM {$table_name} {$where_clause}"; - if ( ! empty( $prepare_values ) ) { - $count_query = $wpdb->prepare( $count_query, ...$prepare_values ); - } - $total = (int) $wpdb->get_var( $count_query ); - - // Get jobs. - $jobs_query = "SELECT * FROM {$table_name} {$where_clause} ORDER BY created_at DESC LIMIT %d OFFSET %d"; - $prepare_values[] = $per_page; - $prepare_values[] = $offset; - - $jobs = $wpdb->get_results( - $wpdb->prepare( $jobs_query, ...$prepare_values ), - ARRAY_A - ); - - // Format jobs for response. - $formatted_jobs = array(); - foreach ( $jobs as $job ) { - $formatted_jobs[] = $this->format_job_response( $job ); - } - - $response = rest_ensure_response( $formatted_jobs ); - $response->header( 'X-WP-Total', $total ); - $response->header( 'X-WP-TotalPages', ceil( $total / $per_page ) ); - - return $response; - } - - /** - * Get a single job by ID. - * - * @since 1.0.0 - * @param WP_REST_Request $request Request object. - * @return WP_REST_Response|WP_Error Response object. - */ - public function get_job( $request ) { - global $wpdb; - - $job_id = $request->get_param( 'id' ); - $table_name = $wpdb->prefix . 'redis_queue_jobs'; - - $job = $wpdb->get_row( - $wpdb->prepare( "SELECT * FROM {$table_name} WHERE job_id = %s", $job_id ), - ARRAY_A - ); - - if ( ! $job ) { - return new WP_Error( - 'job_not_found', - __( 'Job not found.', 'redis-queue-demo' ), - array( 'status' => 404 ) - ); - } - - return rest_ensure_response( $this->format_job_response( $job ) ); - } - - /** - * Create a new job. - * - * @since 1.0.0 - * @param WP_REST_Request $request Request object. - * @return WP_REST_Response|WP_Error Response object. - */ - public function create_job( $request ) { - $job_type = $request->get_param( 'type' ); - $payload = $request->get_param( 'payload' ) ?: array(); - $priority = $request->get_param( 'priority' ) ?: 50; - $queue = $request->get_param( 'queue' ) ?: 'default'; - - try { - // Create job instance based on type. - $job = $this->create_job_instance( $job_type, $payload ); - if ( ! $job ) { - return new WP_Error( - 'invalid_job_type', - __( 'Invalid job type specified.', 'redis-queue-demo' ), - array( 'status' => 400 ) - ); - } - - // Set job properties. - $job->set_priority( $priority ); - $job->set_queue_name( $queue ); - - // Enqueue the job. - $job_id = $this->queue_manager->enqueue( $job ); - - if ( ! $job_id ) { - return new WP_Error( - 'enqueue_failed', - __( 'Failed to enqueue job.', 'redis-queue-demo' ), - array( 'status' => 500 ) - ); - } - - return rest_ensure_response( - array( - 'success' => true, - 'job_id' => $job_id, - 'message' => __( 'Job created and enqueued successfully.', 'redis-queue-demo' ), - ) - ); - - } catch (Exception $e) { - return new WP_Error( - 'job_creation_failed', - $e->getMessage(), - array( 'status' => 500 ) - ); - } - } - - /** - * Delete (cancel) a job. - * - * @since 1.0.0 - * @param WP_REST_Request $request Request object. - * @return WP_REST_Response|WP_Error Response object. - */ - public function delete_job( $request ) { - global $wpdb; - - $job_id = $request->get_param( 'id' ); - $table_name = $wpdb->prefix . 'redis_queue_jobs'; - - // Check if job exists and is cancellable. - $job = $wpdb->get_row( - $wpdb->prepare( - "SELECT * FROM {$table_name} WHERE job_id = %s AND status IN ('queued', 'failed')", - $job_id - ) - ); - - if ( ! $job ) { - return new WP_Error( - 'job_not_found_or_not_cancellable', - __( 'Job not found or cannot be cancelled.', 'redis-queue-demo' ), - array( 'status' => 404 ) - ); - } - - // Update job status to cancelled. - $updated = $wpdb->update( - $table_name, - array( - 'status' => 'cancelled', - 'updated_at' => current_time( 'mysql' ), - ), - array( 'job_id' => $job_id ), - array( '%s', '%s' ), - array( '%s' ) - ); - - if ( false === $updated ) { - return new WP_Error( - 'job_cancellation_failed', - __( 'Failed to cancel job.', 'redis-queue-demo' ), - array( 'status' => 500 ) - ); - } - - return rest_ensure_response( - array( - 'success' => true, - 'job_id' => $job_id, - 'message' => __( 'Job cancelled successfully.', 'redis-queue-demo' ), - ) - ); - } - - /** - * Trigger worker to process jobs. - * - * @since 1.0.0 - * @param WP_REST_Request $request Request object. - * @return WP_REST_Response|WP_Error Response object. - */ - public function trigger_worker( $request ) { - $queues = $request->get_param( 'queues' ) ?: array( 'default' ); - $max_jobs = $request->get_param( 'max_jobs' ) ?: 10; - - if ( ! is_array( $queues ) ) { - $queues = array( $queues ); - } - - try { - $results = $this->sync_worker->process_jobs( $queues, $max_jobs ); - - return rest_ensure_response( - array( - 'success' => $results[ 'success' ], - 'data' => $results, - 'message' => sprintf( - /* translators: %d: number of jobs processed */ - __( 'Worker processed %d jobs.', 'redis-queue-demo' ), - $results[ 'processed' ] ?? 0 - ), - ) - ); - - } catch (Exception $e) { - return new WP_Error( - 'worker_execution_failed', - $e->getMessage(), - array( 'status' => 500 ) - ); - } - } - - /** - * Get worker status. - * - * @since 1.0.0 - * @param WP_REST_Request $request Request object. - * @return WP_REST_Response Response object. - */ - public function get_worker_status( $request ) { - $status = $this->sync_worker->get_status(); - - return rest_ensure_response( - array( - 'success' => true, - 'data' => $status, - ) - ); - } - - /** - * Get queue statistics. - * - * @since 1.0.0 - * @param WP_REST_Request $request Request object. - * @return WP_REST_Response Response object. - */ - public function get_stats( $request ) { - $queue_name = $request->get_param( 'queue' ); - $stats = $this->queue_manager->get_queue_stats( $queue_name ); - - return rest_ensure_response( - array( - 'success' => true, - 'data' => $stats, - ) - ); - } - - /** - * Get system health check. - * - * @since 1.0.0 - * @param WP_REST_Request $request Request object. - * @return WP_REST_Response Response object. - */ - public function get_health( $request ) { - $health = array( - 'redis_connected' => $this->queue_manager->is_connected(), - 'redis_info' => array(), - 'database_status' => $this->check_database_health(), - 'memory_usage' => array( - 'current' => memory_get_usage( true ), - 'peak' => memory_get_peak_usage( true ), - 'limit' => ini_get( 'memory_limit' ), - ), - 'php_version' => PHP_VERSION, - 'wordpress_version' => get_bloginfo( 'version' ), - 'plugin_version' => REDIS_QUEUE_DEMO_VERSION, - ); - - // Get Redis info if connected. - if ( $health[ 'redis_connected' ] ) { - try { - $redis = $this->queue_manager->get_redis_connection(); - if ( $redis && method_exists( $redis, 'info' ) ) { - $redis_info = $redis->info(); - $health[ 'redis_info' ] = array( - 'redis_version' => $redis_info[ 'redis_version' ] ?? 'unknown', - 'used_memory' => $redis_info[ 'used_memory_human' ] ?? 'unknown', - 'connected_clients' => $redis_info[ 'connected_clients' ] ?? 'unknown', - ); - } - } catch (Exception $e) { - $health[ 'redis_info' ][ 'error' ] = $e->getMessage(); - } - } - - $overall_status = $health[ 'redis_connected' ] && $health[ 'database_status' ]; - - return rest_ensure_response( - array( - 'success' => $overall_status, - 'status' => $overall_status ? 'healthy' : 'unhealthy', - 'data' => $health, - ) - ); - } - - /** - * Clear a queue. - * - * @since 1.0.0 - * @param WP_REST_Request $request Request object. - * @return WP_REST_Response|WP_Error Response object. - */ - public function clear_queue( $request ) { - $queue_name = $request->get_param( 'name' ); - - if ( empty( $queue_name ) ) { - return new WP_Error( - 'missing_queue_name', - __( 'Queue name is required.', 'redis-queue-demo' ), - array( 'status' => 400 ) - ); - } - - $result = $this->queue_manager->clear_queue( $queue_name ); - - if ( $result ) { - return rest_ensure_response( - array( - 'success' => true, - 'message' => sprintf( - /* translators: %s: queue name */ - __( 'Queue "%s" cleared successfully.', 'redis-queue-demo' ), - $queue_name - ), - ) - ); - } else { - return new WP_Error( - 'queue_clear_failed', - __( 'Failed to clear queue.', 'redis-queue-demo' ), - array( 'status' => 500 ) - ); - } - } - - /** - * Check if user has permission to access endpoints. - * - * @since 1.0.0 - * @param WP_REST_Request $request Request object. - * @return bool|WP_Error True if user has permission, WP_Error otherwise. - */ - public function check_permissions( $request ) { - // 1. Capability check first (logged-in admin passes immediately). - if ( current_user_can( 'manage_options' ) ) { - $this->last_auth_method = 'cap'; - return true; - } - - // 2. Token authentication fallback with scope + rate limiting. - $settings = get_option( 'redis_queue_settings', array() ); - $api_token = isset( $settings[ 'api_token' ] ) ? $settings[ 'api_token' ] : ''; - $scope = isset( $settings[ 'api_token_scope' ] ) ? $settings[ 'api_token_scope' ] : 'worker'; - $rate_limit_enabled = true; // Always enforce if token used; thresholds read below. - $rate_per_min = isset( $settings[ 'rate_limit_per_minute' ] ) ? (int) $settings[ 'rate_limit_per_minute' ] : 60; - $rate_per_min = $rate_per_min > 0 ? $rate_per_min : 60; - - if ( ! empty( $api_token ) ) { - $provided = ''; - $auth_header = $request->get_header( 'authorization' ); - if ( $auth_header && stripos( $auth_header, 'bearer ' ) === 0 ) { - $provided = trim( substr( $auth_header, 7 ) ); - } - if ( empty( $provided ) ) { - $provided = $request->get_header( 'x-redis-queue-token' ); - } - - if ( ! empty( $provided ) && hash_equals( $api_token, $provided ) ) { - $this->last_auth_method = 'token'; - $this->last_token_used = $provided; // kept only in-memory for this request. - - // Enforce scope: default 'worker' only allows trigger endpoint unless scope set to full. - $route = $request->get_route(); // e.g. /redis-queue/v1/workers/trigger - $allowed = true; - if ( 'full' !== $scope ) { - $allowed_routes = apply_filters( 'redis_queue_demo_token_allowed_routes', array( '/redis-queue/v1/workers/trigger' ), $scope ); - $allowed = in_array( $route, $allowed_routes, true ); - } - $allowed = apply_filters( 'redis_queue_demo_token_scope_allow', $allowed, $scope, $request ); - $this->last_scope_allowed = $allowed; - if ( ! $allowed ) { - return new WP_Error( 'rest_forbidden_scope', __( 'Token scope does not permit this endpoint.', 'redis-queue-demo' ), array( 'status' => 403 ) ); - } - - // Rate limiting (only token requests). - if ( $rate_limit_enabled && $rate_per_min > 0 ) { - $limit_ok = $this->enforce_rate_limit( $provided, $rate_per_min ); - if ( ! $limit_ok ) { - $this->last_rate_limited = true; - return new WP_Error( 'rate_limited', __( 'Rate limit exceeded. Try again later.', 'redis-queue-demo' ), array( 'status' => 429 ) ); - } - } - - return true; - } - } - - $this->last_auth_method = 'none'; - return new WP_Error( 'rest_forbidden', __( 'You do not have permission to access this endpoint.', 'redis-queue-demo' ), array( 'status' => 403 ) ); - } - - /** - * Enforce minute-based rate limit for a token. - * Uses transients for lightweight storage. - * - * @param string $token Token value. - * @param int $per_minute Allowed requests per minute. - * @return bool True if within limit. - */ - private function enforce_rate_limit( $token, $per_minute ) { - $key_root = 'redis_queue_demo_rate_' . substr( hash( 'sha256', $token ), 0, 24 ); - $minute = gmdate( 'YmdHi' ); - $key = $key_root . '_' . $minute; - $count = (int) get_transient( $key ); - $count++; - if ( $count === 1 ) { - // Set transient for remainder of current minute (~60 seconds) to ensure window alignment. - $ttl = 60 - (int) gmdate( 's' ); - set_transient( $key, 1, $ttl ); - return true; - } - if ( $count > $per_minute ) { - return false; - } - // Increment persisted count. - $ttl = 60 - (int) gmdate( 's' ); - set_transient( $key, $count, $ttl ); - return true; - } - - /** - * Maybe log request if logging is enabled in settings. - * @param WP_REST_Response|mixed $response Response. - * @param WP_REST_Request $request Request. - * @return WP_REST_Response|mixed Original response. - */ - private function maybe_log_request( $response, $request ) { - if ( ! $request instanceof WP_REST_Request ) { - return $response; - } - $route = $request->get_route(); - if ( 0 !== strpos( $route, '/' . self::NAMESPACE) ) { - return $response; // Not our namespace. - } - $settings = get_option( 'redis_queue_settings', array() ); - if ( empty( $settings[ 'enable_request_logging' ] ) ) { - return $response; - } - $rotate_kb = isset( $settings[ 'log_rotate_size_kb' ] ) ? (int) $settings[ 'log_rotate_size_kb' ] : 256; - $max_files = isset( $settings[ 'log_max_files' ] ) ? (int) $settings[ 'log_max_files' ] : 5; - $rotate_kb = $rotate_kb > 8 ? $rotate_kb : 256; - $max_files = $max_files > 0 ? $max_files : 5; - $status_code = ( $response instanceof WP_REST_Response ) ? $response->get_status() : 0; - - $line = wp_json_encode( array( - 'ts' => gmdate( 'c' ), - 'method' => $request->get_method(), - 'route' => $route, - 'status' => $status_code, - 'auth' => $this->last_auth_method, - 'scope_ok' => $this->last_scope_allowed, - 'rate_limited' => $this->last_rate_limited, - 'user_id' => get_current_user_id(), - 'ip' => isset( $_SERVER[ 'REMOTE_ADDR' ] ) ? sanitize_text_field( $_SERVER[ 'REMOTE_ADDR' ] ) : '', - ) ); - $this->append_log_line( $line, $rotate_kb, $max_files ); - return $response; - } - - /** - * Append a line to the request log with rotation. - * - * @param string $line JSON log line. - * @param int $rotate_kb Rotation threshold (KB). - * @param int $max_files Max rotated files to keep. - */ - private function append_log_line( $line, $rotate_kb, $max_files ) { - $upload_dir = wp_upload_dir(); - $dir = trailingslashit( $upload_dir[ 'basedir' ] ) . 'redis-queue-demo-logs'; - if ( ! file_exists( $dir ) ) { - wp_mkdir_p( $dir ); - } - $log_file = trailingslashit( $dir ) . 'requests.log'; - $rotate_bytes = $rotate_kb * 1024; - if ( file_exists( $log_file ) && filesize( $log_file ) > $rotate_bytes ) { - $rotated = trailingslashit( $dir ) . 'requests-' . gmdate( 'Ymd-His' ) . '.log'; - @rename( $log_file, $rotated ); - // Cleanup old rotated files. - $files = glob( trailingslashit( $dir ) . 'requests-*.log' ); - if ( is_array( $files ) && count( $files ) > $max_files ) { - sort( $files ); // Oldest first (timestamp in name ensures lexical order). - $excess = array_slice( $files, 0, count( $files ) - $max_files ); - foreach ( $excess as $old ) { - @unlink( $old ); - } - } - } - // Append line. - $fh = @fopen( $log_file, 'ab' ); - if ( $fh ) { - fwrite( $fh, $line . PHP_EOL ); - fclose( $fh ); - } - } - - /** - * Check if user has admin permissions for destructive operations. - * - * @since 1.0.0 - * @param WP_REST_Request $request Request object. - * @return bool|WP_Error True if user has permission, WP_Error otherwise. - */ - public function check_admin_permissions( $request ) { - if ( ! current_user_can( 'manage_options' ) || ! wp_verify_nonce( $request->get_header( 'X-WP-Nonce' ), 'wp_rest' ) ) { - return new WP_Error( - 'rest_forbidden', - __( 'You do not have permission to perform this action.', 'redis-queue-demo' ), - array( 'status' => 403 ) - ); - } - - return true; - } - - /** - * Get collection parameters for jobs endpoint. - * - * @since 1.0.0 - * @return array Collection parameters. - */ - private function get_collection_params() { - return array( - 'page' => array( - 'description' => __( 'Current page of the collection.', 'redis-queue-demo' ), - 'type' => 'integer', - 'default' => 1, - 'minimum' => 1, - ), - 'per_page' => array( - 'description' => __( 'Maximum number of items to be returned in result set.', 'redis-queue-demo' ), - 'type' => 'integer', - 'default' => 10, - 'minimum' => 1, - 'maximum' => 100, - ), - 'status' => array( - 'description' => __( 'Filter jobs by status.', 'redis-queue-demo' ), - 'type' => 'string', - 'enum' => array( 'queued', 'processing', 'completed', 'failed', 'cancelled' ), - ), - 'queue' => array( - 'description' => __( 'Filter jobs by queue name.', 'redis-queue-demo' ), - 'type' => 'string', - ), - ); - } - - /** - * Get parameters for job creation. - * - * @since 1.0.0 - * @return array Job creation parameters. - */ - private function get_create_job_params() { - return array( - 'type' => array( - 'description' => __( 'Job type.', 'redis-queue-demo' ), - 'type' => 'string', - 'required' => true, - 'enum' => array( 'email', 'image_processing', 'api_sync' ), - ), - 'payload' => array( - 'description' => __( 'Job payload data.', 'redis-queue-demo' ), - 'type' => 'object', - 'default' => array(), - ), - 'priority' => array( - 'description' => __( 'Job priority (lower number = higher priority).', 'redis-queue-demo' ), - 'type' => 'integer', - 'default' => 50, - 'minimum' => 0, - 'maximum' => 100, - ), - 'queue' => array( - 'description' => __( 'Queue name.', 'redis-queue-demo' ), - 'type' => 'string', - 'default' => 'default', - ), - ); - } - - /** - * Get parameters for worker trigger. - * - * @since 1.0.0 - * @return array Worker trigger parameters. - */ - private function get_trigger_worker_params() { - return array( - 'queues' => array( - 'description' => __( 'Queue names to process.', 'redis-queue-demo' ), - 'type' => 'array', - 'items' => array( 'type' => 'string' ), - 'default' => array( 'default' ), - ), - 'max_jobs' => array( - 'description' => __( 'Maximum number of jobs to process.', 'redis-queue-demo' ), - 'type' => 'integer', - 'default' => 10, - 'minimum' => 1, - 'maximum' => 100, - ), - ); - } - - /** - * Create a job instance from type and payload. - * - * @since 1.0.0 - * @param string $job_type Job type. - * @param array $payload Job payload. - * @return Queue_Job|null Job instance or null on failure. - */ - private function create_job_instance( $job_type, $payload ) { - switch ( $job_type ) { - case 'email': - return new Email_Job( $payload ); - case 'image_processing': - return new Image_Processing_Job( $payload ); - case 'api_sync': - return new API_Sync_Job( $payload ); - default: - return null; - } - } - - /** - * Format job data for API response. - * - * @since 1.0.0 - * @param array $job Job data from database. - * @return array Formatted job data. - */ - private function format_job_response( $job ) { - $payload = json_decode( $job[ 'payload' ], true ); - $result = $job[ 'result' ] ? json_decode( $job[ 'result' ], true ) : null; - - return array( - 'id' => $job[ 'job_id' ], - 'type' => $job[ 'job_type' ], - 'queue' => $job[ 'queue_name' ], - 'status' => $job[ 'status' ], - 'priority' => (int) $job[ 'priority' ], - 'payload' => $payload, - 'result' => $result, - 'attempts' => (int) $job[ 'attempts' ], - 'max_attempts' => (int) $job[ 'max_attempts' ], - 'timeout' => (int) $job[ 'timeout' ], - 'error_message' => $job[ 'error_message' ], - 'created_at' => $job[ 'created_at' ], - 'updated_at' => $job[ 'updated_at' ], - 'processed_at' => $job[ 'processed_at' ], - 'failed_at' => $job[ 'failed_at' ], - ); - } - - /** - * Check database health. - * - * @since 1.0.0 - * @return bool True if database is healthy. - */ - private function check_database_health() { - global $wpdb; - - $table_name = $wpdb->prefix . 'redis_queue_jobs'; - - // Check if table exists. - $table_exists = $wpdb->get_var( - $wpdb->prepare( - "SHOW TABLES LIKE %s", - $table_name - ) - ); - - return ! empty( $table_exists ); - } -} \ No newline at end of file diff --git a/api/endpoints/class-jobs-endpoint.php b/api/endpoints/class-jobs-endpoint.php deleted file mode 100644 index 9781603..0000000 --- a/api/endpoints/class-jobs-endpoint.php +++ /dev/null @@ -1,50 +0,0 @@ -queue_manager = $queue_manager; - } - - /** - * Register routes for this endpoint. - * - * @since 1.0.0 - */ - public function register_routes() { - // This is handled by the main REST_Controller class - // This file exists for potential future expansion - } -} \ No newline at end of file diff --git a/api/endpoints/class-workers-endpoint.php b/api/endpoints/class-workers-endpoint.php deleted file mode 100644 index 0ea5515..0000000 --- a/api/endpoints/class-workers-endpoint.php +++ /dev/null @@ -1,60 +0,0 @@ -queue_manager = $queue_manager; - $this->job_processor = $job_processor; - } - - /** - * Register routes for this endpoint. - * - * @since 1.0.0 - */ - public function register_routes() { - // This is handled by the main REST_Controller class - // This file exists for potential future expansion - } -} \ No newline at end of file diff --git a/class-github-plugin-updater.php b/class-github-plugin-updater.php deleted file mode 100644 index c1dfee6..0000000 --- a/class-github-plugin-updater.php +++ /dev/null @@ -1,80 +0,0 @@ -github_url = $config[ 'github_url' ]; - $this->plugin_file = $config[ 'plugin_file' ]; - $this->plugin_slug = $config[ 'plugin_slug' ]; - $this->branch = isset( $config[ 'branch' ] ) ? $config[ 'branch' ] : 'main'; - $this->name_regex = isset( $config[ 'name_regex' ] ) ? $config[ 'name_regex' ] : ''; - $this->enable_release_assets = isset( $config[ 'enable_release_assets' ] ) - ? $config[ 'enable_release_assets' ] - : ! empty( $this->name_regex ); - - add_action( 'init', array( $this, 'setup_updater' ) ); - } - - public function setup_updater() { - try { - $update_checker = PucFactory::buildUpdateChecker( - $this->github_url, - $this->plugin_file, - $this->plugin_slug - ); - - $update_checker->setBranch( $this->branch ); - - if ( $this->enable_release_assets && ! empty( $this->name_regex ) ) { - $update_checker->getVcsApi()->enableReleaseAssets( $this->name_regex ); - } - } catch (\Exception $e) { - if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { - error_log( 'GitHub Plugin Updater Error: ' . $e->getMessage() ); - } - } - } - - public static function create( $github_url, $plugin_file, $plugin_slug, $branch = 'main' ) { - return new self( array( - 'github_url' => $github_url, - 'plugin_file' => $plugin_file, - 'plugin_slug' => $plugin_slug, - 'branch' => $branch, - ) ); - } - - public static function create_with_assets( $github_url, $plugin_file, $plugin_slug, $name_regex, $branch = 'main' ) { - return new self( array( - 'github_url' => $github_url, - 'plugin_file' => $plugin_file, - 'plugin_slug' => $plugin_slug, - 'branch' => $branch, - 'name_regex' => $name_regex, - ) ); - } -} diff --git a/composer.json b/composer.json index 820aaf7..0d8189a 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { - "name": "soderlind/redis-queue-demo", + "name": "soderlind/redis-queue", "description": "Redis-backed prioritized, delayed & retryable background jobs for WordPress (token auth, rate limiting, logging)", - "homepage": "https://github.com/soderlind/redis-queue-demo", + "homepage": "https://github.com/soderlind/redis-queue", "type": "wordpress-plugin", "license": "GPL-2.0-or-later", "authors": [ @@ -24,7 +24,7 @@ "scaling" ], "support": { - "issues": "https://github.com/soderlind/redis-queue-demo/issues" + "issues": "https://github.com/soderlind/redis-queue/issues" }, "require": { "yahnis-elsts/plugin-update-checker": "^5.6" diff --git a/debug.php b/debug.php deleted file mode 100644 index eba5510..0000000 --- a/debug.php +++ /dev/null @@ -1,85 +0,0 @@ -queue_manager ) ? 'OK' : 'FAILED' ) . "\n"; -echo " - Job Processor: " . ( is_object( $plugin->job_processor ) ? 'OK' : 'FAILED' ) . "\n"; - -// Test 2: Check Redis connection -echo "\n2. Redis connection:\n"; -if ( $plugin->queue_manager ) { - echo " - Connected: " . ( $plugin->queue_manager->is_connected() ? 'YES' : 'NO' ) . "\n"; - - // Run diagnostics - $diagnostics = $plugin->queue_manager->diagnostic(); - echo " - Test Write: " . ( $diagnostics[ 'test_write' ] ? 'OK' : 'FAILED' ) . "\n"; - echo " - Test Read: " . ( $diagnostics[ 'test_read' ] ? 'OK' : 'FAILED' ) . "\n"; - echo " - Queue Prefix: " . $diagnostics[ 'queue_prefix' ] . "\n"; - echo " - Redis Keys: " . count( $diagnostics[ 'redis_keys' ] ) . "\n"; - if ( ! empty( $diagnostics[ 'redis_keys' ] ) ) { - echo " - Keys: " . implode( ', ', $diagnostics[ 'redis_keys' ] ) . "\n"; - } -} else { - echo " - Queue manager not available\n"; -} - -// Test 3: Try creating a simple job -echo "\n3. Job creation test:\n"; -try { - $job_id = $plugin->enqueue_job( 'email', array( - 'to' => 'test@example.com', - 'subject' => 'Test Email', - 'message' => 'This is a test email', - ) ); - - if ( $job_id ) { - echo " - Job created: $job_id\n"; - - // Check if it's in Redis - $diagnostics = $plugin->queue_manager->diagnostic(); - echo " - Redis keys after job creation: " . count( $diagnostics[ 'redis_keys' ] ) . "\n"; - if ( ! empty( $diagnostics[ 'redis_keys' ] ) ) { - echo " - Keys: " . implode( ', ', $diagnostics[ 'redis_keys' ] ) . "\n"; - } - - // Try to dequeue it - $dequeued = $plugin->queue_manager->dequeue( array( 'default' ) ); - if ( $dequeued ) { - echo " - Job dequeued successfully: " . $dequeued[ 'job_id' ] . "\n"; - echo " - Job type: " . $dequeued[ 'job_type' ] . "\n"; - echo " - Payload keys: " . implode( ', ', array_keys( $dequeued[ 'payload' ] ) ) . "\n"; - } else { - echo " - Failed to dequeue job\n"; - } - - } else { - echo " - Failed to create job\n"; - } -} catch (Exception $e) { - echo " - Exception: " . $e->getMessage() . "\n"; -} - -echo "\n=== Debug Complete ===\n"; \ No newline at end of file diff --git a/docs/worker-rest-api.md b/docs/worker-rest-api.md index f541d62..efebc4d 100644 --- a/docs/worker-rest-api.md +++ b/docs/worker-rest-api.md @@ -335,7 +335,7 @@ If token scope is worker-only and you call a disallowed endpoint: --- ## Logging & Rotation -When enabled, logs are written to: `wp-content/uploads/redis-queue-demo-logs/requests.log` +When enabled, logs are written to: `wp-content/uploads/redis-queue-logs/requests.log` Rotation occurs when the file exceeds the configured size (KB). Old logs are named `requests-YYYYmmdd-HHMMSS.log`. The oldest rotated files are pruned beyond the configured maximum count. Log line sample (JSON): diff --git a/src/API/REST_Controller.php b/src/API/REST_Controller.php index 988a958..e0c2558 100644 --- a/src/API/REST_Controller.php +++ b/src/API/REST_Controller.php @@ -1,12 +1,12 @@ prefix . 'redis_queue_jobs'; $job = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table_name} WHERE job_id = %s", $job_id ), ARRAY_A ); if ( ! $job ) { - return new WP_Error( 'job_not_found', __( 'Job not found.', 'redis-queue-demo' ), [ 'status' => 404 ] ); + return new WP_Error( 'job_not_found', __( 'Job not found.', 'redis-queue' ), [ 'status' => 404 ] ); } return \rest_ensure_response( $this->format_job_response( $job ) ); } @@ -111,15 +111,15 @@ public function create_job( $request ) { try { $job = $this->create_job_instance( $job_type, $payload ); if ( ! $job ) { - return new WP_Error( 'invalid_job_type', __( 'Invalid job type specified.', 'redis-queue-demo' ), [ 'status' => 400 ] ); + return new WP_Error( 'invalid_job_type', __( 'Invalid job type specified.', 'redis-queue' ), [ 'status' => 400 ] ); } $job->set_priority( $priority ); $job->set_queue_name( $queue ); $job_id = $this->queue_manager->enqueue( $job ); if ( ! $job_id ) { - return new WP_Error( 'enqueue_failed', __( 'Failed to enqueue job.', 'redis-queue-demo' ), [ 'status' => 500 ] ); + return new WP_Error( 'enqueue_failed', __( 'Failed to enqueue job.', 'redis-queue' ), [ 'status' => 500 ] ); } - return \rest_ensure_response( [ 'success' => true, 'job_id' => $job_id, 'message' => __( 'Job created and enqueued successfully.', 'redis-queue-demo' ) ] ); + return \rest_ensure_response( [ 'success' => true, 'job_id' => $job_id, 'message' => __( 'Job created and enqueued successfully.', 'redis-queue' ) ] ); } catch (Exception $e) { return new WP_Error( 'job_creation_failed', $e->getMessage(), [ 'status' => 500 ] ); } @@ -131,13 +131,13 @@ public function delete_job( $request ) { $table_name = $wpdb->prefix . 'redis_queue_jobs'; $job = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table_name} WHERE job_id = %s AND status IN ('queued','failed')", $job_id ) ); if ( ! $job ) { - return new WP_Error( 'job_not_found_or_not_cancellable', __( 'Job not found or cannot be cancelled.', 'redis-queue-demo' ), [ 'status' => 404 ] ); + return new WP_Error( 'job_not_found_or_not_cancellable', __( 'Job not found or cannot be cancelled.', 'redis-queue' ), [ 'status' => 404 ] ); } $updated = $wpdb->update( $table_name, [ 'status' => 'cancelled', 'updated_at' => current_time( 'mysql' ) ], [ 'job_id' => $job_id ], [ '%s', '%s' ], [ '%s' ] ); if ( false === $updated ) { - return new WP_Error( 'job_cancellation_failed', __( 'Failed to cancel job.', 'redis-queue-demo' ), [ 'status' => 500 ] ); + return new WP_Error( 'job_cancellation_failed', __( 'Failed to cancel job.', 'redis-queue' ), [ 'status' => 500 ] ); } - return \rest_ensure_response( [ 'success' => true, 'job_id' => $job_id, 'message' => __( 'Job cancelled successfully.', 'redis-queue-demo' ) ] ); + return \rest_ensure_response( [ 'success' => true, 'job_id' => $job_id, 'message' => __( 'Job cancelled successfully.', 'redis-queue' ) ] ); } public function trigger_worker( $request ) { @@ -148,7 +148,7 @@ public function trigger_worker( $request ) { } try { $results = $this->sync_worker->process_jobs( $queues, $max_jobs ); - return \rest_ensure_response( [ 'success' => $results[ 'success' ], 'data' => $results, 'message' => sprintf( __( 'Worker processed %d jobs.', 'redis-queue-demo' ), $results[ 'processed' ] ?? 0 ) ] ); + return \rest_ensure_response( [ 'success' => $results[ 'success' ], 'data' => $results, 'message' => sprintf( __( 'Worker processed %d jobs.', 'redis-queue' ), $results[ 'processed' ] ?? 0 ) ] ); } catch (Exception $e) { return new WP_Error( 'worker_execution_failed', $e->getMessage(), [ 'status' => 500 ] ); } @@ -172,7 +172,7 @@ public function get_health( $request ) { 'memory_usage' => [ 'current' => memory_get_usage( true ), 'peak' => memory_get_peak_usage( true ), 'limit' => ini_get( 'memory_limit' ) ], 'php_version' => PHP_VERSION, 'wordpress_version' => get_bloginfo( 'version' ), - 'plugin_version' => \defined( 'REDIS_QUEUE_DEMO_VERSION' ) ? REDIS_QUEUE_DEMO_VERSION : 'unknown', + 'plugin_version' => \defined( 'REDIS_QUEUE_VERSION' ) ? REDIS_QUEUE_VERSION : 'unknown', ]; if ( $health[ 'redis_connected' ] ) { try { @@ -192,13 +192,13 @@ public function get_health( $request ) { public function clear_queue( $request ) { $queue_name = $request->get_param( 'name' ); if ( empty( $queue_name ) ) { - return new WP_Error( 'missing_queue_name', __( 'Queue name is required.', 'redis-queue-demo' ), [ 'status' => 400 ] ); + return new WP_Error( 'missing_queue_name', __( 'Queue name is required.', 'redis-queue' ), [ 'status' => 400 ] ); } $result = $this->queue_manager->clear_queue( $queue_name ); if ( $result ) { - return \rest_ensure_response( [ 'success' => true, 'message' => sprintf( __( 'Queue "%s" cleared successfully.', 'redis-queue-demo' ), $queue_name ) ] ); + return \rest_ensure_response( [ 'success' => true, 'message' => sprintf( __( 'Queue "%s" cleared successfully.', 'redis-queue' ), $queue_name ) ] ); } - return new WP_Error( 'queue_clear_failed', __( 'Failed to clear queue.', 'redis-queue-demo' ), [ 'status' => 500 ] ); + return new WP_Error( 'queue_clear_failed', __( 'Failed to clear queue.', 'redis-queue' ), [ 'status' => 500 ] ); } public function check_permissions( $request ) { @@ -226,36 +226,36 @@ public function check_permissions( $request ) { $route = $request->get_route(); $allowed = true; if ( 'full' !== $scope ) { - $allowed_routes = apply_filters( 'redis_queue_demo_token_allowed_routes', [ '/redis-queue/v1/workers/trigger' ], $scope ); + $allowed_routes = apply_filters( 'redis_queue_token_allowed_routes', [ '/redis-queue/v1/workers/trigger' ], $scope ); $allowed = in_array( $route, $allowed_routes, true ); } - $allowed = apply_filters( 'redis_queue_demo_token_scope_allow', $allowed, $scope, $request ); + $allowed = apply_filters( 'redis_queue_token_scope_allow', $allowed, $scope, $request ); $this->last_scope_allowed = $allowed; if ( ! $allowed ) { - return new WP_Error( 'rest_forbidden_scope', __( 'Token scope does not permit this endpoint.', 'redis-queue-demo' ), [ 'status' => 403 ] ); + return new WP_Error( 'rest_forbidden_scope', __( 'Token scope does not permit this endpoint.', 'redis-queue' ), [ 'status' => 403 ] ); } if ( $rate_per_min > 0 ) { if ( ! $this->enforce_rate_limit( $provided, $rate_per_min ) ) { $this->last_rate_limited = true; - return new WP_Error( 'rate_limited', __( 'Rate limit exceeded. Try again later.', 'redis-queue-demo' ), [ 'status' => 429 ] ); + return new WP_Error( 'rate_limited', __( 'Rate limit exceeded. Try again later.', 'redis-queue' ), [ 'status' => 429 ] ); } } return true; } } $this->last_auth_method = 'none'; - return new WP_Error( 'rest_forbidden', __( 'You do not have permission to access this endpoint.', 'redis-queue-demo' ), [ 'status' => 403 ] ); + return new WP_Error( 'rest_forbidden', __( 'You do not have permission to access this endpoint.', 'redis-queue' ), [ 'status' => 403 ] ); } public function check_admin_permissions( $request ) { if ( ! current_user_can( 'manage_options' ) || ! wp_verify_nonce( $request->get_header( 'X-WP-Nonce' ), 'wp_rest' ) ) { - return new WP_Error( 'rest_forbidden', __( 'You do not have permission to perform this action.', 'redis-queue-demo' ), [ 'status' => 403 ] ); + return new WP_Error( 'rest_forbidden', __( 'You do not have permission to perform this action.', 'redis-queue' ), [ 'status' => 403 ] ); } return true; } private function enforce_rate_limit( $token, $per_minute ) { - $key_root = 'redis_queue_demo_rate_' . substr( hash( 'sha256', $token ), 0, 24 ); + $key_root = 'redis_queue_rate_' . substr( hash( 'sha256', $token ), 0, 24 ); $minute = gmdate( 'YmdHi' ); $key = $key_root . '_' . $minute; $count = (int) get_transient( $key ); @@ -297,7 +297,7 @@ private function maybe_log_request( $response, $request ) { private function append_log_line( $line, $rotate_kb, $max_files ) { $upload_dir = wp_upload_dir(); - $dir = trailingslashit( $upload_dir[ 'basedir' ] ) . 'redis-queue-demo-logs'; + $dir = trailingslashit( $upload_dir[ 'basedir' ] ) . 'redis-queue-logs'; if ( ! file_exists( $dir ) ) { wp_mkdir_p( $dir ); } diff --git a/src/Admin/Admin_Interface.php b/src/Admin/Admin_Interface.php index 8c0c9be..2e5d224 100644 --- a/src/Admin/Admin_Interface.php +++ b/src/Admin/Admin_Interface.php @@ -1,9 +1,9 @@ \admin_url( 'admin-ajax.php' ), 'nonce' => \wp_create_nonce( 'redis_queue_admin' ), 'restNonce' => \wp_create_nonce( 'wp_rest' ), 'restUrl' => \rest_url( 'redis-queue/v1/' ), 'strings' => [ - 'processing' => __( 'Processing...', 'redis-queue-demo' ), - 'success' => __( 'Success!', 'redis-queue-demo' ), - 'error' => __( 'Error occurred', 'redis-queue-demo' ), - 'confirmClear' => __( 'Are you sure you want to clear this queue?', 'redis-queue-demo' ), - 'workerTriggered' => __( 'Worker triggered successfully', 'redis-queue-demo' ), - 'queueCleared' => __( 'Queue cleared successfully', 'redis-queue-demo' ), + 'processing' => __( 'Processing...', 'redis-queue' ), + 'success' => __( 'Success!', 'redis-queue' ), + 'error' => __( 'Error occurred', 'redis-queue' ), + 'confirmClear' => __( 'Are you sure you want to clear this queue?', 'redis-queue' ), + 'workerTriggered' => __( 'Worker triggered successfully', 'redis-queue' ), + 'queueCleared' => __( 'Queue cleared successfully', 'redis-queue' ), ], ] ); - \wp_enqueue_style( 'redis-queue-admin', \plugin_dir_url( __FILE__ ) . '../../assets/admin.css', [], REDIS_QUEUE_DEMO_VERSION ); + \wp_enqueue_style( 'redis-queue-admin', \plugin_dir_url( __FILE__ ) . '../../assets/admin.css', [], REDIS_QUEUE_VERSION ); } // The following render methods replicate legacy output exactly. @@ -152,7 +152,7 @@ private function save_settings() { public function settings_saved_notice() { ?>
-

+

enqueue_job( $job_type, $payload, [ 'priority' => 10 ] ); + $job_id = redis_queue()->enqueue_job( $job_type, $payload, [ 'priority' => 10 ] ); if ( $job_id ) { wp_send_json_success( [ 'job_id' => $job_id, 'message' => 'Job created and enqueued successfully.' ] ); } else { @@ -237,7 +237,7 @@ public function ajax_debug_test() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( -1 ); } - $plugin = redis_queue_demo(); + $plugin = redis_queue(); $results = [ 'plugin' => [ 'Queue Manager' => $plugin->queue_manager ? 'OK' : 'FAILED', 'Job Processor' => $plugin->job_processor ? 'OK' : 'FAILED' ], 'redis' => [ 'Connected' => $plugin->queue_manager->is_connected() ? 'YES' : 'NO' ] ]; wp_send_json_success( $results ); } diff --git a/src/Admin/partials/dashboard-inline.php b/src/Admin/partials/dashboard-inline.php index 5a76d37..cec1442 100644 --- a/src/Admin/partials/dashboard-inline.php +++ b/src/Admin/partials/dashboard-inline.php @@ -2,58 +2,58 @@ // Dashboard template (ported from legacy admin/class-admin-interface.php) ?>
-

+

-

+

+ class="label">
+ class="label">
-

+

-

+

-

+

-

+

-

+

+ id="trigger-worker"> + id="refresh-stats"> + id="run-diagnostics"> + id="debug-test"> + id="reset-stuck-jobs">
-

+

diff --git a/src/Admin/partials/jobs-inline.php b/src/Admin/partials/jobs-inline.php index 9f7b74f..b489ab4 100644 --- a/src/Admin/partials/jobs-inline.php +++ b/src/Admin/partials/jobs-inline.php @@ -2,59 +2,59 @@ // Jobs page template. ?>
-

+

- + + id="purge-completed-jobs"> + id="purge-failed-jobs"> + id="purge-older-jobs"> + id="purge-all-jobs">
- +
- - - - - - - - + + + + + + + + - + @@ -69,9 +69,9 @@ class="status-badge status-"> @@ -79,7 +79,7 @@ class="status-badge status-"> 1 ) : ?>
- add_query_arg( 'paged', '%#%' ), 'format' => '', 'prev_text' => __( '« Previous', 'redis-queue-demo' ), 'next_text' => __( 'Next »', 'redis-queue-demo' ), 'total' => $total_pages, 'current' => $current_page ] ); ?> + add_query_arg( 'paged', '%#%' ), 'format' => '', 'prev_text' => __( '« Previous', 'redis-queue' ), 'next_text' => __( 'Next »', 'redis-queue' ), 'total' => $total_pages, 'current' => $current_page ] ); ?>
\ No newline at end of file diff --git a/src/Admin/partials/settings-inline.php b/src/Admin/partials/settings-inline.php index edae119..1b4e832 100644 --- a/src/Admin/partials/settings-inline.php +++ b/src/Admin/partials/settings-inline.php @@ -2,136 +2,136 @@ // Settings page template. ?>
-

+

+ data-job-id=""> | + data-job-id="">
- + - + - + - + - + - + - + - + - + - + - + - + - - @@ -139,8 +139,8 @@ class="small-text" min="8" max="16384" />

-

+

-

+

-

+

- +

-

+

- +

- +

-

+


+

- " or "X-Redis-Queue-Token: ". Possession grants the same access as an admin for these endpoints; keep it secret.', 'redis-queue-demo' ); ?> + " or "X-Redis-Queue-Token: ". Possession grants the same access as an admin for these endpoints; keep it secret.', 'redis-queue' ); ?>

+ class="button">

- +

- +

+

- +

- +

- - + - -
+
+
+
+ class="button button-primary">
-

+

+ for="image-operation">
+ for="attachment-id">

- +

+ class="button button-primary">
-

+

- - + -
+
+
+ class="button button-primary">
\ No newline at end of file diff --git a/src/Contracts/Basic_Job_Result.php b/src/Contracts/Basic_Job_Result.php index 777d41c..67e8baa 100644 --- a/src/Contracts/Basic_Job_Result.php +++ b/src/Contracts/Basic_Job_Result.php @@ -1,5 +1,5 @@ 'Soderlind\\RedisQueueDemo\\Jobs\\Email_Job', - 'image_processing' => 'Soderlind\\RedisQueueDemo\\Jobs\\Image_Processing_Job', - 'api_sync' => 'Soderlind\\RedisQueueDemo\\Jobs\\API_Sync_Job', + 'email' => 'Soderlind\\RedisQueue\\Jobs\\Email_Job', + 'image_processing' => 'Soderlind\\RedisQueue\\Jobs\\Image_Processing_Job', + 'api_sync' => 'Soderlind\\RedisQueue\\Jobs\\API_Sync_Job', ]; $job_classes = function_exists( 'apply_filters' ) ? \apply_filters( 'redis_queue_job_classes', $base_map ) : $base_map; if ( isset( $job_classes[ $job_type_normalized ] ) ) { @@ -224,7 +224,7 @@ private function retry_job( $job_id, Queue_Job $job, $attempt ): void { $this->queue_manager->enqueue( $job, $delay ); $this->queue_manager->update_job_status( $job_id, 'queued' ); if ( function_exists( 'do_action' ) ) { - \do_action( 'redis_queue_demo_job_retried', $job_id, $job, $attempt, $delay ); + \do_action( 'redis_queue_job_retried', $job_id, $job, $attempt, $delay ); } } private function mark_job_failed( $job_id, Job_Result $result ): void { @@ -232,7 +232,7 @@ private function mark_job_failed( $job_id, Job_Result $result ): void { $table = $wpdb->prefix . 'redis_queue_jobs'; $wpdb->update( $table, [ 'status' => 'failed', 'result' => ( function_exists( 'wp_json_encode' ) ? \wp_json_encode( $result->to_array() ) : json_encode( $result->to_array() ) ), 'error_message' => $result->get_error_message(), 'failed_at' => ( function_exists( 'current_time' ) ? \current_time( 'mysql' ) : date( 'Y-m-d H:i:s' ) ), 'updated_at' => ( function_exists( 'current_time' ) ? \current_time( 'mysql' ) : date( 'Y-m-d H:i:s' ) ) ], [ 'job_id' => $job_id ], [ '%s', '%s', '%s', '%s', '%s' ], [ '%s' ] ); if ( function_exists( 'do_action' ) ) { - \do_action( 'redis_queue_demo_job_permanently_failed', $job_id, $result ); + \do_action( 'redis_queue_job_permanently_failed', $job_id, $result ); } } private function should_stop_processing(): bool { diff --git a/src/Core/Redis_Queue_Demo.php b/src/Core/Redis_Queue_Demo.php deleted file mode 100644 index a31098d..0000000 --- a/src/Core/Redis_Queue_Demo.php +++ /dev/null @@ -1,202 +0,0 @@ -init_hooks(); - } - - private function init_hooks(): void { - \register_activation_hook( REDIS_QUEUE_DEMO_PLUGIN_FILE, [ $this, 'activate' ] ); - \register_deactivation_hook( REDIS_QUEUE_DEMO_PLUGIN_FILE, [ $this, 'deactivate' ] ); - \add_action( 'plugins_loaded', [ $this, 'load_textdomain' ] ); - \add_action( 'init', [ $this, 'init' ] ); - \add_action( 'rest_api_init', [ $this, 'init_rest_api' ] ); - } - - public function init(): void { - $this->load_dependencies(); - $this->init_components(); - \do_action( 'redis_queue_demo_init', $this ); - } - - private function load_dependencies(): void { - // All core classes now autoloaded via Composer. Legacy requires removed. - } - - private function init_components(): void { - $this->queue_manager = new Redis_Queue_Manager(); - $this->job_processor = new Job_Processor( $this->queue_manager ); - $this->rest_controller = new \Soderlind\RedisQueueDemo\API\REST_Controller( $this->queue_manager, $this->job_processor ); - if ( \is_admin() ) { - // Use namespaced Admin_Interface (legacy admin/class-admin-interface.php retained temporarily for reference / UI parity). - $this->admin_interface = new \Soderlind\RedisQueueDemo\Admin\Admin_Interface( $this->queue_manager, $this->job_processor ); - if ( method_exists( $this->admin_interface, 'init' ) ) { - $this->admin_interface->init(); - } - } - } - - public function init_rest_api(): void { - if ( $this->rest_controller ) { - $this->rest_controller->register_routes(); - } - } - - public function load_textdomain(): void { - \load_plugin_textdomain( 'redis-queue-demo', false, dirname( REDIS_QUEUE_DEMO_PLUGIN_BASENAME ) . '/languages' ); - } - - public function activate(): void { - if ( \version_compare( PHP_VERSION, '8.3', '<' ) ) { - \deactivate_plugins( REDIS_QUEUE_DEMO_PLUGIN_BASENAME ); - \wp_die( \esc_html__( 'Redis Queue Demo requires PHP 8.3 or higher.', 'redis-queue-demo' ), \esc_html__( 'Plugin Activation Error', 'redis-queue-demo' ), [ 'back_link' => true ] ); - } - if ( ! \extension_loaded( 'redis' ) && ! \class_exists( 'Predis\\Client' ) ) { - \deactivate_plugins( REDIS_QUEUE_DEMO_PLUGIN_BASENAME ); - \wp_die( \esc_html__( 'Redis Queue Demo requires either the Redis PHP extension or Predis library.', 'redis-queue-demo' ), \esc_html__( 'Plugin Activation Error', 'redis-queue-demo' ), [ 'back_link' => true ] ); - } - $this->create_tables(); - $this->set_default_options(); - \flush_rewrite_rules(); - \do_action( 'redis_queue_demo_activate' ); - } - - public function deactivate(): void { - \wp_clear_scheduled_hook( 'redis_queue_demo_process_jobs' ); - \flush_rewrite_rules(); - \do_action( 'redis_queue_demo_deactivate' ); - } - - private function create_tables(): void { - global $wpdb; - $charset_collate = $wpdb->get_charset_collate(); - $table_name = $wpdb->prefix . 'redis_queue_jobs'; - $sql = "CREATE TABLE $table_name ( - id bigint(20) unsigned NOT NULL AUTO_INCREMENT, - job_id varchar(255) NOT NULL, - job_type varchar(100) NOT NULL, - queue_name varchar(100) NOT NULL DEFAULT 'default', - priority int(11) NOT NULL DEFAULT 50, - status varchar(20) NOT NULL DEFAULT 'queued', - payload longtext, - result longtext, - attempts int(11) NOT NULL DEFAULT 0, - max_attempts int(11) NOT NULL DEFAULT 3, - timeout int(11) NOT NULL DEFAULT 300, - created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - processed_at datetime NULL, - failed_at datetime NULL, - error_message text, - PRIMARY KEY (id), - UNIQUE KEY job_id (job_id), - KEY status (status), - KEY queue_name (queue_name), - KEY priority (priority), - KEY created_at (created_at) - ) $charset_collate;"; - require_once ABSPATH . 'wp-admin/includes/upgrade.php'; - \dbDelta( $sql ); - } - - private function set_default_options(): void { - $default_options = [ - 'redis_host' => '127.0.0.1', - 'redis_port' => 6379, - 'redis_password' => '', - 'redis_database' => 0, - 'default_queue' => 'default', - 'max_jobs_per_run' => 10, - 'worker_timeout' => 300, - 'max_retries' => 3, - 'retry_backoff' => [ 60, 300, 900 ], - 'enable_logging' => true, - 'cleanup_completed_jobs' => true, - 'cleanup_after_days' => 7, - ]; - foreach ( $default_options as $option => $value ) { - $option_name = 'redis_queue_demo_' . $option; - if ( false === \get_option( $option_name ) ) { - \add_option( $option_name, $value ); - } - } - } - - public function get_option( $option, $default = null ) { - return \get_option( 'redis_queue_demo_' . $option, $default ); - } - public function update_option( $option, $value ) { - return \update_option( 'redis_queue_demo_' . $option, $value ); - } - public function get_queue_manager() { - return $this->queue_manager; - } - public function get_job_processor() { - return $this->job_processor; - } - - public function enqueue_job( $job_type, $payload = [], $options = [] ) { - if ( ! $this->queue_manager ) { - return false; - } - $job = $this->create_job_instance( $job_type, $payload ); - if ( ! $job ) { - return false; - } - if ( isset( $options[ 'priority' ] ) ) { - $job->set_priority( (int) $options[ 'priority' ] ); - } - if ( isset( $options[ 'queue' ] ) ) { - $job->set_queue_name( $options[ 'queue' ] ); - } - if ( isset( $options[ 'delay' ] ) ) { - $job->set_delay_until( time() + (int) $options[ 'delay' ] ); - } - return $this->queue_manager->enqueue( $job ); - } - - private function create_job_instance( $job_type, $payload ) { - switch ( $job_type ) { - case 'email': - return new \Soderlind\RedisQueueDemo\Jobs\Email_Job( $payload ); - case 'image_processing': - return new \Soderlind\RedisQueueDemo\Jobs\Image_Processing_Job( $payload ); - case 'api_sync': - return new \Soderlind\RedisQueueDemo\Jobs\API_Sync_Job( $payload ); - default: - return \apply_filters( 'redis_queue_demo_create_job', null, $job_type, $payload ); - } - } -} diff --git a/src/Jobs/API_Sync_Job.php b/src/Jobs/API_Sync_Job.php index ea120ec..657deb9 100644 --- a/src/Jobs/API_Sync_Job.php +++ b/src/Jobs/API_Sync_Job.php @@ -1,5 +1,5 @@ queue_name; } public function handle_failure( $exception, $attempt ) { - \do_action( 'redis_queue_demo_job_failure', $this, $exception, $attempt ); - if ( \redis_queue_demo()->get_option( 'enable_logging', true ) ) { + \do_action( 'redis_queue_job_failure', $this, $exception, $attempt ); + if ( \redis_queue()->get_option( 'enable_logging', true ) ) { $message = $exception instanceof Exception ? $exception->getMessage() : 'Failure result without exception'; \error_log( sprintf( 'Redis Queue Demo: Job %s failed on attempt %d - %s', $this->get_job_type(), $attempt, $message ) ); } @@ -47,18 +47,18 @@ public function should_retry( $exception, $attempt ) { return false; } if ( ! ( $exception instanceof Exception ) ) { - return \apply_filters( 'redis_queue_demo_should_retry_job', true, $this, null, $attempt ); + return \apply_filters( 'redis_queue_should_retry_job', true, $this, null, $attempt ); } $non_retry = [ 'InvalidArgumentException', 'TypeError', 'ParseError' ]; if ( in_array( get_class( $exception ), $non_retry, true ) ) { return false; } - return \apply_filters( 'redis_queue_demo_should_retry_job', true, $this, $exception, $attempt ); + return \apply_filters( 'redis_queue_should_retry_job', true, $this, $exception, $attempt ); } public function get_retry_delay( $attempt ) { $idx = $attempt - 1; $delay = $this->retry_backoff[ $idx ] ?? min( pow( 2, $attempt ) * 60, 3600 ); - return \apply_filters( 'redis_queue_demo_job_retry_delay', $delay, $this, $attempt ); + return \apply_filters( 'redis_queue_job_retry_delay', $delay, $this, $attempt ); } public function serialize() { return [ 'class' => get_class( $this ), 'payload' => $this->payload, 'priority' => $this->priority, 'retry_attempts' => $this->retry_attempts, 'timeout' => $this->timeout, 'queue_name' => $this->queue_name, 'retry_backoff' => $this->retry_backoff ]; @@ -97,7 +97,7 @@ public function set_retry_backoff( $b ) { return $this; } protected function validate_payload( $payload ) { - return \apply_filters( 'redis_queue_demo_validate_job_payload', true, $payload, $this ); + return \apply_filters( 'redis_queue_validate_job_payload', true, $payload, $this ); } protected function get_payload_value( $key, $default = null ) { return $this->payload[ $key ] ?? $default; diff --git a/src/Jobs/Email_Job.php b/src/Jobs/Email_Job.php index 64a07c3..4846b6e 100644 --- a/src/Jobs/Email_Job.php +++ b/src/Jobs/Email_Job.php @@ -1,8 +1,8 @@ get_payload_value( 'type', 'single' ); - \do_action( 'redis_queue_demo_email_job_failed', $this, $exception, $attempt, $email_type ); + \do_action( 'redis_queue_email_job_failed', $this, $exception, $attempt, $email_type ); } public function should_retry( $exception, $attempt ) { if ( $exception instanceof Exception && str_contains( $exception->getMessage(), 'Invalid email' ) ) { diff --git a/src/Jobs/Image_Processing_Job.php b/src/Jobs/Image_Processing_Job.php index 0a56d2e..23c5666 100644 --- a/src/Jobs/Image_Processing_Job.php +++ b/src/Jobs/Image_Processing_Job.php @@ -1,5 +1,5 @@ config[ 'max_jobs_per_run' ]; } - function_exists( '\do_action' ) && \do_action( 'redis_queue_demo_worker_start', $this, $queues, $max_jobs ); + function_exists( '\do_action' ) && \do_action( 'redis_queue_worker_start', $this, $queues, $max_jobs ); function_exists( '\do_action' ) && \do_action( 'redis_queue_worker_start', $this, $queues, $max_jobs ); try { $results = $this->job_processor->process_jobs( $queues, $max_jobs ); @@ -66,7 +66,7 @@ function_exists( '\do_action' ) && \do_action( 'redis_queue_worker_start', $this } } $this->state = 'idle'; - function_exists( '\do_action' ) && \do_action( 'redis_queue_demo_worker_complete', $this, $results ); + function_exists( '\do_action' ) && \do_action( 'redis_queue_worker_complete', $this, $results ); function_exists( '\do_action' ) && \do_action( 'redis_queue_worker_complete', $this, $results ); return [ 'success' => true, @@ -78,7 +78,7 @@ function_exists( '\do_action' ) && \do_action( 'redis_queue_worker_complete', $t ]; } catch (Exception $e) { $this->state = 'error'; - function_exists( '\do_action' ) && \do_action( 'redis_queue_demo_worker_error', $this, $e ); + function_exists( '\do_action' ) && \do_action( 'redis_queue_worker_error', $this, $e ); function_exists( '\do_action' ) && \do_action( 'redis_queue_worker_error', $this, $e ); return [ 'success' => false, 'error' => $e ? $e->getMessage() : 'Unknown error occurred', 'code' => $e ? $e->getCode() : 0 ]; } catch (Throwable $e) { @@ -284,7 +284,7 @@ public static function create_default( Redis_Queue_Manager $queue_manager, Job_P public function __destruct() { if ( $this->config[ 'cleanup_on_shutdown' ] ) { - function_exists( '\do_action' ) && \do_action( 'redis_queue_demo_worker_shutdown', $this ); + function_exists( '\do_action' ) && \do_action( 'redis_queue_worker_shutdown', $this ); } } } diff --git a/workers/class-sync-worker.php b/workers/class-sync-worker.php deleted file mode 100644 index 05c56cc..0000000 --- a/workers/class-sync-worker.php +++ /dev/null @@ -1,486 +0,0 @@ - 0, - 'jobs_failed' => 0, - 'total_time' => 0, - 'start_time' => null, - 'last_activity' => null, - ); - - /** - * Constructor. - * - * @since 1.0.0 - * @param Redis_Queue_Manager $queue_manager Queue manager instance. - * @param Job_Processor $job_processor Job processor instance. - * @param array $config Worker configuration. - */ - public function __construct( Redis_Queue_Manager $queue_manager, Job_Processor $job_processor, $config = array() ) { - $this->queue_manager = $queue_manager; - $this->job_processor = $job_processor; - $this->config = $this->parse_config( $config ); - $this->stats[ 'start_time' ] = microtime( true ); - } - - /** - * Process jobs from the queue. - * - * @since 1.0.0 - * @param array $queues Queue names to process. - * @param int $max_jobs Maximum number of jobs to process. - * @return array Processing results. - */ - public function process_jobs( $queues = array( 'default' ), $max_jobs = null ) { - if ( ! $this->queue_manager->is_connected() ) { - return array( - 'success' => false, - 'error' => 'Redis connection not available', - ); - } - - $this->state = 'working'; - $this->stats[ 'last_activity' ] = microtime( true ); - - // Use configured max jobs if not specified. - if ( null === $max_jobs ) { - $max_jobs = $this->config[ 'max_jobs_per_run' ]; - } - - /** - * Fires before worker starts processing jobs. - * - * @since 1.0.0 - * @param Sync_Worker $worker Worker instance. - * @param array $queues Queue names. - * @param int $max_jobs Maximum jobs to process. - */ - do_action( 'redis_queue_demo_worker_start', $this, $queues, $max_jobs ); - - try { - $results = $this->job_processor->process_jobs( $queues, $max_jobs ); - - // Update worker statistics. - $this->stats[ 'jobs_processed' ] += $results[ 'processed' ]; - $this->stats[ 'total_time' ] += $results[ 'total_time' ]; - - // Count failed jobs. - foreach ( $results[ 'results' ] as $job_result ) { - if ( ! $job_result[ 'result' ]->is_successful() ) { - $this->stats[ 'jobs_failed' ]++; - } - } - - $this->state = 'idle'; - - /** - * Fires after worker completes processing jobs. - * - * @since 1.0.0 - * @param Sync_Worker $worker Worker instance. - * @param array $results Processing results. - */ - do_action( 'redis_queue_demo_worker_complete', $this, $results ); - - return array( - 'success' => true, - 'processed' => $results[ 'processed' ], - 'total_time' => $results[ 'total_time' ], - 'total_memory' => $results[ 'total_memory' ], - 'results' => $results[ 'results' ], - 'worker_stats' => $this->get_stats(), - ); - - } catch (Exception $e) { - $this->state = 'error'; - - /** - * Fires when worker encounters an error. - * - * @since 1.0.0 - * @param Sync_Worker $worker Worker instance. - * @param Exception $exception Exception that occurred. - */ - do_action( 'redis_queue_demo_worker_error', $this, $e ); - - return array( - 'success' => false, - 'error' => $e ? $e->getMessage() : 'Unknown error occurred', - 'code' => $e ? $e->getCode() : 0, - ); - - } catch (Throwable $e) { - $this->state = 'error'; - - return array( - 'success' => false, - 'error' => $e ? $e->getMessage() : 'Unknown throwable error occurred', - 'code' => $e ? $e->getCode() : 0, - ); - - } finally { - $this->stats[ 'last_activity' ] = microtime( true ); - } - } - - /** - * Process a single job by ID. - * - * @since 1.0.0 - * @param string $job_id Job ID to process. - * @return array Processing result. - */ - public function process_job_by_id( $job_id ) { - global $wpdb; - - $table_name = $wpdb->prefix . 'redis_queue_jobs'; - - // Get job data from database. - $job_data = $wpdb->get_row( - $wpdb->prepare( - "SELECT * FROM {$table_name} WHERE job_id = %s AND status IN ('queued', 'failed')", - $job_id - ), - ARRAY_A - ); - - if ( ! $job_data ) { - return array( - 'success' => false, - 'error' => 'Job not found or not processable', - ); - } - - $this->state = 'working'; - $this->stats[ 'last_activity' ] = microtime( true ); - - try { - $payload_array = json_decode( $job_data[ 'payload' ], true ); - if ( ! is_array( $payload_array ) ) { - $payload_array = array(); - } - $job_type = $job_data[ 'job_type' ]; - // Infer class similar to namespaced worker. - $map = array( - 'email' => 'Soderlind\\RedisQueueDemo\\Jobs\\Email_Job', - 'image_processing' => 'Soderlind\\RedisQueueDemo\\Jobs\\Image_Processing_Job', - 'api_sync' => 'Soderlind\\RedisQueueDemo\\Jobs\\API_Sync_Job', - ); - $job_class = null; - if ( isset( $map[ $job_type ] ) ) { - $job_class = $map[ $job_type ]; - } elseif ( str_contains( $job_type, '\\' ) && class_exists( $job_type ) ) { - $job_class = $job_type; - } - if ( empty( $job_class ) ) { - // Log and abort gracefully; update status to failed to avoid infinite loop. - error_log( 'Redis Queue Demo (legacy worker): Unable to infer class for job_id=' . $job_id . ' job_type=' . $job_type ); - $wpdb->update( $wpdb->prefix . 'redis_queue_jobs', array( 'status' => 'failed', 'error_message' => 'Unresolvable job class', 'updated_at' => current_time( 'mysql' ) ), array( 'job_id' => $job_id ), array( '%s', '%s', '%s' ), array( '%s' ) ); - return array( 'success' => false, 'job_id' => $job_id, 'error' => 'Unresolvable job class', 'code' => 0 ); - } - $serialized_job = array( - 'class' => $job_class, - 'payload' => $payload_array, - 'options' => isset( $payload_array[ 'options' ] ) ? $payload_array[ 'options' ] : array(), - 'timestamp' => $job_data[ 'created_at' ], - ); - $processed_job_data = array( - 'job_id' => $job_id, - 'job_type' => $job_type, - 'queue_name' => $job_data[ 'queue_name' ], - 'priority' => $job_data[ 'priority' ], - 'payload' => $payload_array, - 'serialized_job' => $serialized_job, - ); - $result = $this->job_processor->process_job( $processed_job_data ); - - $this->stats[ 'jobs_processed' ]++; - $this->stats[ 'last_activity' ] = microtime( true ); - - if ( ! $result->is_successful() ) { - $this->stats[ 'jobs_failed' ]++; - } - - $this->state = 'idle'; - - return array( - 'success' => true, - 'job_id' => $job_id, - 'job_result' => $result, - 'worker_stats' => $this->get_stats(), - ); - - } catch (Exception $e) { - $this->state = 'error'; - $this->stats[ 'jobs_failed' ]++; - - return array( - 'success' => false, - 'job_id' => $job_id, - 'error' => $e->getMessage(), - 'code' => $e->getCode(), - ); - } - } - - /** - * Get worker status information. - * - * @since 1.0.0 - * @return array Worker status. - */ - public function get_status() { - $uptime = microtime( true ) - $this->stats[ 'start_time' ]; - - return array( - 'state' => $this->state, - 'uptime' => $uptime, - 'redis_connected' => $this->queue_manager->is_connected(), - 'config' => $this->config, - 'stats' => $this->get_stats(), - 'current_job' => $this->job_processor->get_current_job(), - 'memory_usage' => array( - 'current' => memory_get_usage( true ), - 'peak' => memory_get_peak_usage( true ), - 'limit' => $this->get_memory_limit(), - ), - ); - } - - /** - * Get worker statistics. - * - * @since 1.0.0 - * @return array Worker statistics. - */ - public function get_stats() { - $uptime = microtime( true ) - $this->stats[ 'start_time' ]; - $jobs_per_second = $uptime > 0 ? $this->stats[ 'jobs_processed' ] / $uptime : 0; - - return array_merge( $this->stats, array( - 'uptime' => $uptime, - 'jobs_per_second' => $jobs_per_second, - 'success_rate' => $this->calculate_success_rate(), - ) ); - } - - /** - * Reset worker statistics. - * - * @since 1.0.0 - * @return void - */ - public function reset_stats() { - $this->stats = array( - 'jobs_processed' => 0, - 'jobs_failed' => 0, - 'total_time' => 0, - 'start_time' => microtime( true ), - 'last_activity' => null, - ); - } - - /** - * Update worker configuration. - * - * @since 1.0.0 - * @param array $config New configuration. - * @return void - */ - public function update_config( $config ) { - $this->config = $this->parse_config( $config ); - } - - /** - * Get worker configuration. - * - * @since 1.0.0 - * @return array Worker configuration. - */ - public function get_config() { - return $this->config; - } - - /** - * Check if worker should stop processing. - * - * @since 1.0.0 - * @return bool True if worker should stop. - */ - public function should_stop() { - // Check memory usage. - $memory_limit = $this->get_memory_limit(); - $current_usage = memory_get_usage( true ); - - if ( $memory_limit > 0 && $current_usage > ( $memory_limit * 0.8 ) ) { - return true; - } - - // Check execution time. - $uptime = microtime( true ) - $this->stats[ 'start_time' ]; - if ( $uptime > $this->config[ 'max_execution_time' ] ) { - return true; - } - - return false; - } - - /** - * Parse and validate worker configuration. - * - * @since 1.0.0 - * @param array $config Raw configuration. - * @return array Parsed configuration. - */ - private function parse_config( $config ) { - $defaults = array( - 'max_jobs_per_run' => redis_queue_demo()->get_option( 'max_jobs_per_run', 10 ), - 'memory_limit' => ini_get( 'memory_limit' ), - 'max_execution_time' => redis_queue_demo()->get_option( 'worker_timeout', 300 ), - 'sleep_interval' => 1, // Seconds between queue checks (not used in sync worker). - 'retry_failed_jobs' => true, - 'cleanup_on_shutdown' => true, - ); - - $parsed = array_merge( $defaults, $config ); - - // Validate numeric values. - $parsed[ 'max_jobs_per_run' ] = max( 1, (int) $parsed[ 'max_jobs_per_run' ] ); - $parsed[ 'max_execution_time' ] = max( 30, (int) $parsed[ 'max_execution_time' ] ); - $parsed[ 'sleep_interval' ] = max( 1, (int) $parsed[ 'sleep_interval' ] ); - - return $parsed; - } - - /** - * Calculate success rate percentage. - * - * @since 1.0.0 - * @return float Success rate as percentage. - */ - private function calculate_success_rate() { - if ( $this->stats[ 'jobs_processed' ] === 0 ) { - return 100.0; - } - - $successful = $this->stats[ 'jobs_processed' ] - $this->stats[ 'jobs_failed' ]; - return ( $successful / $this->stats[ 'jobs_processed' ] ) * 100; - } - - /** - * Get memory limit in bytes. - * - * @since 1.0.0 - * @return int Memory limit in bytes, or 0 if unlimited. - */ - private function get_memory_limit() { - $memory_limit = ini_get( 'memory_limit' ); - if ( '-1' === $memory_limit ) { - return 0; // Unlimited. - } - - $unit = strtolower( substr( $memory_limit, -1 ) ); - $value = (int) $memory_limit; - - switch ( $unit ) { - case 'g': - $value *= 1024 * 1024 * 1024; - break; - case 'm': - $value *= 1024 * 1024; - break; - case 'k': - $value *= 1024; - break; - } - - return $value; - } - - /** - * Create a worker instance with default configuration. - * - * @since 1.0.0 - * @param Redis_Queue_Manager $queue_manager Queue manager instance. - * @param Job_Processor $job_processor Job processor instance. - * @return Sync_Worker - */ - public static function create_default( Redis_Queue_Manager $queue_manager, Job_Processor $job_processor ) { - return new self( $queue_manager, $job_processor ); - } - - /** - * Destructor - cleanup on shutdown. - * - * @since 1.0.0 - */ - public function __destruct() { - if ( $this->config[ 'cleanup_on_shutdown' ] ) { - /** - * Fires when worker is shutting down. - * - * @since 1.0.0 - * @param Sync_Worker $worker Worker instance. - */ - do_action( 'redis_queue_demo_worker_shutdown', $this ); - } - } -} \ No newline at end of file