Skip to content

Commit ba48d83

Browse files
authored
Add comment count upgrade routine (#1080)
* First pass at upgrade routine * Adjust hooks and scheduling * Some more small adjustments * Move update routine to migration class * Remove reference to Upgrader * Use existing locking mechanism * Update tests to account for new locking logic * Remove duplicate lock test
1 parent f080cbd commit ba48d83

File tree

4 files changed

+173
-2
lines changed

4 files changed

+173
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
* Send "new follower" emails
1414
* Send "direct message" emails
1515
* Account for custom comment types when calculating comment counts
16+
* Plugin upgrade routine that automatically updates comment counts
1617

1718
### Improved
1819

includes/class-migration.php

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class Migration {
2121
*/
2222
public static function init() {
2323
\add_action( 'activitypub_migrate', array( self::class, 'async_migration' ) );
24+
\add_action( 'activitypub_update_comment_counts', array( self::class, 'update_comment_counts' ), 10, 2 );
2425

2526
self::maybe_migrate();
2627
}
@@ -52,9 +53,20 @@ public static function get_version() {
5253

5354
/**
5455
* Locks the database migration process to prevent simultaneous migrations.
56+
*
57+
* @return bool|int True if the lock was successful, timestamp of existing lock otherwise.
5558
*/
5659
public static function lock() {
57-
\update_option( 'activitypub_migration_lock', \time() );
60+
global $wpdb;
61+
62+
// Try to lock.
63+
$lock_result = (bool) $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO `$wpdb->options` ( `option_name`, `option_value`, `autoload` ) VALUES (%s, %s, 'no') /* LOCK */", 'activitypub_migration_lock', \time() ) ); // phpcs:ignore WordPress.DB
64+
65+
if ( ! $lock_result ) {
66+
$lock_result = \get_option( 'activitypub_migration_lock' );
67+
}
68+
69+
return $lock_result;
5870
}
5971

6072
/**
@@ -115,7 +127,7 @@ public static function maybe_migrate() {
115127

116128
$version_from_db = self::get_version();
117129

118-
// Check for inital migration.
130+
// Check for initial migration.
119131
if ( ! $version_from_db ) {
120132
self::add_default_settings();
121133
$version_from_db = ACTIVITYPUB_PLUGIN_VERSION;
@@ -146,6 +158,9 @@ public static function maybe_migrate() {
146158
if ( \version_compare( $version_from_db, '4.1.0', '<' ) ) {
147159
self::migrate_to_4_1_0();
148160
}
161+
if ( \version_compare( $version_from_db, '4.5.0', '<' ) ) {
162+
\wp_schedule_single_event( \time() + MINUTE_IN_SECONDS, 'activitypub_update_comment_counts' );
163+
}
149164

150165
/**
151166
* Fires when the system has to be migrated.
@@ -372,6 +387,64 @@ public static function migrate_to_4_1_0() {
372387
);
373388
}
374389

390+
/**
391+
* Update comment counts for posts in batches.
392+
*
393+
* @see Comment::pre_wp_update_comment_count_now()
394+
* @param int $batch_size Optional. Number of posts to process per batch. Default 100.
395+
* @param int $offset Optional. Number of posts to skip. Default 0.
396+
*/
397+
public static function update_comment_counts( $batch_size = 100, $offset = 0 ) {
398+
global $wpdb;
399+
400+
// Bail if the existing lock is still valid.
401+
if ( self::is_locked() ) {
402+
\wp_schedule_single_event(
403+
time() + ( 5 * MINUTE_IN_SECONDS ),
404+
'activitypub_update_comment_counts',
405+
array(
406+
'batch_size' => $batch_size,
407+
'offset' => $offset,
408+
)
409+
);
410+
return;
411+
}
412+
413+
self::lock();
414+
415+
Comment::register_comment_types();
416+
$comment_types = Comment::get_comment_type_slugs();
417+
$type_inclusion = "AND comment_type IN ('" . implode( "','", $comment_types ) . "')";
418+
419+
// Get and process this batch.
420+
$post_ids = $wpdb->get_col( // phpcs:ignore WordPress.DB
421+
$wpdb->prepare(
422+
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
423+
"SELECT DISTINCT comment_post_ID FROM {$wpdb->comments} WHERE comment_approved = '1' {$type_inclusion} ORDER BY comment_post_ID LIMIT %d OFFSET %d",
424+
$batch_size,
425+
$offset
426+
)
427+
);
428+
429+
foreach ( $post_ids as $post_id ) {
430+
\wp_update_comment_count_now( $post_id );
431+
}
432+
433+
if ( count( $post_ids ) === $batch_size ) {
434+
// Schedule next batch.
435+
\wp_schedule_single_event(
436+
time() + MINUTE_IN_SECONDS,
437+
'activitypub_update_comment_counts',
438+
array(
439+
'batch_size' => $batch_size,
440+
'offset' => $offset + $batch_size,
441+
)
442+
);
443+
}
444+
445+
self::unlock();
446+
}
447+
375448
/**
376449
* Set the defaults needed for the plugin to work.
377450
*

readme.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ For reasons of data protection, it is not possible to see the followers of other
138138
* Added: Send "new follower" emails
139139
* Added: Send "direct message" emails
140140
* Added: Account for custom comment types when calculating comment counts
141+
* Added: Plugin upgrade routine that automatically updates comment counts
141142
* Improved: Email templates for Likes and Reposts
142143
* Improved: Interactions moderation
143144
* Improved: Compatibility with Akismet

tests/includes/class-test-migration.php

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
namespace Activitypub\Tests;
99

1010
use Activitypub\Migration;
11+
use Activitypub\Comment;
1112

1213
/**
1314
* Test class for Activitypub Migrate.
@@ -184,4 +185,99 @@ public function test_migrate_to_4_1_0() {
184185
$this->assertEquals( $custom, $template );
185186
$this->assertFalse( $content_type );
186187
}
188+
189+
/**
190+
* Tests that a new migration lock can be successfully acquired when no lock exists.
191+
*
192+
* @covers ::lock
193+
*/
194+
public function test_lock_acquire_new() {
195+
$this->assertFalse( get_option( 'activitypub_migration_lock' ) );
196+
197+
$this->assertTrue( Migration::lock() );
198+
199+
// Clean up.
200+
delete_option( 'activitypub_migration_lock' );
201+
}
202+
203+
/**
204+
* Tests retrieving the timestamp of an existing lock.
205+
*
206+
* @covers ::lock
207+
*/
208+
public function test_lock_get_existing() {
209+
$lock_time = time() - MINUTE_IN_SECONDS; // Set lock to 1 minute ago.
210+
update_option( 'activitypub_migration_lock', $lock_time );
211+
212+
$lock_result = Migration::lock();
213+
214+
$this->assertEquals( $lock_time, $lock_result );
215+
216+
// Clean up.
217+
delete_option( 'activitypub_migration_lock' );
218+
}
219+
220+
/**
221+
* Tests update_comment_counts() properly cleans up the lock.
222+
*
223+
* @covers ::update_comment_counts
224+
*/
225+
public function test_update_comment_counts_with_lock() {
226+
// Register comment types.
227+
Comment::register_comment_types();
228+
229+
// Create test comments.
230+
$post_id = $this->factory->post->create();
231+
$comment_id = $this->factory->comment->create(
232+
array(
233+
'comment_post_ID' => $post_id,
234+
'comment_approved' => '1',
235+
'comment_type' => 'repost', // One of the registered comment types.
236+
)
237+
);
238+
239+
Migration::update_comment_counts( 10, 0 );
240+
241+
// Verify lock was cleaned up.
242+
$this->assertFalse( get_option( 'activitypub_migration_lock' ) );
243+
244+
// Clean up.
245+
wp_delete_comment( $comment_id, true );
246+
wp_delete_post( $post_id, true );
247+
}
248+
249+
/**
250+
* Tests update_comment_counts() with existing valid lock.
251+
*
252+
* @covers ::update_comment_counts
253+
*/
254+
public function test_update_comment_counts_with_existing_valid_lock() {
255+
// Register comment types.
256+
Comment::register_comment_types();
257+
258+
// Set a lock.
259+
Migration::lock();
260+
261+
Migration::update_comment_counts( 10, 0 );
262+
263+
// Verify a scheduled event was created.
264+
$next_scheduled = wp_next_scheduled(
265+
'activitypub_update_comment_counts',
266+
array(
267+
'batch_size' => 10,
268+
'offset' => 0,
269+
)
270+
);
271+
$this->assertNotFalse( $next_scheduled );
272+
273+
// Clean up.
274+
delete_option( 'activitypub_migration_lock' );
275+
wp_clear_scheduled_hook(
276+
'activitypub_update_comment_counts',
277+
array(
278+
'batch_size' => 10,
279+
'offset' => 0,
280+
)
281+
);
282+
}
187283
}

0 commit comments

Comments
 (0)