Cm_RedisSession - Version 1.9.2.0

Version Notes

1.9.2.0

Download this release

Release Info

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


Code changes from version 1.8.0.0 to 1.9.2.0

app/code/community/Cm/RedisSession/Model/Session.php CHANGED
@@ -1,11 +1,40 @@
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.
@@ -22,28 +51,14 @@
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;
@@ -51,7 +66,12 @@ class Cm_RedisSession_Model_Session extends Mage_Core_Model_Mysql4_Session
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;
@@ -62,69 +82,82 @@ class Cm_RedisSession_Model_Session extends Mage_Core_Model_Mysql4_Session
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
  *
@@ -136,30 +169,15 @@ class Cm_RedisSession_Model_Session extends Mage_Core_Model_Mysql4_Session
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;
@@ -177,76 +195,38 @@ class Cm_RedisSession_Model_Session extends Mage_Core_Model_Mysql4_Session
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
  }
@@ -256,19 +236,6 @@ class Cm_RedisSession_Model_Session extends Mage_Core_Model_Mysql4_Session
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
 
@@ -281,17 +248,13 @@ class Cm_RedisSession_Model_Session extends Mage_Core_Model_Mysql4_Session
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;
@@ -304,115 +267,123 @@ class Cm_RedisSession_Model_Session extends Mage_Core_Model_Mysql4_Session
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
 
@@ -425,84 +396,42 @@ class Cm_RedisSession_Model_Session extends Mage_Core_Model_Mysql4_Session
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
  }
@@ -513,8 +442,10 @@ class Cm_RedisSession_Model_Session extends Mage_Core_Model_Mysql4_Session
513
  } else {
514
  error_log("$e");
515
  }
 
516
  return FALSE;
517
  }
 
518
  return TRUE;
519
  }
520
 
@@ -527,21 +458,16 @@ class Cm_RedisSession_Model_Session extends Mage_Core_Model_Mysql4_Session
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
 
@@ -553,14 +479,8 @@ class Cm_RedisSession_Model_Session extends Mage_Core_Model_Mysql4_Session
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;
@@ -583,10 +503,52 @@ class Cm_RedisSession_Model_Session extends Mage_Core_Model_Mysql4_Session
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
  /**
@@ -597,50 +559,32 @@ class Cm_RedisSession_Model_Session extends Mage_Core_Model_Mysql4_Session
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
 
@@ -652,12 +596,15 @@ class Cm_RedisSession_Model_Session extends Mage_Core_Model_Mysql4_Session
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
 
@@ -682,8 +629,8 @@ class Cm_RedisSession_Model_Session extends Mage_Core_Model_Mysql4_Session
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
 
@@ -698,7 +645,7 @@ class Cm_RedisSession_Model_Session extends Mage_Core_Model_Mysql4_Session
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'])) {
@@ -707,6 +654,23 @@ class Cm_RedisSession_Model_Session extends Mage_Core_Model_Mysql4_Session
707
  return $data;
708
  }
709
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
710
  /**
711
  * @return string
712
  */
1
  <?php
2
+ /*
3
+ ==New BSD License==
4
+
5
+ Copyright (c) 2013, Colin Mollenhour
6
+ All rights reserved.
7
+
8
+ Redistribution and use in source and binary forms, with or without
9
+ modification, are permitted provided that the following conditions are met:
10
+
11
+ * Redistributions of source code must retain the above copyright
12
+ notice, this list of conditions and the following disclaimer.
13
+ * Redistributions in binary form must reproduce the above copyright
14
+ notice, this list of conditions and the following disclaimer in the
15
+ documentation and/or other materials provided with the distribution.
16
+ * The name of Colin Mollenhour may not be used to endorse or promote products
17
+ derived from this software without specific prior written permission.
18
+ * Redistributions in any form must not change the Cm_RedisSession namespace.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
21
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
22
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
24
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
25
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
26
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
27
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
29
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+ */
31
  /**
32
  * Redis session handler with optimistic locking.
33
  *
34
  * Features:
35
  * - Falls back to mysql handler if it can't connect to redis. Mysql handler falls back to file handler.
36
  * - When a session's data exceeds the compression threshold the session data will be compressed.
37
+ * - Compression libraries supported are 'gzip', 'lzf', 'lz4' and 'snappy'.
38
  * - Compression can be enabled, disabled, or reconfigured on the fly with no loss of session data.
39
  * - Expiration is handled by Redis. No garbage collection needed.
40
  * - Logs when sessions are not written due to not having or losing their lock.
51
  */
