PluginProbe
Blocks – Reusable Content, Shortcodes & Site Variables / trunk
Blocks – Reusable Content, Shortcodes & Site Variables vtrunk
26.09.03.15 26.08.31.21 26.08.22.20 26.08.22.22 26.08.22.17 26.08.22.13 26.08.21.19 26.08.07.23 26.07.19.14 26.07.13.21 26.07.13.17 26.07.12.13 026.07.07.21 026.07.05.18 026.06.26.20 026.06.26.21 026.06.08.20 026.05.13.14 026.04.29.10 trunk 026.02.22.22 026.03.16.23 026.04.23.13
blocks / includes / signls / src / Scheduler.php

Scheduler.php in Blocks – Reusable Content, Shortcodes & Site Variables trunk, at includes/signls/src/Scheduler.php

188 lines 5.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Product-scoped durable scheduling.
4 *
5 * @package signls-sdk
6 * @license GPL-3.0-or-later
7 */
8
9 namespace Signls\Sdk\V1;
10
11 defined( 'ABSPATH' ) || exit;
12
13 final class Scheduler {
14
15 private $product;
16
17 private $state;
18
19 private $adapter;
20
21 private $routine_hook;
22
23 private $refresh_hook;
24
25 public function __construct( string $product, StateStore $state, ProductAdapterInterface $adapter ) {
26 $hash = substr( hash( 'sha256', $product ), 0, 12 );
27 $this->product = $product;
28 $this->state = $state;
29 $this->adapter = $adapter;
30 $this->routine_hook = 'signls_sdk_v1_' . $hash . '_routine';
31 $this->refresh_hook = 'signls_sdk_v1_' . $hash . '_refresh';
32 }
33
34 public function register(): void {
35 add_action( $this->routine_hook, array( $this, 'run' ) );
36 add_action( $this->refresh_hook, array( $this, 'run' ) );
37 add_action( 'admin_init', array( $this, 'failsafe' ), 999 );
38 }
39
40 public function activate( string $install_id, string $device_id ): void {
41 if ( 'enabled' !== $this->state->get( 'consent_status', 'unset' ) ) {
42 return;
43 }
44 if ( wp_next_scheduled( $this->routine_hook ) ) {
45 return;
46 }
47 $delay = 300 + self::jitter( $this->product . '|' . $install_id . '|' . $device_id, 21300 );
48 wp_schedule_single_event( time() + $delay, $this->routine_hook );
49 }
50
51 public function relevant_change(): void {
52 $this->state->set_many(
53 array(
54 'last_relevant_activity' => time(),
55 'cadence_mode' => 'daily',
56 )
57 );
58 if ( $this->quarantine_deferred() ) {
59 return;
60 }
61 if ( ! wp_next_scheduled( $this->refresh_hook ) ) {
62 wp_schedule_single_event( time() + 300 + self::jitter( $this->product . '|refresh', 600 ), $this->refresh_hook );
63 }
64 }
65
66 public function run(): void {
67 if ( 'enabled' !== $this->state->get( 'consent_status', 'unset' ) ) {
68 return;
69 }
70 $result = Runtime::run( $this->product );
71 if ( in_array( $result['class'], array( 'delivery_busy', 'delivery_lock_unavailable' ), true ) ) {
72 return;
73 }
74 $this->schedule_next();
75 }
76
77 public function schedule_next(): void {
78 if ( 'enabled' !== $this->state->get( 'consent_status', 'unset' ) ) {
79 return;
80 }
81 if ( wp_next_scheduled( $this->routine_hook ) ) {
82 wp_clear_scheduled_hook( $this->routine_hook );
83 }
84 $last_activity = (int) $this->state->get( 'last_relevant_activity', time() );
85 $sparse = time() - $last_activity >= 30 * DAY_IN_SECONDS;
86 $interval = $sparse ? WEEK_IN_SECONDS : DAY_IN_SECONDS;
87 $mode = $sparse ? 'sparse' : 'daily';
88 $this->state->set( 'cadence_mode', $mode );
89 wp_schedule_single_event( time() + $interval + self::jitter( $this->product . '|' . gmdate( 'Y-m-d' ), 3600 ), $this->routine_hook );
90 }
91
92 public function schedule_retry( int $when ): void {
93 if ( 'enabled' !== $this->state->get( 'consent_status', 'unset' ) ) {
94 return;
95 }
96 if ( $this->quarantine_deferred() ) {
97 return;
98 }
99 if ( ! wp_next_scheduled( $this->refresh_hook ) ) {
100 wp_schedule_single_event( max( time() + 60, $when ), $this->refresh_hook );
101 }
102 }
103
104 public function settle_immediate( array $result, bool $intent_pending ): void {
105 if ( 'enabled' !== $this->state->get( 'consent_status', 'unset' ) ) {
106 return;
107 }
108 if ( ! empty( $result['quarantined'] ) || ! empty( $result['permanent'] ) ) {
109 wp_clear_scheduled_hook( $this->refresh_hook );
110 $this->schedule_next();
111 return;
112 }
113 if ( $intent_pending ) {
114 return;
115 }
116 if ( ! empty( $result['ok'] ) ) {
117 wp_clear_scheduled_hook( $this->refresh_hook );
118 $this->schedule_next();
119 return;
120 }
121
122 $when = in_array( $result['class'], array( 'delivery_busy', 'delivery_lock_unavailable' ), true )
123 ? time() + 60
124 : (int) $this->state->get( 'next_retry_at', time() + 900 );
125 wp_clear_scheduled_hook( $this->refresh_hook );
126 wp_schedule_single_event( max( time() + 60, $when ), $this->refresh_hook );
127 }
128
129 public function clear(): void {
130 wp_clear_scheduled_hook( $this->routine_hook );
131 wp_clear_scheduled_hook( $this->refresh_hook );
132 }
133
134 public function failsafe(): void {
135 if ( 'enabled' !== $this->state->get( 'consent_status', 'unset' ) ) {
136 return;
137 }
138 $last = (int) $this->state->get( 'last_acknowledged_at', 0 );
139 $mode = (string) $this->state->get( 'cadence_mode', 'daily' );
140 $age = 'sparse' === $mode ? 9 * DAY_IN_SECONDS : 36 * HOUR_IN_SECONDS;
141 if ( $last > 0 && time() - $last < $age ) {
142 return;
143 }
144 if ( $this->quarantine_deferred() ) {
145 return;
146 }
147 if ( ! wp_next_scheduled( $this->refresh_hook ) ) {
148 wp_schedule_single_event( time() + 300 + self::jitter( $this->product . '|failsafe', 600 ), $this->refresh_hook );
149 }
150 }
151
152 public function routine_hook(): string {
153 return $this->routine_hook;
154 }
155
156 public function refresh_hook(): string {
157 return $this->refresh_hook;
158 }
159
160 private static function jitter( string $seed, int $maximum ): int {
161 $hex = substr( hash( 'sha256', $seed ), 0, 8 );
162 return (int) ( hexdec( $hex ) % ( max( 1, $maximum ) + 1 ) );
163 }
164
165 private function quarantine_deferred(): bool {
166 if (
167 '' === (string) $this->state->get( 'pending_body', '' )
168 || '' === (string) $this->state->get( 'pending_quarantine_class', '' )
169 || (int) $this->state->get( 'pending_quarantine_probe_at', 0 ) <= time()
170 ) {
171 return false;
172 }
173
174 try {
175 $contract = $this->adapter->contract();
176 $revision = isset( $contract['snapshot_payload_revision'] ) ? (int) $contract['snapshot_payload_revision'] : 1;
177 $revision = $revision > 0 ? $revision : 1;
178 return (
179 Transport::version() === (string) $this->state->get( 'pending_sdk_version', '' )
180 && $this->adapter->product_version() === (string) $this->state->get( 'pending_product_version', '' )
181 && $revision === (int) $this->state->get( 'pending_payload_revision', 1 )
182 );
183 } catch ( \Throwable $error ) {
184 return false;
185 }
186 }
187 }
188