1d18967d59c5fbf17c884fa0cccc5e8e5985de41
[moodle.git] / lib / classes / lock / postgres_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  * Postgres advisory 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  * Postgres advisory locking factory.
32  *
33  * Postgres locking implementation using advisory locks. Some important points. Postgres has
34  * 2 different forms of lock functions, some accepting a single int, and some accepting 2 ints. This implementation
35  * uses the 2 int version so that it uses a separate namespace from the session locking. The second note,
36  * is because postgres uses integer keys for locks, we first need to map strings to a unique integer. This is done
37  * using a prefix of a sha1 hash converted to an integer.
38  *
39  * @package   core
40  * @category  lock
41  * @copyright Damyon Wiese 2013
42  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
43  */
44 class postgres_lock_factory implements lock_factory {
46     /** @var int $dblockid - used as a namespace for these types of locks (separate from session locks) */
47     protected $dblockid = -1;
49     /** @var array $lockidcache - static cache for string -> int conversions required for pg advisory locks. */
50     protected static $lockidcache = array();
52     /** @var \moodle_database $db Hold a reference to the global $DB */
53     protected $db;
55     /** @var string $type Used to prefix lock keys */
56     protected $type;
58     /** @var array $openlocks - List of held locks - used by auto-release */
59     protected $openlocks = array();
61     /**
62      * Calculate a unique instance id based on the database name and prefix.
63      * @return int.
64      */
65     protected function get_unique_db_instance_id() {
66         global $CFG;
68         $strkey = $CFG->dbname . ':' . $CFG->prefix;
69         $intkey = crc32($strkey);
70         // Normalize between 64 bit unsigned int and 32 bit signed ints. Php could return either from crc32.
71         if (PHP_INT_SIZE == 8) {
72             if ($intkey > 0x7FFFFFFF) {
73                 $intkey -= 0x100000000;
74             }
75         }
77         return $intkey;
78     }
80     /**
81      * Almighty constructor.
82      * @param string $type - Used to prefix lock keys.
83      */
84     public function __construct($type) {
85         global $DB;
87         $this->type = $type;
88         $this->dblockid = $this->get_unique_db_instance_id();
89         // Save a reference to the global $DB so it will not be released while we still have open locks.
90         $this->db = $DB;
92         \core_shutdown_manager::register_function(array($this, 'auto_release'));
93     }
95     /**
96      * Is available.
97      * @return boolean - True if this lock type is available in this environment.
98      */
99     public function is_available() {
100         return $this->db->get_dbfamily() === 'postgres';
101     }
103     /**
104      * Return information about the blocking behaviour of the lock type on this platform.
105      * @return boolean - Defer to the DB driver.
106      */
107     public function supports_timeout() {
108         return true;
109     }
111     /**
112      * Will this lock type will be automatically released when a process ends.
113      *
114      * @return boolean - Via shutdown handler.
115      */
116     public function supports_auto_release() {
117         return true;
118     }
120     /**
121      * Multiple locks for the same resource can NOT be held by a single process.
122      *
123      * @deprecated since Moodle 4.0.
124      * @return boolean - false.
125      */
126     public function supports_recursion() {
127         debugging('The function supports_recursion() is deprecated, please do not use it anymore.',
128             DEBUG_DEVELOPER);
129         return false;
130     }
132     /**
133      * This function generates the unique index for a specific lock key using
134      * a sha1 prefix converted to decimal.
135      *
136      * @param string $key
137      * @return int
138      * @throws \moodle_exception
139      */
140     protected function get_index_from_key($key) {
142         // A prefix of 7 hex chars is chosen as fffffff is the largest hex code
143         // which when converted to decimal (268435455) fits inside a 4 byte int
144         // which is the second param to pg_try_advisory_lock().
145         $hash = substr(sha1($key), 0, 7);
146         $index = hexdec($hash);
147         return $index;
148     }
150     /**
151      * Create and get a lock
152      *
153      * @param string $resource - The identifier for the lock. Should use frankenstyle prefix.
154      * @param int $timeout - The number of seconds to wait for a lock before giving up.
155      * @param int $maxlifetime - Unused by this lock type.
156      * @return boolean - true if a lock was obtained.
157      */
158     public function get_lock($resource, $timeout, $maxlifetime = 86400) {
159         $giveuptime = time() + $timeout;
161         $token = $this->get_index_from_key($this->type . '_' . $resource);
163         if (isset($this->openlocks[$token])) {
164             return false;
165         }
167         $params = [
168             'locktype' => $this->dblockid,
169             'token' => $token
170         ];
172         $locked = false;
174         do {
175             $result = $this->db->get_record_sql('SELECT pg_try_advisory_lock(:locktype, :token) AS locked', $params);
176             $locked = $result->locked === 't';
177             if (!$locked && $timeout > 0) {
178                 usleep(rand(10000, 250000)); // Sleep between 10 and 250 milliseconds.
179             }
180             // Try until the giveup time.
181         } while (!$locked && time() < $giveuptime);
183         if ($locked) {
184             $this->openlocks[$token] = 1;
185             return new lock($token, $this);
186         }
187         return false;
188     }
190     /**
191      * Release a lock that was previously obtained with @lock.
192      * @param lock $lock - a lock obtained from this factory.
193      * @return boolean - true if the lock is no longer held (including if it was never held).
194      */
195     public function release_lock(lock $lock) {
196         $params = array('locktype' => $this->dblockid,
197                         'token' => $lock->get_key());
198         $result = $this->db->get_record_sql('SELECT pg_advisory_unlock(:locktype, :token) AS unlocked', $params);
199         $result = $result->unlocked === 't';
200         if ($result) {
201             unset($this->openlocks[$lock->get_key()]);
202         }
203         return $result;
204     }
206     /**
207      * Extend a lock that was previously obtained with @lock.
208      *
209      * @deprecated since Moodle 4.0.
210      * @param lock $lock - a lock obtained from this factory.
211      * @param int $maxlifetime - the new lifetime for the lock (in seconds).
212      * @return boolean - true if the lock was extended.
213      */
214     public function extend_lock(lock $lock, $maxlifetime = 86400) {
215         debugging('The function extend_lock() is deprecated, please do not use it anymore.',
216             DEBUG_DEVELOPER);
217         // Not supported by this factory.
218         return false;
219     }
221     /**
222      * Auto release any open locks on shutdown.
223      * This is required, because we may be using persistent DB connections.
224      */
225     public function auto_release() {
226         // Called from the shutdown handler. Must release all open locks.
227         foreach ($this->openlocks as $key => $unused) {
228             $lock = new lock($key, $this);
229             $lock->release();
230         }
231     }