Skip to content

Commit d1d4b02

Browse files
Merge pull request #11796 from google/enhancement/11599-monitor-scheduler
Enhancement/11599 monitor scheduler
2 parents 6f62109 + aa394e7 commit d1d4b02

File tree

8 files changed

+224
-3
lines changed

8 files changed

+224
-3
lines changed

includes/Core/Email_Reporting/Email_Reporting.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,14 @@ class Email_Reporting {
105105
*/
106106
protected $initiator_task;
107107

108+
/**
109+
* Monitor task instance.
110+
*
111+
* @since n.e.x.t
112+
* @var Monitor_Task
113+
*/
114+
protected $monitor_task;
115+
108116
/**
109117
* Worker task instance.
110118
*
@@ -145,6 +153,7 @@ public function __construct(
145153
$this->email_log = new Email_Log( $this->context );
146154
$this->scheduler = new Email_Reporting_Scheduler( $frequency_planner );
147155
$this->initiator_task = new Initiator_Task( $this->scheduler, $subscribed_users_query );
156+
$this->monitor_task = new Monitor_Task( $this->scheduler, $this->settings );
148157
$this->worker_task = new Worker_Task( $max_execution_limiter, $batch_query, $this->scheduler );
149158
}
150159

@@ -163,8 +172,10 @@ public function register() {
163172

164173
if ( $this->settings->is_email_reporting_enabled() ) {
165174
$this->scheduler->schedule_initiator_events();
175+
$this->scheduler->schedule_monitor();
166176

167177
add_action( Email_Reporting_Scheduler::ACTION_INITIATOR, array( $this->initiator_task, 'handle_callback_action' ), 10, 1 );
178+
add_action( Email_Reporting_Scheduler::ACTION_MONITOR, array( $this->monitor_task, 'handle_monitor_action' ) );
168179
add_action( Email_Reporting_Scheduler::ACTION_WORKER, array( $this->worker_task, 'handle_callback_action' ), 10, 3 );
169180

170181
} else {
@@ -178,6 +189,7 @@ function ( $old_value, $new_value ) {
178189

179190
if ( $is_enabled && ! $was_enabled ) {
180191
$this->scheduler->schedule_initiator_events();
192+
$this->scheduler->schedule_monitor();
181193
return;
182194
}
183195

includes/Core/Email_Reporting/Email_Reporting_Scheduler.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class Email_Reporting_Scheduler {
2424
const ACTION_INITIATOR = 'googlesitekit_email_reporting_initiator';
2525
const ACTION_WORKER = 'googlesitekit_email_reporting_worker';
2626
const ACTION_FALLBACK = 'googlesitekit_email_reporting_fallback';
27+
const ACTION_MONITOR = 'googlesitekit_email_reporting_monitor';
2728

2829
/**
2930
* Frequency planner instance.
@@ -122,13 +123,26 @@ public function schedule_fallback( $frequency, $timestamp, $delay = HOUR_IN_SECO
122123
wp_schedule_single_event( $timestamp + $delay, self::ACTION_FALLBACK, array( $frequency ) );
123124
}
124125

126+
/**
127+
* Ensures the monitor event is scheduled daily.
128+
*
129+
* @since n.e.x.t
130+
*/
131+
public function schedule_monitor() {
132+
if ( wp_next_scheduled( self::ACTION_MONITOR ) ) {
133+
return;
134+
}
135+
136+
wp_schedule_event( time(), 'daily', self::ACTION_MONITOR );
137+
}
138+
125139
/**
126140
* Unschedules all email reporting related events.
127141
*
128142
* @since n.e.x.t
129143
*/
130144
public function unschedule_all() {
131-
foreach ( array( self::ACTION_INITIATOR, self::ACTION_WORKER, self::ACTION_FALLBACK ) as $hook ) {
145+
foreach ( array( self::ACTION_INITIATOR, self::ACTION_WORKER, self::ACTION_FALLBACK, self::ACTION_MONITOR ) as $hook ) {
132146
wp_unschedule_hook( $hook );
133147
}
134148
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
/**
3+
* Class Google\Site_Kit\Core\Email_Reporting\Monitor_Task
4+
*
5+
* @package Google\Site_Kit\Core\Email_Reporting
6+
* @copyright 2025 Google LLC
7+
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
8+
* @link https://sitekit.withgoogle.com
9+
*/
10+
11+
namespace Google\Site_Kit\Core\Email_Reporting;
12+
13+
use Google\Site_Kit\Core\User\Email_Reporting_Settings as User_Email_Reporting_Settings;
14+
15+
/**
16+
* Restores missing initiator schedules for email reporting.
17+
*
18+
* @since n.e.x.t
19+
* @access private
20+
* @ignore
21+
*/
22+
class Monitor_Task {
23+
24+
/**
25+
* Scheduler instance.
26+
*
27+
* @since n.e.x.t
28+
*
29+
* @var Email_Reporting_Scheduler
30+
*/
31+
private $scheduler;
32+
33+
/**
34+
* Settings instance.
35+
*
36+
* @since n.e.x.t
37+
*
38+
* @var Email_Reporting_Settings
39+
*/
40+
private $settings;
41+
42+
/**
43+
* Constructor.
44+
*
45+
* @since n.e.x.t
46+
*
47+
* @param Email_Reporting_Scheduler $scheduler Scheduler instance.
48+
* @param Email_Reporting_Settings $settings Email reporting settings.
49+
*/
50+
public function __construct( Email_Reporting_Scheduler $scheduler, Email_Reporting_Settings $settings ) {
51+
$this->scheduler = $scheduler;
52+
$this->settings = $settings;
53+
}
54+
55+
/**
56+
* Handles the monitor cron callback.
57+
*
58+
* The monitor ensures each initiator schedule exists and recreates any
59+
* missing ones without disturbing existing events.
60+
*
61+
* @since n.e.x.t
62+
*/
63+
public function handle_monitor_action() {
64+
if ( ! $this->settings->is_email_reporting_enabled() ) {
65+
return;
66+
}
67+
68+
foreach ( array( User_Email_Reporting_Settings::FREQUENCY_WEEKLY, User_Email_Reporting_Settings::FREQUENCY_MONTHLY, User_Email_Reporting_Settings::FREQUENCY_QUARTERLY ) as $frequency ) {
69+
if ( wp_next_scheduled( Email_Reporting_Scheduler::ACTION_INITIATOR, array( $frequency ) ) ) {
70+
continue;
71+
}
72+
73+
$this->scheduler->schedule_initiator_once( $frequency );
74+
}
75+
}
76+
}

includes/Core/Util/Uninstallation.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ class Uninstallation {
6060
Email_Reporting_Scheduler::ACTION_INITIATOR,
6161
Email_Reporting_Scheduler::ACTION_WORKER,
6262
Email_Reporting_Scheduler::ACTION_FALLBACK,
63+
Email_Reporting_Scheduler::ACTION_MONITOR,
6364
OAuth_Client::CRON_REFRESH_PROFILE_DATA,
6465
Remote_Features_Cron::CRON_ACTION,
6566
Synchronize_AdSenseLinked::CRON_SYNCHRONIZE_ADSENSE_LINKED,

tests/phpunit/integration/Core/Email_Reporting/Email_ReportingTest.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ public function test_register_schedules_initiators_when_enabled() {
5353
sprintf( 'Expected initiator to be scheduled for frequency %s.', $frequency )
5454
);
5555
}
56+
57+
$this->assertNotFalse(
58+
wp_next_scheduled( Email_Reporting_Scheduler::ACTION_MONITOR ),
59+
'Expected monitor event to be scheduled daily.'
60+
);
5661
}
5762

5863
public function test_disabling_unschedules_all_events() {
@@ -68,6 +73,11 @@ public function test_disabling_unschedules_all_events() {
6873
sprintf( 'Initiator should be unscheduled for frequency %s when reporting disabled.', $frequency )
6974
);
7075
}
76+
77+
$this->assertFalse(
78+
wp_next_scheduled( Email_Reporting_Scheduler::ACTION_MONITOR ),
79+
'Monitor event should be unscheduled when reporting is disabled.'
80+
);
7181
}
7282

7383
public function test_register_clears_existing_events_when_disabled() {
@@ -78,21 +88,23 @@ public function test_register_clears_existing_events_when_disabled() {
7888
$worker_timestamp = time();
7989
wp_schedule_single_event( time() + 50, Email_Reporting_Scheduler::ACTION_WORKER, array( 'batch', User_Email_Reporting_Settings::FREQUENCY_WEEKLY, $worker_timestamp ) );
8090
wp_schedule_single_event( time() + 50, Email_Reporting_Scheduler::ACTION_FALLBACK, array( User_Email_Reporting_Settings::FREQUENCY_WEEKLY ) );
91+
wp_schedule_event( time() + 50, 'daily', Email_Reporting_Scheduler::ACTION_MONITOR );
8192

8293
$email_reporting = $this->create_email_reporting();
8394
$email_reporting->register();
8495

8596
$this->assertFalse( wp_next_scheduled( Email_Reporting_Scheduler::ACTION_INITIATOR, array( User_Email_Reporting_Settings::FREQUENCY_WEEKLY ) ), 'Initiator event should be cleared when reporting is disabled.' );
8697
$this->assertFalse( wp_next_scheduled( Email_Reporting_Scheduler::ACTION_WORKER, array( 'batch', User_Email_Reporting_Settings::FREQUENCY_WEEKLY, $worker_timestamp ) ), 'Worker event should be cleared when reporting is disabled.' );
8798
$this->assertFalse( wp_next_scheduled( Email_Reporting_Scheduler::ACTION_FALLBACK, array( User_Email_Reporting_Settings::FREQUENCY_WEEKLY ) ), 'Fallback event should be cleared when reporting is disabled.' );
99+
$this->assertFalse( wp_next_scheduled( Email_Reporting_Scheduler::ACTION_MONITOR ), 'Monitor event should be cleared when reporting is disabled.' );
88100
}
89101

90102
private function create_email_reporting() {
91103
return new Email_Reporting( $this->context, $this->modules, $this->options, $this->user_options );
92104
}
93105

94106
private function clear_scheduled_events() {
95-
foreach ( array( Email_Reporting_Scheduler::ACTION_INITIATOR, Email_Reporting_Scheduler::ACTION_WORKER, Email_Reporting_Scheduler::ACTION_FALLBACK ) as $hook ) {
107+
foreach ( array( Email_Reporting_Scheduler::ACTION_INITIATOR, Email_Reporting_Scheduler::ACTION_WORKER, Email_Reporting_Scheduler::ACTION_FALLBACK, Email_Reporting_Scheduler::ACTION_MONITOR ) as $hook ) {
96108
wp_unschedule_hook( $hook );
97109
}
98110
}

tests/phpunit/integration/Core/Email_Reporting/Email_Reporting_SchedulerTest.php

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,23 +122,43 @@ public function test_schedule_fallback_prevents_duplicates() {
122122
$this->assertSame( $first, $second, 'Scheduling the same fallback twice should reuse the original timestamp for frequency "' . $frequency . '".' );
123123
}
124124

125+
public function test_schedule_monitor_registers_daily_event_once() {
126+
$before = time();
127+
128+
$this->scheduler->schedule_monitor();
129+
$event = wp_get_scheduled_event( Email_Reporting_Scheduler::ACTION_MONITOR );
130+
131+
$this->assertNotFalse( $event, 'Monitor event should be created on first call.' );
132+
$this->assertSame( 'daily', $event->schedule, 'Monitor event should recur daily.' );
133+
$this->assertGreaterThanOrEqual( $before, $event->timestamp, 'Monitor event should run no earlier than the scheduling time.' );
134+
135+
$this->scheduler->schedule_monitor();
136+
$this->assertSame(
137+
$event->timestamp,
138+
wp_next_scheduled( Email_Reporting_Scheduler::ACTION_MONITOR ),
139+
'Monitor scheduling should be idempotent.'
140+
);
141+
}
142+
125143
public function test_unschedule_all_clears_events() {
126144
$this->scheduler->schedule_initiator_once( Email_Reporting_Settings::FREQUENCY_WEEKLY );
127145
$worker_timestamp = time();
128146
$fallback_timestamp = time();
129147

130148
$this->scheduler->schedule_worker( 'batch', Email_Reporting_Settings::FREQUENCY_WEEKLY, $worker_timestamp );
131149
$this->scheduler->schedule_fallback( Email_Reporting_Settings::FREQUENCY_WEEKLY, $fallback_timestamp );
150+
$this->scheduler->schedule_monitor();
132151

133152
$this->scheduler->unschedule_all();
134153

135154
$this->assertFalse( wp_next_scheduled( Email_Reporting_Scheduler::ACTION_INITIATOR, array( Email_Reporting_Settings::FREQUENCY_WEEKLY ) ), 'Initiator hook should be unscheduled for weekly frequency.' );
136155
$this->assertFalse( wp_next_scheduled( Email_Reporting_Scheduler::ACTION_WORKER, array( 'batch', Email_Reporting_Settings::FREQUENCY_WEEKLY, $worker_timestamp ) ), 'Worker hook should be unscheduled for batch "batch".' );
137156
$this->assertFalse( wp_next_scheduled( Email_Reporting_Scheduler::ACTION_FALLBACK, array( Email_Reporting_Settings::FREQUENCY_WEEKLY ) ), 'Fallback hook should be unscheduled for weekly frequency.' );
157+
$this->assertFalse( wp_next_scheduled( Email_Reporting_Scheduler::ACTION_MONITOR ), 'Monitor hook should be unscheduled when clearing all events.' );
138158
}
139159

140160
private function clear_scheduled_events() {
141-
foreach ( array( Email_Reporting_Scheduler::ACTION_INITIATOR, Email_Reporting_Scheduler::ACTION_WORKER, Email_Reporting_Scheduler::ACTION_FALLBACK ) as $hook ) {
161+
foreach ( array( Email_Reporting_Scheduler::ACTION_INITIATOR, Email_Reporting_Scheduler::ACTION_WORKER, Email_Reporting_Scheduler::ACTION_FALLBACK, Email_Reporting_Scheduler::ACTION_MONITOR ) as $hook ) {
142162
wp_unschedule_hook( $hook );
143163
}
144164
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
/**
3+
* Class Google\Site_Kit\Tests\Core\Email_Reporting\Monitor_TaskTest
4+
*
5+
* @package Google\Site_Kit\Tests\Core\Email_Reporting
6+
*/
7+
8+
namespace Google\Site_Kit\Tests\Core\Email_Reporting;
9+
10+
use Google\Site_Kit\Core\Email_Reporting\Email_Reporting_Scheduler;
11+
use Google\Site_Kit\Core\Email_Reporting\Email_Reporting_Settings;
12+
use Google\Site_Kit\Core\Email_Reporting\Monitor_Task;
13+
use Google\Site_Kit\Core\User\Email_Reporting_Settings as User_Email_Reporting_Settings;
14+
use Google\Site_Kit\Tests\TestCase;
15+
16+
class Monitor_TaskTest extends TestCase {
17+
18+
/**
19+
* @var \PHPUnit_Framework_MockObject_MockObject|Email_Reporting_Scheduler
20+
*/
21+
private $scheduler;
22+
23+
/**
24+
* @var \PHPUnit_Framework_MockObject_MockObject|Email_Reporting_Settings
25+
*/
26+
private $settings;
27+
28+
/**
29+
* @var Monitor_Task
30+
*/
31+
private $task;
32+
33+
public function set_up() {
34+
parent::set_up();
35+
36+
$this->scheduler = $this->getMockBuilder( Email_Reporting_Scheduler::class )
37+
->disableOriginalConstructor()
38+
->setMethods( array( 'schedule_initiator_once' ) )
39+
->getMock();
40+
41+
$this->settings = $this->getMockBuilder( Email_Reporting_Settings::class )
42+
->disableOriginalConstructor()
43+
->setMethods( array( 'is_email_reporting_enabled' ) )
44+
->getMock();
45+
46+
$this->task = new Monitor_Task( $this->scheduler, $this->settings );
47+
48+
$this->clear_scheduled_initiators();
49+
}
50+
51+
public function tear_down() {
52+
$this->clear_scheduled_initiators();
53+
parent::tear_down();
54+
}
55+
56+
public function test_handle_monitor_action_bails_when_disabled() {
57+
$this->settings->expects( $this->once() )
58+
->method( 'is_email_reporting_enabled' )
59+
->willReturn( false );
60+
61+
$this->scheduler->expects( $this->never() )
62+
->method( 'schedule_initiator_once' );
63+
64+
$this->task->handle_monitor_action();
65+
}
66+
67+
public function test_handle_monitor_action_restores_missing_frequencies() {
68+
$this->settings->expects( $this->once() )
69+
->method( 'is_email_reporting_enabled' )
70+
->willReturn( true );
71+
72+
wp_schedule_single_event( time() + HOUR_IN_SECONDS, Email_Reporting_Scheduler::ACTION_INITIATOR, array( User_Email_Reporting_Settings::FREQUENCY_WEEKLY ) );
73+
wp_schedule_single_event( time() + HOUR_IN_SECONDS, Email_Reporting_Scheduler::ACTION_INITIATOR, array( User_Email_Reporting_Settings::FREQUENCY_MONTHLY ) );
74+
75+
$this->scheduler->expects( $this->once() )
76+
->method( 'schedule_initiator_once' )
77+
->with( User_Email_Reporting_Settings::FREQUENCY_QUARTERLY );
78+
79+
$this->task->handle_monitor_action();
80+
}
81+
82+
private function clear_scheduled_initiators() {
83+
wp_unschedule_hook( Email_Reporting_Scheduler::ACTION_INITIATOR );
84+
}
85+
}

tests/phpunit/integration/Core/Util/UninstallationTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ public function test_scheduled_events_include_email_reporting_hooks() {
4949
$this->assertContains( Email_Reporting_Scheduler::ACTION_INITIATOR, Uninstallation::SCHEDULED_EVENTS, 'Initiator hook should be cleared on uninstall/reset/deactivation.' );
5050
$this->assertContains( Email_Reporting_Scheduler::ACTION_WORKER, Uninstallation::SCHEDULED_EVENTS, 'Worker hook should be cleared on uninstall/reset/deactivation.' );
5151
$this->assertContains( Email_Reporting_Scheduler::ACTION_FALLBACK, Uninstallation::SCHEDULED_EVENTS, 'Fallback hook should be cleared on uninstall/reset/deactivation.' );
52+
$this->assertContains( Email_Reporting_Scheduler::ACTION_MONITOR, Uninstallation::SCHEDULED_EVENTS, 'Monitor hook should be cleared on uninstall/reset/deactivation.' );
5253
}
5354

5455
public function test_uninstall_using_proxy() {

0 commit comments

Comments
 (0)