blob: 6c013a6573245dab6d697d4c99d28a6ab4c738a9 [file] [log] [blame]
Andrey Andreev43f6cdb2014-08-27 22:26:40 +03001<?php
2/**
3 * CodeIgniter
4 *
5 * An open source application development framework for PHP 5.2.4 or newer
6 *
7 * NOTICE OF LICENSE
8 *
9 * Licensed under the Open Software License version 3.0
10 *
11 * This source file is subject to the Open Software License (OSL 3.0) that is
12 * bundled with this package in the files license.txt / license.rst. It is
13 * also available through the world wide web at this URL:
14 * http://opensource.org/licenses/OSL-3.0
15 * If you did not receive a copy of the license and are unable to obtain it
16 * through the world wide web, please send an email to
17 * licensing@ellislab.com so we can send you a copy immediately.
18 *
19 * @package CodeIgniter
20 * @author Andrey Andreev
21 * @copyright Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
22 * @license http://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
23 * @link http://codeigniter.com
24 * @since Version 3.0
25 * @filesource
26 */
27defined('BASEPATH') OR exit('No direct script access allowed');
28
29/**
30 * CodeIgniter Session Redis Driver
31 *
32 * @package CodeIgniter
33 * @subpackage Libraries
34 * @category Sessions
35 * @author Andrey Andreev
36 * @link http://codeigniter.com/user_guide/libraries/sessions.html
37 */
38class CI_Session_redis_driver extends CI_Session_driver implements SessionHandlerInterface {
39
40 /**
41 * Save path
42 *
43 * @var string
44 */
45 protected $_save_path;
46
47 /**
48 * phpRedis instance
49 *
50 * @var resource
51 */
52 protected $_redis;
53
54 /**
55 * Key prefix
56 *
57 * @var string
58 */
59 protected $_key_prefix = 'ci_session:';
60
61 /**
62 * Lock key
63 *
64 * @var string
65 */
66 protected $_lock_key;
67
68 // ------------------------------------------------------------------------
69
70 /**
71 * Class constructor
72 *
73 * @param array $params Configuration parameters
74 * @return void
75 */
76 public function __construct(&$params)
77 {
78 parent::__construct($params);
79
80 if (empty($this->_save_path))
81 {
82 log_message('error', 'Session: No Redis save path configured.');
83 }
84 elseif (preg_match('#(?:tcp://)?([^:]+)(?:\:(\d+))?(\?.+)?#', $this->_save_path, $matches))
85 {
86 $this->_save_path = array(
87 'host' => $matches[1],
88 'port' => empty($matches[2]) ? NULL : $matches[2],
89 'password' => preg_match('#auth=([^\s&]+)#', $matches[3], $match) ? $match[1] : NULL,
90 'database' => preg_match('#database=(\d+)#', $matches[3], $match) ? (int) $match[1] : NULL,
91 'timeout' => preg_match('#timeout=(\d+\.\d+)#', $matches[3], $match) ? (float) $match[1] : NULL
92 );
93
94 preg_match('#prefix=([^\s&]+)#', $matches[3], $match) && $this->_key_prefix = $match[1];
95 }
96 else
97 {
98 log_message('error', 'Session: Invalid Redis save path format: '.$this->_save_path);
99 }
100
101 if ($this->_match_ip === TRUE)
102 {
103 $this->_key_prefix .= $_SERVER['REMOTE_ADDR'].':';
104 }
105 }
106
107 // ------------------------------------------------------------------------
108
109 public function open($save_path, $name)
110 {
111 if (empty($this->_save_path))
112 {
113 return FALSE;
114 }
115
116 $redis = new Redis();
117 if ( ! $redis->connect($this->_save_path['host'], $this->_save_path['port'], $this->_save_path['timeout']))
118 {
119 log_message('error', 'Session: Unable to connect to Redis with the configured settings.');
120 }
121 elseif (isset($this->_save_path['password']) && ! $redis->auth($this->_save_path['password']))
122 {
123 log_message('error', 'Session: Unable to authenticate to Redis instance.');
124 }
125 elseif (isset($this->_save_path['database']) && ! $redis->select($this->_save_path['database']))
126 {
127 log_message('error', 'Session: Unable to select Redis database with index '.$this->_save_path['database']);
128 }
129 else
130 {
131 $this->_redis = $redis;
132 return TRUE;
133 }
134
135 return FALSE;
136 }
137
138 // ------------------------------------------------------------------------
139
140 public function read($session_id)
141 {
142 if (isset($this->_redis) && $this->_get_lock($session_id))
143 {
144 $session_data = (string) $this->_redis->get($this->_key_prefix.$session_id);
145 $this->_fingerprint = md5($session_data);
146 return $session_data;
147 }
148
149 return FALSE;
150 }
151
152 public function write($session_id, $session_data)
153 {
154 if (isset($this->_redis, $this->_lock_key))
155 {
156 $this->_redis->setTimeout($this->_lock_key, 10, time());
157 if ($this->_fingerprint !== ($fingerprint = md5($session_data)))
158 {
159 if ($this->_redis->set($this->_key_prefix.$session_id, $session_data, $this->_expiration))
160 {
161 $this->_fingerprint = $fingerprint;
162 return TRUE;
163 }
164
165 return FALSE;
166 }
167
168 return $this->_redis->setTimeout($this->_key_prefix.$session_id, $this->_expiration);
169 }
170
171 return FALSE;
172 }
173
174 // ------------------------------------------------------------------------
175
176 public function close()
177 {
178 if (isset($this->_redis))
179 {
180 try {
181 if ($this->_redis->ping() === '+PONG')
182 {
183 isset($this->_lock_key) && $this->_redis->delete($this->_lock_key);
184 if ( ! $this->_redis->close())
185 {
186 return FALSE;
187 }
188 }
189 }
190 catch (RedisException $e)
191 {
192 log_message('error', 'Session: Got RedisException on close(): '.$e->getMessage());
193 }
194
195 $this->_redis = NULL;
196 return TRUE;
197 }
198
199 return FALSE;
200 }
201
202 // ------------------------------------------------------------------------
203
204 public function destroy($session_id)
205 {
206 if (isset($this->_redis, $this->_lock_key))
207 {
208 if ($this->_redis->delete($this->_key_prefix.$session_id) !== 1)
209 {
210 log_message('debug', 'Session: Redis::delete() expected to return 1, got '.var_export($result, TRUE).' instead.');
211 }
212
213 return ($this->_cookie_destroy() && $this->close());
214 }
215
216 return $this->close();
217 }
218
219 // ------------------------------------------------------------------------
220
221 public function gc($maxlifetime)
222 {
223 // TODO: keys()/getKeys() is said to be performance-intensive,
224 // although it supports patterns (*, [charlist] at the very least).
225 // scan() seems to be recommended, but requires redis 2.8
226 // Not sure if we need any of these though, as we set keys with expire times
227 return TRUE;
228 }
229
230 // ------------------------------------------------------------------------
231
232 protected function _get_lock($session_id)
233 {
234 if (isset($this->_lock_key))
235 {
236 return $this->_redis->setTimeout($this->_lock_key, 5);
237 }
238
239 $lock_key = $this->_key_prefix.$session_id.':lock';
240 if (($ttl = $this->_redis->ttl($lock_key)) < 1)
241 {
242 if ( ! $this->_redis->setex($lock_key, 5, time()))
243 {
244 log_message('error', 'Session: Error while trying to obtain lock for '.$this->_key_prefix.$session_id);
245 return FALSE;
246 }
247
248 $this->_lock_key = $lock_key;
249
250 if ($ttl === -1)
251 {
252 log_message('debug', 'Session: Lock for '.$this->_key_prefix.$session_id.' had no TTL, overriding.');
253 }
254
255 $this->_lock = TRUE;
256 return TRUE;
257 }
258
259 // Another process has the lock, we'll try to wait for it to free itself ...
260 $attempt = 0;
261 while ($attempt++ < 5)
262 {
263 usleep(($ttl * 1000000) - 20000);
264 if (($ttl = $this->_redis->ttl($lock_key)) > 0)
265 {
266 continue;
267 }
268
269 if ( ! $this->_redis->setex($lock_key, 5, time()))
270 {
271 log_message('error', 'Session: Error while trying to obtain lock for '.$this->_key_prefix.$session_id);
272 return FALSE;
273 }
274
275 $this->_lock_key = $lock_key;
276 break;
277 }
278
279 if ($attempt === 5)
280 {
281 log_message('error', 'Session: Unable to obtain lock for '.$this->_key_prefix.$session_id.' after 5 attempts, aborting.');
282 return FALSE;
283 }
284
285 $this->_lock = TRUE;
286 return TRUE;
287 }
288
289 // ------------------------------------------------------------------------
290
291 protected function _release_lock()
292 {
293 if (isset($this->_redis, $this->_lock_key) && $this->_lock)
294 {
295 if ( ! $this->_redis->delete($this->_lock_key))
296 {
297 log_message('error', 'Session: Error while trying to free lock for '.$this->_key_prefix.$session_id);
298 return FALSE;
299 }
300
301 $this->_lock_key = NULL;
302 $this->_lock = FALSE;
303 }
304
305 return TRUE;
306 }
307
308}
309
310/* End of file Session_redis_driver.php */
311/* Location: ./system/libraries/Session/drivers/Session_redis_driver.php */