PluginProbe
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance / 4.6.1
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance v4.6.1
4.7.0 4.6.1 4.6.0 4.5.5 4.5.4 4.5.3 4.5.2 3.2.20 3.2.21 3.2.22 3.2.3 3.2.5 3.2.6 3.2.7 3.2.9 3.3.0 3.3.1 3.3.2 3.4.0 3.4.1 3.4.2 3.5.0 3.6.0 3.7.0 3.7.1 All 111 releases
wp-optimize / vendor / team-updraft / common-libs / src / updraft-semaphore / class-updraft-semaphore.php

class-updraft-semaphore.php in WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance 4.6.1, at vendor/team-updraft/common-libs/src/updraft-semaphore/class-updraft-semaphore.php

215 lines 8.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- we try to reduce overhead by bypassing WP APIs and other extra layers; Some custom complex queries tailored specifically to our needs, giving us full control over the SQL commands and data manipulation
3 // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- some query operations need to always receive the most up-to-date or actual data directly from the database, reducing the risk of serving stale information
4 if (!defined('ABSPATH')) die('No direct access.');
5
6 /**
7 * Class Updraft_Semaphore_3_0
8 *
9 * This class is much simpler to use than the the previous series, as it has dropped support for complicated cases that were not being used. It also now only uses a single row in the options database, and takes care of creating it itself internally.
10 *
11 * Logging, though, may be noisier, unless your loggers are taking note of the log level and only registering what is required.
12 *
13 * Example of use (a lock that will expire if not released within 300 seconds)
14 *
15 * See test.php for a longer example (including logging).
16 *
17 * $my_lock = new Updraft_Semaphore_3_0('my_lock_name', 300);
18 * // If getting the lock does not succeed first time, try again up to twice
19 * if ($my_lock->lock(2)) {
20 * try {
21 * // do stuff ...
22 * } catch (Exception $e) {
23 * // We are making sure we release the lock in case of an error
24 * } catch (Error $e) {
25 * // We are making sure we release the lock in case of an error
26 * }
27 * $my_lock->release();
28 * } else {
29 * error_log("Sorry, could not get the lock");
30 * }
31 */
32 class Updraft_Semaphore_3_0 {
33
34 // Time after which the lock will expire (in seconds)
35 protected $locked_for;
36
37 // Name for the lock in the WP options table
38 protected $option_name;
39
40 // Lock status - a boolean
41 protected $acquired = false;
42
43 // An array of loggers
44 protected $loggers = array();
45
46 /**
47 * Constructor. Instantiating does not lock anything, but sets up the details for future operations.
48 *
49 * @param String $name - a unique (across the WP site) name for the lock. Should be no more than 51 characters in length (because of the use of the WP options table, with some further characters used internally)
50 * @param Integer $locked_for - time (in seconds) after which the lock will expire if not released. This needs to be positive if you don't want bad things to happen.
51 * @param Array $loggers - an array of loggers
52 */
53 public function __construct($name, $locked_for = 300, $loggers = array()) {
54 $this->option_name = 'updraft_lock_'.$name;
55 $this->locked_for = $locked_for;
56 $this->loggers = $loggers;
57 }
58
59 /**
60 * Internal function to make sure that the lock is set up in the database
61 *
62 * @return Integer - 0 means 'failed' (which could include that someone else concurrently created it); 1 means 'already existed'; 2 means 'exists, because we created it). The intention is that non-zero results mean that the lock exists.
63 */
64 private function ensure_database_initialised() {
65
66 global $wpdb;
67
68 $sql = $wpdb->prepare("SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name = %s", $this->option_name);
69
70 if (1 === (int) $wpdb->get_var($sql)) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $sql is already prepared via $wpdb->prepare() above
71 $this->log('Lock option ('.$this->option_name.', '.$wpdb->options.') already existed in the database', 'debug');
72 return 1;
73 }
74
75 $sql = $wpdb->prepare("INSERT INTO {$wpdb->options} (option_name, option_value, autoload) VALUES(%s, '0', 'no');", $this->option_name);
76
77 $rows_affected = $wpdb->query($sql); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $sql is already prepared via $wpdb->prepare() above
78
79 if ($rows_affected > 0) {
80 $this->log('Lock option ('.$this->option_name.', '.$wpdb->options.') was created in the database', 'debug');
81 } else {
82 $this->log('Lock option ('.$this->option_name.', '.$wpdb->options.') failed to be created in the database (could already exist)', 'notice');
83 }
84
85 return ($rows_affected > 0) ? 2 : 0;
86 }
87
88 /**
89 * Attempt to acquire the lock. If it was already acquired, then nothing extra will be done (the method will be a no-op).
90 *
91 * @param Integer $retries - how many times to retry (after a 1 second sleep each time)
92 *
93 * @return Boolean - whether the lock was successfully acquired or not
94 */
95 public function lock($retries = 0) {
96
97 if ($this->acquired) return true;
98
99 global $wpdb;
100
101 $time_now = time();
102 $acquire_until = $time_now + $this->locked_for;
103
104 $sql = $wpdb->prepare("UPDATE {$wpdb->options} SET option_value = %s WHERE option_name = %s AND option_value < %d", $acquire_until, $this->option_name, $time_now);
105
106 if (1 === $wpdb->query($sql)) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $sql is already prepared via $wpdb->prepare() above
107 $this->log('Lock ('.$this->option_name.', '.$wpdb->options.') acquired', 'info');
108 $this->acquired = true;
109 return true;
110 }
111
112 // See if the failure was caused by the row not existing (we check this only after failure, because it should only occur once on the site)
113 if (!$this->ensure_database_initialised()) return false;
114
115 do {
116 // Now that the row has been created, try again
117 if (1 === $wpdb->query($sql)) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $sql is already prepared via $wpdb->prepare() above
118 $this->log('Lock ('.$this->option_name.', '.$wpdb->options.') acquired after initialising the database', 'info');
119 $this->acquired = true;
120 return true;
121 }
122 $retries--;
123 if ($retries >=0) {
124 $this->log('Lock ('.$this->option_name.', '.$wpdb->options.') not yet acquired; sleeping', 'debug');
125 sleep(1);
126 // As a second has passed, update the time we are aiming for
127 $time_now = time();
128 $acquire_until = $time_now + $this->locked_for;
129 $sql = $wpdb->prepare("UPDATE {$wpdb->options} SET option_value = %s WHERE option_name = %s AND option_value < %d", $acquire_until, $this->option_name, $time_now);
130 }
131 } while ($retries >= 0);
132
133 $this->log('Lock ('.$this->option_name.', '.$wpdb->options.') could not be acquired (it is locked)', 'info');
134
135 return false;
136 }
137
138 /**
139 * Release the lock
140 *
141 * N.B. We don't attempt to unlock it unless we locked it. i.e. Lost locks are left to expire rather than being forced. (If we want to force them, we'll need to introduce a new parameter).
142 *
143 * @return Boolean - if it returns false, then the lock was apparently not locked by us (and the caller will most likely therefore ignore the result, whatever it is).
144 */
145 public function release() {
146 if (!$this->acquired) return false;
147 global $wpdb;
148 $sql = $wpdb->prepare("UPDATE {$wpdb->options} SET option_value = '0' WHERE option_name = %s", $this->option_name);
149
150 $this->log('Lock option ('.$this->option_name.', '.$wpdb->options.') released', 'info');
151
152 $result = (int) $wpdb->query($sql) === 1; // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $sql is already prepared via $wpdb->prepare() above
153
154 $this->acquired = false;
155
156 return $result;
157 }
158
159 /**
160 * Cleans up the DB of any residual data. This should not be used as part of ordinary unlocking; only as part of deinstalling, or if you otherwise know that the lock will not be used again. If calling this, it's redundant to first unlock (and a no-op to attempt to do so afterwards).
161 */
162 public function delete() {
163 $this->acquired = false;
164
165 global $wpdb;
166 $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->options} WHERE option_name = %s", $this->option_name));
167
168 $this->log('Lock option ('.$this->option_name.', '.$wpdb->options.') was deleted from the database');
169 }
170
171 /**
172 * Captures and logs any given messages
173 *
174 * @param String $message - the error message
175 * @param String $level - the message level (debug, notice, info, warning, error)
176 */
177 public function log($message, $level = 'info') {
178 if (isset($this->loggers)) {
179 foreach ($this->loggers as $logger) {
180 $logger->log($message, $level);
181 }
182 }
183 }
184
185 /**
186 * Sets the list of loggers for this instance (removing any others).
187 *
188 * @param Array $loggers - the loggers for this task
189 */
190 public function set_loggers($loggers) {
191 $this->loggers = array();
192 foreach ($loggers as $logger) {
193 $this->add_logger($logger);
194 }
195 }
196
197 /**
198 * Add a logger to loggers list
199 *
200 * @param Callable $logger - a logger (a method with a callable function 'log', taking string parameters $level $message)
201 */
202 public function add_logger($logger) {
203 $this->loggers[] = $logger;
204 }
205
206 /**
207 * Return the current list of loggers
208 *
209 * @return Array
210 */
211 public function get_loggers() {
212 return $this->loggers;
213 }
214 }
215