1d2ccf6159e784ef212e96ec4733fb341ac66761
[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 be held by a single process.
122      *
123      * @deprecated since Moodle 4.0.
124      * @return boolean - Defer to the DB driver.
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 true;
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      * @param string $resource - The identifier for the lock. Should use frankenstyle prefix.
153      * @param int $timeout - The number of seconds to wait for a lock before giving up.
154      * @param int $maxlifetime - Unused by this lock type.
155      * @return boolean - true if a lock was obtained.
156      */
157     public function get_lock($resource, $timeout, $maxlifetime = 86400) {
158         $giveuptime = time() + $timeout;
160         $token = $this->get_index_from_key($this->type . '_' . $resource);
162         $params = array('locktype' => $this->dblockid,
163                         'token' => $token);
165         $locked = false;
167         do {
168             $result = $this->db->get_record_sql('SELECT pg_try_advisory_lock(:locktype, :token) AS locked', $params);
169             $locked = $result->locked === 't';
170             if (!$locked && $timeout > 0) {
171                 usleep(rand(10000, 250000)); // Sleep between 10 and 250 milliseconds.
172             }
173             // Try until the giveup time.
174         } while (!$locked && time() < $giveuptime);
176         if ($locked) {
177             $this->openlocks[$token] = 1;
178             return new lock($token, $this);
179         }
180         return false;
181     }
183     /**
184      * Release a lock that was previously obtained with @lock.
185      * @param lock $lock - a lock obtained from this factory.
186      * @return boolean - true if the lock is no longer held (including if it was never held).
187      */
188     public function release_lock(lock $lock) {
189         $params = array('locktype' => $this->dblockid,
190                         'token' => $lock->get_key());
191         $result = $this->db->get_record_sql('SELECT pg_advisory_unlock(:locktype, :token) AS unlocked', $params);
192         $result = $result->unlocked === 't';
193         if ($result) {
194             unset($this->openlocks[$lock->get_key()]);
195         }
196         return $result;
197     }
199     /**
200      * Extend a lock that was previously obtained with @lock.
201      *
202      * @deprecated since Moodle 4.0.
203      * @param lock $lock - a lock obtained from this factory.
204      * @param int $maxlifetime - the new lifetime for the lock (in seconds).
205      * @return boolean - true if the lock was extended.
206      */
207     public function extend_lock(lock $lock, $maxlifetime = 86400) {
208         debugging('The function extend_lock() is deprecated, please do not use it anymore.',
209             DEBUG_DEVELOPER);
210         // Not supported by this factory.
211         return false;
212     }
214     /**
215      * Auto release any open locks on shutdown.
216      * This is required, because we may be using persistent DB connections.
217      */
218     public function auto_release() {
219         // Called from the shutdown handler. Must release all open locks.
220         foreach ($this->openlocks as $key => $unused) {
221             $lock = new lock($key, $this);
222             $lock->release();
223         }
224     }