public function testHashes() { $this->assertEquals(1, $this->credis->hSet('hash', 'field1', 'foo')); $this->assertEquals(0, $this->credis->hSet('hash', 'field1', 'foo')); $this->assertEquals('foo', $this->credis->hGet('hash', 'field1')); $this->assertEquals(NULL, $this->credis->hGet('hash', 'x')); $this->assertTrue($this->credis->hMSet('hash', array('field2' => 'Hello', 'field3' => 'World'))); $this->assertEquals(array('foo', 'Hello', FALSE), $this->credis->hMGet('hash', array('field1', 'field2', 'nilfield'))); $this->assertEquals(array(), $this->credis->hGetAll('nohash')); $this->assertEquals(array('field1' => 'foo', 'field2' => 'Hello', 'field3' => 'World'), $this->credis->hGetAll('hash')); }
public function testHashes() { $this->assertEquals(1, $this->credis->hSet('hash', 'field1', 'foo')); $this->assertEquals(0, $this->credis->hSet('hash', 'field1', 'foo')); $this->assertEquals('foo', $this->credis->hGet('hash', 'field1')); $this->assertEquals(NULL, $this->credis->hGet('hash', 'x')); $this->assertTrue($this->credis->hMSet('hash', array('field2' => 'Hello', 'field3' => 'World'))); $this->assertEquals(array('foo', 'Hello', FALSE), $this->credis->hMGet('hash', array('field1', 'field2', 'nilfield'))); $this->assertEquals(array(), $this->credis->hGetAll('nohash')); $this->assertEquals(array('field1' => 'foo', 'field2' => 'Hello', 'field3' => 'World'), $this->credis->hGetAll('hash')); // Test long hash values $longString = str_repeat(md5('asd'), 4096); // 128k (redis.h REDIS_INLINE_MAX_SIZE = 64k) $this->assertEquals(1, $this->credis->hMSet('long_hash', array('count' => 1, 'data' => $longString)), 'Set long hash value'); $this->assertEquals($longString, $this->credis->hGet('long_hash', 'data'), 'Get long hash value'); }
/** * Save some string datas into a cache record * * Note : $data is always "string" (serialization is done by the * core not by the backend) * * @param string $data Datas to cache * @param string $id Cache id * @param array $tags Array of strings, the cache record will be tagged by each string entry * @param bool|int $specificLifetime If != false, set a specific lifetime for this cache record (null => infinite lifetime) * @throws CredisException * @return boolean True if no problem */ public function save($data, $id, $tags = array(), $specificLifetime = false) { if (!is_array($tags)) { $tags = $tags ? array($tags) : array(); } $lifetime = $this->getLifetime($specificLifetime); // Get list of tags previously assigned $oldTags = $this->_decodeData($this->_redis->hGet(self::PREFIX_KEY . $id, self::FIELD_TAGS)); $oldTags = $oldTags ? explode(',', $oldTags) : array(); $this->_redis->pipeline()->multi(); // Set the data $result = $this->_redis->hMSet(self::PREFIX_KEY . $id, array(self::FIELD_DATA => $this->_encodeData($data, $this->_compressData), self::FIELD_TAGS => $this->_encodeData(implode(',', $tags), $this->_compressTags), self::FIELD_MTIME => time(), self::FIELD_INF => $lifetime ? 0 : 1)); if (!$result) { throw new CredisException("Could not set cache key {$id}"); } // Set expiration if specified if ($lifetime) { $this->_redis->expire(self::PREFIX_KEY . $id, min($lifetime, self::MAX_LIFETIME)); } // Process added tags if ($tags) { // Update the list with all the tags $this->_redis->sAdd(self::SET_TAGS, $tags); // Update the id list for each tag foreach ($tags as $tag) { $this->_redis->sAdd(self::PREFIX_TAG_IDS . $tag, $id); } } // Process removed tags if ($remTags = $oldTags ? array_diff($oldTags, $tags) : FALSE) { // Update the id list for each tag foreach ($remTags as $tag) { $this->_redis->sRem(self::PREFIX_TAG_IDS . $tag, $id); } } // Update the list with all the ids if ($this->_notMatchingTags) { $this->_redis->sAdd(self::SET_IDS, $id); } $this->_redis->exec(); return TRUE; }
/** * Fetch session data * * @param string $sessionId * @return string */ public function read($sessionId) { $this->profilerStart(__METHOD__); // Get lock on session. Increment the "lock" field and if the new value is 1, we have the lock. $sessionId = self::SESSION_PREFIX . $sessionId; $tries = $waiting = $lock = 0; $lockPid = $oldLockPid = NULL; // Restart waiting for lock when current lock holder changes $detectZombies = FALSE; $breakAfter = $this->_getBreakAfter(); if ($this->_logLevel >= \Zend_Log::WARN) { $timeStart = microtime(true); } if ($this->_logLevel >= \Zend_Log::DEBUG) { $this->_log(sprintf("Attempting to take lock on ID %s", $sessionId)); } $this->_redis->select($this->_dbNum); while ($this->_useLocking) { // Increment lock value for this session and retrieve the new value $oldLock = $lock; $lock = $this->_redis->hIncrBy($sessionId, 'lock', 1); // Get the pid of the process that has the lock if ($lock != 1 && $tries + 1 >= $breakAfter) { $lockPid = $this->_redis->hGet($sessionId, 'pid'); } // If we got the lock, update with our pid and reset lock and expiration if ($lock == 1 || $tries >= $breakAfter && $oldLockPid == $lockPid) { $this->_hasLock = TRUE; break; } else { if (!$waiting) { $i = 0; do { $waiting = $this->_redis->hIncrBy($sessionId, 'wait', 1); } while (++$i < $this->_maxConcurrency && $waiting < 1); } else { // Detect broken sessions (e.g. caused by fatal errors) if ($detectZombies) { $detectZombies = FALSE; if ($lock > $oldLock && $lock + 1 < $oldLock + $waiting) { // Reset session to fresh state if ($this->_logLevel >= \Zend_Log::INFO) { $this->_log(sprintf("Detected zombie waiter after %.5f seconds for ID %s (%d waiting)\n %s (%s - %s)", microtime(true) - $timeStart, $sessionId, $waiting, Mage::app()->getRequest()->getRequestUri(), Mage::app()->getRequest()->getClientIp(), Mage::app()->getRequest()->getHeader('User-Agent')), \Zend_Log::INFO); } $waiting = $this->_redis->hIncrBy($sessionId, 'wait', -1); continue; } } // Limit concurrent lock waiters to prevent server resource hogging if ($waiting >= $this->_maxConcurrency) { // Overloaded sessions get 503 errors $this->_redis->hIncrBy($sessionId, 'wait', -1); $this->_sessionWritten = TRUE; // Prevent session from getting written $writes = $this->_redis->hGet($sessionId, 'writes'); if ($this->_logLevel >= \Zend_Log::WARN) { $this->_log(sprintf("Session concurrency exceeded for ID %s; displaying HTTP 503 (%s waiting, %s total requests)\n %s (%s - %s)", $sessionId, $waiting, $writes, Mage::app()->getRequest()->getRequestUri(), Mage::app()->getRequest()->getClientIp(), Mage::app()->getRequest()->getHeader('User-Agent')), \Zend_Log::WARN); } $this->_logLevel = -1; // Disable further logging require_once Mage::getBaseDir() . DS . 'errors' . DS . '503.php'; exit; } } } $tries++; $oldLockPid = $lockPid; $sleepTime = self::SLEEP_TIME; // Detect dead lock waiters if ($tries % self::DETECT_ZOMBIES == 1) { $detectZombies = TRUE; $sleepTime += 10000; // sleep + 0.01 seconds } // Detect dead lock holder every 10 seconds (only works on same node as lock holder) if ($tries % self::DETECT_ZOMBIES == 0) { $this->profilerStart(__METHOD__ . '-detect-zombies'); if ($this->_logLevel >= \Zend_Log::DEBUG) { $this->_log(sprintf("Checking for zombies after %.5f seconds of waiting...", microtime(true) - $timeStart)); } $pid = $this->_redis->hGet($sessionId, 'pid'); if ($pid && !$this->_pidExists($pid)) { // Allow a live process to get the lock $this->_redis->hSet($sessionId, 'lock', 0); if ($this->_logLevel >= \Zend_Log::INFO) { $this->_log(sprintf("Detected zombie process (%s) for %s (%s waiting)\n %s (%s - %s)", $pid, $sessionId, $waiting, Mage::app()->getRequest()->getRequestUri(), Mage::app()->getRequest()->getClientIp(), Mage::app()->getRequest()->getHeader('User-Agent')), \Zend_Log::INFO); } $this->profilerStop(__METHOD__ . '-detect-zombies'); continue; } $this->profilerStop(__METHOD__ . '-detect-zombies'); } // Timeout if ($tries >= $breakAfter + $this->_failAfter) { $this->_hasLock = FALSE; if ($this->_logLevel >= \Zend_Log::NOTICE) { $this->_log(sprintf("Giving up on read lock for ID %s after %.5f seconds (%d attempts)", $sessionId, microtime(true) - $timeStart, $tries), \Zend_Log::NOTICE); } break; } else { if ($this->_logLevel >= \Zend_Log::DEBUG) { $this->_log(sprintf("Waiting %.2f seconds for lock on ID %s (%d tries, lock pid is %s, %.5f seconds elapsed)", $sleepTime / 1000000, $sessionId, $tries, $lockPid, microtime(true) - $timeStart)); } $this->profilerStart(__METHOD__ . '-wait'); usleep($sleepTime); $this->profilerStop(__METHOD__ . '-wait'); } } self::$failedLockAttempts = $tries; // Session can be read even if it was not locked by this pid! if ($this->_logLevel >= \Zend_Log::DEBUG) { $timeStart = microtime(true); } list($sessionData, $sessionWrites) = $this->_redis->hMGet($sessionId, array('data', 'writes')); $this->profilerStop(__METHOD__); if ($this->_logLevel >= \Zend_Log::DEBUG) { $this->_log(sprintf("Data read for ID %s in %.5f seconds", $sessionId, microtime(true) - $timeStart)); } $this->_sessionWrites = (int) $sessionWrites; // This process is no longer waiting for a lock if ($tries > 0) { $this->_redis->hIncrBy($sessionId, 'wait', -1); } // This process has the lock, save the pid if ($this->_hasLock) { $setData = array('pid' => $this->_getPid(), 'lock' => 1); // Save request data in session so if a lock is broken we can know which page it was for debugging if ($this->_logLevel >= \Zend_Log::INFO) { if (empty($_SERVER['REQUEST_METHOD'])) { $setData['req'] = $_SERVER['SCRIPT_NAME']; } else { $setData['req'] = "{$_SERVER['REQUEST_METHOD']} {$_SERVER['SERVER_NAME']}{$_SERVER['REQUEST_URI']}"; } if ($lock != 1) { $this->_log(sprintf("Successfully broke lock for ID %s after %.5f seconds (%d attempts). Lock: %d\nLast request of broken lock: %s", $sessionId, microtime(true) - $timeStart, $tries, $lock, $this->_redis->hGet($sessionId, 'req')), \Zend_Log::INFO); } } } // Set session data and expiration $this->_redis->pipeline(); if (!empty($setData)) { $this->_redis->hMSet($sessionId, $setData); } $this->_redis->expire($sessionId, min($this->getLifeTime(), $this->_maxLifetime)); $this->_redis->exec(); // Reset flag in case of multiple session read/write operations $this->_sessionWritten = FALSE; return $sessionData ? $this->_decodeData($sessionData) : ''; }
/** * Fetch session data * * @param string $sessionId * @return string * @throws ConcurrentConnectionsExceededException */ public function read($sessionId) { // Get lock on session. Increment the "lock" field and if the new value is 1, we have the lock. $sessionId = self::SESSION_PREFIX . $sessionId; $tries = $waiting = $lock = 0; $lockPid = $oldLockPid = null; // Restart waiting for lock when current lock holder changes $detectZombies = false; $breakAfter = $this->_getBreakAfter(); $timeStart = microtime(true); $this->_log(sprintf("Attempting to take lock on ID %s", $sessionId)); $this->_redis->select($this->_dbNum); while ($this->_useLocking) { // Increment lock value for this session and retrieve the new value $oldLock = $lock; $lock = $this->_redis->hIncrBy($sessionId, 'lock', 1); // Get the pid of the process that has the lock if ($lock != 1 && $tries + 1 >= $breakAfter) { $lockPid = $this->_redis->hGet($sessionId, 'pid'); } // If we got the lock, update with our pid and reset lock and expiration if ($lock == 1 || $tries >= $breakAfter && $oldLockPid == $lockPid) { $this->_hasLock = true; break; } else { if (!$waiting) { $i = 0; do { $waiting = $this->_redis->hIncrBy($sessionId, 'wait', 1); } while (++$i < $this->_maxConcurrency && $waiting < 1); } else { // Detect broken sessions (e.g. caused by fatal errors) if ($detectZombies) { $detectZombies = false; // Lock shouldn't be less than old lock (another process broke the lock) if ($lock > $oldLock && $lock + 1 < $oldLock + $waiting) { // Reset session to fresh state $this->_log(sprintf("Detected zombie waiter after %.5f seconds for ID %s (%d waiting)", microtime(true) - $timeStart, $sessionId, $waiting), LoggerInterface::INFO); $waiting = $this->_redis->hIncrBy($sessionId, 'wait', -1); continue; } } // Limit concurrent lock waiters to prevent server resource hogging if ($waiting >= $this->_maxConcurrency) { // Overloaded sessions get 503 errors $this->_redis->hIncrBy($sessionId, 'wait', -1); $this->_sessionWritten = true; // Prevent session from getting written $writes = $this->_redis->hGet($sessionId, 'writes'); $this->_log(sprintf('Session concurrency exceeded for ID %s; displaying HTTP 503 (%s waiting, %s total ' . 'requests)', $sessionId, $waiting, $writes), LoggerInterface::WARNING); throw new ConcurrentConnectionsExceededException(); } } } $tries++; $oldLockPid = $lockPid; $sleepTime = self::SLEEP_TIME; // Detect dead lock waiters if ($tries % self::DETECT_ZOMBIES == 1) { $detectZombies = true; $sleepTime += 10000; // sleep + 0.01 seconds } // Detect dead lock holder every 10 seconds (only works on same node as lock holder) if ($tries % self::DETECT_ZOMBIES == 0) { $this->_log(sprintf("Checking for zombies after %.5f seconds of waiting...", microtime(true) - $timeStart)); $pid = $this->_redis->hGet($sessionId, 'pid'); if ($pid && !$this->_pidExists($pid)) { // Allow a live process to get the lock $this->_redis->hSet($sessionId, 'lock', 0); $this->_log(sprintf("Detected zombie process (%s) for %s (%s waiting)", $pid, $sessionId, $waiting), LoggerInterface::INFO); continue; } } // Timeout if ($tries >= $breakAfter + $this->_failAfter) { $this->_hasLock = false; $this->_log(sprintf('Giving up on read lock for ID %s after %.5f seconds (%d attempts)', $sessionId, microtime(true) - $timeStart, $tries), LoggerInterface::NOTICE); break; } else { $this->_log(sprintf("Waiting %.2f seconds for lock on ID %s (%d tries, lock pid is %s, %.5f seconds elapsed)", $sleepTime / 1000000, $sessionId, $tries, $lockPid, microtime(true) - $timeStart)); usleep($sleepTime); } } $this->failedLockAttempts = $tries; // Session can be read even if it was not locked by this pid! $timeStart2 = microtime(true); list($sessionData, $sessionWrites) = $this->_redis->hMGet($sessionId, array('data', 'writes')); $this->_log(sprintf("Data read for ID %s in %.5f seconds", $sessionId, microtime(true) - $timeStart2)); $this->_sessionWrites = (int) $sessionWrites; // This process is no longer waiting for a lock if ($tries > 0) { $this->_redis->hIncrBy($sessionId, 'wait', -1); } // This process has the lock, save the pid if ($this->_hasLock) { $setData = array('pid' => $this->_getPid(), 'lock' => 1); // Save request data in session so if a lock is broken we can know which page it was for debugging if (empty($_SERVER['REQUEST_METHOD'])) { $setData['req'] = $_SERVER['SCRIPT_NAME']; } else { $setData['req'] = "{$_SERVER['REQUEST_METHOD']} {$_SERVER['SERVER_NAME']}{$_SERVER['REQUEST_URI']}"; } if ($lock != 1) { $this->_log(sprintf("Successfully broke lock for ID %s after %.5f seconds (%d attempts). Lock: %d\nLast request of '\n . 'broken lock: %s", $sessionId, microtime(true) - $timeStart, $tries, $lock, $this->_redis->hGet($sessionId, 'req')), LoggerInterface::INFO); } } // Set session data and expiration $this->_redis->pipeline(); if (!empty($setData)) { $this->_redis->hMSet($sessionId, $setData); } $this->_redis->expire($sessionId, 3600 * 6); // Expiration will be set to correct value when session is written $this->_redis->exec(); // Reset flag in case of multiple session read/write operations $this->_sessionWritten = false; return $sessionData ? (string) $this->_decodeData($sessionData) : ''; }