now = $now ?? static fn (): DateTimeImmutable => new DateTimeImmutable( 'now' ); } /** * Hook both checks. Scheduling waits for init (see RetentionCron). */ public function register(): void { add_action( 'init', array( $this, 'schedule' ) ); add_action( self::HOOK, array( $this, 'verify_chain' ) ); // After ExpiryCron's own handler, so what just expired is not counted. add_action( ExpiryCron::HOOK, array( $this, 'check_backlog' ), 20 ); } /** * Ensure the daily verification is scheduled. Safe to call repeatedly. */ public function schedule(): void { if ( false === wp_next_scheduled( self::HOOK ) ) { wp_schedule_event( time(), 'daily', self::HOOK ); } } /** * Remove the schedule (deactivation). */ public static function unschedule(): void { wp_clear_scheduled_hook( self::HOOK ); } /** * Verify the chain; raise adaf_anomaly_chain_broken( $entry_id, $reminder ). */ public function verify_chain(): void { $state = $this->state(); $broken = $this->writer->verify(); if ( null === $broken ) { unset( $state['chain'] ); $this->save( $state ); return; } $now = ( $this->now )()->getTimestamp(); $previous = $state['chain'] ?? null; $same = is_array( $previous ) && ( $previous['entry'] ?? null ) === $broken; if ( $same && $now - (int) ( $previous['at'] ?? 0 ) < self::REMIND_AFTER ) { return; } $state['chain'] = array( 'entry' => $broken, 'at' => $now, ); $this->save( $state ); do_action( 'adaf_anomaly_chain_broken', $broken, $same ); } /** * Check the queue; raise adaf_anomaly_queue_backlog( $count, $oldest ). */ public function check_backlog(): void { $state = $this->state(); $oldest = $this->pending->oldest_held_at(); if ( null === $oldest ) { unset( $state['backlog'] ); $this->save( $state ); return; } $now = ( $this->now )()->getTimestamp(); if ( $now - $oldest->getTimestamp() < $this->threshold() ) { return; } $last = $state['backlog']['at'] ?? null; if ( is_int( $last ) && $now - $last < self::REMIND_AFTER ) { return; } $state['backlog'] = array( 'at' => $now ); $this->save( $state ); do_action( 'adaf_anomaly_queue_backlog', $this->pending->count_by_status( 'pending' ), $oldest ); } /** * Backlog threshold: a day, or half the pending TTL if that is sooner. */ private function threshold(): int { $ttl = get_option( 'adaf_pending_ttl', PendingActionService::DEFAULT_TTL ); $ttl = is_numeric( $ttl ) && (int) $ttl > 0 ? (int) $ttl : PendingActionService::DEFAULT_TTL; return min( self::BACKLOG_AFTER, intdiv( $ttl, 2 ) ); } /** * Stored alarm state. * * @return array */ private function state(): array { $state = get_option( self::OPTION_STATE, array() ); return is_array( $state ) ? $state : array(); } /** * Store alarm state. Not autoloaded: read only by cron. * * @param array $state State. */ private function save( array $state ): void { update_option( self::OPTION_STATE, $state, false ); } }