11ec1a187a26a78fbd6af77af399275a9a51cd1e
[moodle.git] / lib / classes / lock / db_record_lock_factory.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * This is a db record locking factory.
19  *
20  * @package    core
21  * @category   lock
22  * @copyright  Damyon Wiese 2013
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
26 namespace core\lock;
28 defined('MOODLE_INTERNAL') || die();
30 /**
31  * This is a db record locking factory.
32  *
33  * This lock factory uses record locks relying on sql of the form "SET XXX where YYY" and checking if the
34  * value was set. It supports timeouts, autorelease and can work on any DB. The downside - is this
35  * will always be slower than some shared memory type locking function.
36  *
37  * @package   core
38  * @category  lock
39  * @copyright Damyon Wiese 2013
40  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
41  */
42 class db_record_lock_factory implements lock_factory {
44     /** @var \moodle_database $db Hold a reference to the global $DB */
45     protected $db;
47     /** @var string $type Used to prefix lock keys */
48     protected $type;
50     /** @var array $openlocks - List of held locks - used by auto-release */
51     protected $openlocks = array();
53     /**
54      * Is available.
55      * @return boolean - True if this lock type is available in this environment.
56      */
57     public function is_available() {
58         return true;
59     }
61     /**
62      * Almighty constructor.
63      * @param string $type - Used to prefix lock keys.
64      */
65     public function __construct($type) {
66         global $DB;
68         $this->type = $type;
69         // Save a reference to the global $DB so it will not be released while we still have open locks.
70         $this->db = $DB;
72         \core_shutdown_manager::register_function(array($this, 'auto_release'));
73     }
75     /**
76      * Return information about the blocking behaviour of the lock type on this platform.
77      * @return boolean - True
78      */
79     public function supports_timeout() {
80         return true;
81     }
83     /**
84      * Will this lock type will be automatically released when a process ends.
85      *
86      * @return boolean - True (shutdown handler)
87      */
88     public function supports_auto_release() {
89         return true;
90     }
92     /**
93      * Multiple locks for the same resource can be held by a single process.
94      *
95      * @deprecated since Moodle 4.0.
96      * @return boolean - False - not process specific.
97      */
98     public function supports_recursion() {
99         debugging('The function supports_recursion() is deprecated, please do not use it anymore.',
100             DEBUG_DEVELOPER);
101         return false;
102     }
104     /**
105      * This function generates a unique token for the lock to use.
106      * It is important that this token is not solely based on time as this could lead
107      * to duplicates in a clustered environment (especially on VMs due to poor time precision).
108      */
109     protected function generate_unique_token() {
110         return \core\uuid::generate();
111     }
113     /**
114      * Create and get a lock
115      * @param string $resource - The identifier for the lock. Should use frankenstyle prefix.
116      * @param int $timeout - The number of seconds to wait for a lock before giving up.
117      * @param int $maxlifetime - Unused by this lock type.
118      * @return boolean - true if a lock was obtained.
119      */
120     public function get_lock($resource, $timeout, $maxlifetime = 86400) {
122         $token = $this->generate_unique_token();
123         $now = time();
124         $giveuptime = $now + $timeout;
125         $expires = $now + $maxlifetime;
127         $resourcekey = $this->type . '_' . $resource;
129         if (!$this->db->record_exists('lock_db', array('resourcekey' => $resourcekey))) {
130             $record = new \stdClass();
131             $record->resourcekey = $resourcekey;
132             $result = $this->db->insert_record('lock_db', $record);
133         }
135         $params = array('expires' => $expires,
136                         'token' => $token,
137                         'resourcekey' => $resourcekey,
138                         'now' => $now);
139         $sql = 'UPDATE {lock_db}
140                    SET
141                        expires = :expires,
142                        owner = :token
143                  WHERE
144                        resourcekey = :resourcekey AND
145                        (owner IS NULL OR expires < :now)';
147         do {
148             $now = time();
149             $params['now'] = $now;
150             $this->db->execute($sql, $params);
152             $countparams = array('owner' => $token, 'resourcekey' => $resourcekey);
153             $result = $this->db->count_records('lock_db', $countparams);
154             $locked = $result === 1;
155             if (!$locked && $timeout > 0) {
156                 usleep(rand(10000, 250000)); // Sleep between 10 and 250 milliseconds.
157             }
158             // Try until the giveup time.
159         } while (!$locked && $now < $giveuptime);
161         if ($locked) {
162             $this->openlocks[$token] = 1;
163             return new lock($token, $this);
164         }
166         return false;
167     }
169     /**
170      * Release a lock that was previously obtained with @lock.
171      * @param lock $lock - a lock obtained from this factory.
172      * @return boolean - true if the lock is no longer held (including if it was never held).
173      */
174     public function release_lock(lock $lock) {
175         $params = array('noexpires' => null,
176                         'token' => $lock->get_key(),
177                         'noowner' => null);
179         $sql = 'UPDATE {lock_db}
180                     SET
181                         expires = :noexpires,
182                         owner = :noowner
183                     WHERE
184                         owner = :token';
185         $result = $this->db->execute($sql, $params);
186         if ($result) {
187             unset($this->openlocks[$lock->get_key()]);
188         }
189         return $result;
190     }
192     /**
193      * Extend a lock that was previously obtained with @lock.
194      * @param lock $lock - a lock obtained from this factory.
195      * @param int $maxlifetime - the new lifetime for the lock (in seconds).
196      * @return boolean - true if the lock was extended.
197      */
198     public function extend_lock(lock $lock, $maxlifetime = 86400) {
199         $now = time();
200         $expires = $now + $maxlifetime;
201         $params = array('expires' => $expires,
202                         'token' => $lock->get_key());
204         $sql = 'UPDATE {lock_db}
205                     SET
206                         expires = :expires,
207                     WHERE
208                         owner = :token';
210         $this->db->execute($sql, $params);
211         $countparams = array('owner' => $lock->get_key());
212         $result = $this->count_records('lock_db', $countparams);
214         return $result === 0;
215     }
217     /**
218      * Auto release any open locks on shutdown.
219      * This is required, because we may be using persistent DB connections.
220      */
221     public function auto_release() {
222         // Called from the shutdown handler. Must release all open locks.
223         foreach ($this->openlocks as $key => $unused) {
224             $lock = new lock($key, $this);
225             $lock->release();
226         }
227     }