Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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/>.
16
 
17
namespace core\lock;
18
 
19
use coding_exception;
20
 
21
/**
22
 * Postgres advisory locking factory.
23
 *
24
 * Postgres locking implementation using advisory locks. Some important points. Postgres has
25
 * 2 different forms of lock functions, some accepting a single int, and some accepting 2 ints. This implementation
26
 * uses the 2 int version so that it uses a separate namespace from the session locking. The second note,
27
 * is because postgres uses integer keys for locks, we first need to map strings to a unique integer. This is done
11 efrain 28
 * using a prefix of a sha1 hash converted to an integer. There is a realistic chance of collisions by using this
29
 * prefix when locking multiple resources at the same time (multiple resource identifiers leading to the
30
 * same token/prefix). We need to deal with that.
1 efrain 31
 *
32
 * @package   core
33
 * @category  lock
34
 * @copyright Damyon Wiese 2013
35
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
36
 */
37
class postgres_lock_factory implements lock_factory {
38
 
39
    /** @var int $dblockid - used as a namespace for these types of locks (separate from session locks) */
40
    protected $dblockid = -1;
41
 
42
    /** @var array $lockidcache - static cache for string -> int conversions required for pg advisory locks. */
43
    protected static $lockidcache = array();
44
 
45
    /** @var \moodle_database $db Hold a reference to the global $DB */
46
    protected $db;
47
 
48
    /** @var string $type Used to prefix lock keys */
49
    protected $type;
50
 
11 efrain 51
    /** @var int[] $resourcetokens Mapping of held locks (resource identifier => internal token) */
52
    protected $resourcetokens = [];
1 efrain 53
 
11 efrain 54
    /** @var int[] $locksbytoken Mapping of held locks (db connection => internal token => number of locks held) */
55
    static protected $locksbytoken = [];
56
 
1 efrain 57
    /**
58
     * Calculate a unique instance id based on the database name and prefix.
59
     * @return int.
60
     */
61
    protected function get_unique_db_instance_id() {
62
        global $CFG;
63
 
64
        $strkey = $CFG->dbname . ':' . $CFG->prefix;
65
        $intkey = crc32($strkey);
66
        // Normalize between 64 bit unsigned int and 32 bit signed ints. Php could return either from crc32.
67
        if (PHP_INT_SIZE == 8) {
68
            if ($intkey > 0x7FFFFFFF) {
69
                $intkey -= 0x100000000;
70
            }
71
        }
72
 
73
        return $intkey;
74
    }
75
 
76
    /**
77
     * Almighty constructor.
78
     * @param string $type - Used to prefix lock keys.
79
     */
80
    public function __construct($type) {
81
        global $DB;
82
 
83
        $this->type = $type;
84
        $this->dblockid = $this->get_unique_db_instance_id();
85
        // Save a reference to the global $DB so it will not be released while we still have open locks.
86
        $this->db = $DB;
87
 
88
        \core_shutdown_manager::register_function(array($this, 'auto_release'));
89
    }
90
 
91
    /**
92
     * Is available.
93
     * @return boolean - True if this lock type is available in this environment.
94
     */
95
    public function is_available() {
96
        return $this->db->get_dbfamily() === 'postgres';
97
    }
98
 
99
    /**
100
     * Return information about the blocking behaviour of the lock type on this platform.
101
     * @return boolean - Defer to the DB driver.
102
     */
103
    public function supports_timeout() {
104
        return true;
105
    }
106
 
107
    /**
108
     * Will this lock type will be automatically released when a process ends.
109
     *
110
     * @return boolean - Via shutdown handler.
111
     */
112
    public function supports_auto_release() {
113
        return true;
114
    }
115
 
116
    /**
117
     * @deprecated since Moodle 3.10.
118
     */
119
    public function supports_recursion() {
120
        throw new coding_exception('The function supports_recursion() has been removed, please do not use it anymore.');
121
    }
122
 
123
    /**
124
     * This function generates the unique index for a specific lock key using
125
     * a sha1 prefix converted to decimal.
126
     *
127
     * @param string $key
128
     * @return int
129
     * @throws \moodle_exception
130
     */
131
    protected function get_index_from_key($key) {
132
 
133
        // A prefix of 7 hex chars is chosen as fffffff is the largest hex code
134
        // which when converted to decimal (268435455) fits inside a 4 byte int
135
        // which is the second param to pg_try_advisory_lock().
136
        $hash = substr(sha1($key), 0, 7);
137
        $index = hexdec($hash);
138
        return $index;
139
    }
140
 
141
    /**
142
     * Create and get a lock
143
     *
144
     * @param string $resource - The identifier for the lock. Should use frankenstyle prefix.
145
     * @param int $timeout - The number of seconds to wait for a lock before giving up.
146
     * @param int $maxlifetime - Unused by this lock type.
11 efrain 147
     * @return \core\lock\lock|boolean - An instance of \core\lock\lock if the lock was obtained, or false.
1 efrain 148
     */
149
    public function get_lock($resource, $timeout, $maxlifetime = 86400) {
11 efrain 150
        $dbid = spl_object_id($this->db);
1 efrain 151
        $giveuptime = time() + $timeout;
11 efrain 152
        $resourcekey = $this->type . '_' . $resource;
153
        $token = $this->get_index_from_key($resourcekey);
1 efrain 154
 
11 efrain 155
        if (isset($this->resourcetokens[$resourcekey])) {
1 efrain 156
            return false;
157
        }
158
 
11 efrain 159
        if (isset(self::$locksbytoken[$dbid][$token])) {
160
            // There is a hash collision, another resource identifier leads to the same token.
161
            // As we already hold an advisory lock for this token, we just increase the counter.
162
            self::$locksbytoken[$dbid][$token]++;
163
            $this->resourcetokens[$resourcekey] = $token;
164
            return new lock($resourcekey, $this);
165
        }
166
 
1 efrain 167
        $params = [
168
            'locktype' => $this->dblockid,
169
            'token' => $token
170
        ];
171
 
172
        $locked = false;
173
 
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);
182
 
183
        if ($locked) {
11 efrain 184
            self::$locksbytoken[$dbid][$token] = 1;
185
            $this->resourcetokens[$resourcekey] = $token;
186
            return new lock($resourcekey, $this);
1 efrain 187
        }
188
        return false;
189
    }
