From 86ebe5bcb92fe7551e3a461ba4c4815120ab09a0 Mon Sep 17 00:00:00 2001 From: Andrej Hudec Date: Sun, 26 Feb 2012 01:09:51 +0100 Subject: [PATCH] Redis Profiler Storage fixed typo and tests - updated profiler tests - added testPurge() method - fixed find() method --- .../FrameworkExtension.php | 1 + .../Profiler/RedisProfilerStorage.php | 365 ++++++++++++++++++ .../Profiler/AbstractProfilerStorageTest.php | 26 ++ .../Profiler/RedisProfilerStorageTest.php | 63 +++ 4 files changed, 455 insertions(+) create mode 100644 src/Symfony/Component/HttpKernel/Profiler/RedisProfilerStorage.php create mode 100644 tests/Symfony/Tests/Component/HttpKernel/Profiler/RedisProfilerStorageTest.php diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 69364ea5de82..5a76df3b492a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -208,6 +208,7 @@ private function registerProfilerConfiguration(array $config, ContainerBuilder $ 'mongodb' => 'Symfony\Component\HttpKernel\Profiler\MongoDbProfilerStorage', 'memcache' => 'Symfony\Component\HttpKernel\Profiler\MemcacheProfilerStorage', 'memcached' => 'Symfony\Component\HttpKernel\Profiler\MemcachedProfilerStorage', + 'redis' => 'Symfony\Component\HttpKernel\Profiler\RedisProfilerStorage', ); list($class, ) = explode(':', $config['dsn'], 2); if (!isset($supported[$class])) { diff --git a/src/Symfony/Component/HttpKernel/Profiler/RedisProfilerStorage.php b/src/Symfony/Component/HttpKernel/Profiler/RedisProfilerStorage.php new file mode 100644 index 000000000000..34a2185c56a2 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Profiler/RedisProfilerStorage.php @@ -0,0 +1,365 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Profiler; + +use Redis; + +/** + * RedisProfilerStorage stores profiling information in a Redis. + * + * @author Andrej Hudec + */ +class RedisProfilerStorage implements ProfilerStorageInterface +{ + const TOKEN_PREFIX = 'sf_profiler_'; + + protected $dsn; + protected $lifetime; + + /** + * @var Redis + */ + private $redis; + + /** + * Constructor. + * + * @param string $dsn A data source name + * @param string $username + * @param string $password + * @param int $lifetime The lifetime to use for the purge + */ + public function __construct($dsn, $username = '', $password = '', $lifetime = 86400) + { + $this->dsn = $dsn; + $this->lifetime = (int) $lifetime; + } + + /** + * {@inheritdoc} + */ + public function find($ip, $url, $limit, $method) + { + $indexName = $this->getIndexName(); + + $indexContent = $this->getValue($indexName, Redis::SERIALIZER_NONE); + + if (!$indexContent) { + return array(); + } + + $profileList = explode("\n", $indexContent); + $result = array(); + + foreach ($profileList as $item) { + + if ($limit === 0) { + break; + } + + if ($item == '') { + continue; + } + + list($itemToken, $itemIp, $itemMethod, $itemUrl, $itemTime, $itemParent) = explode("\t", $item, 6); + + if ($ip && false === strpos($itemIp, $ip) || $url && false === strpos($itemUrl, $url) || $method && false === strpos($itemMethod, $method)) { + continue; + } + + $result[$itemToken] = array( + 'token' => $itemToken, + 'ip' => $itemIp, + 'method' => $itemMethod, + 'url' => $itemUrl, + 'time' => $itemTime, + 'parent' => $itemParent, + ); + --$limit; + } + + usort($result, function($a, $b) { + if ($a['time'] === $b['time']) { + return 0; + } + return $a['time'] > $b['time'] ? -1 : 1; + }); + + return $result; + } + + /** + * {@inheritdoc} + */ + public function purge() + { + //dangerous: + //$this->getRedis()->flushDB(); + + //delete only items from index + $indexName = $this->getIndexName(); + + $indexContent = $this->getValue($indexName, Redis::SERIALIZER_NONE); + + if (!$indexContent) { + return false; + } + + $profileList = explode("\n", $indexContent); + + $result = array(); + + foreach ($profileList as $item) { + + if ($item == '') { + continue; + } + + $pos = strpos($item, "\t"); + if (false !== $pos) { + $result[] = $this->getItemName(substr($item, 0, $pos)); + } + } + + $result[] = $indexName; + + return $this->delete($result); + } + + /** + * {@inheritdoc} + */ + public function read($token) + { + if (empty($token)) { + return false; + } + + $profile = $this->getValue($this->getItemName($token), Redis::SERIALIZER_PHP); + + if (false !== $profile) { + $profile = $this->createProfileFromData($token, $profile); + } + + return $profile; + } + + /** + * {@inheritdoc} + */ + public function write(Profile $profile) + { + $data = array( + 'token' => $profile->getToken(), + 'parent' => $profile->getParentToken(), + 'children' => array_map(function ($p) { return $p->getToken(); }, $profile->getChildren()), + 'data' => $profile->getCollectors(), + 'ip' => $profile->getIp(), + 'method' => $profile->getMethod(), + 'url' => $profile->getUrl(), + 'time' => $profile->getTime(), + ); + + if ($this->setValue($this->getItemName($profile->getToken()), $data, $this->lifetime, Redis::SERIALIZER_PHP)) { + // Add to index + $indexName = $this->getIndexName(); + + $indexRow = implode("\t", array( + $profile->getToken(), + $profile->getIp(), + $profile->getMethod(), + $profile->getUrl(), + $profile->getTime(), + $profile->getParentToken(), + )) . "\n"; + + return $this->appendValue($indexName, $indexRow, $this->lifetime); + } + + return false; + } + + /** + * Internal convenience method that returns the instance of Redis + * + * @return Redis + */ + protected function getRedis() + { + if (null === $this->redis) { + if (!preg_match('#^redis://(?(?=\[.*\])\[(.*)\]|(.*)):(.*)$#', $this->dsn, $matches)) { + throw new \RuntimeException('Please check your configuration. You are trying to use Redis with an invalid dsn. "' . $this->dsn . '". The expected format is redis://host:port, redis://127.0.0.1:port, redis://[::1]:port'); + } + + $host = $matches[1]?: $matches[2]; + $port = $matches[3]; + + if (!extension_loaded('redis')) { + throw new \RuntimeException('RedisProfilerStorage requires redis extension to be loaded.'); + } + + $redis = new Redis; + $redis->connect($host, $port); + + $redis->setOption(Redis::OPT_PREFIX, self::TOKEN_PREFIX); + + $this->redis = $redis; + } + + return $this->redis; + } + + private function createProfileFromData($token, $data, $parent = null) + { + $profile = new Profile($token); + $profile->setIp($data['ip']); + $profile->setMethod($data['method']); + $profile->setUrl($data['url']); + $profile->setTime($data['time']); + $profile->setCollectors($data['data']); + + if (!$parent && $data['parent']) { + $parent = $this->read($data['parent']); + } + + if ($parent) { + $profile->setParent($parent); + } + + foreach ($data['children'] as $token) { + if (!$token) { + continue; + } + + if (!$childProfileData = $this->getValue($this->getItemName($token), Redis::SERIALIZER_PHP)) { + continue; + } + + $profile->addChild($this->createProfileFromData($token, $childProfileData, $profile)); + } + + return $profile; + } + + /** + * Get item name + * + * @param string $token + * + * @return string + */ + private function getItemName($token) + { + $name = $token; + + if ($this->isItemNameValid($name)) { + return $name; + } + + return false; + } + + /** + * Get name of index + * + * @return string + */ + private function getIndexName() + { + $name = 'index'; + + if ($this->isItemNameValid($name)) { + return $name; + } + + return false; + } + + private function isItemNameValid($name) + { + $length = strlen($name); + + if ($length > 2147483648) { + throw new \RuntimeException(sprintf('The Redis item key "%s" is too long (%s bytes). Allowed maximum size is 2^31 bytes.', $name, $length)); + } + + return true; + } + + /** + * Retrieve item from the Redis server + * + * @param string $key + * @param int $serializer + * + * @return mixed + */ + private function getValue($key, $serializer = Redis::SERIALIZER_NONE) + { + $redis = $this->getRedis(); + $redis->setOption(Redis::OPT_SERIALIZER, $serializer); + + return $redis->get($key); + } + + /** + * Store an item on the Redis server under the specified key + * + * @param string $key + * @param mixed $value + * @param int $expiration + * @param int $serializer + * + * @return boolean + */ + private function setValue($key, $value, $expiration = 0, $serializer = Redis::SERIALIZER_NONE) + { + $redis = $this->getRedis(); + $redis->setOption(Redis::OPT_SERIALIZER, $serializer); + + return $redis->setex($key, $expiration, $value); + } + + /** + * Append data to an existing item on the Redis server + * + * @param string $key + * @param string $value + * @param int $expiration + * + * @return boolean + */ + private function appendValue($key, $value, $expiration = 0) + { + $redis = $this->getRedis(); + $redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_NONE); + + if ($redis->exists($key)) { + $redis->append($key, $value); + return $redis->setTimeout($key, $expiration); + } + + return $redis->setex($key, $expiration, $value); + } + + /** + * Remove specified keys + * + * @param array $key + * + * @return boolean + */ + private function delete(array $keys) + { + return (bool) $this->getRedis()->delete($keys); + } +} diff --git a/tests/Symfony/Tests/Component/HttpKernel/Profiler/AbstractProfilerStorageTest.php b/tests/Symfony/Tests/Component/HttpKernel/Profiler/AbstractProfilerStorageTest.php index fdd363812b9a..df5021246c2c 100644 --- a/tests/Symfony/Tests/Component/HttpKernel/Profiler/AbstractProfilerStorageTest.php +++ b/tests/Symfony/Tests/Component/HttpKernel/Profiler/AbstractProfilerStorageTest.php @@ -183,6 +183,32 @@ public function testRetrieveByEmptyUrlAndIp() $this->getStorage()->purge(); } + public function testPurge() + { + $profile = new Profile('token1'); + $profile->setIp('127.0.0.1'); + $profile->setUrl('http://example.com/'); + $profile->setMethod('GET'); + $this->getStorage()->write($profile); + + $this->assertTrue(false !== $this->getStorage()->read('token1')); + $this->assertCount(1, $this->getStorage()->find('127.0.0.1', '', 10, 'GET')); + + $profile = new Profile('token2'); + $profile->setIp('127.0.0.1'); + $profile->setUrl('http://example.net/'); + $profile->setMethod('GET'); + $this->getStorage()->write($profile); + + $this->assertTrue(false !== $this->getStorage()->read('token2')); + $this->assertCount(2, $this->getStorage()->find('127.0.0.1', '', 10, 'GET')); + + $this->getStorage()->purge(); + + $this->assertEmpty($this->getStorage()->read('token'), '->purge() removes all data stored by profiler'); + $this->assertCount(0, $this->getStorage()->find('127.0.0.1', '', 10, 'GET'), '->purge() removes all items from index'); + } + /** * @return \Symfony\Component\HttpKernel\Profiler\ProfilerStorageInterface */ diff --git a/tests/Symfony/Tests/Component/HttpKernel/Profiler/RedisProfilerStorageTest.php b/tests/Symfony/Tests/Component/HttpKernel/Profiler/RedisProfilerStorageTest.php new file mode 100644 index 000000000000..34059449f40f --- /dev/null +++ b/tests/Symfony/Tests/Component/HttpKernel/Profiler/RedisProfilerStorageTest.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Tests\Component\HttpKernel\Profiler; + +use Symfony\Component\HttpKernel\Profiler\RedisProfilerStorage; +use Symfony\Component\HttpKernel\Profiler\Profile; + +class DummyRedisProfilerStorage extends RedisProfilerStorage +{ + public function getRedis() + { + return parent::getRedis(); + } +} + +class RedisProfilerStorageTest extends AbstractProfilerStorageTest +{ + protected static $storage; + + protected function setUp() + { + if (!extension_loaded('redis')) { + $this->markTestSkipped('RedisProfilerStorageTest requires redis extension to be loaded'); + } + + self::$storage = new DummyRedisProfilerStorage('redis://127.0.0.1:6379', '', '', 86400); + try { + self::$storage->getRedis(); + + self::$storage->purge(); + + } catch(\Exception $e) { + self::$storage = false; + $this->markTestSkipped('RedisProfilerStorageTest requires that there is a Redis server present on localhost'); + } + } + + protected function tearDown() + { + if (self::$storage) { + self::$storage->purge(); + self::$storage->getRedis()->close(); + self::$storage = false; + } + } + + /** + * @return \Symfony\Component\HttpKernel\Profiler\ProfilerStorageInterface + */ + protected function getStorage() + { + return self::$storage; + } +}