52
  class Cm_RedisSession_Model_Session extends Mage_Core_Model_Mysql4_Session
53
  {
54
+ const SLEEP_TIME = 500000; /* Sleep 0.5 seconds between lock attempts (1,000,000 == 1 second) */
55
  const FAIL_AFTER = 15; /* Try to break lock for at most this many seconds */
56
+ const DETECT_ZOMBIES = 20; /* Try to detect zombies every this many tries */
 
57
  const SESSION_PREFIX = 'sess_';
58
  const LOG_FILE = 'redis_session.log';
59
 
60
  /* Bots get shorter session lifetimes */
61
+ const BOT_REGEX = '/^alexa|^blitz\.io|bot|^browsermob|crawl|^curl|^facebookexternalhit|feed|google web preview|^ia_archiver|indexer|^java|jakarta|^libwww-perl|^load impact|^magespeedtest|monitor|^Mozilla$|nagios|^\.net|^pinterest|postrank|slurp|spider|uptime|^wget|yandex/i';
 
 
 
 
 
 
 
 
 
 
 
 
 
62
 
63
  const DEFAULT_TIMEOUT = 2.5;
64
  const DEFAULT_COMPRESSION_THRESHOLD = 2048;
66
  const DEFAULT_LOG_LEVEL = 1;
67
  const DEFAULT_MAX_CONCURRENCY = 6; /* The maximum number of concurrent lock waiters per session */
68
  const DEFAULT_BREAK_AFTER = 30; /* Try to break the lock after this many seconds */
69
+ const DEFAULT_FIRST_LIFETIME = 600; /* The session lifetime for non-bots on the first write */
70
+ const DEFAULT_BOT_FIRST_LIFETIME = 60; /* The session lifetime for bots on the first write */
71
  const DEFAULT_BOT_LIFETIME = 7200; /* The session lifetime for bots - shorter to prevent bots from wasting backend storage */
72
+ const DEFAULT_DISABLE_LOCKING = FALSE; /* Session locking is enabled by default */
73
+ const DEFAULT_MAX_LIFETIME = 2592000; /* Redis backend limit */
74
+ const DEFAULT_MIN_LIFETIME = 60;
75
 
76
  /** @var bool */
77
  protected $_useRedis;
82
  /** @var int */
83
  protected $_dbNum;
84
 
85
+ protected $_config;
86
  protected $_compressionThreshold;
87
  protected $_compressionLib;
88
  protected $_logLevel;
89
  protected $_maxConcurrency;
90
  protected $_breakAfter;
91
+ protected $_useLocking;
 
92
  protected $_hasLock;
93
  protected $_sessionWritten; // avoid infinite loops
94
+ protected $_sessionWrites; // set expire time based on activity
95
+ protected $_maxLifetime;
96
+ protected $_minLifetime;
97
 
98
  static public $failedLockAttempts = 0; // for debug or informational purposes
99
 
100
  public function __construct()
101
  {
102
+ $this->_config = $config = Mage::getConfig()->getNode('global/redis_session');
103
+ if (!$config) {
104
+ $this->_useRedis = FALSE;
105
+ Mage::log('Redis configuration does not exist, falling back to MySQL handler.', Zend_Log::EMERG);
106
+ parent::__construct();
107
+ return;
108
+ }
109
+
110
+ $this->_logLevel = ((int) $config->descend('log_level') ?: self::DEFAULT_LOG_LEVEL);
111
+ if ($this->_logLevel >= Zend_Log::DEBUG) {
112
+ $timeStart = microtime(true);
 
 
 
 
 
113
  }
114
+
115
+ // Database config
116
+ $host = ((string) $config->descend('host') ?: '127.0.0.1');
117
+ $port = ((int) $config->descend('port') ?: '6379');
118
+ $pass = ((string) $config->descend('password') ?: '');
119
+ $timeout = ((float) $config->descend('timeout') ?: self::DEFAULT_TIMEOUT);
120
+ $persistent = ((string) $config->descend('persistent') ?: '');
121
+ $this->_dbNum = ((int) $config->descend('db') ?: 0);
122
+
123
+ // General config
124
+ $this->_compressionThreshold = ((int) $config->descend('compression_threshold') ?: self::DEFAULT_COMPRESSION_THRESHOLD);
125
+ $this->_compressionLib = ((string) $config->descend('compression_lib') ?: self::DEFAULT_COMPRESSION_LIB);
126
+ $this->_maxConcurrency = ((int) $config->descend('max_concurrency') ?: self::DEFAULT_MAX_CONCURRENCY);
127
+ $this->_breakAfter = ((float) $config->descend('break_after_'.session_name()) ?: self::DEFAULT_BREAK_AFTER);
128
+ $this->_maxLifetime = ((int) $config->descend('max_lifetime') ?: self::DEFAULT_MAX_LIFETIME);
129
+ $this->_minLifetime = ((int) $config->descend('min_lifetime') ?: self::DEFAULT_MIN_LIFETIME);
130
+ $this->_useLocking = defined('CM_REDISSESSION_LOCKING_ENABLED')
131
+ ? CM_REDISSESSION_LOCKING_ENABLED
132
+ : ! ((int) $config->descend('disable_locking') ?: self::DEFAULT_DISABLE_LOCKING);
133
+
134
+ // Use sleep time multiplier so break time is in seconds
135
+ $this->_breakAfter = (int) round((1000000 / self::SLEEP_TIME) * $this->_breakAfter);
136
+ $this->_failAfter = (int) round((1000000 / self::SLEEP_TIME) * self::FAIL_AFTER);
137
+
138
+ // Connect and authenticate
139
  $this->_redis = new Credis_Client($host, $port, $timeout, $persistent);
140
  if (!empty($pass)) {
141
  $this->_redis->auth($pass) or Zend_Cache::throwException('Unable to authenticate with the redis server.');
142
  }
143
  $this->_redis->setCloseOnDestruct(FALSE); // Destructor order cannot be predicted
144
  $this->_useRedis = TRUE;
145
+ if ($this->_logLevel >= Zend_Log::DEBUG) {
146
+ $this->_log(sprintf("%s initialized for connection to %s:%s after %.5f seconds",
147
+ get_class($this), $host, $port, (microtime(true) - $timeStart)
148
+ ));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  }
150
  }
151
 
152
+ /**
153
+ * @param $msg
154
+ * @param $level
155
+ */
156
+ protected function _log($msg, $level = Zend_Log::DEBUG)
157
+ {
158
+ Mage::log("{$this->_getPid()}: $msg", $level, self::LOG_FILE);
159
+ }
160
+
161
  /**
162
  * Check DB connection
163
  *
169
 
170
  try {
171
  $this->_redis->connect();
172
+ if ($this->_logLevel >= Zend_Log::DEBUG) {
173
+ $this->_log("Connected to Redis");
 
 
 
 
 
 
 
174
  }
175
  return TRUE;
176
  }
177
  catch (Exception $e) {
178
  Mage::logException($e);
179
  $this->_redis = NULL;
180
+ Mage::log('Unable to connect to Redis; falling back to MySQL handler', Zend_Log::EMERG);
 
 
 
 
 
 
 
 
181
 
182
  // Fall-back to MySQL handler. If this fails, the file handler will be used.
183
  $this->_useRedis = FALSE;
195
  public function read($sessionId)
196
  {
197
  if ( ! $this->_useRedis) return parent::read($sessionId);
198
+ Varien_Profiler::start(__METHOD__);
199
 
200
  // Get lock on session. Increment the "lock" field and if the new value is 1, we have the lock.
 
201
  $sessionId = self::SESSION_PREFIX.$sessionId;
202
  $tries = $waiting = $lock = 0;
203
+ $lockPid = $oldLockPid = NULL; // Restart waiting for lock when current lock holder changes
204
  $detectZombies = FALSE;
205
+ if ($this->_logLevel >= Zend_Log::WARN) {
206
+ $timeStart = microtime(true);
 
 
 
 
 
 
 
 
 
207
  }
208
+ if ($this->_logLevel >= Zend_Log::DEBUG) {
209
+ $this->_log(sprintf("Attempting to take lock on ID %s", $sessionId));
210
+ }
211
+ $this->_redis->select($this->_dbNum);
212
+ while ($this->_useLocking)
213
  {
214
  // Increment lock value for this session and retrieve the new value
215
  $oldLock = $lock;
216
  $lock = $this->_redis->hIncrBy($sessionId, 'lock', 1);
217
 
218
+ // Get the pid of the process that has the lock
219
+ if ($lock != 1 && $tries + 1 >= $this->_breakAfter) {
220
+ $lockPid = $this->_redis->hGet($sessionId, 'pid');
221
+ }
222
+
223
  // If we got the lock, update with our pid and reset lock and expiration
224
+ if ( $lock == 1 // We actually do have the lock
225
+ || (
226
+ $tries >= $this->_breakAfter // We are done waiting and want to start trying to break it
227
+ && $oldLockPid == $lockPid // Nobody else got the lock while we were waiting
228
+ )
229
+ ) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
  $this->_hasLock = TRUE;
231
  break;
232
  }
236
  $i = 0;
237
  do {
238
  $waiting = $this->_redis->hIncrBy($sessionId, 'wait', 1);
 
 
 
 
 
 
 
 
 
 
 
 
 
239
  } while (++$i < $this->_maxConcurrency && $waiting < 1);
240
  }
241
 
248
  && $lock + 1 < $oldLock + $waiting // lock should be old+waiting, otherwise there must be a dead process
249
  ) {
250
  // Reset session to fresh state
251
+ if ($this->_logLevel >= Zend_Log::INFO) {
252
+ $this->_log(sprintf(
253
+ "Detected zombie waiter after %.5f seconds for ID %s (%d waiting)\n %s (%s - %s)",
254
+ (microtime(true) - $timeStart),
255
+ $sessionId, $waiting,
256
+ Mage::app()->getRequest()->getRequestUri(), Mage::app()->getRequest()->getClientIp(), Mage::app()->getRequest()->getHeader('User-Agent')
257
+ ), Zend_Log::INFO);
 
 
 
 
258
  }
259
  $waiting = $this->_redis->hIncrBy($sessionId, 'wait', -1);
260
  continue;
267
  $this->_redis->hIncrBy($sessionId, 'wait', -1);
268
  $this->_sessionWritten = TRUE; // Prevent session from getting written
269
  $writes = $this->_redis->hGet($sessionId, 'writes');
270
+ if ($this->_logLevel >= Zend_Log::WARN) {
271
+ $this->_log(sprintf(
272
+ "Session concurrency exceeded for ID %s; displaying HTTP 503 (%s waiting, %s total requests)\n %s (%s - %s)",
273
+ $sessionId, $waiting, $writes,
274
+ Mage::app()->getRequest()->getRequestUri(), Mage::app()->getRequest()->getClientIp(), Mage::app()->getRequest()->getHeader('User-Agent')
275
+ ), Zend_Log::WARN);
 
 
 
 
276
  }
277
+ $this->_logLevel = -1; // Disable further logging
278
  require_once(Mage::getBaseDir() . DS . 'errors' . DS . '503.php');
279
  exit;
280
  }
281
  }
282
 
283
  $tries++;
284
+ $oldLockPid = $lockPid;
285
+ $sleepTime = self::SLEEP_TIME;
286
 
287
+ // Detect dead lock waiters
288
+ if ($tries % self::DETECT_ZOMBIES == 1) {
289
  $detectZombies = TRUE;
290
+ $sleepTime += 10000; // sleep + 0.01 seconds
 
291
  }
292
+ // Detect dead lock holder every 10 seconds (only works on same node as lock holder)
293
  if ($tries % self::DETECT_ZOMBIES == 0) {
294
+ Varien_Profiler::start(__METHOD__.'-detect-zombies');
295
+ if ($this->_logLevel >= Zend_Log::DEBUG) {
296
+ $this->_log(sprintf(
297
+ "Checking for zombies after %.5f seconds of waiting...", (microtime(true) - $timeStart)
298
+ ));
 
 
 
 
299
  }
300
  $pid = $this->_redis->hGet($sessionId, 'pid');
301
  if ($pid && ! $this->_pidExists($pid)) {
302
  // Allow a live process to get the lock
303
  $this->_redis->hSet($sessionId, 'lock', 0);
304
+ if ($this->_logLevel >= Zend_Log::INFO) {
305
+ $this->_log(sprintf(
306
+ "Detected zombie process (%s) for %s (%s waiting)\n %s (%s - %s)",
307
+ $pid, $sessionId, $waiting,
308
+ Mage::app()->getRequest()->getRequestUri(), Mage::app()->getRequest()->getClientIp(), Mage::app()->getRequest()->getHeader('User-Agent')
309
+ ), Zend_Log::INFO);
 
 
 
 
 
 
310
  }
311
+ Varien_Profiler::stop(__METHOD__.'-detect-zombies');
312
  continue;
313
  }
314
+ Varien_Profiler::stop(__METHOD__.'-detect-zombies');
315
  }
316
  // Timeout
317
+ if ($tries >= $this->_breakAfter + $this->_failAfter) {
318
  $this->_hasLock = FALSE;
319
+ if ($this->_logLevel >= Zend_Log::NOTICE) {
320
+ $this->_log(sprintf("Giving up on read lock for ID %s after %.5f seconds (%d attempts)", $sessionId, (microtime(true) - $timeStart), $tries), Zend_Log::NOTICE);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321
  }
322
  break;
323
  }
324
  else {
325
+ if ($this->_logLevel >= Zend_Log::DEBUG) {
326
+ $this->_log(sprintf(
327
+ "Waiting %.2f seconds for lock on ID %s (%d tries, lock pid is %s, %.5f seconds elapsed)",
328
+ $sleepTime / 1000000, $sessionId, $tries, $lockPid, (microtime(true) - $timeStart)
329
+ ));
330
+ }
331
+ Varien_Profiler::start(__METHOD__.'-wait');
332
+ usleep($sleepTime);
333
+ Varien_Profiler::stop(__METHOD__.'-wait');
334
  }
335
  }
336
  self::$failedLockAttempts = $tries;
337
 
338
+ // Session can be read even if it was not locked by this pid!
339
+ if ($this->_logLevel >= Zend_Log::DEBUG) {
340
+ $timeStart = microtime(true);
341
+ }
342
+ list($sessionData, $sessionWrites) = $this->_redis->hMGet($sessionId, array('data','writes'));
343
+ Varien_Profiler::stop(__METHOD__);
344
+ if ($this->_logLevel >= Zend_Log::DEBUG) {
345
+ $this->_log(sprintf("Data read for ID %s in %.5f seconds", $sessionId, (microtime(true) - $timeStart)));
346
+ }
347
+ $this->_sessionWrites = (int) $sessionWrites;
348
+
349
+ $this->_redis->pipeline();
350
+
351
  // This process is no longer waiting for a lock
352
  if ($tries > 0) {
353
  $this->_redis->hIncrBy($sessionId, 'wait', -1);
354
  }
355
 
356
+ // This process has the lock, save the pid
357
+ if ($this->_hasLock) {
358
+ $setData = array(
359
+ 'pid' => $this->_getPid(),
360
+ 'lock' => 1,
 
 
 
 
 
 
361
  );
362
+
363
+ // Save request data in session so if a lock is broken we can know which page it was for debugging
364
+ if ($this->_logLevel >= Zend_Log::INFO) {
365
+ if (empty($_SERVER['REQUEST_METHOD'])) {
366
+ $setData['req'] = $_SERVER['SCRIPT_NAME'];
367
+ } else {
368
+ $setData['req'] = "{$_SERVER['REQUEST_METHOD']} {$_SERVER['SERVER_NAME']}{$_SERVER['REQUEST_URI']}";
369
+ }
370
+ if ($lock != 1) {
371
+ $this->_log(sprintf(
372
+ "Successfully broke lock for ID %s after %.5f seconds (%d attempts). Lock: %d\nLast request of broken lock: %s",
373
+ $sessionId, (microtime(true) - $timeStart), $tries, $lock, $this->_redis->hGet($sessionId, 'req')
374
+ ), Zend_Log::INFO);
375
+ }
376
+ }
377
+ $this->_redis->hMSet($sessionId, $setData);
378
  }
379
+
380
+ // Set session expiration
381
+ $this->_redis->expire($sessionId, min($this->getLifeTime(), $this->_maxLifetime));
382
+ $this->_redis->exec();
383
+
384
+ // Reset flag in case of multiple session read/write operations
385
+ $this->_sessionWritten = FALSE;
386
+
387
  return $sessionData ? $this->_decodeData($sessionData) : '';
388
  }
389
 
396
  */
397
  public function write($sessionId, $sessionData)
398
  {
399
+ Varien_Profiler::start(__METHOD__);
400
  if ( ! $this->_useRedis) return parent::write($sessionId, $sessionData);
401
  if ($this->_sessionWritten) {
402
+ if ($this->_logLevel >= Zend_Log::DEBUG) {
403
+ $this->_log(sprintf("Repeated session write detected; skipping for ID %s", $sessionId));
 
 
 
 
 
 
 
404
  }
405
+ Varien_Profiler::stop(__METHOD__);
406
  return TRUE;
407
  }
408
  $this->_sessionWritten = TRUE;
409
+ if ($this->_logLevel >= Zend_Log::WARN) {
410
+ $timeStart = microtime(true);
 
 
 
 
 
 
 
 
 
411
  }
412
 
413
  // Do not overwrite the session if it is locked by another pid
414
  try {
415
  if($this->_dbNum) $this->_redis->select($this->_dbNum); // Prevent conflicts with other connections?
416
+
417
+ if ( ! $this->_useLocking
418
+ || ( ! ($pid = $this->_redis->hGet('sess_'.$sessionId, 'pid')) || $pid == $this->_getPid())
419
+ ) {
 
 
 
 
 
 
 
 
420
  $this->_writeRawSession($sessionId, $sessionData, $this->getLifeTime());
421
+ if ($this->_logLevel >= Zend_Log::DEBUG) {
422
+ $this->_log(sprintf("Data written to ID %s in %.5f seconds", $sessionId, (microtime(true) - $timeStart)));
 
 
 
 
 
 
 
 
423
  }
424
  }
425
  else {
426
+ if ($this->_logLevel >= Zend_Log::WARN) {
427
  if ($this->_hasLock) {
428
+ $this->_log(sprintf("Did not write session for ID %s: another process took the lock.",
429
+ $sessionId
430
+ ), Zend_Log::WARN);
 
 
 
 
 
 
431
  } else {
432
+ $this->_log(sprintf("Did not write session for ID %s: unable to acquire lock.",
433
+ $sessionId
434
+ ), Zend_Log::WARN);
 
 
 
 
 
 
435
  }
436
  }
437
  }
442
  } else {
443
  error_log("$e");
444
  }
445
+ Varien_Profiler::stop(__METHOD__);
446
  return FALSE;
447
  }
448
+ Varien_Profiler::stop(__METHOD__);
449
  return TRUE;
450
  }
