-
Notifications
You must be signed in to change notification settings - Fork 29
fix: recover the database refresh chain a killed run ends #1385
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
594871c
fix: keep the DB refresh scheduled when Action Scheduler creation fails
Alexia-Soare 38786b1
fix: clear a WP-Cron event left beside the Action Scheduler action
Alexia-Soare 22ccbba
test: skip instead of fatal when Action Scheduler is not loaded
Alexia-Soare afa7686
fix: recover the refresh chain a killed run ends, and throttle the check
Alexia-Soare c8cf512
fix: start a recovered run now, and stop rewriting an autoloaded option
Alexia-Soare 3ea7339
test: prove the filter argument order instead of assuming it
Alexia-Soare d69c338
test: do not depend on plugin_basename() for the lifecycle hook name
Alexia-Soare 29adb15
test: say why the Action Scheduler precondition fails instead of skip…
Alexia-Soare fb1f58d
fix: do not drop the refresh on an unknown interval or a fractional o…
Alexia-Soare 091ffcf
docs: note that tear_down restores hooks, so test filters need no rem…
Alexia-Soare 8b32e01
fix: record the check window only when a trigger exists
Alexia-Soare 842a76c
fix: treat a live WP-Cron fallback as scheduled for the check window
Alexia-Soare f814207
refactor: read the cron schedules once when scheduling the refresh
Alexia-Soare dbd3176
fix: keep the old WP-Cron event until its replacement is scheduled
Alexia-Soare ede25f0
fix: unschedule the old refresh event without passing its arguments
Alexia-Soare da6fa8f
docs: trim the comments
Alexia-Soare 02d9630
test: type the pending action IDs as numeric strings
Alexia-Soare File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,166 @@ | ||
| <?php | ||
| /** | ||
| * Tests for the recurring DB refresh trigger (regression for #1384). | ||
| * | ||
| * @package visualizer | ||
| * @subpackage Tests | ||
| * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License | ||
| */ | ||
|
|
||
| /** | ||
| * Database charts refresh only while `visualizer_schedule_refresh_db` has a live trigger. | ||
| * | ||
| * Action Scheduler returns 0 instead of throwing when it cannot create an action, so the | ||
| * plugin used to drop the WP-Cron fallback for an action that was never stored, leaving | ||
| * nothing scheduled and no way back. | ||
| */ | ||
| class Test_Visualizer_Schedule_Refresh_Db extends WP_UnitTestCase { | ||
|
|
||
| const HOOK = 'visualizer_schedule_refresh_db'; | ||
| const GROUP = 'visualizer'; | ||
|
|
||
| /** | ||
| * Start every test with neither scheduler armed; the bootstrap activates the plugin. | ||
| */ | ||
| public function set_up() { | ||
| parent::set_up(); | ||
| as_unschedule_all_actions( self::HOOK, array(), self::GROUP ); | ||
| wp_clear_scheduled_hook( self::HOOK ); | ||
| } | ||
|
Alexia-Soare marked this conversation as resolved.
|
||
|
|
||
| /** | ||
| * Whether anything will fire the refresh hook again. | ||
| * | ||
| * @return bool | ||
| */ | ||
| private function has_trigger(): bool { | ||
| return false !== as_next_scheduled_action( self::HOOK, array(), self::GROUP ) | ||
| || false !== wp_next_scheduled( self::HOOK ); | ||
| } | ||
|
Alexia-Soare marked this conversation as resolved.
|
||
|
|
||
| /** | ||
| * Fire a plugin lifecycle hook the way WordPress does. | ||
| * | ||
| * @param string $action Either `activate` or `deactivate`. | ||
| */ | ||
| private function lifecycle( string $action ) { | ||
| do_action( $action . '_' . plugin_basename( VISUALIZER_BASEFILE ), false ); | ||
| } | ||
|
|
||
| /** | ||
| * Precondition: Action Scheduler is usable, otherwise the rest proves nothing. | ||
| */ | ||
| public function test_action_scheduler_is_available() { | ||
| $this->assertTrue( function_exists( 'as_schedule_recurring_action' ), 'Action Scheduler must be loaded' ); | ||
| $this->assertTrue( ActionScheduler::is_initialized(), 'Action Scheduler must be initialized' ); | ||
|
Alexia-Soare marked this conversation as resolved.
Outdated
|
||
| } | ||
|
Alexia-Soare marked this conversation as resolved.
|
||
|
|
||
| /** | ||
| * A refused Action Scheduler creation must leave the WP-Cron fallback in place. | ||
| */ | ||
| public function test_activation_keeps_a_trigger_when_action_scheduler_creation_fails() { | ||
| add_filter( 'pre_as_schedule_recurring_action', '__return_zero' ); | ||
| $this->lifecycle( 'activate' ); | ||
| remove_filter( 'pre_as_schedule_recurring_action', '__return_zero' ); | ||
|
Alexia-Soare marked this conversation as resolved.
|
||
|
|
||
| $this->assertTrue( $this->has_trigger(), 'activation must not leave the refresh hook with no trigger at all' ); | ||
| } | ||
|
|
||
| /** | ||
| * A site that already lost both schedulers must recover on an ordinary request. | ||
| */ | ||
| public function test_recovery_restores_a_trigger_when_both_schedulers_are_empty() { | ||
| $this->assertFalse( $this->has_trigger(), 'precondition: nothing is scheduled' ); | ||
|
|
||
| $this->setup_module()->maybe_reschedule_refresh_db(); | ||
|
|
||
| $this->assertTrue( $this->has_trigger(), 'a missing refresh trigger must be restored without another activation' ); | ||
| } | ||
|
|
||
| /** | ||
| * A legacy WP-Cron event must move onto Action Scheduler and stop firing twice. | ||
| */ | ||
| public function test_legacy_wp_cron_event_migrates_to_action_scheduler() { | ||
| wp_schedule_event( time(), 'visualizer_ten_minutes', self::HOOK ); | ||
| $this->assertNotFalse( wp_next_scheduled( self::HOOK ), 'precondition: a legacy WP-Cron event exists' ); | ||
|
|
||
| $this->setup_module()->maybe_reschedule_refresh_db(); | ||
|
|
||
| $this->assertNotFalse( as_next_scheduled_action( self::HOOK, array(), self::GROUP ), 'the refresh must move onto Action Scheduler' ); | ||
| $this->assertFalse( wp_next_scheduled( self::HOOK ), 'the superseded WP-Cron event must not survive the migration' ); | ||
| } | ||
|
|
||
| /** | ||
| * While Action Scheduler keeps refusing, later requests must leave the fallback alone. | ||
| * | ||
| * Re-arming it on every request pins the event to a past timestamp, so the refresh runs | ||
| * on every cron spawn instead of every ten minutes. | ||
| */ | ||
| public function test_recovery_does_not_drag_a_live_wp_cron_event_back_into_the_past() { | ||
| add_filter( 'pre_as_schedule_recurring_action', '__return_zero' ); | ||
|
|
||
| $this->setup_module()->maybe_reschedule_refresh_db(); | ||
| $this->assertNotFalse( wp_next_scheduled( self::HOOK ), 'precondition: the fallback is armed' ); | ||
|
|
||
| // mimic WP-Cron having run the event and rescheduled it forward. | ||
| wp_clear_scheduled_hook( self::HOOK ); | ||
| $future = time() + 600; | ||
| wp_schedule_event( $future, 'visualizer_ten_minutes', self::HOOK ); | ||
|
|
||
| $this->setup_module()->maybe_reschedule_refresh_db(); | ||
| remove_filter( 'pre_as_schedule_recurring_action', '__return_zero' ); | ||
|
|
||
| $this->assertSame( $future, wp_next_scheduled( self::HOOK ), 'a later request must not make the refresh due again' ); | ||
| } | ||
|
|
||
| /** | ||
| * A concurrent request must not be able to create a second recurring action. | ||
| * | ||
| * Action Scheduler creates the next recurrence only after the current one completes, so | ||
| * every interval there is a moment with nothing pending. A visitor arriving in that | ||
| * window used to add a duplicate, and duplicates never go away on their own. | ||
| */ | ||
| public function test_recovery_does_not_create_a_second_action_when_a_concurrent_request_wins_the_race() { | ||
| $done = false; | ||
| // Stand in for another request that schedules between our lookup and our write. | ||
| $racer = function ( $pre, $timestamp, $interval, $hook, $args, $group, $priority, $unique ) use ( &$done ) { | ||
| if ( ! $done ) { | ||
| $done = true; | ||
| as_schedule_recurring_action( $timestamp, $interval, $hook, $args, $group, $unique ); | ||
|
Alexia-Soare marked this conversation as resolved.
Outdated
|
||
| } | ||
| return null; | ||
| }; | ||
|
Alexia-Soare marked this conversation as resolved.
|
||
| add_filter( 'pre_as_schedule_recurring_action', $racer, 10, 9 ); | ||
|
Alexia-Soare marked this conversation as resolved.
Outdated
|
||
|
|
||
| $this->setup_module()->maybe_reschedule_refresh_db(); | ||
| remove_filter( 'pre_as_schedule_recurring_action', $racer, 10 ); | ||
|
|
||
| $this->assertTrue( $done, 'precondition: the race was actually simulated' ); | ||
| $this->assertCount( 1, $this->pending_actions(), 'a lost race must not leave the refresh scheduled twice' ); | ||
| } | ||
|
|
||
| /** | ||
| * Every pending refresh action. | ||
| * | ||
| * @return array | ||
| */ | ||
| private function pending_actions(): array { | ||
|
Alexia-Soare marked this conversation as resolved.
Outdated
|
||
| return as_get_scheduled_actions( | ||
| array( | ||
| 'hook' => self::HOOK, | ||
| 'group' => self::GROUP, | ||
| 'status' => ActionScheduler_Store::STATUS_PENDING, | ||
| ), | ||
| 'ids' | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * The registered setup module. | ||
| * | ||
| * @return Visualizer_Module_Setup | ||
| */ | ||
| private function setup_module(): Visualizer_Module_Setup { | ||
| return Visualizer_Plugin::instance()->getModule( Visualizer_Module_Setup::NAME ); | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.