From 5356148cc4ff2710e2720f7c2ae82de2f4ca5699 Mon Sep 17 00:00:00 2001 From: Mykyta Synelnikov Date: Tue, 15 Apr 2025 14:27:59 +0300 Subject: [PATCH] Implement batch processing for users with empty account statuses Introduced a new batch process to handle users lacking an `account_status` meta efficiently. Refactored legacy methods, added async scheduling, and created helper functions to manage and track progress. These changes improve performance and reliability for large user bases. --- includes/admin/class-users-columns.php | 3 - includes/admin/core/class-admin-notices.php | 164 +++++++++++++------- includes/common/actions/class-users.php | 69 +++++--- includes/common/class-users.php | 25 +++ includes/core/class-setup.php | 40 ++--- includes/core/class-user.php | 3 - tests/generate-empty-status-users.php | 34 ++++ uninstall.php | 2 +- 8 files changed, 230 insertions(+), 110 deletions(-) create mode 100644 tests/generate-empty-status-users.php diff --git a/includes/admin/class-users-columns.php b/includes/admin/class-users-columns.php index 81165829..5ad62275 100644 --- a/includes/admin/class-users-columns.php +++ b/includes/admin/class-users-columns.php @@ -350,9 +350,6 @@ if ( ! class_exists( 'um\admin\Users_Columns' ) ) { return; } - // Set default statuses if not already done. - UM()->setup()->set_default_user_status(); - $id = 'um_user_status'; // need to add there additional nonce field because WordPress native _wpnonce field isn't visible on the users.php screen then custom actions diff --git a/includes/admin/core/class-admin-notices.php b/includes/admin/core/class-admin-notices.php index 0fbc12ab..be73e0f4 100644 --- a/includes/admin/core/class-admin-notices.php +++ b/includes/admin/core/class-admin-notices.php @@ -47,6 +47,8 @@ if ( ! class_exists( 'um\admin\core\Admin_Notices' ) ) { $this->lock_registration(); + $this->empty_status_users(); + $this->extensions_page(); $this->child_theme_required(); @@ -204,17 +206,17 @@ if ( ! class_exists( 'um\admin\core\Admin_Notices' ) ) { ob_start(); ?> -
- +
+
- array( + 'href' => array(), + ), + 'strong' => array(), + ); + + $this->add_notice( + 'empty_status_users', + array( + 'class' => 'info', + // translators: %1$d: Background update for users is complete; %2$d: Total users for background update. + 'message' => '

' . wp_kses( sprintf( __( 'Background process is running: Setting user statuses %1$d/%2$d.', 'ultimate-member' ), $empty_status_users[0], $empty_status_users[1] ), $allowed_html ) . '

', + 'dismissible' => false, + ) + ); + } + /** * Checking if the "Membership - Anyone can register" WordPress general setting is active */ @@ -256,11 +289,11 @@ if ( ! class_exists( 'um\admin\core\Admin_Notices' ) ) { ?>

- All Access Pass – Get access to all Ultimate Member extensions at a significant discount with our All Access Pass.', 'ultimate-member' ) ?> + All Access Pass – Get access to all Ultimate Member extensions at a significant discount with our All Access Pass.', 'ultimate-member' ); ?>

- +

@@ -290,7 +323,7 @@ if ( ! class_exists( 'um\admin\core\Admin_Notices' ) ) { $path = str_replace( '//', '/', $path ); if ( ! file_exists( $path ) ) { - $old = umask(0); + $old = umask( 0 ); @mkdir( $path, 0777, true ); umask( $old ); } @@ -326,9 +359,12 @@ if ( ! class_exists( 'um\admin\core\Admin_Notices' ) ) { 'woocommerce', ); - $slugs = array_map( function( $item ) { - return 'um-' . $item . '/um-' . $item . '.php'; - }, $old_extensions ); + $slugs = array_map( + function ( $item ) { + return 'um-' . $item . '/um-' . $item . '.php'; + }, + $old_extensions + ); $active_plugins = UM()->dependencies()->get_active_plugins(); foreach ( $slugs as $slug ) { @@ -622,7 +658,7 @@ if ( ! class_exists( 'um\admin\core\Admin_Notices' ) ) { $arr_inactive_license_keys[] = $license->item_name; } - $invalid_license++; + ++$invalid_license; } if ( ! empty( $arr_inactive_license_keys ) ) { @@ -676,34 +712,41 @@ if ( ! class_exists( 'um\admin\core\Admin_Notices' ) ) {

- +  

- add_notice( 'upgrade', array( - 'class' => 'error', - 'message' => $message, - ), 4 ); - } else { - if ( isset( $_GET['msg'] ) && 'updated' === sanitize_key( $_GET['msg'] ) ) { - if ( isset( $_GET['page'] ) && 'um_options' === sanitize_key( $_GET['page'] ) ) { - $this->add_notice( 'settings_upgrade', array( - 'class' => 'updated', - 'message' => '

' . __( 'Settings successfully upgraded', 'ultimate-member' ) . '

', - ), 4 ); - } else { - $this->add_notice( - 'upgrade', - array( - 'class' => 'updated', - // translators: %1$s is a plugin name title; %2$s is a plugin version. - 'message' => '

' . sprintf( __( '%1$s %2$s Successfully Upgraded', 'ultimate-member' ), UM_PLUGIN_NAME, UM_VERSION ) . '

', - ), - 4 - ); - } + $this->add_notice( + 'upgrade', + array( + 'class' => 'error', + 'message' => $message, + ), + 4 + ); + } elseif ( isset( $_GET['msg'] ) && 'updated' === sanitize_key( $_GET['msg'] ) ) { + if ( isset( $_GET['page'] ) && 'um_options' === sanitize_key( $_GET['page'] ) ) { + $this->add_notice( + 'settings_upgrade', + array( + 'class' => 'updated', + 'message' => '

' . __( 'Settings successfully upgraded', 'ultimate-member' ) . '

', + ), + 4 + ); + } else { + $this->add_notice( + 'upgrade', + array( + 'class' => 'updated', + // translators: %1$s is a plugin name title; %2$s is a plugin version. + 'message' => '

' . sprintf( __( '%1$s %2$s Successfully Upgraded', 'ultimate-member' ), UM_PLUGIN_NAME, UM_VERSION ) . '

', + ), + 4 + ); } } } @@ -734,18 +777,18 @@ if ( ! class_exists( 'um\admin\core\Admin_Notices' ) ) { ?>

-  |  -  |  - +  |  +  |  +

- +

- +

@@ -754,7 +797,7 @@ if ( ! class_exists( 'um\admin\core\Admin_Notices' ) ) {

- +

@@ -763,17 +806,22 @@ if ( ! class_exists( 'um\admin\core\Admin_Notices' ) ) {

- +

- add_notice( 'reviews_notice', array( - 'class' => 'updated', - 'message' => $message, - 'dismissible' => true - ), 1 ); + $this->add_notice( + 'reviews_notice', + array( + 'class' => 'updated', + 'message' => $message, + 'dismissible' => true, + ), + 1 + ); } @@ -782,7 +830,8 @@ if ( ! class_exists( 'um\admin\core\Admin_Notices' ) ) { */ function future_changed() { - ob_start(); ?> + ob_start(); + ?>

- add_notice( 'future_changes', array( - 'class' => 'updated', - 'message' => $message, - ), 2 ); + $this->add_notice( + 'future_changes', + array( + 'class' => 'updated', + 'message' => $message, + ), + 2 + ); } /** diff --git a/includes/common/actions/class-users.php b/includes/common/actions/class-users.php index dae0ce7d..d034f3cf 100644 --- a/includes/common/actions/class-users.php +++ b/includes/common/actions/class-users.php @@ -20,16 +20,16 @@ if ( ! class_exists( 'um\common\actions\Users' ) ) { const INTERVAL = 3600; - const SCHEDULE_ACTION = 'um_schedule_account_status_check'; + const SCHEDULE_ACTION = 'um_schedule_empty_account_status_check'; const BATCH_SIZE = 50; - const BATCH_ACTION = 'um_check_account_status_batch'; + const BATCH_ACTION = 'um_set_default_account_status'; public function __construct() { add_action( 'init', array( &$this, 'add_recurring_action' ) ); add_action( self::SCHEDULE_ACTION, array( &$this, 'status_check' ) ); - add_action( self::BATCH_ACTION, array( &$this, 'batch_check' ) ); + add_action( self::BATCH_ACTION, array( &$this, 'batch_check' ), 10, 3 ); } public function add_recurring_action() { @@ -45,31 +45,34 @@ if ( ! class_exists( 'um\common\actions\Users' ) ) { } public function status_check() { - global $wpdb; - $total_users = $wpdb->get_var( - "SELECT COUNT(u.ID) - FROM {$wpdb->users} u - LEFT JOIN {$wpdb->usermeta} um ON u.ID = um.user_id AND um.meta_key = 'account_status' - LEFT JOIN {$wpdb->usermeta} um2 ON u.ID = um2.user_id AND um2.meta_key = 'um_registration_in_progress' - WHERE ( um.meta_value IS NULL OR um.meta_value = '' ) AND - um2.meta_value IS NULL OR um2.meta_value != '1'" - ); - $total_users = absint( $total_users ); - + $total_users = UM()->common()->users()::get_empty_status_users(); if ( empty( $total_users ) ) { return; } - for ( $offset = 0; $offset < $total_users; $offset += self::BATCH_SIZE ) { - UM()->maybe_action_scheduler()->enqueue_async_action( self::BATCH_ACTION, array( $offset ) ); - } + UM()->maybe_action_scheduler()->enqueue_async_action( + self::BATCH_ACTION, + array( + 'page' => 1, + 'total' => $total_users, + 'pages' => ceil( $total_users / self::BATCH_SIZE ), + ) + ); } - public function batch_check( $offset ) { + /** + * Perform batch checking for users based on specific conditions. + * Ignore users with `um_registration_in_progress` that can be in the process of the registration. + * Get users with empty `account_status` meta. + * + * @param int $page The current page number. + * @param int $total The total number of users to process. + * @param int $pages The total number of pages to process. + */ + public function batch_check( $page, $total, $pages ) { $users = new WP_User_Query( array( 'number' => self::BATCH_SIZE, - 'offset' => $offset, 'fields' => 'ids', 'meta_query' => array( 'relation' => 'AND', @@ -104,8 +107,34 @@ if ( ! class_exists( 'um\common\actions\Users' ) ) { $results = $users->get_results(); if ( ! empty( $results ) ) { + $um_empty_status_users = get_option( '_um_log_empty_status_users', array( 0, 0 ) ); + if ( ! is_array( $um_empty_status_users ) ) { + $um_empty_status_users = array( 0, count( $results ) ); + } + foreach ( $results as $user_id ) { - UM()->common()->users()->approve( $user_id, true, true ); + $res = UM()->common()->users()->approve( $user_id, true, true ); + if ( $res ) { + ++$um_empty_status_users[0]; + } + } + + if ( $um_empty_status_users[0] < $um_empty_status_users[1] ) { + update_option( '_um_log_empty_status_users', $um_empty_status_users ); + } else { + delete_option( '_um_log_empty_status_users' ); + } + + $next_page = $page + 1; + if ( $next_page <= $pages ) { + UM()->maybe_action_scheduler()->enqueue_async_action( + self::BATCH_ACTION, + array( + 'page' => $next_page, + 'total' => $total, + 'pages' => $pages, + ) + ); } } } diff --git a/includes/common/class-users.php b/includes/common/class-users.php index 61587997..f5d1f933 100644 --- a/includes/common/class-users.php +++ b/includes/common/class-users.php @@ -830,4 +830,29 @@ class Users { $user = WP_Session_Tokens::get_instance( $user_id ); $user->destroy_all(); } + + /** + * Retrieve the number of users with empty `account_status` usermeta. + * + * @return int + */ + public static function get_empty_status_users() { + global $wpdb; + + $total_users = $wpdb->get_var( + "SELECT COUNT(u.ID) + FROM {$wpdb->users} u + LEFT JOIN {$wpdb->usermeta} um ON u.ID = um.user_id AND um.meta_key = 'account_status' + LEFT JOIN {$wpdb->usermeta} um2 ON u.ID = um2.user_id AND um2.meta_key = 'um_registration_in_progress' + WHERE ( um.meta_value IS NULL OR um.meta_value = '' ) AND + um2.meta_value IS NULL OR um2.meta_value != '1'" + ); + + $total_users = absint( $total_users ); + // In WordPress, an underscore prefix before the option name (e.g., _my_option_name) is commonly used to indicate that the option is private. + // This option has a format: {updated_users}/{total_users_for_update}. + update_option( '_um_log_empty_status_users', array( 0, $total_users ) ); + + return $total_users; + } } diff --git a/includes/core/class-setup.php b/includes/core/class-setup.php index 27a342eb..eb6d37f4 100644 --- a/includes/core/class-setup.php +++ b/includes/core/class-setup.php @@ -310,37 +310,21 @@ KEY meta_value_indx (um_value(191)) * @since 2.4.2 */ public function set_default_user_status() { - $result = get_transient( 'um_count_users_unassigned' ); - if ( false === $result ) { - $args = array( - 'fields' => 'ids', - 'number' => 0, - 'meta_query' => array( - array( - 'key' => 'account_status', - 'compare' => 'NOT EXISTS', - ), - ), - 'um_custom_user_query' => true, - ); - - $users = new WP_User_Query( $args ); - if ( empty( $users ) || is_wp_error( $users ) ) { - $result = array(); - } else { - $result = $users->get_results(); - } - - set_transient( 'um_count_users_unassigned', $result, DAY_IN_SECONDS ); - } - - if ( empty( $result ) ) { + $total_users = UM()->common()->users()::get_empty_status_users(); + if ( empty( $total_users ) ) { return; } - foreach ( $result as $user_id ) { - update_user_meta( $user_id, 'account_status', 'approved' ); - } + // If there are some users without `account_status` then run the first async batch for update. + $batch_size = 50; // See the class constant value `\um\common\actions\Users::BATCH_ACTION`. + UM()->maybe_action_scheduler()->enqueue_async_action( + $batch_size, + array( + 'page' => 1, + 'total' => $total_users, + 'pages' => ceil( $total_users / $batch_size ), + ) + ); } /** diff --git a/includes/core/class-user.php b/includes/core/class-user.php index f8d70584..66baf197 100644 --- a/includes/core/class-user.php +++ b/includes/core/class-user.php @@ -640,7 +640,6 @@ if ( ! class_exists( 'um\core\User' ) ) { UM()->files()->remove_dir( UM()->files()->upload_temp ); UM()->files()->remove_dir( UM()->uploader()->get_upload_base_dir() . um_user( 'ID' ) . DIRECTORY_SEPARATOR ); - delete_transient( 'um_count_users_unassigned' ); delete_transient( 'um_count_users_pending_dot' ); } @@ -959,8 +958,6 @@ if ( ! class_exists( 'um\core\User' ) ) { /** This action is documented in ultimate-member/includes/common/um-actions-register.php */ do_action( 'um_user_register', $user_id, $_POST, null ); } - - delete_transient( 'um_count_users_unassigned' ); } diff --git a/tests/generate-empty-status-users.php b/tests/generate-empty-status-users.php new file mode 100644 index 00000000..55e26ae2 --- /dev/null +++ b/tests/generate-empty-status-users.php @@ -0,0 +1,34 @@ + $random_user_name, + 'user_pass' => 'q1q2q1q2', + 'user_email' => $random_user_email, + 'first_name' => $random_first_name, + 'last_name' => $random_last_name, + 'role' => 'subscriber', + ); + + $user_id = wp_insert_user( $userdata ); + + if ( is_wp_error( $user_id ) ) { + // Something went wrong, handle the error + var_dump( 'User creation failed: ' . $user_id->get_error_message() ); + } else { + var_dump( 'User creation complete: ID:' . $user_id . ' Username:' . $userdata['user_login'] ); + } +} + +exit; diff --git a/uninstall.php b/uninstall.php index 6cf0d554..5cf4e8ab 100644 --- a/uninstall.php +++ b/uninstall.php @@ -127,7 +127,7 @@ if ( ! empty( $delete_options ) ) { delete_transient( "um_count_users_{$status}" ); } delete_transient( 'um_count_users_pending_dot' ); - delete_transient( 'um_count_users_unassigned' ); + delete_transient( 'um_count_users_unassigned' ); // legacy but still need to delete while uninstall. //remove all users cache UM()->user()->remove_cache_all_users();