451
 
458
  public function destroy($sessionId)
459
  {
460
  if ( ! $this->_useRedis) return parent::destroy($sessionId);
461
+ Varien_Profiler::start(__METHOD__);
462
 
463
+ if ($this->_logLevel >= Zend_Log::DEBUG) {
464
+ $this->_log(sprintf("Destroying ID %s", $sessionId));
 
 
 
 
 
 
 
465
  }
466
  $this->_redis->pipeline();
467
  if($this->_dbNum) $this->_redis->select($this->_dbNum);
468
  $this->_redis->del(self::SESSION_PREFIX.$sessionId);
469
  $this->_redis->exec();
470
+ Varien_Profiler::stop(__METHOD__);
471
  return TRUE;
472
  }
473
 
479
  public function close()
480
  {
481
  if ( ! $this->_useRedis) return parent::close();
482
+ if ($this->_logLevel >= Zend_Log::DEBUG) {
483
+ $this->_log("Closing connection");
 
 
 
 
 
 
484
  }
485
  if ($this->_redis) $this->_redis->close();
486
  return TRUE;
503
  */
504
  public function getLifeTime()
505
  {
506
+ if ( ! $this->_config) return parent::getLifeTime();
507
+
508
+ if ($this->_lifeTime === NULL) {
509
+ $lifeTime = NULL;
510
+
511
+ // Detect bots by user agent
512
+ $botLifetime = ((int) $this->_config->descend('bot_lifetime') ?: self::DEFAULT_BOT_LIFETIME);
513
+ if ($botLifetime) {
514
+ $userAgent = empty($_SERVER['HTTP_USER_AGENT']) ? FALSE : $_SERVER['HTTP_USER_AGENT'];
515
+ $isBot = ! $userAgent || preg_match(self::BOT_REGEX, $userAgent);
516
+ if ($isBot) {
517
+ if ($this->_logLevel > Zend_Log::DEBUG) {
518
+ $this->_log(sprintf("Bot detected for user agent: %s", $userAgent));
519
+ }
520
+ if ( $this->_sessionWrites <= 1
521
+ && ($botFirstLifetime = ((int) $this->_config->descend('bot_first_lifetime') ?: self::DEFAULT_BOT_FIRST_LIFETIME))
522
+ ) {
523
+ $lifeTime = $botFirstLifetime * (1+$this->_sessionWrites);
524
+ } else {
525
+ $lifeTime = min(parent::getLifeTime(), $botLifetime);
526
+ }
527
+ }
528
+ }
529
+
530
+ // Use different lifetime for first write
531
+ if ($lifeTime === NULL && $this->_sessionWrites <= 1) {
532
+ $firstLifetime = ((int) $this->_config->descend('first_lifetime') ?: self::DEFAULT_FIRST_LIFETIME);
533
+ if ($firstLifetime) {
534
+ $lifeTime = $firstLifetime * (1+$this->_sessionWrites);
535
+ }
536
+ }
537
+
538
+ // Neither bot nor first write
539
+ if ($lifeTime === NULL) {
540
+ $lifeTime = parent::getLifeTime();
541
+ }
542
+
543
+ $this->_lifeTime = $lifeTime;
544
+ if ($this->_lifeTime < $this->_minLifetime) {
545
+ $this->_lifeTime = $this->_minLifetime;
546
+ }
547
+ if ($this->_lifeTime > $this->_maxLifetime) {
548
+ $this->_lifeTime = $this->_maxLifetime;
549
+ }
550
  }
551
+ return $this->_lifeTime;
552
  }
553
 
554
  /**
559
  */
560
  public function _encodeData($data)
561
  {
562
+ Varien_Profiler::start(__METHOD__);
563
  $originalDataSize = strlen($data);
564
  if ($this->_compressionThreshold > 0 && $this->_compressionLib != 'none' && $originalDataSize >= $this->_compressionThreshold) {
565
+ if ($this->_logLevel >= Zend_Log::DEBUG) {
566
+ $this->_log(sprintf("Compressing %s bytes with %s", $originalDataSize,$this->_compressionLib));
567
+ $timeStart = microtime(true);
 
 
 
 
 
 
 
 
 
568
  }
569
+ $prefix = ':'.substr($this->_compressionLib,0,2).':';
570
  switch($this->_compressionLib) {
571
  case 'snappy': $data = snappy_compress($data); break;
572
  case 'lzf': $data = lzf_compress($data); break;
573
+ case 'lz4': $data = lz4_compress($data); $prefix = ':l4:'; break;
574
  case 'gzip': $data = gzcompress($data, 1); break;
575
  }
576
  if($data) {
577
+ $data = $prefix.$data;
578
+ if ($this->_logLevel >= Zend_Log::DEBUG) {
579
+ $this->_log(sprintf("Data compressed by %.1f percent in %.5f seconds",
580
+ ($originalDataSize == 0 ? 0 : (100 - (strlen($data) / $originalDataSize * 100))), (microtime(true) - $timeStart)
581
+ ));
 
 
 
 
 
 
582
  }
583
+ } else if ($this->_logLevel >= Zend_Log::WARN) {
584
+ $this->_log(sprintf("Could not compress session data using %s", $this->_compressionLib), Zend_Log::WARN);
 
 
 
 
 
 
 
585
  }
586
  }
587
+ Varien_Profiler::stop(__METHOD__);
588
  return $data;
589
  }
590
 
596
  */
597
  public function _decodeData($data)
598
  {
599
+ Varien_Profiler::start(__METHOD__);
600
  switch (substr($data,0,4)) {
601
  // asking the data which library it uses allows for transparent changes of libraries
602
+ case ':sn:': $data = snappy_uncompress(substr($data,4)); break;
603
+ case ':lz:': $data = lzf_decompress(substr($data,4)); break;
604
+ case ':l4:': $data = lz4_uncompress(substr($data,4)); break;
605
+ case ':gz:': $data = gzuncompress(substr($data,4)); break;
606
  }
607
+ Varien_Profiler::stop(__METHOD__);
608
  return $data;
609
  }
610
 
629
  'data' => $this->_encodeData($data),
630
  'lock' => 0, // 0 so that next lock attempt will get 1
631
  ))
