PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 1.3.1
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v1.3.1
5.11.1 5.11.0 5.10.2 5.10.1 trunk 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.3.2 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.1.0 4.1.1 4.1.2 4.1.3 4.10.0 4.11.0 4.12.0 4.13.0 4.13.2 4.13.3 4.13.4 4.13.5 4.14.0 4.14.1 4.14.2 4.15.0 4.15.1 4.15.2 4.15.3 4.2.0 4.3.0 4.3.1 4.4.1 4.4.2 4.5.0 4.6.0 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.10.0 5.2.0 5.2.1 5.2.2 5.3.0 5.3.1 5.3.2 5.3.3 5.6.0 5.6.1 5.7.0 5.7.1 5.8.0 5.8.1 5.8.2
matomo / app / core / Concurrency / LockBackend / MySqlLockBackend.php
matomo / app / core / Concurrency / LockBackend Last commit date
MySqlLockBackend.php 6 years ago
MySqlLockBackend.php
146 lines
1 <?php
2 /**
3 * Piwik - free/libre analytics platform
4 *
5 * @link http://piwik.org
6 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
7 *
8 */
9
10 namespace Piwik\Concurrency\LockBackend;
11
12
13 use Piwik\Common;
14 use Piwik\Concurrency\LockBackend;
15 use Piwik\Db;
16 use Piwik\DbHelper;
17
18 class MySqlLockBackend implements LockBackend
19 {
20 const TABLE_NAME = 'locks';
21
22 /**
23 * fyi: does not support list keys at the moment just because not really needed so much just yet
24 */
25 public function getKeysMatchingPattern($pattern)
26 {
27 $sql = sprintf('SELECT SQL_NO_CACHE distinct `key` FROM %s WHERE `key` like ? and %s', self::getTableName(), $this->getQueryPartExpiryTime());
28 $pattern = str_replace('*', '%', $pattern);
29 $keys = Db::fetchAll($sql, array($pattern));
30 $raw = array_column($keys, 'key');
31 return $raw;
32 }
33
34 public function setIfNotExists($key, $value, $ttlInSeconds)
35 {
36 if (empty($ttlInSeconds)) {
37 $ttlInSeconds = 999999999;
38 }
39
40 // FYI: We used to have an INSERT INTO ... ON DUPLICATE UPDATE ... However, this can be problematic in concurrency issues
41 // because the ON DUPLICATE UPDATE may work successfully for 2 jobs at the same time but only one of them got the lock then.
42 // This would be perfectly fine if we did something like `return $this->get($key) === $value` to 100% detect which process
43 // got the lock as we do now. However, maybe the expireTime gets overwritten with a wrong value or so. That's why we
44 // rather try to get the lock with the insert only because only one job can succeed with this. If below flow with the
45 // delete becomes to slow, we may be able to use the INSERT INTO ... ON DUPLICATE UPDATE again.
46
47 if ($this->get($key)) {
48 return false; // a value is set, won't be possible to insert
49 }
50
51 $tablePrefixed = self::getTableName();
52
53 // remove any existing but expired lock
54 // todo: we could combine get() and keyExists() in one query!
55 if ($this->keyExists($key)) {
56 // most of the time an expired key should not exist... we don't want to lock the row unncessarily therefore we check first
57 // if value exists...
58 $sql = sprintf('DELETE FROM %s WHERE `key` = ? and not (%s)', $tablePrefixed, $this->getQueryPartExpiryTime());
59 Db::query($sql, array($key));
60 }
61
62 $query = sprintf('INSERT INTO %s (`key`, `value`, `expiry_time`)
63 VALUES (?,?,(UNIX_TIMESTAMP() + ?))',
64 $tablePrefixed);
65 // we make sure to update the row if the key is expired and consider it as "deleted"
66
67 try {
68 Db::query($query, array($key, $value, (int) $ttlInSeconds));
69 } catch (\Exception $e) {
70 if ($e->getCode() == 23000
71 || strpos($e->getMessage(), 'Duplicate entry') !== false
72 || strpos($e->getMessage(), ' 1062 ') !== false) {
73 return false;
74 }
75 throw $e;
76 }
77
78 // we make sure we got the lock
79 return $this->get($key) === $value;
80 }
81
82 public function get($key)
83 {
84 $sql = sprintf('SELECT SQL_NO_CACHE `value` FROM %s WHERE `key` = ? AND %s LIMIT 1', self::getTableName(), $this->getQueryPartExpiryTime());
85 return Db::fetchOne($sql, array($key));
86 }
87
88 public function deleteIfKeyHasValue($key, $value)
89 {
90 if (empty($value)) {
91 return false;
92 }
93
94 $sql = sprintf('DELETE FROM %s WHERE `key` = ? and `value` = ?', self::getTableName());
95 return $this->queryDidMakeChange($sql, array($key, $value));
96 }
97
98 public function expireIfKeyHasValue($key, $value, $ttlInSeconds)
99 {
100 if (empty($value)) {
101 return false;
102 }
103
104 // we need to use unix_timestamp in mysql and not time() in php since the local time might be different on each server
105 // better to rely on one central DB server time only
106 $sql = sprintf('UPDATE %s SET expiry_time = (UNIX_TIMESTAMP() + ?) WHERE `key` = ? and `value` = ?', self::getTableName());
107 $success = $this->queryDidMakeChange($sql, array((int) $ttlInSeconds, $key, $value));
108
109 if (!$success) {
110 // the above update did not work because the same time was already set and we just tried to set the same ttl
111 // again too fast within one second
112 return $value === $this->get($key);
113 }
114
115 return true;
116 }
117
118 public function keyExists($key)
119 {
120 $sql = sprintf('SELECT SQL_NO_CACHE 1 FROM %s WHERE `key` = ? LIMIT 1', self::getTableName());
121 $value = Db::fetchOne($sql, array($key));
122 return !empty($value);
123 }
124
125 private function queryDidMakeChange($sql, $bind = array())
126 {
127 $query = Db::query($sql, $bind);
128 if (is_object($query) && method_exists($query, 'rowCount')) {
129 // anything else but mysqli in tracker mode
130 return (bool) $query->rowCount();
131 } else {
132 // mysqli in tracker mode
133 return (bool) Db::get()->rowCount($query);
134 }
135 }
136
137 private static function getTableName()
138 {
139 return Common::prefixTable(self::TABLE_NAME);
140 }
141
142 private function getQueryPartExpiryTime()
143 {
144 return 'UNIX_TIMESTAMP() <= expiry_time';
145 }
146 }