Skip to content

Implement “Worker” scheduled events #11547

@benbowler

Description

@benbowler

Feature Description


Do not alter or remove anything below. The following sections will be managed by moderators only.

Acceptance Criteria

  • A "Worker" scheduled event is defined, which is spawned by the "Initiator" scheduler (11546).
  • The "Worker" sets googlesitekit_email_reporting_worker_lock_{$frequency} transient with timestamp (scoped to the current frequency) expiring in 1min when starting.
  • The "Worker" event tracks its execution time using the WP_START_TIMESTAMP constant.
  • The "Worker" must exit immediately if the current execution time reaches within 10 seconds of the PHP max_execution time to prevent interruption, even if there are more reports to send.
  • The "Worker" event must exit immediately if it is triggered 24 hours after the initial trigger date of the frequency to prevent infinite running.
  • When the "Worker" event runs, it first retrieves all CPT posts (googlesitekit_email_log) for its batch ID to get user IDs and statuses.
  • The "Worker" must exit immediately without spawning a follow-up event if all user reports in the batch are marked as sent or failed (and the attempt count for each failed report is >= 3).
  • If the exit conditions are not met, the "Worker" spawns a follow-up "Worker" scheduled event for 11 minutes from current one.
  • For each user ID being processed in the batch, the "Worker":

Implementation Brief

Add includes\Core\Email_Reporting\Max_Execution_Limiter.php class

  • It should accept $max_execution_time argument in the constructor and store it in the class property, treat any 0/negative/falsy result as 30 (for default fallback)
  • Add should_abort( $initiator_timestamp ):
    • It checks whether the current time (microtime( true )) has passed either the execution deadline derived from WP_START_TIMESTAMP and the configured max-execution budget (minus a 10-second safety window) (eq WP_START_TIMESTAMP + $this->resolve_budget_seconds() - 10) or the initiator timestamp plus 24 hours - to prevent infinite loops we limit it to 24h (eq $initiator_timestamp + DAY_IN_SECONDS)
      • If either bound is met it returns true so the worker exits.

Add includes\Core\Email_Reporting\Email_Log_Batch_Query

  • Define constant MAX_ATTEMPTS = 3
  • Add get_pending_ids( $batch_id, $max_attempts )
    • Resolve the batch via a single WP_Query:
    • Limit it to the Email_Log::POST_TYPE post type
    • Restrict post_status to the three internal statuses ( e.g. Email_Log::STATUS_SCHEDULED, STATUS_SENT, STATUS_FAILED )
    • Filter by a meta_query on batch_id
    • Request IDs only, and disable unnecessary caches (no_found_rows, update_post_meta_cache, update_post_term_cache).
    • Walk the resulting IDs, reading status via get_post_status() and attempts via get_post_meta() track whether every post is complete and build a list of “still eligible” IDs (scheduled or failed with attempts < self::MAX_ATTEMPTS) in $pending_ids.
      • Return resulting array $pending_ids
  • Add is_complete
    • Return the result of empty( $this->get_pending_ids )
  • Add increment_attempt( $post_id )
    • Retrieve the post by $post_id and update it's Email_Log::META_SEND_ATTEMPTS meta with $current_attempts + 1
  • Add update_status( $post_id, $status )
    • Retrieve the post by $post_id and update it's status with passed $status argument

Update Google\Site_Kit\Core\Email_Reporting\Email_Reporting

  • In register() method:
    • Instantiate Max_Execution_Limiter, Email_Log_Batch_Query and Worker_Task
    • Pass (int) ini_get( 'max_execution_time' ) to the Max_Execution_Limiter
    • Hook into the Email_Reporting_Scheduler::ACTION_WORKER and invoke handle_callback_action from Worker_Task instance.

Add Core\Email_Reporting\Worker_Task

  • It accepts instances of Email_Log_Batch_Query and Email_Reporting_Scheduler in the constructor and sets them as a class property.
  • Add handle_worker_action( $batch_id, $frequency, $initiator_timestamp )
    • Store the transient name in a var, eq $transient_name = googlesitekit_email_reporting_worker_lock_{$frequency};
    • At the start of the function guard against concurrent runs: if get_transient( $transient_name ) is truthy, return immediately. Otherwise set it via set_transient( $transient_name, time(), MINUTE_IN_SECONDS ) and wrap the rest of the method in a try { ... } finally { delete_transient( $transient_name ) } to guarantee cleanup.
    • Immediately after setting the lock, call guard - Max_Execution_Limiter::should_abort( $initiator_timestamp ) and bail out if it returns true.
    • Exit immediately if the batch query Email_Log_Batch_Query::is_complete() return true
    • Otherwise, resolve the batch IDs via Email_Log_Batch_Query::get_pending_ids()
    • Schedule the follow-up worker (Email_Reporting_Scheduler::schedule_worker() forwarding the same parameters passed to the current worker)
    • Then use the guard Max_Execution_Limiter::should_abort( $initiator_timestamp ) - return early if it is true.
    • Otherwise start the loop on returned $pending_ids list:
      • Walk the post IDs one by one, calling the guard should_abort() and return early if it is true before each item.
      • For each post increment its attempt via Email_Log_Batch_Query::increment_attempt().
      • After the loop, and before rest of the logic is implemented in follow up issue, run a should_abort() guard again and return early if it is true

Test Coverage

  • Add tests for tests/phpunit/unit/Core/Email_Reporting/Max_Execution_LimiterTest.php:
    • Verify that it caches the passed limit ($max_execution_time) and falls back to 30 when $max_execution_time is 0/false.
    • Verify that should_abort() returns false when within both limits and true when either the execution deadline or the 24h initiator window has passed.
  • Add tests for Worker_Task class
    • Test that when no lock is set the worker acquires and clears the lock even on early returns; when the lock already exists it exits immediately.
    • Test that Worker exits without rescheduling when the batch query returns no posts or every post is sent/failed with attempts ≥ 3.
    • Test that with pending posts present, the worker schedules a follow-up event carrying the same arguments.
    • Verify that pending posts have their send_attempts meta incremented, while posts already maxed on attempts remain unchanged.
  • Add tests for Email_Log_Batch_Query:
    • Verify posts are correctly returned - filter should not return posts under sent status
    • is_complete returns true as expected when there are pending IDs otherwise it return false
    • increment_attempt and update_status are correctly updating the posts with meta/status

QA Brief

  • Enable the feature flag and install a plugin that lets you see WP scheduled events (like WP Crontrol).
  • Toggle the feature in the admin settings, when the feature is enabled you should see registered events for the initiator task (filter the hook names by googlesitekit):
Image

Choose any of the initiator tasks, and click run now option on that event

  • Page will reload, at the top of the events list should be a new event that is scheduled for 60/59s - googlesitekit_email_reporting_worker. It should show same frequency in the parameters as the one for initiator you choose to run
Image

Changelog entry

  • Implement “Worker” scheduled events for Email Reporting.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P0High priorityTeam SIssues for Squad 1Type: EnhancementImprovement of an existing feature

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions