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 / StatsRefreshLock.php

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

97 lines 3.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 * Coordinates stats refresh work through WordPress option locks.
9 */
10 class ABJ_404_Solution_StatsRefreshLock {
11
12 /**
13 * Distributed lock lifetime. This must exceed the longest guarded query
14 * budget (the 60-second high-impact count) so a live worker is never
15 * mistaken for a stale lock while its query is still running.
16 *
17 * @var int
18 */
19 const REFRESH_LOCK_COOLDOWN_SECONDS = 120;
20
21 /** @var ABJ_404_Solution_DatabaseCoreInterface */
22 private $dbCore;
23
24 /** @var array<string, string> Cache key to exact row value held by this instance. */
25 private $heldValues = array();
26
27 /** @param ABJ_404_Solution_DatabaseCoreInterface $dbCore */
28 public function __construct(ABJ_404_Solution_DatabaseCoreInterface $dbCore) {
29 $this->dbCore = $dbCore;
30 }
31
32 /** @param string $cacheKey @return bool */
33 public function acquire(string $cacheKey): bool {
34 $lockKey = $this->getOptionName($cacheKey);
35 $lockRow = $this->lockRow();
36 $now = abj_clock()->now();
37 $claimValue = ABJ_404_Solution_ExclusiveOptionRow::uniqueClaimValue((string)$now);
38
39 if ($lockRow->claim(array('optionName' => $lockKey, 'value' => $claimValue))) {
40 $this->heldValues[$cacheKey] = $claimValue;
41 return true;
42 }
43
44 // A row already exists. Read it from the table (not from the options
45 // cache, which answers a concurrent request with its own write) and
46 // displace it only when it has genuinely aged out. Both the removal and
47 // the retry are conditional/atomic, so several requests finding the
48 // same expired lock still produce exactly one winner.
49 $lockValue = $lockRow->valueOf($lockKey);
50 if ($lockValue === '') {
51 $claimValue = ABJ_404_Solution_ExclusiveOptionRow::uniqueClaimValue((string)$now);
52 $claimed = $lockRow->claim(array('optionName' => $lockKey, 'value' => $claimValue));
53 if ($claimed) {
54 $this->heldValues[$cacheKey] = $claimValue;
55 }
56 return $claimed;
57 }
58
59 $timestampPart = explode(':', $lockValue, 2)[0];
60 $lockTs = is_numeric($timestampPart) ? (int)$timestampPart : 0;
61 if ($lockTs > 0 && ($now - $lockTs) > self::REFRESH_LOCK_COOLDOWN_SECONDS) {
62 $lockRow->releaseIfValueIs(array('optionName' => $lockKey, 'value' => $lockValue));
63 $claimValue = ABJ_404_Solution_ExclusiveOptionRow::uniqueClaimValue((string)$now);
64 $claimed = $lockRow->claim(array('optionName' => $lockKey, 'value' => $claimValue));
65 if ($claimed) {
66 $this->heldValues[$cacheKey] = $claimValue;
67 }
68 return $claimed;
69 }
70
71 return false;
72 }
73
74 /** @param string $cacheKey @return void */
75 public function release(string $cacheKey): void {
76 if (!isset($this->heldValues[$cacheKey])) {
77 return;
78 }
79 $this->lockRow()->releaseIfValueIs(array(
80 'optionName' => $this->getOptionName($cacheKey),
81 'value' => $this->heldValues[$cacheKey],
82 ));
83 unset($this->heldValues[$cacheKey]);
84 }
85
86 /** The lock row itself. Stateless, so a fresh instance costs nothing.
87 * @return ABJ_404_Solution_ExclusiveOptionRow */
88 private function lockRow(): ABJ_404_Solution_ExclusiveOptionRow {
89 return new ABJ_404_Solution_ExclusiveOptionRow();
90 }
91
92 /** @param string $cacheKey @return string */
93 private function getOptionName(string $cacheKey): string {
94 return $this->dbCore->tableNameResolver()->getLowercasePrefix() . 'abj404_view_cache_lock_' . md5((string)$cacheKey);
95 }
96 }
97