PluginProbe
404 Solution / trunk
404 Solution vtrunk
4.3.5 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 4.2.0 4.1.19 4.1.18 4.1.17 4.1.16 4.1.15 4.1.13 4.1.12 4.1.11 4.1.10 4.1.9 4.1.8 4.1.7 4.1.6 4.1.5 4.1.4 4.1.3 trunk 2.30.0 All 109 releases
404-solution / includes / stats / StatsDashboardSnapshotCache.php

StatsDashboardSnapshotCache.php in 404 Solution trunk, at includes/stats/StatsDashboardSnapshotCache.php

145 lines 4.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('ABSPATH')) {
4 exit;
5 }
6
7 /**
8 * Owns the dashboard stats snapshot cache and stale-fallback refresh policy.
9 */
10 class ABJ_404_Solution_StatsDashboardSnapshotCache {
11
12 /** @var int Retention for dashboard stats snapshot payload. */
13 const STATS_DASHBOARD_CACHE_TTL_SECONDS = 86400;
14 /** @var int Minimum time between full stats snapshot recomputes. */
15 const STATS_DASHBOARD_REFRESH_COOLDOWN_SECONDS = 30;
16
17 /** @var ABJ_404_Solution_StatsReadRepository */
18 private $statsReadRepository;
19 /** @var ABJ_404_Solution_StatsRefreshLock */
20 private $refreshLock;
21 /** @var ABJ_404_Solution_Logging */
22 private $logger;
23
24 /**
25 * @param ABJ_404_Solution_StatsReadRepository $statsReadRepository
26 * @param ABJ_404_Solution_StatsRefreshLock $refreshLock
27 * @param ABJ_404_Solution_Logging $logging
28 */
29 public function __construct(
30 ABJ_404_Solution_StatsReadRepository $statsReadRepository,
31 ABJ_404_Solution_StatsRefreshLock $refreshLock,
32 $logging
33 ) {
34 $this->statsReadRepository = $statsReadRepository;
35 $this->refreshLock = $refreshLock;
36 $this->logger = $logging;
37 }
38
39 /**
40 * @param bool $allowStale
41 * @return array{refreshed_at:int,hash:string,data:array<string, mixed>}
42 */
43 public function getStatsDashboardSnapshot($allowStale = true) {
44 $cached = $this->getFromCache();
45 if ($cached !== null && !empty($cached['data']) && $allowStale) {
46 return $cached;
47 }
48
49 return $this->refresh(false);
50 }
51
52 /**
53 * @param bool $force
54 * @return array{refreshed_at:int,hash:string,data:array<string, mixed>}
55 */
56 public function refresh($force = false) {
57 $cached = $this->getFromCache();
58 $hasCachedData = ($cached !== null && !empty($cached['data']));
59 $cachedAge = $hasCachedData ? max(0, abj_clock()->now() - (is_scalar($cached['refreshed_at'] ?? 0) ? intval($cached['refreshed_at'] ?? 0) : 0)) : PHP_INT_MAX;
60
61 if (!$force && $hasCachedData && $cachedAge < self::STATS_DASHBOARD_REFRESH_COOLDOWN_SECONDS) {
62 return $cached;
63 }
64
65 $lockKey = 'stats-dashboard:' . $this->getCacheKey();
66 $lockAcquired = $this->refreshLock->acquire($lockKey);
67 if (!$lockAcquired && $hasCachedData) {
68 return $cached;
69 }
70
71 try {
72 $data = $this->statsReadRepository->buildStatsDashboardSnapshotData();
73 $payload = array(
74 'refreshed_at' => abj_clock()->now(),
75 'hash' => $this->hash($data),
76 'data' => $data,
77 );
78 if (function_exists('set_transient')) {
79 set_transient($this->getCacheKey(), $payload, self::STATS_DASHBOARD_CACHE_TTL_SECONDS);
80 }
81 return $payload;
82 } catch (Throwable $e) {
83 if ($hasCachedData) {
84 $this->logger->debugMessage(__FUNCTION__ . ' failed to recompute stats snapshot; returning cached snapshot. Error: ' . $e->getMessage());
85 return $cached;
86 }
87 throw $e;
88 } finally {
89 if ($lockAcquired) {
90 $this->refreshLock->release($lockKey);
91 }
92 }
93 }
94
95 /** @return array{refreshed_at:int,hash:string,data:array<string, mixed>}|null */
96 private function getFromCache() {
97 if (!function_exists('get_transient')) {
98 return null;
99 }
100 $cached = get_transient($this->getCacheKey());
101 if (!is_array($cached)) {
102 return null;
103 }
104 if (!array_key_exists('data', $cached) || !is_array($cached['data'])) {
105 return null;
106 }
107 $data = array();
108 foreach ($cached['data'] as $key => $value) {
109 if (is_string($key)) {
110 $data[$key] = $value;
111 }
112 }
113
114 return array(
115 'refreshed_at' => is_scalar($cached['refreshed_at'] ?? null) ? intval($cached['refreshed_at']) : 0,
116 'hash' => is_string($cached['hash'] ?? null) ? $cached['hash'] : '',
117 'data' => $data,
118 );
119 }
120
121 /** @return string */
122 private function getCacheKey(): string {
123 $blogId = 1;
124 if (function_exists('get_current_blog_id')) {
125 $blogId = absint(get_current_blog_id());
126 if ($blogId <= 0) {
127 $blogId = 1;
128 }
129 }
130 return 'abj404_stats_dashboard_snapshot_v1_' . $blogId;
131 }
132
133 /**
134 * @param array<string, mixed> $data
135 * @return string
136 */
137 private function hash($data) {
138 $encoded = function_exists('wp_json_encode') ? wp_json_encode($data) : json_encode($data);
139 if (!is_string($encoded)) {
140 $encoded = '';
141 }
142 return md5($encoded);
143 }
144 }
145