190
 
191
    /**
192
     * Release a lock that was previously obtained with @lock.
193
     * @param lock $lock - a lock obtained from this factory.
194
     * @return boolean - true if the lock is no longer held (including if it was never held).
195
     */
196
    public function release_lock(lock $lock) {
11 efrain 197
        $dbid = spl_object_id($this->db);
198
        $resourcekey = $lock->get_key();
199
 
200
        if (isset($this->resourcetokens[$resourcekey])) {
201
            $token = $this->resourcetokens[$resourcekey];
202
        } else {
203
            return true;
204
        }
205
 
206
        if (self::$locksbytoken[$dbid][$token] > 1) {
207
            // There are multiple resource identifiers that lead to the same token.
208
            // We will unlock the token later when the last resource is released.
209
            unset($this->resourcetokens[$resourcekey]);
210
            self::$locksbytoken[$dbid][$token]--;
211
            return true;
212
        }
213
 
214
        $params = [
215
            'locktype' => $this->dblockid,
216
            'token' => $token,
217
        ];
1 efrain 218
        $result = $this->db->get_record_sql('SELECT pg_advisory_unlock(:locktype, :token) AS unlocked', $params);
219
        $result = $result->unlocked === 't';
220
        if ($result) {
11 efrain 221
            unset($this->resourcetokens[$resourcekey]);
222
            unset(self::$locksbytoken[$dbid][$token]);
1 efrain 223
        }
224
        return $result;
225
    }
226
 
227
    /**
228
     * @deprecated since Moodle 3.10.
229
     */
230
    public function extend_lock() {
231
        throw new coding_exception('The function extend_lock() has been removed, please do not use it anymore.');
232
    }
233
 
234
    /**
235
     * Auto release any open locks on shutdown.
236
     * This is required, because we may be using persistent DB connections.
237
     */
238
    public function auto_release() {
239
        // Called from the shutdown handler. Must release all open locks.
11 efrain 240
        foreach ($this->resourcetokens as $resourcekey => $unused) {
241
            $lock = new lock($resourcekey, $this);
1 efrain 242
            $lock->release();
243
        }
244
    }
245
 
246
}