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/**
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 */
25
26namespace core\lock;
27
28defined('MOODLE_INTERNAL') || die();
29
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 */
42class db_record_lock_factory implements lock_factory {
43
44    /** @var \moodle_database $db Hold a reference to the global $DB */
45    protected $db;
46
47    /** @var string $type Used to prefix lock keys */
48    protected $type;
49
50    /** @var array $openlocks - List of held locks - used by auto-release */
51    protected $openlocks = array();
52
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    }
60
61    /**
62     * Almighty constructor.
63     * @param string $type - Used to prefix lock keys.
64     */
65    public function __construct($type) {
66        global $DB;
67
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;
71
72        \core_shutdown_manager::register_function(array($this, 'auto_release'));
73    }
74
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    }
82
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    }
91
92    /**
93     * Multiple locks for the same resource can be held by a single process.
94     * @return boolean - False - not process specific.
95     */
96    public function supports_recursion() {
97        return false;
98    }
99
100    /**
101     * This function generates a unique token for the lock to use.
102     * It is important that this token is not solely based on time as this could lead
103     * to duplicates in a clustered environment (especially on VMs due to poor time precision).
104     */
105    protected function generate_unique_token() {
106        return \core\uuid::generate();
107    }
108
109    /**
110     * Create and get a lock
111     * @param string $resource - The identifier for the lock. Should use frankenstyle prefix.
112     * @param int $timeout - The number of seconds to wait for a lock before giving up.
113     * @param int $maxlifetime - Unused by this lock type.
114     * @return boolean - true if a lock was obtained.
115     */
116    public function get_lock($resource, $timeout, $maxlifetime = 86400) {
117
118        $token = $this->generate_unique_token();
119        $now = time();
120        $giveuptime = $now + $timeout;
121        $expires = $now + $maxlifetime;
122
123        $resourcekey = $this->type . '_' . $resource;
124
125        if (!$this->db->record_exists('lock_db', array('resourcekey' => $resourcekey))) {
126            $record = new \stdClass();
127            $record->resourcekey = $resourcekey;
128            $result = $this->db->insert_record('lock_db', $record);
129        }
130
131        $params = array('expires' => $expires,
132                        'token' => $token,
133                        'resourcekey' => $resourcekey,
134                        'now' => $now);
135        $sql = 'UPDATE {lock_db}
136                   SET
137                       expires = :expires,
138                       owner = :token
139                 WHERE
140                       resourcekey = :resourcekey AND
141                       (owner IS NULL OR expires < :now)';
142
143        do {
144            $now = time();
145            $params['now'] = $now;
146            $this->db->execute($sql, $params);
147
148            $countparams = array('owner' => $token, 'resourcekey' => $resourcekey);
149            $result = $this->db->count_records('lock_db', $countparams);
150            $locked = $result === 1;
151            if (!$locked && $timeout > 0) {
152                usleep(rand(10000, 250000)); // Sleep between 10 and 250 milliseconds.
153            }
154            // Try until the giveup time.
155        } while (!$locked && $now < $giveuptime);
156
157        if ($locked) {
158            $this->openlocks[$token] = 1;
159            return new lock($token, $this);
160        }
161
162        return false;
163    }
164
165    /**
166     * Release a lock that was previously obtained with @lock.
167     * @param lock $lock - a lock obtained from this factory.
168     * @return boolean - true if the lock is no longer held (including if it was never held).
169     */
170    public function release_lock(lock $lock) {
171        $params = array('noexpires' => null,
172                        'token' => $lock->get_key(),
173                        'noowner' => null);
174
175        $sql = 'UPDATE {lock_db}
176                    SET
177                        expires = :noexpires,
178                        owner = :noowner
179                    WHERE
180                        owner = :token';
181        $result = $this->db->execute($sql, $params);
182        if ($result) {
183            unset($this->openlocks[$lock->get_key()]);
184        }
185        return $result;
186    }
187
188    /**
189     * Extend a lock that was previously obtained with @lock.
190     * @param lock $lock - a lock obtained from this factory.
191     * @param int $maxlifetime - the new lifetime for the lock (in seconds).
192     * @return boolean - true if the lock was extended.
193     */
194    public function extend_lock(lock $lock, $maxlifetime = 86400) {
195        $now = time();
196        $expires = $now + $maxlifetime;
197        $params = array('expires' => $expires,
198                        'token' => $lock->get_key());
199
200        $sql = 'UPDATE {lock_db}
201                    SET
202                        expires = :expires,
203                    WHERE
204                        owner = :token';
205
206        $this->db->execute($sql, $params);
207        $countparams = array('owner' => $lock->get_key());
208        $result = $this->count_records('lock_db', $countparams);
209
210        return $result === 0;
211    }
212
213    /**
214     * Auto release any open locks on shutdown.
215     * This is required, because we may be using persistent DB connections.
216     */
217    public function auto_release() {
218        // Called from the shutdown handler. Must release all open locks.
219        foreach ($this->openlocks as $key => $unused) {
220            $lock = new lock($key, $this);
221            $lock->release();
222        }
223    }
224}
225