dispatcher = $dispatcher; $this->health = $health; $this->warmer = $warmer; $this->settings = $settings; $this->monitor = $monitor; $this->brief = $brief; } /** * Schedules the hourly event if missing. * * @return void */ public static function schedule() { if ( ! wp_next_scheduled( self::HOOK ) ) { wp_schedule_event( time() + MINUTE_IN_SECONDS, 'hourly', self::HOOK ); } } /** * Records that a background job is running, for the "cron is late" warning. * * @return void */ public static function beat() { update_option( self::HEARTBEAT, time(), false ); } /** * When a background job last ran. * * @return int Unix time, 0 if never. */ public static function last_beat() { return (int) get_option( self::HEARTBEAT, 0 ); } /** * Removes the event (plugin deactivation). * * @return void */ public static function unschedule() { wp_clear_scheduled_hook( self::HOOK ); } /** * The hourly handler. * * @return void */ public function run() { if ( function_exists( 'set_time_limit' ) ) { set_time_limit( 300 ); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- Background job talking to remote APIs. } self::beat(); try { $this->dispatcher->dispatch_due(); self::outcome( 'delivery', true ); } catch ( \Throwable $e ) { // Keep the daily jobs running even if email sending hit a bug. self::outcome( 'delivery', false ); } $today = wp_date( 'Y-m-d' ); if ( get_option( self::DAILY_FLAG ) === $today ) { $this->send_brief(); return; } $lock = OptionLock::acquire( 'turtlereport_daily_lock', 600 ); if ( null === $lock ) { return; } try { wp_cache_delete( self::DAILY_FLAG, 'options' ); if ( get_option( self::DAILY_FLAG ) === $today ) { return; } $this->dispatcher->purge(); $this->health->check_all(); if ( $this->settings->get( 'warm_cache' ) ) { $this->warmer->warm_all(); } if ( $this->monitor ) { $this->monitor->check_all(); } if ( ! $lock->renew( 600 ) ) { self::outcome( 'daily', false ); return; } update_option( self::DAILY_FLAG, $today, false ); self::outcome( 'daily', true ); } catch ( \Throwable $e ) { self::outcome( 'daily', false ); } finally { $lock->release(); } // After the day's check, so a brief due now uses fresh alerts. $this->send_brief(); } /** * Sends the Founder Weekly Brief when it is due. * * @return void */ private function send_brief() { if ( ! $this->brief ) { return; } try { $before = $this->brief->last_outcome(); $this->brief->maybe_send(); $after = $this->brief->last_outcome(); if ( $this->brief->interrupted() ) { self::outcome( 'brief', false ); } elseif ( $before !== $after ) { self::outcome( 'brief', 'accepted' === $after['status'] ); } } catch ( \Throwable $e ) { self::outcome( 'brief', false ); } } /** * Sets or clears a job's failure flag. Only a timestamp is stored, never * exception text, which may contain secrets. This does not retry anything; * each job keeps its own retry policy. * * @param string $job Job name. * @param bool $ok Whether this run completed. * @return void */ private static function outcome( $job, $ok ) { try { OptionUpdate::change( self::FAILURES, static function ( array $failures ) use ( $job, $ok ) { if ( $ok ) { unset( $failures[ $job ] ); } else { $failures[ $job ] = time(); } return $failures; } ); } catch ( \RuntimeException $e ) { // Best effort: a contended status flag must not abort the remaining jobs. return; } } }