Proyectos de Subversion Moodle

Rev

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