diff --git a/README.txt b/README.txt index 4348e05..17625f6 100644 --- a/README.txt +++ b/README.txt @@ -3,7 +3,7 @@ Contributors: rwngallego, mociofiletto Donate link: https://github.com/perfectyorg Tags: Push Notifications, Web Push Notifications, Notifications, User engagement Requires at least: 5.0 -Tested up to: 5.7 +Tested up to: 5.8 Stable tag: 1.3.2 Requires PHP: 7.2 License: GPLv2 or later @@ -97,6 +97,8 @@ You can create an issue in our Github repo: = 1.3.3 = * Send logs to error_log() by default when logging is not even enabled. Fixes [#85](https://github.com/perfectyorg/perfecty-push-wp/issues/85) +* Tested up to WordPress 5.8 +* Unleashing mechanism for stalled notification jobs. Fixes [#86](https://github.com/perfectyorg/perfecty-push-wp/issues/86) = 1.3.2 = * Add the plugin links shown in the WordPress Plugin installer diff --git a/admin/class-perfecty-push-admin-notifications-table.php b/admin/class-perfecty-push-admin-notifications-table.php index 5925895..01a1fb4 100644 --- a/admin/class-perfecty-push-admin-notifications-table.php +++ b/admin/class-perfecty-push-admin-notifications-table.php @@ -65,7 +65,7 @@ function column_payload( $item ) { function column_status( $item ) { if ( $item['status'] == Perfecty_Push_Lib_Db::NOTIFICATIONS_STATUS_SCHEDULED ) { - $timestamp = Perfecty_Push_Lib_Db::get_notification_scheduled_time( $item['id'] ); + $timestamp = Perfecty_Push_Lib_Push_Server::get_notification_scheduled_time( $item['id'] ); return sprintf( '%s %s', $item['status'], diff --git a/admin/partials/perfecty-push-admin-notifications-view.php b/admin/partials/perfecty-push-admin-notifications-view.php index a699e82..ac99897 100644 --- a/admin/partials/perfecty-push-admin-notifications-view.php +++ b/admin/partials/perfecty-push-admin-notifications-view.php @@ -51,7 +51,7 @@
status == Perfecty_Push_Lib_Db::NOTIFICATIONS_STATUS_SCHEDULED ) { - $timestamp = Perfecty_Push_Lib_Db::get_notification_scheduled_time( $item->id ); + $timestamp = Perfecty_Push_Lib_Push_Server::get_notification_scheduled_time( $item->id ); echo esc_html( $item->status ) . ' ' . esc_html__( 'at', 'perfecty-push-notifications' ) . ' ' . get_date_from_gmt( date( 'Y-m-d H:i:s', $timestamp ), 'Y-m-d H:i:s' ); } else { echo esc_html( $item->status ); diff --git a/docker-compose.yml b/docker-compose.yml index 92e92fc..bcfcf30 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,5 +35,4 @@ services: MYSQL_RANDOM_ROOT_PASSWORD: '1' networks: - perfecty-net: - name: perfecty-net \ No newline at end of file + perfecty-net: \ No newline at end of file diff --git a/includes/class-perfecty-push.php b/includes/class-perfecty-push.php index f96e8f5..22caa98 100644 --- a/includes/class-perfecty-push.php +++ b/includes/class-perfecty-push.php @@ -311,7 +311,7 @@ private function define_admin_hooks() { $this->loader->add_action( 'admin_menu', $plugin_admin, 'register_admin_menu' ); $this->loader->add_action( 'admin_init', $plugin_admin, 'register_options' ); $this->loader->add_action( 'admin_init', $plugin_admin, 'check_cron' ); - $this->loader->add_action( 'perfecty_push_broadcast_notification_event', $plugin_admin, 'execute_broadcast_batch', 10, 1 ); + $this->loader->add_action( Perfecty_Push_Lib_Push_Server::BROADCAST_HOOK, $plugin_admin, 'execute_broadcast_batch', 10, 1 ); $this->loader->add_action( 'add_meta_boxes', $plugin_admin, 'register_metaboxes' ); $this->loader->add_action( 'save_post', $plugin_admin, 'on_save_post' ); $this->loader->add_action( 'transition_post_status', $plugin_admin, 'on_transition_post_status', 10, 3 ); diff --git a/lib/class-perfecty-push-lib-cron-check.php b/lib/class-perfecty-push-lib-cron-check.php index ce35222..0688442 100644 --- a/lib/class-perfecty-push-lib-cron-check.php +++ b/lib/class-perfecty-push-lib-cron-check.php @@ -11,7 +11,7 @@ class Perfecty_Push_Lib_Cron_Check { private const FAILURES_COUNT = 'perfecty_push_cron_failures'; private const SCHEDULE_OFFSET = 60; // 60 seconds private const MAX_DIFF = 60; // 60 seconds - private const THRESHOLD = 3; // consider error after 3 failures + private const THRESHOLD = 5; // consider error after 5 failures /** * Runs the ticker and checks if the executions are correctly done diff --git a/lib/class-perfecty-push-lib-db.php b/lib/class-perfecty-push-lib-db.php index 00bc643..bac944a 100644 --- a/lib/class-perfecty-push-lib-db.php +++ b/lib/class-perfecty-push-lib-db.php @@ -8,7 +8,7 @@ class Perfecty_Push_Lib_Db { private static $allowed_users_fields = 'id,uuid,wp_user_id,endpoint,key_auth,key_p256dh,remote_ip,created_at'; - private static $allowed_notifications_fields = 'id,payload,total,succeeded,last_cursor,batch_size,status,is_taken,created_at,finished_at'; + private static $allowed_notifications_fields = 'id,payload,total,succeeded,last_cursor,batch_size,status,is_taken,created_at,finished_at,last_execution_at'; private static $allowed_logs_fields = 'level,message,created_at'; public const NOTIFICATIONS_STATUS_SCHEDULED = 'scheduled'; @@ -76,6 +76,7 @@ public static function db_create() { status varchar(15) DEFAULT 'scheduled' NOT NULL, is_taken tinyint(1) DEFAULT 0 NOT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP NOT NULL, + last_execution_at datetime NULL, finished_at datetime NULL, PRIMARY KEY (id) ) $charset;"; @@ -395,6 +396,27 @@ public static function get_notification( $notification_id ) { return $result; } + /** + * Get notifications that are stalled: + * - Status = "Running" + * - Not taken + * - Last execution > 30 seconds + * + * @return array The result + */ + public static function get_notifications_stalled() { + global $wpdb; + + $sql = $wpdb->prepare( + 'SELECT ' . self::$allowed_notifications_fields . + ' FROM ' . self::notifications_table() . + ' WHERE status = %s AND is_taken = %d AND last_execution_at <= NOW() - INTERVAL 30 SECOND', + self::NOTIFICATIONS_STATUS_RUNNING, + 0 + ); + return $wpdb->get_results( $sql ); + } + /** * Get notifications * @@ -556,13 +578,14 @@ public static function update_notification( $notification ) { $result = $wpdb->update( self::notifications_table(), array( - 'payload' => $notification->payload, - 'total' => $notification->total, - 'succeeded' => $notification->succeeded, - 'last_cursor' => $notification->last_cursor, - 'batch_size' => $notification->batch_size, - 'status' => $notification->status, - 'is_taken' => $notification->is_taken, + 'payload' => $notification->payload, + 'total' => $notification->total, + 'succeeded' => $notification->succeeded, + 'last_cursor' => $notification->last_cursor, + 'batch_size' => $notification->batch_size, + 'status' => $notification->status, + 'is_taken' => $notification->is_taken, + 'last_execution_at' => $notification->last_execution_at, ), array( 'id' => $notification->id ) ); @@ -672,7 +695,7 @@ public static function delete_notifications( $notification_ids ) { // First delete scheduled events. foreach ( $notification_ids as $nid ) { $args = array( intval( $nid ) ); - wp_clear_scheduled_hook( 'perfecty_push_broadcast_notification_event', $args ); + wp_clear_scheduled_hook( Perfecty_Push_Lib_Push_Server::BROADCAST_HOOK, $args ); } return $wpdb->query( 'DELETE FROM ' . self::notifications_table() . " WHERE id IN ($ids)" ); } @@ -696,18 +719,6 @@ private static function take_untake_notification( $notification_id, $take ) { return $result; } - /** - * Get scheduled time - * - * @param $notification_id int Notification id - * @return int|bool Scheduled Unix timestamp for the notification or false - */ - public static function get_notification_scheduled_time( $notification_id ) { - $args = array( intval( $notification_id ) ); - $result = wp_next_scheduled( 'perfecty_push_broadcast_notification_event', $args ); - return $result; - } - /** * Inserts a log entry in the DB * diff --git a/lib/class-perfecty-push-lib-push-server.php b/lib/class-perfecty-push-lib-push-server.php index a20dc3a..c8e5ca2 100644 --- a/lib/class-perfecty-push-lib-push-server.php +++ b/lib/class-perfecty-push-lib-push-server.php @@ -12,6 +12,7 @@ class Perfecty_Push_Lib_Push_Server { public const DEFAULT_BATCH_SIZE = 30; + public const BROADCAST_HOOK = 'perfecty_push_broadcast_notification_event'; private static $auth; private static $webpush; @@ -135,7 +136,7 @@ public static function schedule_broadcast_async( $payload, $scheduled_time = nul $date = new DateTime( $scheduled_time ); $scheduled_time = $date->getTimestamp(); } - $result = wp_schedule_single_event( $scheduled_time, 'perfecty_push_broadcast_notification_event', array( $notification_id ) ); + $result = wp_schedule_single_event( $scheduled_time, self::BROADCAST_HOOK, array( $notification_id ) ); Log::info( 'Scheduling job id=' . $notification_id . ', result: ' . $result ); do_action( 'perfecty_push_broadcast_scheduled', $payload ); @@ -181,6 +182,32 @@ public static function broadcast( $payload ) { return self::schedule_broadcast_async( $payload ); } + /** + * Get scheduled time + * + * @param $notification_id int Notification id + * @return int|bool Scheduled Unix timestamp for the notification or false + */ + public static function get_notification_scheduled_time( $notification_id ) { + $args = array( intval( $notification_id ) ); + $result = wp_next_scheduled( self::BROADCAST_HOOK, $args ); + return $result; + } + + /** + * Get the job notifications that are stalled and + * schedule the execution automatically + */ + public static function unleash_stalled() { + $running = Perfecty_Push_Lib_Db::get_notifications_stalled(); + foreach ( $running as $item ) { + if ( ! wp_next_scheduled( self::BROADCAST_HOOK, array( $item->id ) ) ) { + wp_schedule_single_event( time(), self::BROADCAST_HOOK, array( $item->id ) ); + Log::info( 'An stalled notification job was unleashed, id = ' . $item->id ); + } + } + } + /** * Execute one broadcast batch * @@ -199,9 +226,7 @@ public static function execute_broadcast_batch( $notification_id ) { // if it has been taken but not released, that means a wrong state if ( $notification->is_taken ) { - Log::error( 'Halted, notification taken but not released, notification_id: ' . $notification_id ); - Perfecty_Push_Lib_Db::mark_notification_failed( $notification_id ); - Perfecty_Push_Lib_Db::untake_notification( $notification_id ); + Log::error( 'Halted, notification job already taken, notification_id: ' . $notification_id ); return false; } @@ -238,13 +263,14 @@ public static function execute_broadcast_batch( $notification_id ) { // we send one batch $result = self::send_notification( $notification->payload, $users ); if ( is_array( $result ) ) { - $notification = Perfecty_Push_Lib_Db::get_notification( $notification_id ); - $total_batch = $result[0]; - $succeeded = $result[1]; - $notification->last_cursor += $total_batch; - $notification->succeeded += $succeeded; - $notification->is_taken = 0; - $result = Perfecty_Push_Lib_Db::update_notification( $notification ); + $notification = Perfecty_Push_Lib_Db::get_notification( $notification_id ); + $total_batch = $result[0]; + $succeeded = $result[1]; + $notification->last_cursor += $total_batch; + $notification->succeeded += $succeeded; + $notification->is_taken = 0; + $notification->last_execution_at = current_time( 'mysql', 1 ); + $result = Perfecty_Push_Lib_Db::update_notification( $notification ); Log::info( 'Notification batch for id=' . $notification_id . ' sent. Cursor: ' . $notification->last_cursor . ', Total: ' . $total_batch . ', Succeeded: ' . $succeeded ); if ( ! $result ) { @@ -259,9 +285,12 @@ public static function execute_broadcast_batch( $notification_id ) { } // execute the next batch - $result = wp_schedule_single_event( time(), 'perfecty_push_broadcast_notification_event', array( $notification_id ) ); - Log::info( 'Scheduling next batch for id=' . $notification_id . ' . Result: ' . $result ); - + if ( ! wp_next_scheduled( self::BROADCAST_HOOK, array( $notification_id ) ) ) { + $result = wp_schedule_single_event( time(), self::BROADCAST_HOOK, array( $notification_id ) ); + Log::info( 'Scheduling next batch for id=' . $notification_id . ' . Result: ' . $result ); + } else { + Log::warning( "Don't schedule next batch, it's already scheduled, id=" . $notification_id ); + } return true; } diff --git a/public/class-perfecty-push-public.php b/public/class-perfecty-push-public.php index 0fda6d7..9112f4a 100644 --- a/public/class-perfecty-push-public.php +++ b/public/class-perfecty-push-public.php @@ -78,6 +78,7 @@ public function enqueue_scripts() { public function print_head() { $options = get_option( 'perfecty_push' ); + Perfecty_Push_Lib_Push_Server::unleash_stalled(); require_once plugin_dir_path( __FILE__ ) . 'partials/perfecty-push-public-head.php'; } diff --git a/tests/test-push-server.php b/tests/test-push-server.php index e676dcd..0507442 100644 --- a/tests/test-push-server.php +++ b/tests/test-push-server.php @@ -421,11 +421,11 @@ public function test_execute_broadcast_batch_notification_taken() { // there should not be any execution afterwards $next_after_execution = wp_next_scheduled( 'perfecty_push_broadcast_notification_event', array( $notification_id ) ); - // should have been marked as failed + // the notification status should be the same (scheduled) $notification = Perfecty_Push_Lib_Db::get_notification( $notification_id ); $this->assertSame( false, $res ); - $this->assertSame( 'failed', $notification->status ); + $this->assertSame( 'scheduled', $notification->status ); $this->assertFalse( $next_after_execution ); } }