Skip to content

Commit 504a9eb

Browse files
Merge pull request #11781 from google/enhancement/11547-worker-task
Enhancement/11547 worker task
2 parents 3e1cb7c + 6534794 commit 504a9eb

File tree

7 files changed

+831
-0
lines changed

7 files changed

+831
-0
lines changed
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
<?php
2+
/**
3+
* Class Google\Site_Kit\Core\Email_Reporting\Email_Log_Batch_Query
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 WP_Query;
14+
15+
/**
16+
* Helper for querying and updating email log batches.
17+
*
18+
* @since n.e.x.t
19+
* @access private
20+
* @ignore
21+
*/
22+
class Email_Log_Batch_Query {
23+
24+
const MAX_ATTEMPTS = 3;
25+
26+
/**
27+
* Retrieves IDs for pending logs within a batch.
28+
*
29+
* @since n.e.x.t
30+
*
31+
* @param string $batch_id Batch identifier.
32+
* @param int $max_attempts Maximum delivery attempts allowed.
33+
* @return array Pending post IDs that still require processing.
34+
*/
35+
public function get_pending_ids( $batch_id, $max_attempts = self::MAX_ATTEMPTS ) {
36+
$batch_id = (string) $batch_id;
37+
$max_attempts = (int) $max_attempts;
38+
39+
$query = $this->get_batch_query( $batch_id );
40+
41+
$pending_ids = array();
42+
43+
foreach ( $query->posts as $post_id ) {
44+
$status = get_post_status( $post_id );
45+
46+
if ( Email_Log::STATUS_SENT === $status ) {
47+
continue;
48+
}
49+
50+
if ( Email_Log::STATUS_FAILED === $status ) {
51+
$attempts = (int) get_post_meta( $post_id, Email_Log::META_SEND_ATTEMPTS, true );
52+
53+
if ( $attempts >= $max_attempts ) {
54+
continue;
55+
}
56+
}
57+
58+
$pending_ids[] = (int) $post_id;
59+
}
60+
61+
return $pending_ids;
62+
}
63+
64+
/**
65+
* Builds a batch query object limited to a specific batch ID.
66+
*
67+
* @since n.e.x.t
68+
*
69+
* @param string $batch_id Batch identifier.
70+
* @return WP_Query Query returning IDs only.
71+
*/
72+
private function get_batch_query( $batch_id ) {
73+
return new WP_Query(
74+
array(
75+
'post_type' => Email_Log::POST_TYPE,
76+
'post_status' => array(
77+
Email_Log::STATUS_SCHEDULED,
78+
Email_Log::STATUS_SENT,
79+
Email_Log::STATUS_FAILED,
80+
),
81+
'posts_per_page' => -1,
82+
'fields' => 'ids',
83+
'no_found_rows' => true,
84+
'update_post_meta_cache' => false,
85+
'update_post_term_cache' => false,
86+
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
87+
'meta_query' => array(
88+
array(
89+
'key' => Email_Log::META_BATCH_ID,
90+
'value' => $batch_id,
91+
),
92+
),
93+
)
94+
);
95+
}
96+
97+
/**
98+
* Determines whether all posts in the batch completed delivery.
99+
*
100+
* @since n.e.x.t
101+
*
102+
* @param string $batch_id Batch identifier.
103+
* @param int $max_attempts Maximum delivery attempts allowed.
104+
* @return bool True if the batch has no remaining pending posts.
105+
*/
106+
public function is_complete( $batch_id, $max_attempts = self::MAX_ATTEMPTS ) {
107+
return empty( $this->get_pending_ids( $batch_id, $max_attempts ) );
108+
}
109+
110+
/**
111+
* Increments the send attempt counter for a log post.
112+
*
113+
* @since n.e.x.t
114+
*
115+
* @param int $post_id Log post ID.
116+
* @return void Nothing returned.
117+
*/
118+
public function increment_attempt( $post_id ) {
119+
$post = get_post( $post_id );
120+
121+
if ( ! $post || Email_Log::POST_TYPE !== $post->post_type ) {
122+
return;
123+
}
124+
125+
$current_attempts = (int) get_post_meta( $post_id, Email_Log::META_SEND_ATTEMPTS, true );
126+
127+
update_post_meta( $post_id, Email_Log::META_SEND_ATTEMPTS, $current_attempts + 1 );
128+
}
129+
130+
/**
131+
* Updates the post status for a log post.
132+
*
133+
* @since n.e.x.t
134+
*
135+
* @param int $post_id Log post ID.
136+
* @param string $status New status slug.
137+
* @return void Nothing returned.
138+
*/
139+
public function update_status( $post_id, $status ) {
140+
$post = get_post( $post_id );
141+
142+
if ( ! $post || Email_Log::POST_TYPE !== $post->post_type ) {
143+
return;
144+
}
145+
146+
wp_update_post(
147+
array(
148+
'ID' => $post_id,
149+
'post_status' => $status,
150+
)
151+
);
152+
}
153+
}

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+
* Worker task instance.
110+
*
111+
* @since n.e.x.t
112+
* @var Worker_Task
113+
*/
114+
protected $worker_task;
115+
108116
/**
109117
* Constructor.
110118
*
@@ -130,11 +138,14 @@ public function __construct(
130138

131139
$frequency_planner = new Frequency_Planner();
132140
$subscribed_users_query = new Subscribed_Users_Query( $this->user_settings, $this->modules );
141+
$max_execution_limiter = new Max_Execution_Limiter( (int) ini_get( 'max_execution_time' ) );
142+
$batch_query = new Email_Log_Batch_Query();
133143

134144
$this->rest_controller = new REST_Email_Reporting_Controller( $this->settings );
135145
$this->email_log = new Email_Log( $this->context );
136146
$this->scheduler = new Email_Reporting_Scheduler( $frequency_planner );
137147
$this->initiator_task = new Initiator_Task( $this->scheduler, $subscribed_users_query );
148+
$this->worker_task = new Worker_Task( $max_execution_limiter, $batch_query, $this->scheduler );
138149
}
139150

140151
/**
@@ -154,6 +165,7 @@ public function register() {
154165
$this->scheduler->schedule_initiator_events();
155166

156167
add_action( Email_Reporting_Scheduler::ACTION_INITIATOR, array( $this->initiator_task, 'handle_callback_action' ), 10, 1 );
168+
add_action( Email_Reporting_Scheduler::ACTION_WORKER, array( $this->worker_task, 'handle_callback_action' ), 10, 3 );
157169

158170
} else {
159171
$this->scheduler->unschedule_all();
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php
2+
/**
3+
* Class Google\Site_Kit\Core\Email_Reporting\Max_Execution_Limiter
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+
/**
14+
* Guards long-running email reporting tasks against timeouts.
15+
*
16+
* @since n.e.x.t
17+
* @access private
18+
* @ignore
19+
*/
20+
class Max_Execution_Limiter {
21+
22+
const DEFAULT_LIMIT = 30;
23+
24+
/**
25+
* Maximum execution time budget in seconds.
26+
*
27+
* @since n.e.x.t
28+
*
29+
* @var int
30+
*/
31+
private $max_execution_time;
32+
33+
/**
34+
* Constructor.
35+
*
36+
* @since n.e.x.t
37+
*
38+
* @param int $max_execution_time PHP max_execution_time value.
39+
*/
40+
public function __construct( $max_execution_time ) {
41+
$this->max_execution_time = ( $max_execution_time && $max_execution_time > 0 )
42+
? (int) $max_execution_time
43+
: self::DEFAULT_LIMIT;
44+
}
45+
46+
/**
47+
* Determines whether the worker should abort execution.
48+
*
49+
* @since n.e.x.t
50+
*
51+
* @param int $initiator_timestamp Initial batch timestamp.
52+
* @return bool True when either the runtime or 24h limit has been reached.
53+
*/
54+
public function should_abort( $initiator_timestamp ) {
55+
$now = microtime( true );
56+
$execution_deadline = $this->execution_deadline();
57+
$initiator_deadline = (int) $initiator_timestamp + DAY_IN_SECONDS;
58+
$runtime_budget_used = $execution_deadline > 0 && $now >= $execution_deadline;
59+
60+
return $runtime_budget_used || $now >= $initiator_deadline;
61+
}
62+
63+
/**
64+
* Resolves the maximum execution budget in seconds.
65+
*
66+
* @since n.e.x.t
67+
*
68+
* @return int Number of seconds allotted for execution.
69+
*/
70+
protected function resolve_budget_seconds() {
71+
return $this->max_execution_time;
72+
}
73+
74+
/**
75+
* Calculates the execution deadline timestamp.
76+
*
77+
* @since n.e.x.t
78+
*
79+
* @return float Execution cutoff timestamp.
80+
*/
81+
private function execution_deadline() {
82+
$budget = $this->resolve_budget_seconds();
83+
84+
if ( $budget <= 0 ) {
85+
return 0;
86+
}
87+
88+
$start_time = defined( 'WP_START_TIMESTAMP' ) ? (float) WP_START_TIMESTAMP : microtime( true );
89+
90+
return $start_time + $budget - 10;
91+
}
92+
}

0 commit comments

Comments
 (0)