MDL-46552 session: add support for multiple servers in memcached session driver
[moodle.git] / lib / classes / session / memcached.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  * Memcached based session handler.
19  *
20  * @package    core
21  * @copyright  2013 Petr Skoda {@link http://skodak.org}
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 namespace core\session;
27 defined('MOODLE_INTERNAL') || die();
29 /**
30  * Memcached based session handler.
31  *
32  * @package    core
33  * @copyright  2013 Petr Skoda {@link http://skodak.org}
34  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
35  */
36 class memcached extends handler {
37     /** @var string $savepath save_path string  */
38     protected $savepath;
39     /** @var array $servers list of servers parsed from save_path */
40     protected $servers;
41     /** @var string $prefix session key prefix  */
42     protected $prefix;
43     /** @var int $acquiretimeout how long to wait for session lock */
44     protected $acquiretimeout = 120;
45     /**
46      * @var int $lockexpire how long to wait before expiring the lock so that other requests
47      * may continue execution, ignored if memcached <= 2.1.0.
48      */
49     protected $lockexpire = 7200;
51     /**
52      * Create new instance of handler.
53      */
54     public function __construct() {
55         global $CFG;
57         if (empty($CFG->session_memcached_save_path)) {
58             $this->savepath = '';
59         } else {
60             $this->savepath =  $CFG->session_memcached_save_path;
61         }
63         if (empty($this->savepath)) {
64             $this->servers = array();
65         } else {
66             $this->servers = util::connection_string_to_memcache_servers($this->savepath);
67         }
69         if (empty($CFG->session_memcached_prefix)) {
70             $this->prefix = ini_get('memcached.sess_prefix');
71         } else {
72             $this->prefix = $CFG->session_memcached_prefix;
73         }
75         if (!empty($CFG->session_memcached_acquire_lock_timeout)) {
76             $this->acquiretimeout = (int)$CFG->session_memcached_acquire_lock_timeout;
77         }
79         if (!empty($CFG->session_memcached_lock_expire)) {
80             $this->lockexpire = (int)$CFG->session_memcached_lock_expire;
81         }
82     }
84     /**
85      * Start the session.
86      * @return bool success
87      */
88     public function start() {
89         // NOTE: memcached <= 2.1.0 expires session locks automatically after max_execution_time,
90         //       this leads to major difference compared to other session drivers that timeout
91         //       and stop the second request execution instead.
93         $default = ini_get('max_execution_time');
94         set_time_limit($this->acquiretimeout);
96         $result = parent::start();
98         set_time_limit($default);
99         return $result;
100     }
102     /**
103      * Init session handler.
104      */
105     public function init() {
106         if (!extension_loaded('memcached')) {
107             throw new exception('sessionhandlerproblem', 'error', '', null, 'memcached extension is not loaded');
108         }
109         $version = phpversion('memcached');
110         if (!$version or version_compare($version, '2.0') < 0) {
111             throw new exception('sessionhandlerproblem', 'error', '', null, 'memcached extension version must be at least 2.0');
112         }
113         if (empty($this->savepath)) {
114             throw new exception('sessionhandlerproblem', 'error', '', null, '$CFG->session_memcached_save_path must be specified in config.php');
115         }
117         ini_set('session.save_handler', 'memcached');
118         ini_set('session.save_path', $this->savepath);
119         ini_set('memcached.sess_prefix', $this->prefix);
120         ini_set('memcached.sess_locking', '1'); // Locking is required!
122         // Try to configure lock and expire timeouts - ignored if memcached <=2.1.0.
123         ini_set('memcached.sess_lock_max_wait', $this->acquiretimeout);
124         ini_set('memcached.sess_lock_expire', $this->lockexpire);
125     }
127     /**
128      * Check for existing session with id $sid.
129      *
130      * Note: this verifies the storage backend only, not the actual session records.
131      *
132      * @param string $sid
133      * @return bool true if session found.
134      */
135     public function session_exists($sid) {
136         if (!$this->servers) {
137             return false;
138         }
140         // Go through the list of all servers because
141         // we do not know where the session handler put the
142         // data.
144         foreach ($this->servers as $server) {
145             list($host, $port) = $server;
146             $memcached = new \Memcached();
147             $memcached->addServer($host, $port);
148             $value = $memcached->get($this->prefix . $sid);
149             $memcached->quit();
150             if ($value !== false) {
151                 return true;
152             }
153         }
155         return false;
156     }
158     /**
159      * Kill all active sessions, the core sessions table is
160      * purged afterwards.
161      */
162     public function kill_all_sessions() {
163         global $DB;
164         if (!$this->servers) {
165             return;
166         }
168         // Go through the list of all servers because
169         // we do not know where the session handler put the
170         // data.
172         $memcacheds = array();
173         foreach ($this->servers as $server) {
174             list($host, $port) = $server;
175             $memcached = new \Memcached();
176             $memcached->addServer($host, $port);
177             $memcacheds[] = $memcached;
178         }
180         // Note: this can be significantly improved by fetching keys from memcached,
181         //       but we need to make sure we are not deleting somebody else's sessions.
183         $rs = $DB->get_recordset('sessions', array(), 'id DESC', 'id, sid');
184         foreach ($rs as $record) {
185             foreach ($memcacheds as $memcached) {
186                 $memcached->delete($this->prefix . $record->sid);
187             }
188         }
189         $rs->close();
191         foreach ($memcacheds as $memcached) {
192             $memcached->quit();
193         }
194     }
196     /**
197      * Kill one session, the session record is removed afterwards.
198      * @param string $sid
199      */
200     public function kill_session($sid) {
201         if (!$this->servers) {
202             return;
203         }
205         // Go through the list of all servers because
206         // we do not know where the session handler put the
207         // data.
209         foreach ($this->servers as $server) {
210             list($host, $port) = $server;
211             $memcached = new \Memcached();
212             $memcached->addServer($host, $port);
213             $memcached->delete($this->prefix . $sid);
214             $memcached->quit();
215         }
216     }