632
+ ->hIncrBy($sessionId, 'writes', 1)
633
+ ->expire($sessionId, min($lifetime, $this->_maxLifetime))
634
  ->exec();
635
  }
636
 
645
  throw new Exception('Not connected to redis!');
646
  }
647
 
648
+ $sessionId = strpos($id, 'sess_') === 0 ? $id : 'sess_' . $id;
649
  $this->_redis->select($this->_dbNum);
650
  $data = $this->_redis->hGetAll($sessionId);
651
  if ($data && isset($data['data'])) {
654
  return $data;
655
  }
656
 
657
+ /**
658
+ * Public for testing/inspection purposes only.
659
+ *
660
+ * @param $forceStandalone
661
+ * @return Credis_Client
662
+ */
663
+ public function _redisClient($forceStandalone)
664
+ {
665
+ if ($forceStandalone) {
666
+ $this->_redis->forceStandalone();
667
+ }
668
+ if ($this->_dbNum) {
669
+ $this->_redis->select($this->_dbNum);
670
+ }
671
+ return $this->_redis;
672
+ }
673
+
674
  /**
675
  * @return string
676
  */
app/code/community/Cm/RedisSession/etc/config.xml CHANGED
@@ -1,3 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  <config>
2
  <modules>
3
  <Cm_RedisSession>
