Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/class-analytics-dispatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,10 @@ public function register(): void {
self::log_debug( 'Analytics schema install error: ' . $e->getMessage() );
}

$this->ensure_scheduled();
// Deferred to init: Action Scheduler's data store is only usable
// from init priority 1 onward, and register() runs at plugins_loaded
// — as_*() calls made that early silently schedule nothing.
add_action( 'init', array( $this, 'ensure_scheduled' ) );
}
}

Expand Down Expand Up @@ -159,9 +162,13 @@ public static function clear_scheduled(): void {
* Ensure the hourly flush is scheduled exactly once, preferring Action
* Scheduler and adapting when it appears or disappears.
*
* Runs on init (hooked by {@see register()}) because Action Scheduler's
* data store initializes on init priority 1; called earlier, its API
* functions return without scheduling.
*
* @return void
*/
protected function ensure_scheduled(): void {
public function ensure_scheduled(): void {
try {
if ( $this->action_scheduler_available() ) {
// Migrate a stale WP-Cron recurrence so both backends never fire.
Expand Down
48 changes: 48 additions & 0 deletions tests/AnalyticsDispatcherTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,19 @@ private function make_dispatcher( ?Analytics_Queue_Table $table = null ): Analyt
return new Analytics_Dispatcher( new Settings(), new WP_Http_Client(), $table ?? $this->make_fake_table() );
}

/**
* Fire every callback register() deferred to init, as WordPress would.
*/
private function fire_init_callbacks(): void {
global $wp_test_actions;

foreach ( $wp_test_actions as $action ) {
if ( 'init' === $action['hook'] ) {
call_user_func( $action['callback'] );
}
}
}

/**
* Build a dispatcher that reports Action Scheduler as unavailable.
*/
Expand Down Expand Up @@ -330,12 +343,38 @@ public function test_flush_tolerates_partial_rejection_response(): void {
$this->assertSame( array(), $table->rows );
}

public function test_register_defers_schedule_check_to_init(): void {
global $wp_test_doing_cron, $wp_test_actions, $wp_test_as_recurring_calls, $wp_test_recurring_events;

$wp_test_doing_cron = true;

$this->make_dispatcher()->register();

// register() runs at plugins_loaded, before Action Scheduler's data
// store initializes (init priority 1) — as_*() calls made that early
// silently no-op, so no scheduling API may be touched yet.
$this->assertSame( array(), $wp_test_as_recurring_calls, 'No AS call at registration time.' );
$this->assertSame( array(), $wp_test_recurring_events, 'No WP-Cron schedule at registration time.' );

$init_hooks = array_values(
array_filter( $wp_test_actions, static fn ( array $a ): bool => 'init' === $a['hook'] )
);
$this->assertCount( 1, $init_hooks, 'The schedule check must be deferred to init.' );

// Firing the deferred callback performs the actual scheduling.
call_user_func( $init_hooks[0]['callback'] );

$this->assertCount( 1, $wp_test_as_recurring_calls );
$this->assertSame( self::FLUSH_HOOK, $wp_test_as_recurring_calls[0]['hook'] );
}

public function test_register_schedules_recurring_via_action_scheduler_in_cron_context(): void {
global $wp_test_doing_cron, $wp_test_as_recurring_calls, $wp_test_recurring_events;

$wp_test_doing_cron = true;

$this->make_dispatcher()->register();
$this->fire_init_callbacks();

$this->assertCount( 1, $wp_test_as_recurring_calls );
$call = $wp_test_as_recurring_calls[0];
Expand All @@ -352,6 +391,7 @@ public function test_register_skips_scheduling_when_as_action_exists(): void {
$wp_test_as_has_scheduled = true;

$this->make_dispatcher()->register();
$this->fire_init_callbacks();

$this->assertSame( array(), $wp_test_as_recurring_calls );
}
Expand All @@ -362,6 +402,7 @@ public function test_register_falls_back_to_wp_cron_recurring(): void {
$wp_test_doing_cron = true;

$this->make_dispatcher_without_action_scheduler( $this->make_fake_table() )->register();
$this->fire_init_callbacks();

$this->assertCount( 1, $wp_test_recurring_events );
$this->assertSame( self::FLUSH_HOOK, $wp_test_recurring_events[0]['hook'] );
Expand All @@ -375,6 +416,7 @@ public function test_register_skips_wp_cron_when_already_scheduled(): void {
$wp_test_next_scheduled = time() + 100;

$this->make_dispatcher_without_action_scheduler( $this->make_fake_table() )->register();
$this->fire_init_callbacks();

$this->assertSame( array(), $wp_test_recurring_events );
}
Expand All @@ -386,6 +428,7 @@ public function test_register_migrates_wp_cron_schedule_to_action_scheduler(): v
$wp_test_next_scheduled = time() + 100;

$this->make_dispatcher()->register();
$this->fire_init_callbacks();

// The stale WP-Cron recurrence is cleared so both backends never fire.
$this->assertCount( 1, $wp_test_cleared_hooks );
Expand Down Expand Up @@ -416,6 +459,7 @@ public function install(): void {
};

$this->make_dispatcher( $table )->register();
$this->fire_init_callbacks();

$this->assertCount( 1, $wp_test_as_recurring_calls, 'Scheduling still proceeds after install failure.' );
}
Expand All @@ -430,5 +474,9 @@ public function test_register_does_no_schedule_work_on_front_end(): void {
$this->assertSame( 0, $table->install_calls );
$this->assertSame( array(), $wp_test_as_recurring_calls );
$this->assertSame( array(), $wp_test_recurring_events );

global $wp_test_actions;
$init_hooks = array_filter( $wp_test_actions, static fn ( array $a ): bool => 'init' === $a['hook'] );
$this->assertSame( array(), $init_hooks, 'No deferred schedule check on the front end.' );
}
}
Loading