PluginProbe ʕ •ᴥ•ʔ
Pods – Custom Content Types and Fields / 2.9.19.4
Pods – Custom Content Types and Fields v2.9.19.4
2.7.31.4 2.8.23.5 2.9.19.5 3.0.10.5 3.1.4.3 3.2.8.4 3.3.9.2 2.8.23.4 2.9.19.4 3.0.10.4 3.1.4.2 3.2.8.3 3.3.9.1 trunk 1.14.8 2.7.31.3 2.8.23.3 2.9.19.3 3.0.10.3 3.1.4.1 3.2.0 3.2.1 3.2.1.1 3.2.2 3.2.4 3.2.5 3.2.6 3.2.7 3.2.7.1 3.2.8 3.2.8.1 3.2.8.2 3.3.0 3.3.1 3.3.2 3.3.3 3.3.4 3.3.5 3.3.6 3.3.7 3.3.8 3.3.9
pods / tribe-common / src / Tribe / DB_Lock.php
pods / tribe-common / src / Tribe Last commit date
Admin 2 weeks ago Ajax 2 weeks ago Asset 2 weeks ago Context 2 weeks ago Customizer 2 weeks ago Debug_Bar 2 weeks ago Dialog 2 weeks ago Documentation 2 weeks ago Duplicate 2 weeks ago Editor 2 weeks ago Image 2 weeks ago JSON_LD 2 weeks ago Languages 2 weeks ago Log 2 weeks ago Meta 2 weeks ago Models 2 weeks ago PUE 2 weeks ago Process 2 weeks ago Promoter 2 weeks ago REST 2 weeks ago Repository 2 weeks ago Service_Providers 2 weeks ago Shortcode 2 weeks ago Support 2 weeks ago Tabbed_View 2 weeks ago Tooltip 2 weeks ago Traits 2 weeks ago Utils 2 weeks ago Validator 2 weeks ago Widget 2 weeks ago Abstract_Deactivation.php 2 weeks ago Abstract_Plugin_Register.php 2 weeks ago App_Shop.php 2 weeks ago Assets.php 2 weeks ago Assets_Pipeline.php 2 weeks ago Autoloader.php 2 weeks ago Cache.php 2 weeks ago Cache_Listener.php 2 weeks ago Changelog_Reader.php 2 weeks ago Container.php 2 weeks ago Context.php 2 weeks ago Cost_Utils.php 2 weeks ago Credits.php 2 weeks ago Customizer.php 2 weeks ago DB_Lock.php 2 weeks ago Data.php 2 weeks ago Date_Utils.php 2 weeks ago Db.php 2 weeks ago Debug.php 2 weeks ago Dependency.php 2 weeks ago Deprecation.php 2 weeks ago Editor.php 2 weeks ago Error.php 2 weeks ago Exception.php 2 weeks ago Extension.php 2 weeks ago Extension_Loader.php 2 weeks ago Feature_Detection.php 2 weeks ago Field.php 2 weeks ago Field_Conditional.php 2 weeks ago Freemius.php 2 weeks ago Log.php 2 weeks ago Main.php 2 weeks ago Notices.php 2 weeks ago Plugin_Meta_Links.php 2 weeks ago Plugins.php 2 weeks ago Plugins_API.php 2 weeks ago Post_History.php 2 weeks ago Post_Transient.php 2 weeks ago Promise.php 2 weeks ago Repository.php 2 weeks ago Rewrite.php 2 weeks ago Settings.php 2 weeks ago Settings_Manager.php 2 weeks ago Settings_Tab.php 2 weeks ago Simple_Table.php 2 weeks ago Support.php 2 weeks ago Tabbed_View.php 2 weeks ago Template.php 2 weeks ago Template_Factory.php 2 weeks ago Template_Part_Cache.php 2 weeks ago Templates.php 2 weeks ago Terms.php 2 weeks ago Timezones.php 2 weeks ago Tracker.php 2 weeks ago Updater.php 2 weeks ago Validate.php 2 weeks ago View_Helpers.php 2 weeks ago
DB_Lock.php
353 lines
1 <?php
2 /**
3 * Manages database locks using MySQL fucntions or queries.
4 *
5 * The MySQL functions used by this class are `GET_LOCK`, `IS_FREE_LOCK` and `RELEASE_LOCK`.
6 * The functions are part of MySQL 5.6 and in line with WordPress minimum requirement of MySQL version (5.6).
7 *
8 * @see https://dev.mysql.com/doc/refman/5.6/en/locking-functions.html#function_get-lock
9 *
10 * @since 4.12.6
11 *
12 * @package Tribe
13 */
14
15 namespace Tribe;
16
17 /**
18 * Class DB_Lock
19 *
20 * @since 4.12.6
21 *
22 * @package Tribe
23 */
24 class DB_Lock {
25
26 /**
27 * The prefix of the options used to manage the database lock without use of MySQL functions
28 * in the options table.
29 *
30 * @since 4.12.6
31 *
32 * @var string
33 */
34 public static $db_lock_option_prefix = 'tribe_db_lock_';
35
36 /**
37 * A map, shared among all instance of this trait in the session, of the currently held locks the
38 * time the locks where acquired, a UNIX timestamp w/ micro-seconds.
39 *
40 * @since 4.12.6
41 *
42 * @var array<string,float>
43 */
44 protected static $held_db_locks = [];
45
46 /**
47 * Prunes the stale locks stored in the options table.
48 *
49 * @since 4.12.6
50 *
51 * @return int|false The number of pruned locks, or `false` to indicate the query to prune the locks generated
52 * an error (logged).
53 */
54 public static function prune_stale_db_locks() {
55 global $wpdb;
56 $prefix = static::$db_lock_option_prefix;
57 $affected_rows = $wpdb->query(
58 "DELETE FROM {$wpdb->options}
59 WHERE option_name LIKE '{$prefix}%'
60 AND option_value < ( UNIX_TIMESTAMP() - 86400 )"
61 );
62
63 if ( false === $affected_rows ) {
64 $log_data = [
65 'message' => 'Error while trying to prune stale db locks.',
66 'error' => $wpdb->last_error
67 ];
68 do_action( 'tribe_log', 'error', __CLASS__, $log_data );
69
70 return false;
71 }
72
73 return (int) $affected_rows;
74 }
75
76 /**
77 * Acquires a db lock.
78 *
79 * To ensure back-compatibility with MySQL 5.6, the lock will hash the lock key using SHA1.
80 *
81 * @since 4.12.6
82 *
83 * @param string $lock_key The name of the db lock key to acquire.
84 *
85 * @return bool Whether the lock acquisition was successful or not.
86 */
87 public function acquire_db_lock( $lock_key ) {
88 /**
89 * Filters the timeout, in seconds, of the database lock acquisition attempts.
90 *
91 * The timeout will not be used when locks are managed using queries in place of
92 * MySQL functions.
93 *
94 * @since 4.12.6
95 *
96 * @param int $timeout The timeout, in seconds, of the lock acquisition attempt.
97 * @param string $lock_key The lock key the target of the acquisition attempt.
98 * @param static $this The object that's trying to acquire the lock by means of the trait.
99 */
100 $timeout = apply_filters( 'tribe_db_lock_timeout', 3, $lock_key, $this );
101
102 if ( $this->manage_db_lock_w_mysql_functions() ) {
103 return $this->acquire_db_lock_w_mysql_functions( $lock_key, $timeout );
104 }
105
106 return $this->acquire_db_lock_w_queries( $lock_key );
107 }
108
109 /**
110 * Returns whether the traits should try to acquire and release locks using MySQL `GET_LOCK` and `RELEASE_LOCK`
111 * functions or not.
112 *
113 * If not, then the trait will manage the locks by means of direct SQL queries on the options table.
114 *
115 * @since 4.12.6
116 *
117 * @return bool Whether the trait should use MySQL functions to manage the locks, or not.
118 */
119 protected function manage_db_lock_w_mysql_functions() {
120 /**
121 * Filters whether the database lock should be acquired using the `GET_LOCK` and `RELEASE_LOCK`
122 * MySQL functions or not.
123 *
124 * If the filter returns a falsy value, then the trait will attempt to manage locks using `SELECT`
125 * and `UPDATE` queries on the options table.
126 *
127 * @since 4.12.6
128 */
129 return tribe_is_truthy( apply_filters( 'tribe_db_lock_use_msyql_functions', true ) );
130 }
131
132 /**
133 * Tries to acquire the database lock using MySQL functions (`GET_LOCK` and `IS_FREE_LOCK`).
134 *
135 * @since 4.12.6
136 *
137 * @param string $lock_key The lock key to try and acquire the lock for.
138 * @param int $timeout The timeout, in seconds, to try and acquire the lock.
139 *
140 * @return bool Whether the lock was acquired or not.
141 */
142 protected function acquire_db_lock_w_mysql_functions( $lock_key, $timeout ) {
143 /*
144 * On MySQL 5.6 if a session (a db connection) fires two requests of `GET_LOCK`, the lock is
145 * implicitly released and re-acquired.
146 * While this will not cause issues in the context of different db sessions (e.g. two diff. PHP
147 * processes competing for a lock), it would cause issues when the lock acquisition is attempted
148 * in the context of the same PHP process.
149 * To avoid a read-what-you-write issue in the context of the same request, we check if the lock is
150 * free, using `IS_FREE_LOCK` first.
151 */
152
153 global $wpdb;
154
155 $free = $wpdb->get_var(
156 $wpdb->prepare( 'SELECT IS_FREE_LOCK( SHA1( %s ) )', $lock_key )
157 );
158
159 if ( ! $free ) {
160 return false;
161 }
162
163 $acquired = $wpdb->get_var(
164 $wpdb->prepare( 'SELECT GET_LOCK( SHA1( %s ),%d )', $lock_key, $timeout )
165
166 );
167
168 if ( false === $acquired ) {
169 // Only log errors, a failure to acquire lock is not an error.
170 $log_data = [
171 'message' => 'Error while trying to acquire lock.',
172 'key' => $lock_key,
173 'error' => $wpdb->last_error
174 ];
175 do_action( 'tribe_log', 'error', __CLASS__, $log_data );
176
177 return false;
178 }
179
180 return true;
181 }
182
183 /**
184 * Tries to acquire the lock using SQL queries.
185 *
186 * This kind of lock does not support timeout to avoid sieging the MySQL server during processes
187 * that are most likely already stressing it. Either the lock is available the moment it's required or not.
188 * The method leverages `INSERT IGNORE` that it's available on MySQL 5.6 and is atomic provided one of the values
189 * we're trying to insert is UNIQUE or PRIMARY: `option_name` is UNIQUE in the `options` table.
190 *
191 * @since 4.12.6
192 *
193 * @param string $lock_key The lock key to try and acquire the lock for.
194 *
195 * @return bool Whether the lock was acquired or not.
196 */
197 protected function acquire_db_lock_w_queries( $lock_key ) {
198 global $wpdb;
199 $option_name = $this->get_db_lock_option_name( $lock_key );
200 $lock_time = microtime( true );
201
202 //phpcs:disable
203 $rows_affected = $wpdb->query(
204 $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->options}
205 (option_name, option_value, autoload)
206 VALUES
207 (%s, %s, 'no')",
208 $option_name,
209 $lock_time
210 )
211 );
212 //phpcs:enable
213
214 if ( false === $rows_affected ) {
215 $log_data = [
216 'message' => 'Error while trying to acquire lock with database.',
217 'key' => $lock_key,
218 'option_name' => $option_name,
219 'error' => $wpdb->last_error,
220 ];
221 do_action( 'tribe_log', 'error', __CLASS__, $log_data );
222
223 return false;
224 }
225
226 /*
227 * The `wpdb::query()` method will return the number of affected rows when using `INSERT`.
228 * 1 row affected means we could INSERT and have the lock, 0 rows affected means we could not INSERT
229 * and have not the lock.
230 */
231
232 if ( $rows_affected ) {
233 self::$held_db_locks[ $lock_key ] = $lock_time;
234 }
235
236 return (bool) $rows_affected;
237 }
238
239 /**
240 * Returns the option name used to manage the lock for a key in the options table.
241 *
242 * @since 4.12.6
243 *
244 * @param string $lock_key The lock key to build the option name for.
245 *
246 * @return string The name of the option that will be used to manage the lock for the specified key in the
247 * options table.
248 */
249 public function get_db_lock_option_name( $lock_key ) {
250 return self::$db_lock_option_prefix . $lock_key;
251 }
252
253 /**
254 * Releases the database lock of the record.
255 *
256 * Release a not held db lock will return `null`, not `false`.
257 *
258 * @since 4.12.6
259 *
260 * @param string $lock_key The name of the lock to release.
261 *
262 * @return bool Whether the lock was correctly released or not.
263 */
264 public function release_db_lock( $lock_key ) {
265 if ( $this->manage_db_lock_w_mysql_functions() ) {
266 return $this->release_db_lock_w_mysql_functions( $lock_key );
267 }
268
269 return $this->release_db_lock_w_queries( $lock_key );
270 }
271
272 /**
273 * Releases a DB lock held by the current database session (`$wpdb` instance) by
274 * using the MySQL `RELEASE_LOCK` function.
275 *
276 * @since 4.12.6
277 *
278 * @param string $lock_key The lock key to release the lock for.
279 *
280 * @return bool Whether the lock was correctly released or not.
281 */
282 protected function release_db_lock_w_mysql_functions( $lock_key ) {
283 global $wpdb;
284
285 $released = $wpdb->query(
286 $wpdb->prepare( "SELECT RELEASE_LOCK( SHA1( %s ) )", $lock_key )
287 );
288
289 if ( false === $released ) {
290 $log_data = [
291 'message' => 'Error while trying to release lock.',
292 'key' => $lock_key,
293 'error' => $wpdb->last_error
294 ];
295 do_action( 'tribe_log', 'error', __CLASS__, $log_data );
296
297 return false;
298 }
299
300 return true;
301 }
302
303 /**
304 * Releases a lock using SQL queries.
305 *
306 * Note: differently from the `release_db_lock_w_mysql_functions`, this method will release the lock
307 * even if the current session is not the one holding the lock.
308 * To protect from this the trait uses a map of registered locks and when the locks where registered.
309 *
310 * @since 4.12.6
311 *
312 * @param string $lock_key The lock key to release the lock for.
313 *
314 * @return bool Whether the lock was released or not, errors will be logged, a `false` value is returned if
315 * the lock was not held to begin with.
316 */
317 protected function release_db_lock_w_queries( $lock_key ) {
318 if ( ! isset( self::$held_db_locks[ $lock_key ] ) ) {
319 // Avoid sessions that do nothold the lock to release it.
320 return false;
321 }
322
323 global $wpdb;
324 $option_name = $this->get_db_lock_option_name( $lock_key );
325 //phpcs:disable
326 $rows_affected = $wpdb->delete(
327 $wpdb->options,
328 [ 'option_name' => $option_name ],
329 [ '%s' ]
330 );
331 //phpcs:enable
332
333 if ( false === $rows_affected ) {
334 $log_data = [
335 'message' => 'Error while trying to release lock with database.',
336 'key' => $lock_key,
337 'option_name' => $option_name,
338 'error' => $wpdb->last_error,
339 ];
340 do_action( 'tribe_log', 'error', __CLASS__, $log_data );
341
342 return false;
343 }
344
345 if ( $rows_affected ) {
346 // Lock successfully released.
347 unset( self::$held_db_locks[ $lock_key ] );
348 }
349
350 return (bool) $rows_affected;
351 }
352 }
353