From 26a7110f712b76fd884f614f277057cce151d7ad Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Wed, 19 Feb 2025 11:39:23 -0600 Subject: [PATCH 1/7] Migration: Re-use async batch infrastructure --- CHANGELOG.md | 1 + includes/class-migration.php | 87 ++----------------- readme.txt | 1 + tests/includes/class-test-migration.php | 72 ++------------- tests/includes/class-test-scheduler.php | 63 ++++++++++++++ .../collection/class-test-followers.php | 17 ---- 6 files changed, 79 insertions(+), 162 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index febc13442..ff28a8940 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Outbox now precesses the first batch of followers right away to avoid delays in processing new Activities. * Post bulk edits no longer create Outbox items, unless author or post status change. +* Version upgrade routines now use the async_batch infrastructure to process upgrades. ### Fixed diff --git a/includes/class-migration.php b/includes/class-migration.php index 55127c26e..37ff5ea2b 100644 --- a/includes/class-migration.php +++ b/includes/class-migration.php @@ -22,10 +22,6 @@ class Migration { * Initialize the class, registering WordPress hooks. */ public static function init() { - \add_action( 'activitypub_migrate', array( self::class, 'async_migration' ) ); - \add_action( 'activitypub_upgrade', array( self::class, 'async_upgrade' ), 10, 99 ); - \add_action( 'activitypub_update_comment_counts', array( self::class, 'update_comment_counts' ), 10, 2 ); - self::maybe_migrate(); } @@ -136,13 +132,12 @@ public static function maybe_migrate() { $version_from_db = ACTIVITYPUB_PLUGIN_VERSION; } - // Schedule the async migration. - if ( ! \wp_next_scheduled( 'activitypub_migrate', $version_from_db ) ) { - \wp_schedule_single_event( \time(), 'activitypub_migrate', array( $version_from_db ) ); - } if ( \version_compare( $version_from_db, '0.17.0', '<' ) ) { self::migrate_from_0_16(); } + if ( \version_compare( $version_from_db, '1.0.0', '<' ) ) { + \wp_schedule_single_event( \time(), 'activitypub_async_batch', array( array( self::class, 'migrate_from_0_17' ) ) ); + } if ( \version_compare( $version_from_db, '1.3.0', '<' ) ) { self::migrate_from_1_2_0(); } @@ -162,7 +157,7 @@ public static function maybe_migrate() { self::migrate_to_4_1_0(); } if ( \version_compare( $version_from_db, '4.5.0', '<' ) ) { - \wp_schedule_single_event( \time() + MINUTE_IN_SECONDS, 'activitypub_update_comment_counts' ); + \wp_schedule_single_event( \time() + MINUTE_IN_SECONDS, 'activitypub_async_batch', array( array( self::class, 'update_comment_counts' ) ) ); } if ( \version_compare( $version_from_db, '4.7.1', '<' ) ) { self::migrate_to_4_7_1(); @@ -175,8 +170,8 @@ public static function maybe_migrate() { } if ( \version_compare( $version_from_db, '5.0.0', '<' ) ) { Scheduler::register_schedules(); - \wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'create_post_outbox_items' ) ); - \wp_schedule_single_event( \time() + 15, 'activitypub_upgrade', array( 'create_comment_outbox_items' ) ); + \wp_schedule_single_event( \time(), 'activitypub_async_batch', array( array( self::class, 'create_post_outbox_items' ) ) ); + \wp_schedule_single_event( \time() + 15, 'activitypub_async_batch', array( array( self::class, 'create_comment_outbox_items' ) ) ); add_action( 'init', 'flush_rewrite_rules', 20 ); } if ( \version_compare( $version_from_db, '5.2.0', '<' ) ) { @@ -208,49 +203,6 @@ public static function maybe_migrate() { self::unlock(); } - /** - * Asynchronously migrates the database structure. - * - * @param string $version_from_db The version from which to migrate. - */ - public static function async_migration( $version_from_db ) { - if ( \version_compare( $version_from_db, '1.0.0', '<' ) ) { - self::migrate_from_0_17(); - } - } - - /** - * Asynchronously runs upgrade routines. - * - * @param callable $callback Callable upgrade routine. Must be a method of this class. - * @params mixed ...$args Optional. Parameters that get passed to the callback. - */ - public static function async_upgrade( $callback ) { - $args = \func_get_args(); - - // Bail if the existing lock is still valid. - if ( self::is_locked() ) { - \wp_schedule_single_event( time() + MINUTE_IN_SECONDS, 'activitypub_upgrade', $args ); - return; - } - - self::lock(); - - $callback = array_shift( $args ); // Remove $callback from arguments. - $next = \call_user_func_array( array( self::class, $callback ), $args ); - - self::unlock(); - - if ( ! empty( $next ) ) { - // Schedule the next run, adding the result to the arguments. - \wp_schedule_single_event( - \time() + 30, - 'activitypub_upgrade', - \array_merge( array( $callback ), \array_values( $next ) ) - ); - } - } - /** * Updates the custom template to use shortcodes instead of the deprecated templates. */ @@ -492,25 +444,11 @@ public static function migrate_to_4_7_2() { * @see Comment::pre_wp_update_comment_count_now() * @param int $batch_size Optional. Number of posts to process per batch. Default 100. * @param int $offset Optional. Number of posts to skip. Default 0. + * @return array|null Array with batch size and offset if there are more posts to process, null otherwise. */ public static function update_comment_counts( $batch_size = 100, $offset = 0 ) { global $wpdb; - // Bail if the existing lock is still valid. - if ( self::is_locked() ) { - \wp_schedule_single_event( - time() + ( 5 * MINUTE_IN_SECONDS ), - 'activitypub_update_comment_counts', - array( - 'batch_size' => $batch_size, - 'offset' => $offset, - ) - ); - return; - } - - self::lock(); - Comment::register_comment_types(); $comment_types = Comment::get_comment_type_slugs(); $type_inclusion = "AND comment_type IN ('" . implode( "','", $comment_types ) . "')"; @@ -531,17 +469,10 @@ public static function update_comment_counts( $batch_size = 100, $offset = 0 ) { if ( count( $post_ids ) === $batch_size ) { // Schedule next batch. - \wp_schedule_single_event( - time() + MINUTE_IN_SECONDS, - 'activitypub_update_comment_counts', - array( - 'batch_size' => $batch_size, - 'offset' => $offset + $batch_size, - ) - ); + return array( $batch_size, $offset + $batch_size ); } - self::unlock(); + return null; } /** diff --git a/readme.txt b/readme.txt index cde165cc6..16ded61d8 100644 --- a/readme.txt +++ b/readme.txt @@ -138,6 +138,7 @@ For reasons of data protection, it is not possible to see the followers of other * Added: Show metadata in the New Follower E-Mail. * Changed: Outbox now precesses the first batch of followers right away to avoid delays in processing new Activities. * Changed: Post bulk edits no longer create Outbox items, unless author or post status change. +* Changed: Version upgrade routines now use the async_batch infrastructure to process upgrades. * Fixed: The Outbox purging routine no longer is limited to deleting 5 items at a time. * Fixed: An issue where the outbox could not send object types other than `Base_Object` (introduced in 5.0.0). * Fixed: Ellipses now display correctly in notification emails for Likes and Reposts. diff --git a/tests/includes/class-test-migration.php b/tests/includes/class-test-migration.php index 821bf3eec..b520ea752 100644 --- a/tests/includes/class-test-migration.php +++ b/tests/includes/class-test-migration.php @@ -10,6 +10,7 @@ use Activitypub\Collection\Outbox; use Activitypub\Migration; use Activitypub\Comment; +use Activitypub\Scheduler; /** * Test class for Activitypub Migrate. @@ -390,12 +391,12 @@ public function test_update_comment_counts_with_lock() { Comment::register_comment_types(); // Create test comments. - $post_id = $this->factory->post->create( + $post_id = self::factory()->post->create( array( 'post_author' => 1, ) ); - $comment_id = $this->factory->comment->create( + $comment_id = self::factory()->comment->create( array( 'comment_post_ID' => $post_id, 'comment_approved' => '1', @@ -413,41 +414,6 @@ public function test_update_comment_counts_with_lock() { wp_delete_post( $post_id, true ); } - /** - * Test update_comment_counts() with existing valid lock. - * - * @covers ::update_comment_counts - */ - public function test_update_comment_counts_with_existing_valid_lock() { - // Register comment types. - Comment::register_comment_types(); - - // Set a lock. - Migration::lock(); - - Migration::update_comment_counts( 10, 0 ); - - // Verify a scheduled event was created. - $next_scheduled = wp_next_scheduled( - 'activitypub_update_comment_counts', - array( - 'batch_size' => 10, - 'offset' => 0, - ) - ); - $this->assertNotFalse( $next_scheduled ); - - // Clean up. - delete_option( 'activitypub_migration_lock' ); - wp_clear_scheduled_hook( - 'activitypub_update_comment_counts', - array( - 'batch_size' => 10, - 'offset' => 0, - ) - ); - } - /** * Test create post outbox items. * @@ -519,34 +485,6 @@ public function test_create_outbox_items_batching() { $this->assertEquals( 5, count( $outbox_items ) ); } - /** - * Test async upgrade functionality. - * - * @covers ::async_upgrade - * @covers ::lock - * @covers ::unlock - * @covers ::create_post_outbox_items - */ - public function test_async_upgrade() { - // Test that lock prevents simultaneous upgrades. - Migration::lock(); - Migration::async_upgrade( 'create_post_outbox_items' ); - $scheduled = \wp_next_scheduled( 'activitypub_upgrade', array( 'create_post_outbox_items' ) ); - $this->assertNotFalse( $scheduled ); - Migration::unlock(); - - // Test scheduling next batch when callback returns more work. - Migration::async_upgrade( 'create_post_outbox_items', 1, 0 ); // Small batch size to force multiple batches. - $scheduled = \wp_next_scheduled( 'activitypub_upgrade', array( 'create_post_outbox_items', 1, 1 ) ); - $this->assertNotFalse( $scheduled ); - - // Test no scheduling when callback returns null (no more work). - Migration::async_upgrade( 'create_post_outbox_items', 100, 1000 ); // Large offset to ensure no posts found. - $this->assertFalse( - \wp_next_scheduled( 'activitypub_upgrade', array( 'create_post_outbox_items', 100, 1100 ) ) - ); - } - /** * Test async upgrade with multiple arguments. * @@ -554,8 +492,8 @@ public function test_async_upgrade() { */ public function test_async_upgrade_multiple_args() { // Test that multiple arguments are passed correctly. - Migration::async_upgrade( 'update_comment_counts', 50, 100 ); - $scheduled = \wp_next_scheduled( 'activitypub_upgrade', array( 'update_comment_counts', 50, 150 ) ); + Scheduler::async_batch( array( Migration::class, 'update_comment_counts' ), 50, 100 ); + $scheduled = \wp_next_scheduled( 'activitypub_async_batch', array( array( Migration::class, 'update_comment_counts' ), 50, 150 ) ); $this->assertFalse( $scheduled, 'Should not schedule next batch when no comments found' ); } diff --git a/tests/includes/class-test-scheduler.php b/tests/includes/class-test-scheduler.php index ecf2a4c56..d3ef67384 100644 --- a/tests/includes/class-test-scheduler.php +++ b/tests/includes/class-test-scheduler.php @@ -7,6 +7,8 @@ namespace Activitypub\Tests; +use Activitypub\Comment; +use Activitypub\Migration; use Activitypub\Scheduler; use Activitypub\Collection\Outbox; use Activitypub\Activity\Base_Object; @@ -282,4 +284,65 @@ public function test_purge_outbox_with_different_purge_days() { // Verify posts are deleted. $this->assertEquals( 0, wp_count_posts( Outbox::POST_TYPE )->publish ); } + + /** + * Test update_comment_counts() with existing valid lock. + * + * @covers ::lock + * @covers ::async_batch + */ + public function test_update_comment_counts_with_existing_valid_lock() { + // Register comment types. + Comment::register_comment_types(); + + $callback = array( Migration::class, 'update_comment_counts' ); + $key = \md5( \serialize( $callback ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize + + // Set a lock. + Scheduler::lock( $key ); + + Scheduler::async_batch( $callback, 10, 0 ); + + // Verify a scheduled event was created. + $next_scheduled = wp_next_scheduled( 'activitypub_async_batch', array( $callback, 10, 0 ) ); + $this->assertNotFalse( $next_scheduled ); + + // Clean up. + delete_option( 'activitypub_migration_lock' ); + wp_clear_scheduled_hook( 'activitypub_async_batch', array( $callback, 10, 0 ) ); + } + + /** + * Test async upgrade functionality. + * + * @covers ::async_batch + * @covers ::lock + * @covers ::unlock + */ + public function test_async_upgrade() { + $callback = array( Migration::class, 'create_post_outbox_items' ); + $key = \md5( \serialize( $callback ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize + + // Test that lock prevents simultaneous upgrades. + Scheduler::lock( $key ); + Scheduler::async_batch( $callback ); + $scheduled = \wp_next_scheduled( 'activitypub_async_batch', array( $callback ) ); + $this->assertNotFalse( $scheduled ); + Scheduler::unlock( $key ); + + \remove_action( 'transition_post_status', array( \Activitypub\Scheduler\Post::class, 'schedule_post_activity' ), 33 ); + self::factory()->post->create( array( 'meta_input' => array( 'activitypub_status' => 'federated' ) ) ); + \add_action( 'transition_post_status', array( \Activitypub\Scheduler\Post::class, 'schedule_post_activity' ), 33, 3 ); + + // Test scheduling next batch when callback returns more work. + Scheduler::async_batch( $callback, 1, 0 ); // Small batch size to force multiple batches. + $scheduled = \wp_next_scheduled( 'activitypub_async_batch', array( $callback, 1, 1 ) ); + $this->assertNotFalse( $scheduled ); + + // Test no scheduling when callback returns null (no more work). + Scheduler::async_batch( $callback, 100, 1000 ); // Large offset to ensure no posts found. + $this->assertFalse( + \wp_next_scheduled( 'activitypub_async_batch', array( $callback, 100, 1100 ) ) + ); + } } diff --git a/tests/includes/collection/class-test-followers.php b/tests/includes/collection/class-test-followers.php index 7b427340f..c7e11b0ce 100644 --- a/tests/includes/collection/class-test-followers.php +++ b/tests/includes/collection/class-test-followers.php @@ -358,23 +358,6 @@ public function test_add_duplicate_follower() { $this->assertCount( 1, $meta ); } - /** - * Tests scheduling of migration. - * - * @covers ::maybe_migrate - */ - public function test_migration_scheduling() { - update_option( 'activitypub_db_version', '0.0.1' ); - - \Activitypub\Migration::maybe_migrate(); - - $schedule = \wp_next_scheduled( 'activitypub_migrate', array( '0.0.1' ) ); - $this->assertNotFalse( $schedule ); - - // Clean up. - delete_option( 'activitypub_db_version' ); - } - /** * Data provider for migration test scenarios. * From 4ddc3d0b4c53123606deaa4801851a2a09654f55 Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Tue, 13 May 2025 13:20:09 -0500 Subject: [PATCH 2/7] Update to using new async_batch approach --- includes/class-migration.php | 8 ++++---- includes/class-scheduler.php | 14 +++++++++++--- tests/includes/class-test-migration.php | 3 +-- tests/includes/class-test-scheduler.php | 23 ++++++++++++++--------- 4 files changed, 30 insertions(+), 18 deletions(-) diff --git a/includes/class-migration.php b/includes/class-migration.php index 37b695350..2b3c87b28 100644 --- a/includes/class-migration.php +++ b/includes/class-migration.php @@ -137,7 +137,7 @@ public static function maybe_migrate() { self::migrate_from_0_16(); } if ( \version_compare( $version_from_db, '1.0.0', '<' ) ) { - \wp_schedule_single_event( \time(), 'activitypub_async_batch', array( array( self::class, 'migrate_from_0_17' ) ) ); + \wp_schedule_single_event( \time(), 'activitypub_migrate_from_0_17' ); } if ( \version_compare( $version_from_db, '1.3.0', '<' ) ) { self::migrate_from_1_2_0(); @@ -158,7 +158,7 @@ public static function maybe_migrate() { self::migrate_to_4_1_0(); } if ( \version_compare( $version_from_db, '4.5.0', '<' ) ) { - \wp_schedule_single_event( \time() + MINUTE_IN_SECONDS, 'activitypub_async_batch', array( array( self::class, 'update_comment_counts' ) ) ); + \wp_schedule_single_event( \time() + MINUTE_IN_SECONDS, 'activitypub_update_comment_counts' ); } if ( \version_compare( $version_from_db, '4.7.1', '<' ) ) { self::migrate_to_4_7_1(); @@ -171,8 +171,8 @@ public static function maybe_migrate() { } if ( \version_compare( $version_from_db, '5.0.0', '<' ) ) { Scheduler::register_schedules(); - \wp_schedule_single_event( \time(), 'activitypub_async_batch', array( array( self::class, 'create_post_outbox_items' ) ) ); - \wp_schedule_single_event( \time() + 15, 'activitypub_async_batch', array( array( self::class, 'create_comment_outbox_items' ) ) ); + \wp_schedule_single_event( \time(), 'activitypub_create_post_outbox_items' ); + \wp_schedule_single_event( \time() + 15, 'activitypub_create_comment_outbox_items' ); add_action( 'init', 'flush_rewrite_rules', 20 ); } if ( \version_compare( $version_from_db, '5.2.0', '<' ) ) { diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index a6c475618..e0adcb3fa 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -37,8 +37,12 @@ public static function init() { self::register_schedulers(); self::$batch_callbacks = array( - 'activitypub_send_activity' => array( Dispatcher::class, 'send_to_followers' ), - 'activitypub_retry_activity' => array( Dispatcher::class, 'retry_send_to_followers' ), + 'activitypub_send_activity' => array( Dispatcher::class, 'send_to_followers' ), + 'activitypub_retry_activity' => array( Dispatcher::class, 'retry_send_to_followers' ), + 'activitypub_migrate_from_0_17' => array( Migration::class, 'migrate_from_0_17' ), + 'activitypub_update_comment_counts' => array( Migration::class, 'update_comment_counts' ), + 'activitypub_create_post_outbox_items' => array( Migration::class, 'create_post_outbox_items' ), + 'activitypub_create_comment_outbox_items' => array( Migration::class, 'create_comment_outbox_items' ), ); // Follower Cleanups. @@ -49,6 +53,10 @@ public static function init() { \add_action( 'activitypub_async_batch', array( self::class, 'async_batch' ), 10, 99 ); \add_action( 'activitypub_send_activity', array( self::class, 'async_batch' ), 10, 3 ); \add_action( 'activitypub_retry_activity', array( self::class, 'async_batch' ), 10, 3 ); + \add_action( 'activitypub_migrate_from_0_17', array( self::class, 'async_batch' ) ); + \add_action( 'activitypub_update_comment_counts', array( self::class, 'async_batch' ), 10, 2 ); + \add_action( 'activitypub_create_post_outbox_items', array( self::class, 'async_batch' ), 10, 2 ); + \add_action( 'activitypub_create_comment_outbox_items', array( self::class, 'async_batch' ), 10, 2 ); \add_action( 'activitypub_reprocess_outbox', array( self::class, 'reprocess_outbox' ) ); \add_action( 'activitypub_outbox_purge', array( self::class, 'purge_outbox' ) ); @@ -326,7 +334,7 @@ public static function async_batch() { return; } - $key = \md5( \serialize( $args[0] ?? $args ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize + $key = \md5( \serialize( $callback ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize // Bail if the existing lock is still valid. if ( self::is_locked( $key ) ) { diff --git a/tests/includes/class-test-migration.php b/tests/includes/class-test-migration.php index c6c73813d..22d0abc42 100644 --- a/tests/includes/class-test-migration.php +++ b/tests/includes/class-test-migration.php @@ -177,8 +177,7 @@ public function test_migration_scheduling() { Migration::maybe_migrate(); - $schedule = \wp_next_scheduled( 'activitypub_migrate', array( '0.0.1' ) ); - $this->assertNotFalse( $schedule ); + $this->assertNotFalse( \wp_next_scheduled( 'activitypub_migrate_from_0_17' ) ); // Clean up. delete_option( 'activitypub_db_version' ); diff --git a/tests/includes/class-test-scheduler.php b/tests/includes/class-test-scheduler.php index 73b104401..9477c2b18 100644 --- a/tests/includes/class-test-scheduler.php +++ b/tests/includes/class-test-scheduler.php @@ -361,15 +361,15 @@ public function test_update_comment_counts_with_existing_valid_lock() { // Set a lock. Scheduler::lock( $key ); - Scheduler::async_batch( $callback, 10, 0 ); + \do_action( 'activitypub_update_comment_counts', 10, 0 ); // Verify a scheduled event was created. - $next_scheduled = wp_next_scheduled( 'activitypub_async_batch', array( $callback, 10, 0 ) ); + $next_scheduled = wp_next_scheduled( 'activitypub_update_comment_counts', array( 10, 0 ) ); $this->assertNotFalse( $next_scheduled ); // Clean up. delete_option( 'activitypub_migration_lock' ); - wp_clear_scheduled_hook( 'activitypub_async_batch', array( $callback, 10, 0 ) ); + wp_clear_scheduled_hook( 'activitypub_update_comment_counts', array( 10, 0 ) ); } /** @@ -385,8 +385,10 @@ public function test_async_upgrade() { // Test that lock prevents simultaneous upgrades. Scheduler::lock( $key ); - Scheduler::async_batch( $callback ); - $scheduled = \wp_next_scheduled( 'activitypub_async_batch', array( $callback ) ); + + \do_action( 'activitypub_create_post_outbox_items', 10, 0 ); + + $scheduled = \wp_next_scheduled( 'activitypub_create_post_outbox_items', array( 10, 0 ) ); $this->assertNotFalse( $scheduled ); Scheduler::unlock( $key ); @@ -395,15 +397,18 @@ public function test_async_upgrade() { \add_action( 'transition_post_status', array( \Activitypub\Scheduler\Post::class, 'schedule_post_activity' ), 33, 3 ); // Test scheduling next batch when callback returns more work. - Scheduler::async_batch( $callback, 1, 0 ); // Small batch size to force multiple batches. - $scheduled = \wp_next_scheduled( 'activitypub_async_batch', array( $callback, 1, 1 ) ); + \do_action( 'activitypub_create_post_outbox_items', 1, 0 ); // Small batch size to force multiple batches. + $scheduled = \wp_next_scheduled( 'activitypub_create_post_outbox_items', array( 1, 1 ) ); $this->assertNotFalse( $scheduled ); // Test no scheduling when callback returns null (no more work). - Scheduler::async_batch( $callback, 100, 1000 ); // Large offset to ensure no posts found. + \do_action( 'activitypub_create_post_outbox_items', 100, 1000 ); // Large offset to ensure no posts found. $this->assertFalse( - \wp_next_scheduled( 'activitypub_async_batch', array( $callback, 100, 1100 ) ) + \wp_next_scheduled( 'activitypub_create_post_outbox_items', array( 100, 1100 ) ) ); + } + + /** * Test async_batch method. * * @covers ::async_batch From 181ae022079f2da83472bc177e892e2099204ae4 Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Tue, 13 May 2025 13:29:01 -0500 Subject: [PATCH 3/7] Update return type --- includes/class-migration.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/includes/class-migration.php b/includes/class-migration.php index 2b3c87b28..c18604626 100644 --- a/includes/class-migration.php +++ b/includes/class-migration.php @@ -456,7 +456,7 @@ public static function migrate_to_4_7_2() { * @see Comment::pre_wp_update_comment_count_now() * @param int $batch_size Optional. Number of posts to process per batch. Default 100. * @param int $offset Optional. Number of posts to skip. Default 0. - * @return array|null Array with batch size and offset if there are more posts to process, null otherwise. + * @return int[]|void Array with batch size and offset if there are more posts to process. */ public static function update_comment_counts( $batch_size = 100, $offset = 0 ) { global $wpdb; @@ -483,8 +483,6 @@ public static function update_comment_counts( $batch_size = 100, $offset = 0 ) { // Schedule next batch. return array( $batch_size, $offset + $batch_size ); } - - return null; } /** From 867729838f0fe89c0aba51d393ee88e9ff540645 Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Tue, 13 May 2025 13:29:56 -0500 Subject: [PATCH 4/7] Remove test for removed functionality --- tests/includes/class-test-migration.php | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/tests/includes/class-test-migration.php b/tests/includes/class-test-migration.php index 22d0abc42..9223b1c5c 100644 --- a/tests/includes/class-test-migration.php +++ b/tests/includes/class-test-migration.php @@ -167,22 +167,6 @@ public function test_migrate_actor_mode() { $this->assertEquals( ACTIVITYPUB_ACTOR_MODE, \get_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE ) ); } - /** - * Tests scheduling of migration. - * - * @covers ::maybe_migrate - */ - public function test_migration_scheduling() { - update_option( 'activitypub_db_version', '0.0.1' ); - - Migration::maybe_migrate(); - - $this->assertNotFalse( \wp_next_scheduled( 'activitypub_migrate_from_0_17' ) ); - - // Clean up. - delete_option( 'activitypub_db_version' ); - } - /** * Test migrate to 4.1.0. * From 69bcfb7294153cff080e69a6f08a147ab242c3bd Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Thu, 11 Sep 2025 14:34:08 -0500 Subject: [PATCH 5/7] Replace hardcoded async batch allowlist with extensible registration system * Add Scheduler::register_async_batch_callback() method for dynamic callback registration * Remove hardcoded Migration callbacks from Scheduler class * Update Migration class to register its callbacks through new system * Add safety check to ensure callbacks are registered at proper time * Maintain backward compatibility - async_batch works exactly the same way * Enable any class to add async batch jobs without modifying core files --- includes/class-migration.php | 6 +++- includes/class-scheduler.php | 40 ++++++++++++++++--------- tests/includes/class-test-migration.php | 5 +++- 3 files changed, 35 insertions(+), 16 deletions(-) diff --git a/includes/class-migration.php b/includes/class-migration.php index 9e8b1a79a..2f2d155d8 100644 --- a/includes/class-migration.php +++ b/includes/class-migration.php @@ -7,7 +7,6 @@ namespace Activitypub; -use Activitypub\Activity\Actor; use Activitypub\Collection\Actors; use Activitypub\Collection\Extra_Fields; use Activitypub\Collection\Followers; @@ -25,6 +24,11 @@ class Migration { */ public static function init() { self::maybe_migrate(); + + Scheduler::register_async_batch_callback( 'activitypub_migrate_from_0_17', array( self::class, 'migrate_from_0_17' ) ); + Scheduler::register_async_batch_callback( 'activitypub_update_comment_counts', array( self::class, 'update_comment_counts' ) ); + Scheduler::register_async_batch_callback( 'activitypub_create_post_outbox_items', array( self::class, 'create_post_outbox_items' ) ); + Scheduler::register_async_batch_callback( 'activitypub_create_comment_outbox_items', array( self::class, 'create_comment_outbox_items' ) ); } /** diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index 07a764227..c4cb3f986 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -27,7 +27,10 @@ class Scheduler { * * @var array */ - private static $batch_callbacks = array(); + private static $batch_callbacks = array( + 'activitypub_send_activity' => array( Dispatcher::class, 'send_to_followers' ), + 'activitypub_retry_activity' => array( Dispatcher::class, 'retry_send_to_followers' ), + ); /** * Initialize the class, registering WordPress hooks. @@ -35,15 +38,6 @@ class Scheduler { public static function init() { self::register_schedulers(); - self::$batch_callbacks = array( - 'activitypub_send_activity' => array( Dispatcher::class, 'send_to_followers' ), - 'activitypub_retry_activity' => array( Dispatcher::class, 'retry_send_to_followers' ), - 'activitypub_migrate_from_0_17' => array( Migration::class, 'migrate_from_0_17' ), - 'activitypub_update_comment_counts' => array( Migration::class, 'update_comment_counts' ), - 'activitypub_create_post_outbox_items' => array( Migration::class, 'create_post_outbox_items' ), - 'activitypub_create_comment_outbox_items' => array( Migration::class, 'create_comment_outbox_items' ), - ); - // Follower Cleanups. \add_action( 'activitypub_update_remote_actors', array( self::class, 'update_remote_actors' ) ); \add_action( 'activitypub_cleanup_remote_actors', array( self::class, 'cleanup_remote_actors' ) ); @@ -52,10 +46,6 @@ public static function init() { \add_action( 'activitypub_async_batch', array( self::class, 'async_batch' ), 10, 99 ); \add_action( 'activitypub_send_activity', array( self::class, 'async_batch' ), 10, 3 ); \add_action( 'activitypub_retry_activity', array( self::class, 'async_batch' ), 10, 3 ); - \add_action( 'activitypub_migrate_from_0_17', array( self::class, 'async_batch' ) ); - \add_action( 'activitypub_update_comment_counts', array( self::class, 'async_batch' ), 10, 2 ); - \add_action( 'activitypub_create_post_outbox_items', array( self::class, 'async_batch' ), 10, 2 ); - \add_action( 'activitypub_create_comment_outbox_items', array( self::class, 'async_batch' ), 10, 2 ); \add_action( 'activitypub_reprocess_outbox', array( self::class, 'reprocess_outbox' ) ); \add_action( 'activitypub_outbox_purge', array( self::class, 'purge_outbox' ) ); @@ -81,6 +71,28 @@ public static function register_schedulers() { do_action( 'activitypub_register_schedulers' ); } + /** + * Register a batch callback for async processing. + * + * @param string $hook The cron event hook name. + * @param callable $callback The callback to execute. + */ + public static function register_async_batch_callback( $hook, $callback ) { + if ( \did_action( 'init' ) && ! \doing_action( 'init' ) ) { + \_doing_it_wrong( __METHOD__, 'Async batch callbacks should be registered before or during the init action.', '5.2.0' ); + return; + } + + if ( ! \is_callable( $callback ) ) { + return; + } + + self::$batch_callbacks[ $hook ] = $callback; + + // Register the WordPress action hook to trigger async_batch. + \add_action( $hook, array( self::class, 'async_batch' ), 10, 99 ); + } + /** * Schedule all ActivityPub schedules. */ diff --git a/tests/includes/class-test-migration.php b/tests/includes/class-test-migration.php index 1e84e807d..381e7c722 100644 --- a/tests/includes/class-test-migration.php +++ b/tests/includes/class-test-migration.php @@ -7,6 +7,8 @@ namespace Activitypub\Tests; +use Activitypub\Activity\Actor; +use Activitypub\Collection\Actors; use Activitypub\Collection\Extra_Fields; use Activitypub\Collection\Followers; use Activitypub\Collection\Outbox; @@ -475,7 +477,8 @@ public function test_create_outbox_items_batching() { /** * Test async upgrade with multiple arguments. * - * @covers ::async_upgrade + * @covers ::update_comment_counts + * @covers \Activitypub\Scheduler::async_batch */ public function test_async_upgrade_multiple_args() { // Test that multiple arguments are passed correctly. From 681aa0c26f53929d67e9b9018a2ea1c3ac8186ba Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Thu, 11 Sep 2025 14:36:41 -0500 Subject: [PATCH 6/7] Register Dispatcher callbacks Moves async batch callback registration for activity sending and retrying from Scheduler's static property to Dispatcher initialization. Updates _doing_it_wrong message version to 'unreleased' for clarity. --- includes/class-dispatcher.php | 3 +++ includes/class-scheduler.php | 9 ++------- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/includes/class-dispatcher.php b/includes/class-dispatcher.php index 5b4686de6..cac61e66c 100644 --- a/includes/class-dispatcher.php +++ b/includes/class-dispatcher.php @@ -45,6 +45,9 @@ public static function init() { \add_filter( 'activitypub_additional_inboxes', array( self::class, 'add_inboxes_by_mentioned_actors' ), 10, 3 ); \add_filter( 'activitypub_additional_inboxes', array( self::class, 'add_inboxes_of_replied_urls' ), 10, 3 ); \add_filter( 'activitypub_additional_inboxes', array( self::class, 'add_inboxes_of_relays' ), 10, 3 ); + + Scheduler::register_async_batch_callback( 'activitypub_send_activity', array( self::class, 'send_to_followers' ) ); + Scheduler::register_async_batch_callback( 'activitypub_retry_activity', array( self::class, 'retry_send_to_followers' ) ); } /** diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index c4cb3f986..4bfdd81c7 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -27,10 +27,7 @@ class Scheduler { * * @var array */ - private static $batch_callbacks = array( - 'activitypub_send_activity' => array( Dispatcher::class, 'send_to_followers' ), - 'activitypub_retry_activity' => array( Dispatcher::class, 'retry_send_to_followers' ), - ); + private static $batch_callbacks = array(); /** * Initialize the class, registering WordPress hooks. @@ -44,8 +41,6 @@ public static function init() { // Event callbacks. \add_action( 'activitypub_async_batch', array( self::class, 'async_batch' ), 10, 99 ); - \add_action( 'activitypub_send_activity', array( self::class, 'async_batch' ), 10, 3 ); - \add_action( 'activitypub_retry_activity', array( self::class, 'async_batch' ), 10, 3 ); \add_action( 'activitypub_reprocess_outbox', array( self::class, 'reprocess_outbox' ) ); \add_action( 'activitypub_outbox_purge', array( self::class, 'purge_outbox' ) ); @@ -79,7 +74,7 @@ public static function register_schedulers() { */ public static function register_async_batch_callback( $hook, $callback ) { if ( \did_action( 'init' ) && ! \doing_action( 'init' ) ) { - \_doing_it_wrong( __METHOD__, 'Async batch callbacks should be registered before or during the init action.', '5.2.0' ); + \_doing_it_wrong( __METHOD__, 'Async batch callbacks should be registered before or during the init action.', 'unreleased' ); return; } From 8fc596b17046044b58a1dad21e7d1b51d8f653de Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Fri, 19 Sep 2025 07:46:44 -0500 Subject: [PATCH 7/7] Update includes/class-migration.php Co-authored-by: Matthias Pfefferle --- includes/class-migration.php | 1 + 1 file changed, 1 insertion(+) diff --git a/includes/class-migration.php b/includes/class-migration.php index aace2f990..3c358cc62 100644 --- a/includes/class-migration.php +++ b/includes/class-migration.php @@ -473,6 +473,7 @@ public static function migrate_to_4_7_2() { * @see Comment::pre_wp_update_comment_count_now() * @param int $batch_size Optional. Number of posts to process per batch. Default 100. * @param int $offset Optional. Number of posts to skip. Default 0. + * * @return int[]|void Array with batch size and offset if there are more posts to process. */ public static function update_comment_counts( $batch_size = 100, $offset = 0 ) {