Cm_RedisSession - Version 1.8.0.0

Version Notes

1.8.0.0

Download this release

Release Info

Developer Magento Core Team
Extension Cm_RedisSession
Version 1.8.0.0
Comparing to
See all releases


Version 1.8.0.0

app/code/community/Cm/RedisSession/Model/Session.php ADDED
@@ -0,0 +1,731 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Redis session handler with optimistic locking.
4
+ *
5
+ * Features:
6
+ * - Falls back to mysql handler if it can't connect to redis. Mysql handler falls back to file handler.
7
+ * - When a session's data exceeds the compression threshold the session data will be compressed.
8
+ * - Compression libraries supported are 'gzip', 'lzf' and 'snappy'. Lzf and Snappy are much faster than gzip.
9
+ * - Compression can be enabled, disabled, or reconfigured on the fly with no loss of session data.
10
+ * - Expiration is handled by Redis. No garbage collection needed.
11
+ * - Logs when sessions are not written due to not having or losing their lock.
12
+ * - Limits the number of concurrent lock requests before a 503 error is returned.
13
+ *
14
+ * Locking Algorithm Properties:
15
+ * - Only one process may get a write lock on a session.
16
+ * - A process may lose it's lock if another process breaks it, in which case the session will not be written.
17
+ * - The lock may be broken after BREAK_AFTER seconds and the process that gets the lock is indeterminate.
18
+ * - Only MAX_CONCURRENCY processes may be waiting for a lock for the same session or else a 503 error is returned.
19
+ * - Detects crashed processes to prevent session deadlocks (Linux only).
20
+ * - Detects inactive waiting processes to prevent false-positives in concurrency throttling.
21
+ *
22
+ */
23
+ class Cm_RedisSession_Model_Session extends Mage_Core_Model_Mysql4_Session
24
+ {
25
+ const BREAK_MODULO = 5; /* The lock will only be broken one of of this many tries to prevent multiple processes breaking the same lock */
26
+ const FAIL_AFTER = 15; /* Try to break lock for at most this many seconds */
27
+ const DETECT_ZOMBIES = 10; /* Try to detect zombies every this many seconds */
28
+ const MAX_LIFETIME = 2592000; /* Redis backend limit */
29
+ const SESSION_PREFIX = 'sess_';
30
+ const LOG_FILE = 'redis_session.log';
31
+
32
+ /* Bots get shorter session lifetimes */
33
+ const BOT_REGEX = '/^alexa|^blitz\.io|bot|^browsermob|crawl|^curl|^facebookexternalhit|feed|google web preview|^ia_archiver|^java|jakarta|^load impact|^magespeedtest|monitor|nagios|^pinterest|postrank|slurp|spider|uptime|yandex/i';
34
+
35
+ const XML_PATH_HOST = 'global/redis_session/host';
36
+ const XML_PATH_PORT = 'global/redis_session/port';
37
+ const XML_PATH_PASS = 'global/redis_session/password';
38
+ const XML_PATH_TIMEOUT = 'global/redis_session/timeout';
39
+ const XML_PATH_PERSISTENT = 'global/redis_session/persistent';
40
+ const XML_PATH_DB = 'global/redis_session/db';
41
+ const XML_PATH_COMPRESSION_THRESHOLD = 'global/redis_session/compression_threshold';
42
+ const XML_PATH_COMPRESSION_LIB = 'global/redis_session/compression_lib';
43
+ const XML_PATH_LOG_LEVEL = 'global/redis_session/log_level';
44
+ const XML_PATH_MAX_CONCURRENCY = 'global/redis_session/max_concurrency';
45
+ const XML_PATH_BREAK_AFTER = 'global/redis_session/break_after_%s';
46
+ const XML_PATH_BOT_LIFETIME = 'global/redis_session/bot_lifetime';
47
+
48
+ const DEFAULT_TIMEOUT = 2.5;
49
+ const DEFAULT_COMPRESSION_THRESHOLD = 2048;
50
+ const DEFAULT_COMPRESSION_LIB = 'gzip';
51
+ const DEFAULT_LOG_LEVEL = 1;
52
+ const DEFAULT_MAX_CONCURRENCY = 6; /* The maximum number of concurrent lock waiters per session */
53
+ const DEFAULT_BREAK_AFTER = 30; /* Try to break the lock after this many seconds */
54
+ const DEFAULT_BOT_LIFETIME = 7200; /* The session lifetime for bots - shorter to prevent bots from wasting backend storage */
55
+
56
+ /** @var bool */
57
+ protected $_useRedis;
58
+
59
+ /** @var Credis_Client */
60
+ protected $_redis;
61
+
62
+ /** @var int */
63
+ protected $_dbNum;
64
+
65
+ protected $_compressionThreshold;
66
+ protected $_compressionLib;
67
+ protected $_logLevel;
68
+ protected $_maxConcurrency;
69
+ protected $_breakAfter;
70
+ protected $_botLifetime;
71
+ protected $_isBot = FALSE;
72
+ protected $_hasLock;
73
+ protected $_sessionWritten; // avoid infinite loops
74
+ protected $_timeStart; // re-usable for timing instrumentation
75
+
76
+ static public $failedLockAttempts = 0; // for debug or informational purposes
77
+
78
+ public function __construct()
79
+ {
80
+ $this->_timeStart = microtime(true);
81
+ $host = (string) (Mage::getConfig()->getNode(self::XML_PATH_HOST) ?: '127.0.0.1');
82
+ $port = (int) (Mage::getConfig()->getNode(self::XML_PATH_PORT) ?: '6379');
83
+ $pass = (string) (Mage::getConfig()->getNode(self::XML_PATH_PASS) ?: '');
84
+ $timeout = (float) (Mage::getConfig()->getNode(self::XML_PATH_TIMEOUT) ?: self::DEFAULT_TIMEOUT);
85
+ $persistent = (string) (Mage::getConfig()->getNode(self::XML_PATH_PERSISTENT) ?: '');
86
+ $this->_dbNum = (int) (Mage::getConfig()->getNode(self::XML_PATH_DB) ?: 0);
87
+ $this->_compressionThreshold = (int) (Mage::getConfig()->getNode(self::XML_PATH_COMPRESSION_THRESHOLD) ?: self::DEFAULT_COMPRESSION_THRESHOLD);
88
+ $this->_compressionLib = (string) (Mage::getConfig()->getNode(self::XML_PATH_COMPRESSION_LIB) ?: self::DEFAULT_COMPRESSION_LIB);
89
+ $this->_logLevel = (int) (Mage::getConfig()->getNode(self::XML_PATH_LOG_LEVEL) ?: self::DEFAULT_LOG_LEVEL);
90
+ $this->_maxConcurrency = (int) (Mage::getConfig()->getNode(self::XML_PATH_MAX_CONCURRENCY) ?: self::DEFAULT_MAX_CONCURRENCY);
91
+ $this->_breakAfter = (int) (Mage::getConfig()->getNode(sprintf(self::XML_PATH_BREAK_AFTER, session_name())) ?: self::DEFAULT_BREAK_AFTER);
92
+ $this->_botLifetime = (int) (Mage::getConfig()->getNode(self::XML_PATH_BOT_LIFETIME) ?: self::DEFAULT_BOT_LIFETIME);
93
+ if ($this->_botLifetime) {
94
+ $userAgent = empty($_SERVER['HTTP_USER_AGENT']) ? FALSE : $_SERVER['HTTP_USER_AGENT'];
95
+ $this->_isBot = ! $userAgent || preg_match(self::BOT_REGEX, $userAgent);
96
+ }
97
+ $this->_redis = new Credis_Client($host, $port, $timeout, $persistent);
98
+ if (!empty($pass)) {
99
+ $this->_redis->auth($pass) or Zend_Cache::throwException('Unable to authenticate with the redis server.');
100
+ }
101
+ $this->_redis->setCloseOnDestruct(FALSE); // Destructor order cannot be predicted
102
+ $this->_useRedis = TRUE;
103
+ if ($this->_logLevel >= 7) {
104
+ Mage::log(
105
+ sprintf(
106
+ "%s: %s initialized for connection to %s:%s after %.5f seconds",
107
+ $this->_getPid(),
108
+ get_class($this),
109
+ $host,
110
+ $port,
111
+ (microtime(true) - $this->_timeStart)
112
+ ),
113
+ Zend_Log::DEBUG, self::LOG_FILE
114
+ );
115
+ if ($this->_isBot) {
116
+ Mage::log(
117
+ sprintf(
118
+ "%s: Bot detected for user agent: %s",
119
+ $this->_getPid(),
120
+ $userAgent
121
+ ),
122
+ Zend_Log::DEBUG, self::LOG_FILE
123
+ );
124
+ }
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Check DB connection
130
+ *
131
+ * @return bool
132
+ */
133
+ public function hasConnection()
134
+ {
135
+ if( ! $this->_useRedis) return parent::hasConnection();
136
+
137
+ try {
138
+ $this->_redis->connect();
139
+ if ($this->_logLevel >= 7) {
140
+ Mage::log(
141
+ sprintf("%s: Connected to Redis",
142
+ $this->_getPid()
143
+ ),
144
+ Zend_Log::DEBUG, self::LOG_FILE
145
+ );
146
+ // reset timer
147
+ $this->_timeStart = microtime(true);
148
+ }
149
+ return TRUE;
150
+ }
151
+ catch (Exception $e) {
152
+ Mage::logException($e);
153
+ $this->_redis = NULL;
154
+ if ($this->_logLevel >= 0) {
155
+ Mage::log(
156
+ sprintf(
157
+ "%s: Unable to connect to Redis; falling back to MySQL handler",
158
+ $this->_getPid()
159
+ ),
160
+ Zend_Log::EMERG, self::LOG_FILE
161
+ );
162
+ }
163
+
164
+ // Fall-back to MySQL handler. If this fails, the file handler will be used.
165
+ $this->_useRedis = FALSE;
166
+ parent::__construct();
167
+ return parent::hasConnection();
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Fetch session data
173
+ *
174
+ * @param string $sessionId
175
+ * @return string
176
+ */
177
+ public function read($sessionId)
178
+ {
179
+ if ( ! $this->_useRedis) return parent::read($sessionId);
180
+
181
+ // Get lock on session. Increment the "lock" field and if the new value is 1, we have the lock.
182
+ // If the new value is a multiple of BREAK_MODULO then we are breaking the lock.
183
+ $sessionId = self::SESSION_PREFIX.$sessionId;
184
+ $tries = $waiting = $lock = 0;
185
+ $detectZombies = FALSE;
186
+ if ($this->_logLevel >= 7) {
187
+ Mage::log(
188
+ sprintf(
189
+ "%s: Attempting read lock on ID %s",
190
+ $this->_getPid(),
191
+ $sessionId
192
+ ),
193
+ Zend_Log::DEBUG, self::LOG_FILE
194
+ );
195
+ // reset timer
196
+ $this->_timeStart = microtime(true);
197
+ }
198
+ if($this->_dbNum) $this->_redis->select($this->_dbNum);
199
+ while(1)
200
+ {
201
+ // Increment lock value for this session and retrieve the new value
202
+ $oldLock = $lock;
203
+ $lock = $this->_redis->hIncrBy($sessionId, 'lock', 1);
204
+
205
+ // If we got the lock, update with our pid and reset lock and expiration
206
+ if ($lock == 1 || ($tries >= $this->_breakAfter && $lock % self::BREAK_MODULO == 0)) {
207
+ $setData = array(
208
+ 'pid' => $this->_getPid(),
209
+ 'lock' => 1,
210
+ );
211
+
212
+ // Save request data in session so if a lock is broken we can know which page it was for debugging
213
+ if ($this->_logLevel >= 6)
214
+ {
215
+ $additionalDetails = sprintf(
216
+ "(%s attempts)",
217
+ $tries
218
+ );
219
+ if ($this->_logLevel >= 7)
220
+ {
221
+ $additionalDetails = sprintf(
222
+ "after %.5f seconds ",
223
+ (microtime(true) - $this->_timeStart),
224
+ $tries
225
+ ) . $additionalDetails;
226
+ }
227
+ if (empty($_SERVER['REQUEST_METHOD'])) {
228
+ $setData['req'] = $_SERVER['SCRIPT_NAME'];
229
+ } else {
230
+ $setData['req'] = "{$_SERVER['REQUEST_METHOD']} {$_SERVER['SERVER_NAME']}{$_SERVER['REQUEST_URI']}";
231
+ }
232
+ if ($lock != 1) {
233
+ Mage::log(
234
+ sprintf("%s: Successfully broke lock for ID %s %s. Lock: %s, BREAK_MODULO: %s\nLast request of broken lock: %s",
235
+ $this->_getPid(),
236
+ $sessionId,
237
+ $additionalDetails,
238
+ $lock,
239
+ self::BREAK_MODULO,
240
+ $this->_redis->hGet($sessionId, 'req')
241
+ ),
242
+ Zend_Log::INFO, self::LOG_FILE
243
+ );
244
+ }
245
+ }
246
+ $this->_redis->pipeline()
247
+ ->hMSet($sessionId, $setData)
248
+ ->expire($sessionId, min($this->getLifeTime(), self::MAX_LIFETIME))
249
+ ->exec();
250
+ $this->_hasLock = TRUE;
251
+ break;
252
+ }
253
+
254
+ // Otherwise, add to "wait" counter and continue
255
+ else if ( ! $waiting) {
256
+ $i = 0;
257
+ do {
258
+ $waiting = $this->_redis->hIncrBy($sessionId, 'wait', 1);
259
+ if ($this->_logLevel >= 7) {
260
+ Mage::log(
261
+ sprintf(
262
+ "%s: Waiting for lock on ID %s (%s tries, %s waiting, %.5f seconds elapsed)",
263
+ $this->_getPid(),
264
+ $sessionId,
265
+ $tries,
266
+ $waiting,
267
+ (microtime(true) - $this->_timeStart)
268
+ ),
269
+ Zend_Log::DEBUG, self::LOG_FILE
270
+ );
271
+ }
272
+ } while (++$i < $this->_maxConcurrency && $waiting < 1);
273
+ }
274
+
275
+ // Handle overloaded sessions
276
+ else {
277
+ // Detect broken sessions (e.g. caused by fatal errors)
278
+ if ($detectZombies) {
279
+ $detectZombies = FALSE;
280
+ if ( $lock > $oldLock // lock shouldn't be less than old lock (another process broke the lock)
281
+ && $lock + 1 < $oldLock + $waiting // lock should be old+waiting, otherwise there must be a dead process
282
+ ) {
283
+ // Reset session to fresh state
284
+ if ($this->_logLevel >= 6)
285
+ {
286
+ Mage::log(
287
+ sprintf("%s: Detected zombie waiter after %.5f seconds for ID %s (%s waiting)\n %s (%s - %s)",
288
+ $this->_getPid(),
289
+ (microtime(true) - $this->_timeStart),
290
+ $sessionId, $waiting,
291
+ Mage::app()->getRequest()->getRequestUri(), Mage::app()->getRequest()->getClientIp(), Mage::app()->getRequest()->getHeader('User-Agent')
292
+ ),
293
+ Zend_Log::INFO, self::LOG_FILE
294
+ );
295
+ }
296
+ $waiting = $this->_redis->hIncrBy($sessionId, 'wait', -1);
297
+ continue;
298
+ }
299
+ }
300
+
301
+ // Limit concurrent lock waiters to prevent server resource hogging
302
+ if ($waiting >= $this->_maxConcurrency) {
303
+ // Overloaded sessions get 503 errors
304
+ $this->_redis->hIncrBy($sessionId, 'wait', -1);
305
+ $this->_sessionWritten = TRUE; // Prevent session from getting written
306
+ $writes = $this->_redis->hGet($sessionId, 'writes');
307
+ if ($this->_logLevel >= 4)
308
+ {
309
+ Mage::log(
310
+ sprintf("%s: Session concurrency exceeded for ID %s; displaying HTTP 503 (%s waiting, %s total requests)\n %s (%s - %s)",
311
+ $this->_getPid(),
312
+ $sessionId, $waiting, $writes,
313
+ Mage::app()->getRequest()->getRequestUri(), Mage::app()->getRequest()->getClientIp(), Mage::app()->getRequest()->getHeader('User-Agent')
314
+ ),
315
+ Zend_Log::WARN, self::LOG_FILE
316
+ );
317
+ }
318
+ require_once(Mage::getBaseDir() . DS . 'errors' . DS . '503.php');
319
+ exit;
320
+ }
321
+ }
322
+
323
+ $tries++;
324
+
325
+ // Detect dead waiters
326
+ if ($tries == 1 /* TODO - $tries % 10 == 0 ? */) {
327
+ $detectZombies = TRUE;
328
+ // TODO: allow configuration of sleep period?
329
+ usleep(1500000); // 1.5 seconds
330
+ }
331
+ // Detect dead processes every 10 seconds
332
+ if ($tries % self::DETECT_ZOMBIES == 0) {
333
+ if ($this->_logLevel >= 7) {
334
+ Mage::log(
335
+ sprintf(
336
+ "%s: Checking for zombies after %.5f seconds of waiting...",
337
+ $this->_getPid(),
338
+ (microtime(true) - $this->_timeStart)
339
+ ),
340
+ Zend_Log::DEBUG, self::LOG_FILE
341
+ );
342
+ }
343
+ $pid = $this->_redis->hGet($sessionId, 'pid');
344
+ if ($pid && ! $this->_pidExists($pid)) {
345
+ // Allow a live process to get the lock
346
+ $this->_redis->hSet($sessionId, 'lock', 0);
347
+ if ($this->_logLevel >= 6)
348
+ {
349
+ Mage::log(
350
+ sprintf("%s: Detected zombie process (%s) for %s (%s waiting)\n %s (%s - %s)",
351
+ $this->_getPid(),
352
+ $pid, $sessionId, $waiting,
353
+ Mage::app()->getRequest()->getRequestUri(),
354
+ Mage::app()->getRequest()->getClientIp(),
355
+ Mage::app()->getRequest()->getHeader('User-Agent')
356
+ ),
357
+ Zend_Log::INFO, self::LOG_FILE
358
+ );
359
+ }
360
+ continue;
361
+ }
362
+ }
363
+ // Timeout
364
+ if ($tries >= $this->_breakAfter+self::FAIL_AFTER) {
365
+ $this->_hasLock = FALSE;
366
+ if ($this->_logLevel >= 5) {
367
+ $additionalDetails = sprintf(
368
+ "(%s attempts)",
369
+ $tries
370
+ );
371
+ if ($this->_logLevel >= 7)
372
+ {
373
+ $additionalDetails = sprintf(
374
+ "after %.5f seconds ",
375
+ (microtime(true) - $this->_timeStart),
376
+ $tries
377
+ ) . $additionalDetails;
378
+ }
379
+ Mage::log(
380
+ sprintf(
381
+ "%s: Giving up on read lock for ID %s %s",
382
+ $this->_getPid(),
383
+ $sessionId,
384
+ $additionalDetails
385
+ ),
386
+ Zend_Log::NOTICE, self::LOG_FILE
387
+ );
388
+ }
389
+ break;
390
+ }
391
+ else {
392
+ // TODO: configurable wait period?
393
+ sleep(1);
394
+ }
395
+ }
396
+ self::$failedLockAttempts = $tries;
397
+
398
+ // This process is no longer waiting for a lock
399
+ if ($tries > 0) {
400
+ $this->_redis->hIncrBy($sessionId, 'wait', -1);
401
+ }
402
+
403
+ // Session can be read even if it was not locked by this pid!
404
+ $sessionData = $this->_redis->hGet($sessionId, 'data');
405
+ if ($this->_logLevel >= 7) {
406
+ Mage::log(
407
+ sprintf(
408
+ "%s: Data read for ID %s after %.5f seconds",
409
+ $this->_getPid(),
410
+ $sessionId,
411
+ (microtime(true) - $this->_timeStart)
412
+ ),
413
+ Zend_Log::DEBUG, self::LOG_FILE
414
+ );
415
+ }
416
+ return $sessionData ? $this->_decodeData($sessionData) : '';
417
+ }
418
+
419
+ /**
420
+ * Update session
421
+ *
422
+ * @param string $sessionId
423
+ * @param string $sessionData
424
+ * @return boolean
425
+ */
426
+ public function write($sessionId, $sessionData)
427
+ {
428
+ if ( ! $this->_useRedis) return parent::write($sessionId, $sessionData);
429
+ if ($this->_sessionWritten) {
430
+ if ($this->_logLevel >= 7) {
431
+ Mage::log(
432
+ sprintf(
433
+ "%s: Repeated session write detected; skipping for ID %s",
434
+ $this->_getPid(),
435
+ $sessionId
436
+ ),
437
+ Zend_Log::DEBUG, self::LOG_FILE
438
+ );
439
+ }
440
+ return TRUE;
441
+ }
442
+ $this->_sessionWritten = TRUE;
443
+ if ($this->_logLevel >= 7) {
444
+ Mage::log(
445
+ sprintf(
446
+ "%s: Attempting write to ID %s",
447
+ $this->_getPid(),
448
+ $sessionId
449
+ ),
450
+ Zend_Log::DEBUG, self::LOG_FILE
451
+ );
452
+ // reset timer
453
+ $this->_timeStart = microtime(true);
454
+ }
455
+
456
+ // Do not overwrite the session if it is locked by another pid
457
+ try {
458
+ if($this->_dbNum) $this->_redis->select($this->_dbNum); // Prevent conflicts with other connections?
459
+ $pid = $this->_redis->hGet('sess_'.$sessionId, 'pid'); // PHP Fatal errors cause self::SESSION_PREFIX to not work..
460
+ if ( ! $pid || $pid == $this->_getPid()) {
461
+ if ($this->_logLevel >= 7) {
462
+ Mage::log(
463
+ sprintf(
464
+ "%s: Write lock obtained on ID %s",
465
+ $this->_getPid(),
466
+ $sessionId
467
+ ),
468
+ Zend_Log::DEBUG, self::LOG_FILE
469
+ );
470
+ }
471
+ $this->_writeRawSession($sessionId, $sessionData, $this->getLifeTime());
472
+ if ($this->_logLevel >= 7) {
473
+ Mage::log(
474
+ sprintf(
475
+ "%s: Data written to ID %s after %.5f seconds",
476
+ $this->_getPid(),
477
+ $sessionId,
478
+ (microtime(true) - $this->_timeStart)
479
+ ),
480
+ Zend_Log::DEBUG, self::LOG_FILE
481
+ );
482
+ }
483
+ }
484
+ else {
485
+ if ($this->_logLevel >= 4) {
486
+ if ($this->_hasLock) {
487
+ Mage::log(
488
+ sprintf("%s: Unable to write session after %.5f seconds, another process took the lock for ID %s",
489
+ $this->_getPid(),
490
+ (microtime(true) - $this->_timeStart),
491
+ $sessionId
492
+ ),
493
+ Zend_Log::WARN,
494
+ self::LOG_FILE
495
+ );
496
+ } else {
497
+ Mage::log(
498
+ sprintf("%s: Unable to write session after %.5f seconds, unable to acquire lock on ID %s",
499
+ $this->_getPid(),
500
+ (microtime(true) - $this->_timeStart),
501
+ $sessionId
502
+ ),
503
+ Zend_Log::WARN,
504
+ self::LOG_FILE
505
+ );
506
+ }
507
+ }
508
+ }
509
+ }
510
+ catch(Exception $e) {
511
+ if (class_exists('Mage', false)) {
512
+ Mage::logException($e);
513
+ } else {
514
+ error_log("$e");
515
+ }
516
+ return FALSE;
517
+ }
518
+ return TRUE;
519
+ }
520
+
521
+ /**
522
+ * Destroy session
523
+ *
524
+ * @param string $sessionId
525
+ * @return boolean
526
+ */
527
+ public function destroy($sessionId)
528
+ {
529
+ if ( ! $this->_useRedis) return parent::destroy($sessionId);
530
+
531
+ if ($this->_logLevel >= 7) {
532
+ Mage::log(
533
+ sprintf(
534
+ "%s: Destroying ID %s",
535
+ $this->_getPid(),
536
+ $sessionId
537
+ ),
538
+ Zend_Log::DEBUG, self::LOG_FILE
539
+ );
540
+ }
541
+ $this->_redis->pipeline();
542
+ if($this->_dbNum) $this->_redis->select($this->_dbNum);
543
+ $this->_redis->del(self::SESSION_PREFIX.$sessionId);
544
+ $this->_redis->exec();
545
+ return TRUE;
546
+ }
547
+
548
+ /**
549
+ * Overridden to prevent calling getLifeTime at shutdown
550
+ *
551
+ * @return bool
552
+ */
553
+ public function close()
554
+ {
555
+ if ( ! $this->_useRedis) return parent::close();
556
+ if ($this->_logLevel >= 7) {
557
+ Mage::log(
558
+ sprintf(
559
+ "%s: Closing connection",
560
+ $this->_getPid()
561
+ ),
562
+ Zend_Log::DEBUG, self::LOG_FILE
563
+ );
564
+ }
565
+ if ($this->_redis) $this->_redis->close();
566
+ return TRUE;
567
+ }
568
+
569
+ /**
570
+ * Garbage collection
571
+ *
572
+ * @param int $maxLifeTime ignored
573
+ * @return boolean
574
+ */
575
+ public function gc($maxLifeTime)
576
+ {
577
+ if ( ! $this->_useRedis) return parent::gc($maxLifeTime);
578
+ return TRUE;
579
+ }
580
+
581
+ /**
582
+ * @return int|mixed
583
+ */
584
+ public function getLifeTime()
585
+ {
586
+ if ($this->_isBot) {
587
+ return min(parent::getLifeTime(), $this->_botLifetime);
588
+ }
589
+ return parent::getLifeTime();
590
+ }
591
+
592
+ /**
593
+ * Public for testing purposes only.
594
+ *
595
+ * @param string $data
596
+ * @return string
597
+ */
598
+ public function _encodeData($data)
599
+ {
600
+ $originalDataSize = strlen($data);
601
+ if ($this->_compressionThreshold > 0 && $this->_compressionLib != 'none' && $originalDataSize >= $this->_compressionThreshold) {
602
+ if ($this->_logLevel >= 7) {
603
+ Mage::log(
604
+ sprintf(
605
+ "%s: Compressing %s bytes with %s",
606
+ $this->_getPid(),
607
+ $originalDataSize,
608
+ $this->_compressionLib
609
+ ),
610
+ Zend_Log::DEBUG, self::LOG_FILE
611
+ );
612
+ // reset timer
613
+ $this->_timeStart = microtime(true);
614
+ }
615
+ switch($this->_compressionLib) {
616
+ case 'snappy': $data = snappy_compress($data); break;
617
+ case 'lzf': $data = lzf_compress($data); break;
618
+ case 'gzip': $data = gzcompress($data, 1); break;
619
+ }
620
+ if($data) {
621
+ $data = ':'.substr($this->_compressionLib,0,2).':'.$data;
622
+ if ($this->_logLevel >= 7) {
623
+ Mage::log(
624
+ sprintf(
625
+ "%s: Data compressed by %.1f percent in %.5f seconds",
626
+ $this->_getPid(),
627
+ ($originalDataSize == 0 ? 0 : (100 - (strlen($data) / $originalDataSize * 100))),
628
+ (microtime(true) - $this->_timeStart)
629
+ ),
630
+ Zend_Log::DEBUG, self::LOG_FILE
631
+ );
632
+ }
633
+ } else if ($this->_logLevel >= 4) {
634
+ Mage::log(
635
+ sprintf("%s: Could not compress session data using %s",
636
+ $this->_getPid(),
637
+ $this->_compressionLib
638
+ ),
639
+ Zend_Log::WARN,
640
+ self::LOG_FILE
641
+ );
642
+ }
643
+ }
644
+ return $data;
645
+ }
646
+
647
+ /**
648
+ * Public for testing purposes only.
649
+ *
650
+ * @param string $data
651
+ * @return string
652
+ */
653
+ public function _decodeData($data)
654
+ {
655
+ switch (substr($data,0,4)) {
656
+ // asking the data which library it uses allows for transparent changes of libraries
657
+ case ':sn:': return snappy_uncompress(substr($data,4));
658
+ case ':lz:': return lzf_decompress(substr($data,4));
659
+ case ':gz:': return gzuncompress(substr($data,4));
660
+ }
661
+ return $data;
662
+ }
663
+
664
+ /**
665
+ * Public for testing/import purposes only.
666
+ *
667
+ * @param $id
668
+ * @param $data
669
+ * @param $lifetime
670
+ * @throws Exception
671
+ */
672
+ public function _writeRawSession($id, $data, $lifetime)
673
+ {
674
+ if ( ! $this->_useRedis) {
675
+ throw new Exception('Not connected to redis!');
676
+ }
677
+
678
+ $sessionId = 'sess_' . $id;
679
+ $this->_redis->pipeline()
680
+ ->select($this->_dbNum)
681
+ ->hMSet($sessionId, array(
682
+ 'data' => $this->_encodeData($data),
683
+ 'lock' => 0, // 0 so that next lock attempt will get 1
684
+ ))
685
+ ->hIncrBy($sessionId, 'writes', 1) // For informational purposes only
686
+ ->expire($sessionId, min($lifetime, 2592000))
687
+ ->exec();
688
+ }
689
+
690
+ /**
691
+ * @param string $id
692
+ * @return array
693
+ * @throws Exception
694
+ */
695
+ public function _inspectSession($id)
696
+ {
697
+ if ( ! $this->_useRedis) {
698
+ throw new Exception('Not connected to redis!');
699
+ }
700
+
701
+ $sessionId = 'sess_' . $id;
702
+ $this->_redis->select($this->_dbNum);
703
+ $data = $this->_redis->hGetAll($sessionId);
704
+ if ($data && isset($data['data'])) {
705
+ $data['data'] = $this->_decodeData($data['data']);
706
+ }
707
+ return $data;
708
+ }
709
+
710
+ /**
711
+ * @return string
712
+ */
713
+ public function _getPid()
714
+ {
715
+ return gethostname().'|'.getmypid();
716
+ }
717
+
718
+ /**
719
+ * @param $pid
720
+ * @return bool
721
+ */
722
+ public function _pidExists($pid)
723
+ {
724
+ list($host,$pid) = explode('|', $pid);
725
+ if (PHP_OS != 'Linux' || $host != gethostname()) {
726
+ return TRUE;
727
+ }
728
+ return @file_exists('/proc/'.$pid);
729
+ }
730
+
731
+ }
app/code/community/Cm/RedisSession/etc/config.xml ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <config>
2
+ <modules>
3
+ <Cm_RedisSession>
4
+ <version>0.2</version>
5
+ </Cm_RedisSession>
6
+ </modules>
7
+ <global>
8
+ <models>
9
+ <core_mysql4>
10
+ <rewrite>
11
+ <session>Cm_RedisSession_Model_Session</session>
12
+ </rewrite>
13
+ </core_mysql4>
14
+ </models>
15
+ </global>
16
+ </config>
app/etc/modules/Cm_RedisSession.xml ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
1
+ <config>
2
+ <modules>
3
+ <Cm_RedisSession>
4
+ <active>false</active>
5
+ <codePool>community</codePool>
6
+ </Cm_RedisSession>
7
+ </modules>
8
+ </config>
package.xml ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0"?>
2
+ <package>
3
+ <name>Cm_RedisSession</name>
4
+ <version>1.8.0.0</version>
5
+ <stability>stable</stability>
6
+ <license uri="http://framework.zend.com/license/new-bsd">New BSD</license>
7
+ <channel>community</channel>
8
+ <extends/>
9
+ <summary>Redis session</summary>
10
+ <description>Redis seesion</description>
11
+ <notes>1.8.0.0</notes>
12
+ <authors><author><name>Colin Mollenhour</name><user>core</user><email>colin@mollenhour.com</email></author></authors>
13
+ <date>2013-09-24</date>
14
+ <time>09:09:39</time>
15
+ <contents><target name="magecommunity"><dir name="Cm"><dir name="RedisSession"><dir name="etc"><file name="config.xml" hash="4173754174f995f9f7a141747f417abc"/></dir><dir name="Model"><file name="Session.php" hash="fd4d16e73989a8b93582f8c11f8fdc40"/></dir></dir></dir></target><target name="mageetc"><dir name="modules"><file name="Cm_RedisSession.xml" hash="f36278d589fa562d20d5182c8864a3dd"/></dir></target></contents>
16
+ <compatible/>
17
+ <dependencies><required><php><min>5.2.0</min><max>6.0.0</max></php><package><name>Lib_Credis</name><channel>community</channel><min>1.8.0.0</min><max>1.9.0.0</max></package></required></dependencies>
18
+ </package>