Use scheduled action for resetting large record and meta tables#1543
Use scheduled action for resetting large record and meta tables#1543tharsheblows merged 15 commits intodevelopfrom
Conversation
delawski
left a comment
There was a problem hiding this comment.
The code looks good to me 👍
classes/class-admin.php
Outdated
| /** | ||
| * Used to check if it's a single site, not multisite. | ||
| */ | ||
| const WP_STREAM_SINGLE_SITE = 'single'; | ||
|
|
||
| /** | ||
| * Used to check if it's a multisite with the plugin network enabled. | ||
| */ | ||
| const WP_STREAM_MULTI_NETWORK = 'multisite-network'; | ||
|
|
||
| /** | ||
| * Used to check if it's a multisite with the plugin not network enabled. | ||
| */ | ||
| const WP_STREAM_MULTI_NOT_NETWORK = 'multisite-not-network'; |
There was a problem hiding this comment.
💡 thought: Two things, nitpicks really:
- I feel like this belongs to the
Pluginclass and notAdmin(alternatively, this could be part of some kind of aUtilityclass). What's your take? - I guess it's a matter of preference but I wonder what do you think about having a set of methods for checking the installation type instead of those enum-like string constants? Something like
Admin::is_single_site(),Admin::is_multisite_network_activated()andAdmin::is_multisite_not_network_activated().
It would follow the WordPress convention likeis_network_admin()and make the conditions easier to read (potentially). But then they couldn't be used in aswitchstatement later on and a regularif ... elsewould have to be used.
There was a problem hiding this comment.
I agree with @delawski, but also have one comment about the constants naming:
I'm not sure what's the naming convention in this plugin, but addition of the WP_STREAM_ prefix seems to not be necessary here (these constants are scoped in Admin class).
There was a problem hiding this comment.
I feel like this belongs to the Plugin class and not Admin (alternatively, this could be part of some kind of a Utility class). What's your take?
Yes! I agree. I'll put it in thePluginclass for now and if it feels out of place there, move to aUtilityclass.
I guess it's a matter of preference but I wonder what do you think about having a set of methods for checking the installation type instead of those enum-like string constants? Something like Admin::is_single_site(), Admin::is_multisite_network_activated() and Admin::is_multisite_not_network_activated().
Yes, I like this also, it makes sense to me and would make development easier.
There was a problem hiding this comment.
@bartoszgadomski Yes! You're right, that prefix isn't necessary and also not in line with the naming condition! I'll fix those.
There was a problem hiding this comment.
Commit : a6d2d36
If I missed doing something in that, it's accidental so please tell me
| /** | ||
| * Retrieves the deletion warning message based on the site type | ||
| * and whether or not there is currently a process running to delete the tables. | ||
| * | ||
| * @return string The deletion warning message. | ||
| */ | ||
| public function get_deletion_warning(): string { | ||
| $site_type = $this->plugin->get_site_type(); | ||
|
|
||
| switch ( $site_type ) { | ||
| case Admin::WP_STREAM_MULTI_NETWORK: | ||
| $warning = __( 'Warning: This will delete all activity records from the database for all sites.', 'stream' ); | ||
| break; | ||
| case Admin::WP_STREAM_MULTI_NOT_NETWORK: | ||
| $warning = $this->plugin->is_large_records_table( Admin::get_blog_record_table_size() ) ? | ||
| $this->get_async_deletion_warning() : | ||
| __( 'Warning: This will delete all activity records from the database for this site.', 'stream' ); | ||
| break; | ||
| default: | ||
| $warning = __( 'Warning: This will delete all activity records from the database.', 'stream' ); | ||
| break; | ||
| } | ||
|
|
||
| return $warning; | ||
| } | ||
|
|
||
| /** | ||
| * Retrieves the warning message for asynchronous deletion. | ||
| * | ||
| * This method checks if there is an action scheduler event running already deleting things | ||
| * and returns an appropriate message. | ||
| * | ||
| * @return string The warning message. | ||
| */ | ||
| private function get_async_deletion_warning() { | ||
| // Check if there is an action scheduler event running already deleting things. | ||
| $has_scheduled_action = Admin::is_running_async_deletion(); | ||
|
|
||
| return $has_scheduled_action ? | ||
| __( 'Currently deleting records. Please be patient, this can take a while.', 'stream' ) | ||
| : | ||
| __( 'Warning: This will delete all activity records from the database for this site. If any are added while this is running, they will not be deleted.', 'stream' ); | ||
| } | ||
|
|
There was a problem hiding this comment.
🥜 nitpick: A very minor thing yet I'd like to share with you. I find this code snippet relatively hard to read because of this ternaries and indirection between them (one ternary calls a method which has yet another ternary inside). Do you think it'd be clearer if the get_async_deletion_warning() logic was moved over to the switch statement and the regular if .. else statements used instead of ternaries?
There was a problem hiding this comment.
@delawski I've updated it here to use if ... else statements and checked for the AS action first as I think that's the most robust for future development (in case anything else uses the async deletion, we don't have to worry about the conditional here) plus it's a cheap check. Is that ok?
Also I added more empty lines than usual as I have a bit of trouble seeing if / else statements when there are no empty lines for some reason.
Commit 12ca190
There was a problem hiding this comment.
This looks great, thank you!
Also I added more empty lines than usual as I have a bit of trouble seeing if / else statements when there are no empty lines for some reason.
All good! 😄 I wonder if it would make it easier to read for you if there were only if statements (without any elseif) and they simply returned the warning message directly (there's no other logic besides that if .. else in the function). Anyway, let's leave it as is, it's super clean and readable to me now 👍
classes/class-admin.php
Outdated
| /** | ||
| * Used to check if it's a single site, not multisite. | ||
| */ | ||
| const WP_STREAM_SINGLE_SITE = 'single'; | ||
|
|
||
| /** | ||
| * Used to check if it's a multisite with the plugin network enabled. | ||
| */ | ||
| const WP_STREAM_MULTI_NETWORK = 'multisite-network'; | ||
|
|
||
| /** | ||
| * Used to check if it's a multisite with the plugin not network enabled. | ||
| */ | ||
| const WP_STREAM_MULTI_NOT_NETWORK = 'multisite-not-network'; |
There was a problem hiding this comment.
I agree with @delawski, but also have one comment about the constants naming:
I'm not sure what's the naming convention in this plugin, but addition of the WP_STREAM_ prefix seems to not be necessary here (these constants are scoped in Admin class).
classes/class-admin.php
Outdated
| FROM {$wpdb->stream} AS `stream` | ||
| LEFT JOIN {$wpdb->streammeta} AS `meta` | ||
| ON `meta`.`record_id` = `stream`.`ID` | ||
| WHERE 1=1 AND `blog_id`=%d;", |
There was a problem hiding this comment.
@tharsheblows The WHERE 1=1 is typically added when a SQL query is dynamically generated. While it will be ignored by the database, there's no need to have it in code in this case, as all the "where" clauses are static:
| WHERE 1=1 AND `blog_id`=%d;", | |
| `blog_id`=%d;", |
| // Load Action Scheduler. | ||
| require_once $this->locations['dir'] . '/vendor/woocommerce/action-scheduler/action-scheduler.php'; |
There was a problem hiding this comment.
@tharsheblows There's a risk of conflict if this package was loaded by another plugin. Should this be preceded with function_exists call (or something similar) to only load this dependency if it has not been loaded by another plugin earlier?
There was a problem hiding this comment.
@bartoszgadomski Action Scheduler works when included multiple times! It's a clever library – it loads the most recent version requested so all of the functions expected are available. Eg latest version does it like this
| * @return bool Whether or not this should be considered large. | ||
| */ | ||
| public function is_large_records_table( int $record_number ): bool { | ||
| return apply_filters( 'wp_stream_is_large_records_table', $record_number > 1000000, $record_number ); |
There was a problem hiding this comment.
@tharsheblows Suggestion: adding a docblock comment to all custom hooks would be beneficial for developers to understand how this plugin can be adjusted :)
There was a problem hiding this comment.
@bartoszgadomski ACK yes, I totally forgot.
Fix typo Co-authored-by: Piotr Delawski <delawski@users.noreply.github.com>
Remove redundant cast to int Co-authored-by: Bartosz Gadomski <kontakt@bartoszgadomski.pl>
Co-authored-by: Piotr Delawski <delawski@users.noreply.github.com>
delawski
left a comment
There was a problem hiding this comment.
Thank you for addressing the feedback. The PR looks fabulous!
Fixes #1503
Development notes
Changes to default database reset functionality:
{$wpdb->streammeta}table and the{$wpdb->stream}table.){$wpdb->stream}table: the previous deletion functionality will run{$wpdb->stream}table:{$wpdb->stream}.IDcolumnThe "Reset Stream Database" link in Stream --> Settings --> Advanced will not show while the records are being deleted. The warning given will depend on whether or not it's going to be an asynchronous process.
In my local environment, each pass of deleting 250k records takes 10-15 seconds.
Added filters
wp_stream_is_large_records_table: filter the number of records to be processed when deciding what is a large records table. Default is 1M, ie anything over 1M records to be processed is considered a large records table.wp_stream_batch_size: how many records in the{$wpdb->stream}table will be used when doing the deletion.Incidental additions:
$this->plugin->get_site_type()returns what sort of site it is:Admin::WP_STREAM_MULTI_NETWORKis a multisite with the plugin network activatedAdmin::::WP_STREAM_MULTI_NOT_NETWORKis a multisite where the plugin is not network enabledAdmin::WP_STREAM_SINGLE_SITEis a single siteTesting instructions:
You can use the
npm run large-records-generateto generate a bunch of records. You can see how many you've generated by runningnpm run large-records-showChecklist
contributing.md).Release Changelog