@@ -13,4 +45,4 @@
13
  </core_mysql4>
14
  </models>
15
  </global>
16
- </config>
1
+ <?xml version="1.0"?>
2
+ <!--
3
+ /*
4
+ ==New BSD License==
5
+
6
+ Copyright (c) 2013, Colin Mollenhour
7
+ All rights reserved.
8
+
9
+ Redistribution and use in source and binary forms, with or without
10
+ modification, are permitted provided that the following conditions are met:
11
+
12
+ * Redistributions of source code must retain the above copyright
13
+ notice, this list of conditions and the following disclaimer.
14
+ * Redistributions in binary form must reproduce the above copyright
15
+ notice, this list of conditions and the following disclaimer in the
16
+ documentation and/or other materials provided with the distribution.
17
+ * The name of Colin Mollenhour may not be used to endorse or promote products
18
+ derived from this software without specific prior written permission.
19
+ * The module name must remain Cm_RedisSession.
20
+
21
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
22
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
23
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
24
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
25
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
26
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
27
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
28
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31
+ */
32
+ -->
33
  <config>
34
  <modules>
35
  <Cm_RedisSession>
45
  </core_mysql4>
46
  </models>
47
  </global>
48
+ </config>
package.xml CHANGED
@@ -1,18 +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>
1
  <?xml version="1.0"?>
2
  <package>
3
  <name>Cm_RedisSession</name>
4
+ <version>1.9.2.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.9.2.0</notes>
12
  <authors><author><name>Colin Mollenhour</name><user>core</user><email>colin@mollenhour.com</email></author></authors>
13
+ <date>2015-06-26</date>
14
+ <time>13:48:50</time>
15
+ <contents><target name="magecommunity"><dir name="Cm"><dir name="RedisSession"><dir name="Model"><file name="Session.php" hash="8a751a32ac85da38237012f6c4ac9077"/></dir><dir name="etc"><file name="config.xml" hash="6b32aa3b331c4504c44a7c01a24a0d3d"/></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.9.2.0</min><max>1.9.2.0</max></package></required></dependencies>
18
  </package>