Index: tests/t3lib/cache/backend/t3lib_cache_backend_redisbackendTest.php
===================================================================
--- tests/t3lib/cache/backend/t3lib_cache_backend_redisbackendTest.php	(revision 0)
+++ tests/t3lib/cache/backend/t3lib_cache_backend_redisbackendTest.php	(revision 0)
@@ -0,0 +1,1137 @@
+<?php
+/***************************************************************
+*  Copyright notice
+*
+*  (c) 2010 Christian Kuhn <lolli@schwarzbu.ch>
+*  All rights reserved
+*
+*  This script is part of the TYPO3 project. The TYPO3 project is
+*  free software; you can redistribute it and/or modify
+*  it under the terms of the GNU General Public License as published by
+*  the Free Software Foundation; either version 2 of the License, or
+*  (at your option) any later version.
+*
+*  The GNU General Public License can be found at
+*  http://www.gnu.org/copyleft/gpl.html.
+*
+*  This script is distributed in the hope that it will be useful,
+*  but WITHOUT ANY WARRANTY; without even the implied warranty of
+*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+*  GNU General Public License for more details.
+*
+*  This copyright notice MUST APPEAR in all copies of the script!
+***************************************************************/
+
+/**
+ * Testcase for the cache to redis backend
+ *
+ * This class has functional tests as well as implementation tests:
+ * - The functional tests make API calls to the backend and check expected behaviour
+ * - The implementation tests make additional calls with an own redis instance to
+ *   check stored data structures in the redis server, which can not be checked
+ *   by functional tests alone. Those tests will fail if any changes
+ *   to the internal data structure are done.
+ *
+ * Warning:
+ * The unit tests use and flush redis database numbers 0 and 1!
+ *
+ * @author Christian Kuhn <lolli@schwarzbu.ch>
+ * @package TYPO3
+ * @subpackage tests
+ */
+class t3lib_cache_backend_RedisBackendTest extends tx_phpunit_testcase {
+
+	/**
+	 * @var boolean
+	 */
+	protected $backupGlobals = TRUE;
+
+	/**
+	 * If set, the tearDown() method will flush the cache used by this unit test.
+	 *
+	 * @var t3lib_cache_backend_RedisBackend
+	 */
+	protected $backend = NULL;
+
+	/**
+	 * Own redis instance used in implementation tests
+	 *
+	 * @var Redis
+	 */
+	protected $redis = NULL;
+
+	/**
+	 * Set up this testcase
+	 *
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function setUp() {
+		if (!extension_loaded('redis')) {
+			$this->markTestSkipped('redis extension was not available');
+		}
+
+		try {
+			if (!fsockopen('127.0.0.1', 6379)) {
+				$this->markTestSkipped('redis server not reachable');
+			}
+		} catch (Exception $e) {
+			$this->markTestSkipped('redis server not reachable');
+		}
+
+		$this->backupGlobals = TRUE;
+	}
+
+	/**
+	 * Sets up the redis backend used for testing
+	 *
+	 * @param array Options for the redis backend
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	protected function setUpBackend(array $backendOptions = array()) {
+		$mockCache = $this->getMock('t3lib_cache_frontend_Frontend', array(), array(), '', FALSE);
+		$mockCache->expects($this->any())->method('getIdentifier')->will($this->returnValue('TestCache'));
+
+		$this->backend = new t3lib_cache_backend_RedisBackend($backendOptions);
+		$this->backend->setCache($mockCache);
+	}
+
+	/**
+	 * Sets up an own redis instance for implementation tests
+	 *
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	protected function setUpRedis() {
+		$this->redis = new Redis();
+		$this->redis->connect('127.0.0.1', 6379);
+	}
+
+	/**
+	 * Tear down this testcase
+	 *
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function tearDown() {
+		if ($this->backend instanceof t3lib_cache_backend_RedisBackend) {
+			$this->backend->flush();
+		}
+	}
+
+	/**
+	 * @test Functional
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function constructorThrowsNoExceptionIfPasswordOptionIsSet() {
+		try {
+			$this->setUpBackend(array('password' => 'foo'));
+		} catch (Exception $e) {
+			$this->assertTrue();
+		}
+	}
+
+	/**
+	 * @test Functional
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function constructorThrowsNoExceptionIfGivenDatabaseWasSuccessfullySelected() {
+		try {
+			$this->setUpBackend(array('database' => 1));
+		} catch (Exception $e) {
+			$this->assertTrue();
+		}
+	}
+
+	/**
+	 * @test Functional
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 * @expectedException InvalidArgumentException
+	 */
+	public function setDatabaseThrowsExceptionIfGivenDatabaseNumberIsNotAnInteger() {
+		$this->setUpBackend(array('database' => 'foo'));
+	}
+
+	/**
+	 * @test Functional
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 * @expectedException InvalidArgumentException
+	 */
+	public function setDatabaseThrowsExceptionIfGivenDatabaseNumberIsNegative() {
+		$this->setUpBackend(array('database' => -1));
+	}
+
+	/**
+	 * @test Functional
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 * @expectedException InvalidArgumentException
+	 */
+	public function setCompressionThrowsExceptionIfCompressionParameterIsNotOfTypeBoolean() {
+		$this->setUpBackend(array('compression' => 'foo'));
+	}
+
+	/**
+	 * @test Functional
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 * @expectedException InvalidArgumentException
+	 */
+	public function setCompressionLevelThrowsExceptionIfCompressionLevelIsNotInteger() {
+		$this->setUpBackend(array('compressionLevel' => 'foo'));
+	}
+
+	/**
+	 * @test Functional
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 * @expectedException InvalidArgumentException
+	 */
+	public function setCompressionLevelThrowsExceptionIfCompressionLevelIsNotBetweenMinusOneAndNine() {
+		$this->setUpBackend(array('compressionLevel' => 11));
+	}
+
+	/**
+	 * @test Functional
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 * @expectedException InvalidArgumentException
+	 */
+	public function setThrowsExceptionIfIdentifierIsNotAString() {
+		$this->setUpBackend();
+		$this->backend->set(array(), 'data');
+	}
+
+	/**
+	 * @test Functional
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 * @expectedException t3lib_cache_Exception_InvalidData
+	 */
+	public function setThrowsExceptionIfDataIsNotAString() {
+		$this->setUpBackend();
+		$this->backend->set('identifier' . uniqid(), array());
+	}
+
+	/**
+	 * @test Functional
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 * @expectedException InvalidArgumentException
+	 */
+	public function setThrowsExceptionIfLifetimeIsNegative() {
+		$this->setUpBackend();
+		$this->backend->set('identifier' . uniqid(), 'data', array(), -42);
+	}
+
+	/**
+	 * @test Functional
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 * @expectedException InvalidArgumentException
+	 */
+	public function setThrowsExceptionIfLifetimeIsNotNullOrAnInteger() {
+		$this->setUpBackend();
+		$this->backend->set('identifier' . uniqid(), 'data', array(), array());
+	}
+
+	/**
+	 * @test Implementation
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function setStoresEntriesInSelectedDatabase() {
+		$this->setUpRedis();
+		$this->redis->select(1);
+
+		$this->setUpBackend(array('database' => 1));
+		$identifier = 'identifier' . uniqid();
+		$this->backend->set($identifier, 'data');
+
+		$this->assertTrue($this->redis->exists('identData:' . $identifier));
+	}
+
+	/**
+	 * @test Implementation
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function setSavesStringDataTypeForIdentifierToDataEntry() {
+		$this->setUpBackend();
+		$this->setUpRedis();
+
+		$identifier = 'identifier' . uniqid();
+		$this->backend->set($identifier, 'data');
+
+		$this->assertSame(Redis::REDIS_STRING, $this->redis->type('identData:' . $identifier));
+	}
+
+	/**
+	 * @test Implementation
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function setSavesEntryWithDefaultLifeTime() {
+		$this->setUpBackend();
+		$this->setUpRedis();
+
+		$identifier = 'identifier' . uniqid();
+		$defaultLifetime = 42;
+		$this->backend->setDefaultLifetime($defaultLifetime);
+		$this->backend->set($identifier, 'data');
+
+		$lifetimeRegisteredInBackend = $this->redis->ttl('identData:' . $identifier);
+		$this->assertSame($defaultLifetime, $lifetimeRegisteredInBackend);
+	}
+
+	/**
+	 * @test Implementation
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function setSavesEntryWithSpecifiedLifeTime() {
+		$this->setUpBackend();
+		$this->setUpRedis();
+
+		$identifier = 'identifier' . uniqid();
+		$lifetime = 43;
+		$this->backend->set($identifier, 'data', array(), $lifetime);
+
+		$lifetimeRegisteredInBackend = $this->redis->ttl('identData:' . $identifier);
+		$this->assertSame($lifetime, $lifetimeRegisteredInBackend);
+	}
+
+	/**
+	 * @test Implementation
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function setSavesEntryWithUnlimitedLifeTime() {
+		$this->setUpBackend();
+		$this->setUpRedis();
+
+		$identifier = 'identifier' . uniqid();
+		$this->backend->set($identifier, 'data', array(), 0);
+
+		$lifetimeRegisteredInBackend = $this->redis->ttl('identData:' . $identifier);
+		$this->assertSame(31536000, $lifetimeRegisteredInBackend);
+	}
+
+	/**
+	 * @test Functional
+	 * @author Christian Jul Jensen <julle@typo3.org>
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function setOverwritesExistingEntryWithNewData() {
+		$this->setUpBackend();
+		$data = 'data 1';
+		$identifier = 'identifier' . uniqid();
+		$this->backend->set($identifier, $data);
+		$otherData = 'data 2';
+		$this->backend->set($identifier, $otherData);
+		$fetchedData = $this->backend->get($identifier);
+		$this->assertSame($otherData, $fetchedData);
+	}
+
+	/**
+	 * @test Implementation
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function setOverwritesExistingEntryWithSpecifiedLifetime() {
+		$this->setUpBackend();
+		$this->setUpRedis();
+
+		$data = 'data';
+		$identifier = 'identifier' . uniqid();
+		$this->backend->set($identifier, $data);
+		$lifetime = 42;
+		$this->backend->set($identifier, $data, array(), $lifetime);
+
+		$lifetimeRegisteredInBackend = $this->redis->ttl('identData:' . $identifier);
+		$this->assertSame($lifetime, $lifetimeRegisteredInBackend);
+	}
+
+	/**
+	 * @test Implementation
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function setOverwritesExistingEntryWithNewDefaultLifetime() {
+		$this->setUpBackend();
+		$this->setUpRedis();
+
+		$data = 'data';
+		$identifier = 'identifier' . uniqid();
+		$lifetime = 42;
+		$this->backend->set($identifier, $data, array(), $lifetime);
+		$newDefaultLifetime = 43;
+		$this->backend->setDefaultLifetime($newDefaultLifetime);
+		$this->backend->set($identifier, $data, array(), $newDefaultLifetime);
+
+		$lifetimeRegisteredInBackend = $this->redis->ttl('identData:' . $identifier);
+		$this->assertSame($newDefaultLifetime, $lifetimeRegisteredInBackend);
+	}
+
+	/**
+	 * @test Implementation
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function setOverwritesExistingEntryWithNewUnlimitedLifetime() {
+		$this->setUpBackend();
+		$this->setUpRedis();
+
+		$data = 'data';
+		$identifier = 'identifier' . uniqid();
+		$lifetime = 42;
+		$this->backend->set($identifier, $data, array(), $lifetime);
+		$this->backend->set($identifier, $data, array(), 0);
+
+		$lifetimeRegisteredInBackend = $this->redis->ttl('identData:' . $identifier);
+		$this->assertSame(31536000, $lifetimeRegisteredInBackend);
+	}
+
+	/**
+	 * @test Implementation
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function setSavesSetDataTypeForIdentifierToTagsSet() {
+		$this->setUpBackend();
+		$this->setUpRedis();
+
+		$identifier = 'identifier' . uniqid();
+		$this->backend->set($identifier, 'data', array('tag'));
+
+		$this->assertSame(Redis::REDIS_SET, $this->redis->type('identTags:' . $identifier));
+	}
+
+	/**
+	 * @test Implementation
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function setSavesSpecifiedTagsInIdentifierToTagsSet() {
+		$this->setUpBackend();
+		$this->setUpRedis();
+
+		$identifier = 'identifier' . uniqid();
+		$tags = array('thatTag', 'thisTag');
+		$this->backend->set($identifier, 'data', $tags);
+
+		$savedTags = $this->redis->sMembers('identTags:' . $identifier);
+		sort($savedTags);
+		$this->assertSame($tags, $savedTags);
+	}
+
+	/**
+	 * @test Implementation
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function setRemovesAllPreviouslySetTagsFromIdentifierToTagsSet() {
+		$this->setUpBackend();
+		$this->setUpRedis();
+
+		$identifier = 'identifier' . uniqid();
+		$tags = array('fooTag', 'barTag');
+		$this->backend->set($identifier, 'data', $tags);
+		$this->backend->set($identifier, 'data', array());
+
+		$this->assertSame(array(), $this->redis->sMembers('identTags:' . $identifier));
+	}
+
+	/**
+	 * @test Implementation
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function setRemovesMultiplePreviouslySetTagsFromIdentifierToTagsSet() {
+		$this->setUpBackend();
+		$this->setUpRedis();
+
+		$identifier = 'identifier' . uniqid();
+		$firstTagSet = array('tag1', 'tag2', 'tag3', 'tag4');
+		$this->backend->set($identifier, 'data', $firstTagSet);
+		$secondTagSet = array('tag1', 'tag3');
+		$this->backend->set($identifier, 'data', $secondTagSet);
+
+		$actualTagSet = $this->redis->sMembers('identTags:' . $identifier);
+		sort($actualTagSet);
+		$this->assertSame($secondTagSet, $actualTagSet);
+	}
+
+	/**
+	 * @test Implementation
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function setSavesSetDataTypeForTagToIdentifiersSet() {
+		$this->setUpBackend();
+		$this->setUpRedis();
+
+		$identifier = 'identifier' . uniqid();
+		$tag = 'tag';
+		$this->backend->set($identifier, 'data', array($tag));
+
+		$this->assertSame(Redis::REDIS_SET, $this->redis->type('tagIdents:' . $tag));
+	}
+
+	/**
+	 * @test Implementation
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function setSavesIdentifierInTagToIdentifiersSetOfSpecifiedTag() {
+		$this->setUpBackend();
+		$this->setUpRedis();
+
+		$identifier = 'identifier' . uniqid();
+		$tag = 'thisTag';
+		$this->backend->set($identifier, 'data', array($tag));
+
+		$savedTagToIdentifiersMemberArray = $this->redis->sMembers('tagIdents:' . $tag);
+		$this->assertSame(array($identifier), $savedTagToIdentifiersMemberArray);
+	}
+
+	/**
+	 * @test Implementation
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function setAppendsSecondIdentifierInTagToIdentifiersEntry() {
+		$this->setUpBackend();
+		$this->setUpRedis();
+
+		$firstIdentifier = 'identifier' . uniqid();
+		$tag = 'thisTag';
+		$this->backend->set($firstIdentifier, 'data', array($tag));
+		$secondIdentifier = 'identifier' . uniqid();
+		$this->backend->set($secondIdentifier, 'data', array($tag));
+
+		$savedTagToIdentifiersMemberArray = $this->redis->sMembers('tagIdents:' . $tag);
+		sort($savedTagToIdentifiersMemberArray);
+		$identifierArray = array($firstIdentifier, $secondIdentifier);
+		sort($identifierArray);
+		$this->assertSame(array($firstIdentifier, $secondIdentifier), $savedTagToIdentifiersMemberArray);
+	}
+
+	/**
+	 * @test Implementation
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function setRemovesIdentifierFromTagToIdentifiersEntryIfTagIsOmittedOnConsecutiveSet() {
+		$this->setUpBackend();
+		$this->setUpRedis();
+
+		$identifier = 'identifier' . uniqid();
+		$tag = 'thisTag';
+		$this->backend->set($identifier, 'data', array($tag));
+		$this->backend->set($identifier, 'data', array());
+
+		$savedTagToIdentifiersMemberArray = $this->redis->sMembers('tagIdents:' . $tag);
+		$this->assertSame(array(), $savedTagToIdentifiersMemberArray);
+	}
+
+	/**
+	 * @test Implementation
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function setAddsIdentifierInTagToIdentifiersEntryIfTagIsAddedOnConsecutiveSet() {
+		$this->setUpBackend();
+		$this->setUpRedis();
+
+		$identifier = 'identifier' . uniqid();
+		$this->backend->set($identifier, 'data');
+		$tag = 'thisTag';
+		$this->backend->set($identifier, 'data', array($tag));
+
+		$savedTagToIdentifiersMemberArray = $this->redis->sMembers('tagIdents:' . $tag);
+		$this->assertSame(array($identifier), $savedTagToIdentifiersMemberArray);
+	}
+
+	/**
+	 * @test Implementation
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function setSavesCompressedDataWithEnabledCompression() {
+		$this->setUpBackend(
+			array(
+				'compression' => TRUE,
+			)
+		);
+		$this->setUpRedis();
+
+		$identifier = 'identifier' . uniqid();
+		$data = 'some data ' . microtime();
+		$this->backend->set($identifier, $data);
+
+		$uncompresedStoredData = '';
+		try {
+			$uncompresedStoredData = @gzuncompress($this->redis->get('identData:' . $identifier));
+		} catch (Exception $e) {
+		}
+
+		$this->assertEquals($data, $uncompresedStoredData, 'Original and compressed data don\'t match');
+	}
+
+	/**
+	 * @test Implementation
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function setSavesPlaintextDataWithEnabledCompressionAndCompressionLevel0() {
+		$this->setUpBackend(
+			array(
+				'compression' => TRUE,
+				'compressionLevel' => 0,
+			)
+		);
+		$this->setUpRedis();
+
+		$identifier = 'identifier' . uniqid();
+		$data = 'some data ' . microtime();
+		$this->backend->set($identifier, $data);
+
+		$this->assertGreaterThan(0, substr_count($this->redis->get('identData:' . $identifier), $data), 'Plaintext data not found');
+	}
+
+	/**
+	 * @test Functional
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 * @expectedException InvalidArgumentException
+	 */
+	public function hasThrowsExceptionIfIdentifierIsNotAString() {
+		$this->setUpBackend();
+		$this->backend->has(array());
+	}
+
+	/**
+	 * @test Functional
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function hasReturnsFalseForNotExistingEntry() {
+		$this->setUpBackend();
+		$identifier = 'identifier' . uniqid();
+		$this->assertFalse($this->backend->has($identifier));
+	}
+
+	/**
+	 * @test Functional
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function hasReturnsTrueForPreviouslySetEntry() {
+		$this->setUpBackend();
+		$identifier = 'identifier' . uniqid();
+		$this->backend->set($identifier, 'data');
+		$this->assertTrue($this->backend->has($identifier));
+	}
+
+	/**
+	 * @test Functional
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 * @expectedException InvalidArgumentException
+	 */
+	public function getThrowsExceptionIfIdentifierIsNotAString() {
+		$this->setUpBackend();
+		$this->backend->get(array());
+	}
+
+	/**
+	 * @test Functional
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function getReturnsPreviouslyCompressedSetEntry() {
+		$this->setUpBackend(
+			array(
+				'compression' => TRUE,
+			)
+		);
+		$data = 'data';
+		$identifier = 'identifier' . uniqid();
+		$this->backend->set($identifier, $data);
+		$fetchedData = $this->backend->get($identifier);
+		$this->assertSame($data, $fetchedData);
+	}
+
+	/**
+	 * @test Functional
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function getReturnsPreviouslySetEntry() {
+		$this->setUpBackend();
+		$data = 'data';
+		$identifier = 'identifier' . uniqid();
+		$this->backend->set($identifier, $data);
+		$fetchedData = $this->backend->get($identifier);
+		$this->assertSame($data, $fetchedData);
+	}
+
+	/**
+	 * @test Functional
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 * @expectedException InvalidArgumentException
+	 */
+	public function removeThrowsExceptionIfIdentifierIsNotAString() {
+		$this->setUpBackend();
+		$this->backend->remove(array());
+	}
+
+	/**
+	 * @test Functional
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function removeReturnsFalseIfNoEntryWasDeleted() {
+		$this->setUpBackend();
+		$this->assertFalse($this->backend->remove('identifier' . uniqid()));
+	}
+
+	/**
+	 * @test Functional
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function removeReturnsTrueIfAnEntryWasDeleted() {
+		$this->setUpBackend();
+		$identifier = 'identifier' . uniqid();
+		$this->backend->set($identifier, 'data');
+		$this->assertTrue($this->backend->remove($identifier));
+	}
+
+	/**
+	 * @test Functional
+	 * @author Christian Jul Jensen <julle@typo3.org>
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function removeDeletesEntryFromCache() {
+		$this->setUpBackend();
+		$identifier = 'identifier' . uniqid();
+		$this->backend->set($identifier, 'data');
+		$this->backend->remove($identifier);
+		$this->assertFalse($this->backend->has($identifier));
+	}
+
+	/**
+	 * @test Implementation
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function removeDeletesIdentifierToTagEntry() {
+		$this->setUpBackend();
+		$this->setUpRedis();
+
+		$identifier = 'identifier' . uniqid();
+		$tag = 'thisTag';
+		$this->backend->set($identifier, 'data', array($tag));
+		$this->backend->remove($identifier);
+
+		$this->assertFalse($this->redis->exists('identTags:' . $identifier));
+	}
+
+	/**
+	 * @test Implementation
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function removeDeletesIdentifierFromTagToIdentifiersSet() {
+		$this->setUpBackend();
+		$this->setUpRedis();
+
+		$identifier = 'identifier' . uniqid();
+		$tag = 'thisTag';
+		$this->backend->set($identifier, 'data', array($tag));
+		$this->backend->remove($identifier);
+
+		$tagToIdentifiersMemberArray = $this->redis->sMembers('tagIdents:' . $tag);
+		$this->assertSame(array(), $tagToIdentifiersMemberArray);
+	}
+
+	/**
+	 * @test Implementation
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function removeDeletesIdentifierFromTagToIdentifiersSetWithMultipleEntries() {
+		$this->setUpBackend();
+		$this->setUpRedis();
+
+		$firstIdentifier = 'identifier' . uniqid();
+		$secondIdentifier = 'identifier' . uniqid();
+		$tag = 'thisTag';
+		$this->backend->set($firstIdentifier, 'data', array($tag));
+		$this->backend->set($secondIdentifier, 'data', array($tag));
+		$this->backend->remove($firstIdentifier);
+
+		$tagToIdentifiersMemberArray = $this->redis->sMembers('tagIdents:' . $tag);
+		$this->assertSame(array($secondIdentifier), $tagToIdentifiersMemberArray);
+	}
+
+	/**
+	 * @test Functional
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 * @expectedException InvalidArgumentException
+	 */
+	public function findIdentifiersByTagThrowsExceptionIfTagIsNotAString() {
+		$this->setUpBackend();
+		$this->backend->findIdentifiersByTag(array());
+	}
+
+	/**
+	 * @test Functional
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function findIdentifiersByTagReturnsEmptyArrayForNotExistingTag() {
+		$this->setUpBackend();
+		$this->assertSame(array(), $this->backend->findIdentifiersByTag('thisTag'));
+	}
+
+	/**
+	 * @test Functional
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function findIdentifiersByTagReturnsAllIdentifiersTagedWithSpecifiedTag() {
+		$this->setUpBackend();
+
+		$firstIdentifier = 'identifier' . uniqid();
+		$secondIdentifier = 'identifier' . uniqid();
+		$thirdIdentifier = 'identifier' . uniqid();
+		$tagsForFirstIdentifier = array('thisTag');
+		$tagsForSecondIdentifier = array('thatTag');
+		$tagsForThirdIdentifier = array('thisTag', 'thatTag');
+
+		$this->backend->set($firstIdentifier, 'data', $tagsForFirstIdentifier);
+		$this->backend->set($secondIdentifier, 'data', $tagsForSecondIdentifier);
+		$this->backend->set($thirdIdentifier, 'data', $tagsForThirdIdentifier);
+
+		$expectedResult = array($firstIdentifier, $thirdIdentifier);
+		$actualResult = $this->backend->findIdentifiersByTag('thisTag');
+		sort($actualResult);
+
+		$this->assertSame($expectedResult, $actualResult);
+	}
+
+	/**
+	 * @test Functional
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function findIdentifiersByTagsReturnEmptyArrayForNotExistingTag() {
+		$this->setUpBackend();
+		$this->assertEquals(array(), $this->backend->findIdentifiersByTags(array('thisTags')));
+	}
+
+	/**
+	 * @test Functional
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function findIdentifiersByTagsReturnsIdentifiersTaggedWithAllSpecifiedTags() {
+		$this->setUpBackend();
+
+		$identifier = 'identifier' . uniqid();
+		$this->backend->set($identifier . 'A', 'data', array('tag1'));
+		$this->backend->set($identifier . 'B', 'data', array('tag2'));
+		$this->backend->set($identifier . 'C', 'data', array('tag1', 'tag2'));
+		$this->backend->set($identifier . 'D', 'data', array('tag1', 'tag2', 'tag3'));
+
+		$expectedResult = array($identifier . 'C', $identifier . 'D');
+		$actualResult = $this->backend->findIdentifiersByTags(array('tag1', 'tag2'));
+		sort($actualResult);
+
+		$this->assertEquals($expectedResult, $actualResult);
+	}
+
+
+
+	/**
+	 * @test Implementation
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function flushRemovesAllEntriesFromCache() {
+		$this->setUpBackend();
+		$this->setUpRedis();
+
+		$identifier = 'identifier' . uniqid();
+		$this->backend->set($identifier, 'data');
+		$this->backend->flush();
+
+		$this->assertSame(array(), $this->redis->getKeys('*'));
+	}
+
+	/**
+	 * @test Functional
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 * @expectedException InvalidArgumentException
+	 */
+	public function flushByTagThrowsExceptionIfTagIsNotAString() {
+		$this->setUpBackend();
+		$this->backend->flushByTag(array());
+	}
+
+	/**
+	 * @test Functional
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function flushByTagRemovesEntriesTaggedWithSpecifiedTag() {
+		$this->setUpBackend();
+
+		$identifier = 'identifier' . uniqid();
+		$this->backend->set($identifier . 'A', 'data', array('tag1'));
+		$this->backend->set($identifier . 'B', 'data', array('tag2'));
+		$this->backend->set($identifier . 'C', 'data', array('tag1', 'tag2'));
+		$this->backend->flushByTag('tag1');
+
+		$expectedResult = array(FALSE, TRUE, FALSE);
+		$actualResult = array(
+			$this->backend->has($identifier . 'A'),
+			$this->backend->has($identifier . 'B'),
+			$this->backend->has($identifier . 'C'),
+		);
+		$this->assertSame($expectedResult, $actualResult);
+	}
+
+	/**
+	 * @test Implementation
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function flushByTagRemovesTemporarySet() {
+		$this->setUpBackend();
+		$this->setUpRedis();
+
+		$identifier = 'identifier' . uniqid();
+		$this->backend->set($identifier . 'A', 'data', array('tag1'));
+		$this->backend->set($identifier . 'C', 'data', array('tag1', 'tag2'));
+		$this->backend->flushByTag('tag1');
+
+		$this->assertSame(array(), $this->redis->getKeys('temp*'));
+	}
+
+	/**
+	 * @test Implementation
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function flushByTagRemovesIdentifierToTagsSetOfEntryTaggedWithGivenTag() {
+		$this->setUpBackend();
+		$this->setUpRedis();
+
+		$identifier = 'identifier' . uniqid();
+		$tag = 'tag1';
+		$this->backend->set($identifier, 'data', array($tag));
+		$this->backend->flushByTag($tag);
+
+		$this->assertFalse($this->redis->exists('identTags:' . $identifier));
+	}
+
+	/**
+	 * @test Implementation
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function flushByTagDoesNotRemoveIdentifierToTagsSetOfUnrelatedEntry() {
+		$this->setUpBackend();
+		$this->setUpRedis();
+
+		$identifierToBeRemoved = 'identifier' . uniqid();
+		$tagToRemove = 'tag1';
+		$this->backend->set($identifierToBeRemoved, 'data', array($tagToRemove));
+
+		$identifierNotToBeRemoved = 'identifier' . uniqid();
+		$tagNotToRemove = 'tag2';
+		$this->backend->set($identifierNotToBeRemoved, 'data', array($tagNotToRemove));
+
+		$this->backend->flushByTag($tagToRemove);
+
+		$this->assertSame(array($tagNotToRemove), $this->redis->sMembers('identTags:' . $identifierNotToBeRemoved));
+	}
+
+	/**
+	 * @test Implementation
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function flushByTagRemovesTagToIdentifiersSetOfGivenTag() {
+		$this->setUpBackend();
+		$this->setUpRedis();
+
+		$identifier = 'identifier' . uniqid();
+		$tag = 'tag1';
+		$this->backend->set($identifier, 'data', array($tag));
+		$this->backend->flushByTag($tag);
+
+		$this->assertFalse($this->redis->exists('tagIdents:' . $tag));
+	}
+
+	/**
+	 * @test Implementation
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function flushByTagRemovesIdentifiersTaggedWithGivenTagFromTagToIdentifiersSets() {
+		$this->setUpBackend();
+		$this->setUpRedis();
+
+		$identifier = 'identifier' . uniqid();
+		$this->backend->set($identifier . 'A', 'data', array('tag1', 'tag2'));
+		$this->backend->set($identifier . 'B', 'data', array('tag1', 'tag2'));
+		$this->backend->set($identifier . 'C', 'data', array('tag2'));
+
+		$this->backend->flushByTag('tag1');
+
+		$this->assertSame(array($identifier . 'C'), $this->redis->sMembers('tagIdents:tag2'));
+	}
+
+	/**
+	 * @test Implementation
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function flushByTagsRemovesEntriesTaggedWithSpecifiedTags() {
+		$this->setUpBackend();
+
+		$identifier = 'identifier' . uniqid();
+		$this->backend->set($identifier . 'A', 'data', array('tag1'));
+		$this->backend->set($identifier . 'B', 'data', array('tag2'));
+		$this->backend->set($identifier . 'C', 'data', array('tag3'));
+		$this->backend->set($identifier . 'D', 'data', array('tag1', 'tag2'));
+		$this->backend->set($identifier . 'E', 'data', array('tag1', 'tag3'));
+		$this->backend->flushByTags(array('tag1', 'tag2'));
+
+		$expectedResult = array(FALSE, FALSE, TRUE, FALSE, FALSE);
+		$actualResult = array(
+			$this->backend->has($identifier . 'A'),
+			$this->backend->has($identifier . 'B'),
+			$this->backend->has($identifier . 'C'),
+			$this->backend->has($identifier . 'D'),
+			$this->backend->has($identifier . 'E'),
+		);
+
+		$this->assertEquals($expectedResult, $actualResult);
+	}
+
+	/**
+	 * @test Implementation
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function flushByTagsRemovesIdentifierToTagsSetOfEntriesTaggedWithGivenTags() {
+		$this->setUpBackend();
+		$this->setUpRedis();
+
+		$identifier = 'identifier' . uniqid();
+		$this->backend->set($identifier . 'A', 'data', array('tag1', 'tag2'));
+		$this->backend->set($identifier . 'B', 'data', array('tag1', 'tag3'));
+		$this->backend->set($identifier . 'C', 'data', array('tag2', 'tag4'));
+		$this->backend->set($identifier . 'D', 'data', array('tag3', 'tag4'));
+
+		$this->backend->flushByTags(array('tag1', 'tag2'));
+
+		$expectedResult = array(FALSE, FALSE, FALSE, TRUE);
+		$actualResult = array(
+			$this->redis->exists('identTags:' . $identifier . 'A'),
+			$this->redis->exists('identTags:' . $identifier . 'B'),
+			$this->redis->exists('identTags:' . $identifier . 'C'),
+			$this->redis->exists('identTags:' . $identifier . 'D'),
+		);
+
+		$this->assertEquals($expectedResult, $actualResult);
+	}
+
+	/**
+	 * @test Implementation
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function flushByTagsRemovesTagToIdentifiersSetsOfGivenTags() {
+		$this->setUpBackend();
+		$this->setUpRedis();
+
+		$identifier = 'identifier' . uniqid();
+		$this->backend->set($identifier . 'A', 'data', array('tag1', 'tag2'));
+		$this->backend->set($identifier . 'B', 'data', array('tag1', 'tag3'));
+		$this->backend->set($identifier . 'C', 'data', array('tag2', 'tag4'));
+		$this->backend->set($identifier . 'D', 'data', array('tag3', 'tag4'));
+
+		$this->backend->flushByTags(array('tag1', 'tag2'));
+
+		$expectedResult = array(FALSE, FALSE, TRUE, TRUE);
+		$actualResult = array(
+			$this->redis->exists('tagIdents:tag1'),
+			$this->redis->exists('tagIdents:tag2'),
+			$this->redis->exists('tagIdents:tag3'),
+			$this->redis->exists('tagIdents:tag4'),
+		);
+
+		$this->assertEquals($expectedResult, $actualResult);
+	}
+
+	/**
+	 * @test Implementation
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function flushByTagsRemovesIdentifiersTaggedWithGivenTagsFromTagToIdentifiersSets() {
+		$this->setUpBackend();
+		$this->setUpRedis();
+
+		$identifier = 'identifier' . uniqid();
+		$this->backend->set('A' . $identifier, 'data', array('tag1', 'tag2'));
+		$this->backend->set('B' . $identifier, 'data', array('tag1', 'tag3'));
+		$this->backend->set('C' . $identifier, 'data', array('tag3', 'tag4'));
+		$this->backend->set('D' . $identifier, 'data', array('tag3', 'tag4', 'tag5'));
+
+		$this->backend->flushByTags(array('tag1', 'tag2'));
+
+		$expectedResult = array(
+			array('C' . $identifier, 'D' . $identifier),
+			array('C' . $identifier, 'D' . $identifier),
+			array('D' . $identifier),
+		);
+
+		$tag3Identifiers = $this->redis->sMembers('tagIdents:tag3');
+		$tag4Identifiers = $this->redis->sMembers('tagIdents:tag4');
+		$tag5Identifiers = $this->redis->sMembers('tagIdents:tag5');
+		sort($tag3Identifiers);
+		sort($tag4Identifiers);
+
+		$actualResult = array(
+			$tag3Identifiers,
+			$tag4Identifiers,
+			$tag5Identifiers,
+		);
+
+		$this->assertEquals($expectedResult, $actualResult);
+	}
+
+	/**
+	 * @test Implementation
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function collectGarbageDoesNotRemoveNotExpiredIdentifierToDataEntry() {
+		$this->setUpBackend();
+		$this->setUpRedis();
+
+		$identifier = 'identifier' . uniqid();
+		$this->backend->set($identifier . 'A', 'data', array('tag'));
+		$this->backend->set($identifier . 'B', 'data', array('tag'));
+
+		$this->redis->delete('identData:' . $identifier . 'A');
+
+		$this->backend->collectGarbage();
+
+		$this->assertTrue($this->redis->exists('identData:' . $identifier . 'B'));
+	}
+
+	/**
+	 * @test Implementation
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function collectGarbageRemovesLeftOverIdentifierToTagsSet() {
+		$this->setUpBackend();
+		$this->setUpRedis();
+
+		$identifier = 'identifier' . uniqid();
+		$this->backend->set($identifier . 'A', 'data', array('tag'));
+		$this->backend->set($identifier . 'B', 'data', array('tag'));
+
+		$this->redis->delete('identData:' . $identifier . 'A');
+
+		$this->backend->collectGarbage();
+
+		$expectedResult = array(FALSE, TRUE);
+		$actualResult = array(
+			$this->redis->exists('identTags:' . $identifier . 'A'),
+			$this->redis->exists('identTags:' . $identifier . 'B'),
+		);
+		$this->assertSame($expectedResult, $actualResult);
+	}
+
+	/**
+	 * @test Implementation
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function collectGarbageRemovesExpiredIdentifierFromTagsToIdentifierSet() {
+		$this->setUpBackend();
+		$this->setUpRedis();
+
+		$identifier = 'identifier' . uniqid();
+		$this->backend->set($identifier . 'A', 'data', array('tag1', 'tag2'));
+		$this->backend->set($identifier . 'B', 'data', array('tag2'));
+
+		$this->redis->delete('identData:' . $identifier . 'A');
+
+		$this->backend->collectGarbage();
+
+		$expectedResult = array(
+			array(),
+			array($identifier . 'B')
+		);
+		$actualResult = array(
+			$this->redis->sMembers('tagIdents:tag1'),
+			$this->redis->sMembers('tagIdents:tag2'),
+		);
+		$this->assertSame($expectedResult, $actualResult);
+	}
+}
+?>
Index: t3lib/config_default.php
===================================================================
--- t3lib/config_default.php	(revision 9388)
+++ t3lib/config_default.php	(working copy)
@@ -118,6 +118,7 @@
 				't3lib_cache_backend_GlobalsBackend'         => 't3lib_cache_backend_GlobalsBackend',
 				't3lib_cache_backend_MemcachedBackend'       => 't3lib_cache_backend_MemcachedBackend',
 				't3lib_cache_backend_PdoBackend'             => 't3lib_cache_backend_PdoBackend',
+				't3lib_cache_backend_RedisBackend'           => 't3lib_cache_backend_RedisBackend',
 				't3lib_cache_backend_ApcBackend'             => 't3lib_cache_backend_ApcBackend',
 				't3lib_cache_backend_NullBackend'            => 't3lib_cache_backend_NullBackend',
 				't3lib_cache_backend_TransientMemoryBackend' => 't3lib_cache_backend_TransientMemoryBackend',
Index: t3lib/cache/backend/class.t3lib_cache_backend_redisbackend.php
===================================================================
--- t3lib/cache/backend/class.t3lib_cache_backend_redisbackend.php	(revision 0)
+++ t3lib/cache/backend/class.t3lib_cache_backend_redisbackend.php	(revision 0)
@@ -0,0 +1,702 @@
+<?php
+/***************************************************************
+*  Copyright notice
+*
+*  (c) 2010 Christian Kuhn <lolli@schwarzbu.ch>
+*  All rights reserved
+*
+*  This script is part of the TYPO3 project. The TYPO3 project is
+*  free software; you can redistribute it and/or modify
+*  it under the terms of the GNU General Public License as published by
+*  the Free Software Foundation; either version 2 of the License, or
+*  (at your option) any later version.
+*
+*  The GNU General Public License can be found at
+*  http://www.gnu.org/copyleft/gpl.html.
+*
+*  This script is distributed in the hope that it will be useful,
+*  but WITHOUT ANY WARRANTY; without even the implied warranty of
+*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+*  GNU General Public License for more details.
+*
+*  This copyright notice MUST APPEAR in all copies of the script!
+***************************************************************/
+
+/**
+ * A caching backend which stores cache entries by using Redis with phpredis
+ * PHP module. Redis is a noSQL database with very good scaling characteristics
+ * in proportion to the amount of entries and data size.
+ *
+ * @see http://code.google.com/p/redis/
+ * @see http://github.com/owlient/phpredis
+ *
+ * Warning:
+ * Redis and phpredis are young projects with very high development speed.
+ * This implementation should be considered as experimental for now,
+ * internals might break or change while the dependent projects mature.
+ *
+ * Successfully tested with:
+ * - redis
+ *   version 2.0.0-rc2, version 1.2.0 does not work
+ *   git version 9fd01051bf8400babcca73a76a67dfc1847633ff from 2010-11-12
+ * - phpredis
+ *   git version 0abb9e5ec07b8a8c20b5 from 2010-07-18
+ *   git version 12769b03c8ec17b25573e0453003712011bba241 from 2010-11-08
+ *
+ * Implementation based on ext:rediscache by Christopher Hlubek - networkteam GmbH
+ *
+ * This backend uses the following types of redis keys:
+ * - identData:xxx, value type "string", volatile, expires after given lifetime
+ *   xxx is the given identifier name, value is the cache data
+ * - identTags:xxx, value type "set"
+ *   xxx is the given identifier name, value is a set of associated tags.
+ *   This is a "reverse" tag index. It provides quick access for all tags
+ *   associated with this identifier and is used when removing the identifier.
+ * - tagIdents:xxx, value type "set"
+ *   xxx is a tag name, value is a set of associated identifiers.
+ *   This is "forward" tag index. It is mainly used for flushing content by tag.
+ * - temp:xxx, value type "set"
+ *   xxx is a unique id, value is a set of identifiers. Used as temporary key
+ *   used in flushByTag() and flushByTags(), removed after usage again.
+ *
+ * Each cache using this backend should use an own redis database to
+ * avoid namespace problems. By default redis has 16 databases which are
+ * identified with numbers 0 .. 15. setDatabase() can be used to select one.
+ * The unit tests use and flush database numbers 0 and 1, production use should start from 2.
+ *
+ * @package TYPO3
+ * @subpackage t3lib_cache
+ * @api
+ * @scope prototype
+ */
+class t3lib_cache_backend_RedisBackend extends t3lib_cache_backend_AbstractBackend {
+
+	/**
+	 * Faked unlimited lifetime = 31536000 (1 Year).
+	 * In redis an entry does not have a lifetime by default (it's not "volatile").
+	 * Entries can be made volatile either with EXPIRE after it has been SET,
+	 * or with SETEX, which is a combined SET and EXPIRE command.
+	 * But an entry can not be made "unvolatile" again. To set a volatile entry to
+	 * not volatile again, it must be DELeted and SET without a following EXPIRE.
+	 * To save these additional calls on every set(),
+	 * we just make every entry volatile and treat a high number as "unlimited"
+	 *
+	 * @see http://code.google.com/p/redis/wiki/ExpireCommand
+	 * @var integer Faked unlimited lifetime
+	 */
+	const FAKED_UNLIMITED_LIFETIME = 31536000;
+
+	/**
+	 * @var string Key prefix for identifier->data entries
+	 */
+	const IDENTIFIER_DATA_PREFIX = 'identData:';
+
+	/**
+	 * @var string Key prefix for identifier->tags sets
+	 */
+	const IDENTIFIER_TAGS_PREFIX = 'identTags:';
+
+	/**
+	 * @var string Key prefix for tag->identifiers sets
+	 */
+	const TAG_IDENTIFIERS_PREFIX = 'tagIdents:';
+
+	/**
+	 * @var Redis Instance of the PHP redis class
+	 */
+	protected $redis;
+
+	/**
+	 * @var boolean Indicates wether the server is connected
+	 */
+	protected $connected = FALSE;
+
+	/**
+	 * @var string Hostname / IP of the Redis server, defaults to 127.0.0.1.
+	 */
+	protected $hostname = '127.0.0.1';
+
+	/**
+	 * @var integer Port of the Redis server, defaults to 6379
+	 */
+	protected $port = 6379;
+
+	/**
+	 * @var integer Number of selected database, defaults to 0
+	 */
+	protected $database = 0;
+
+	/**
+	 * @var string Password for redis authentication
+	 */
+	protected $password = '';
+
+	/**
+	 * @var boolean Indicates wether data is compressed or not (requires php zlib)
+	 */
+	protected $compression = FALSE;
+
+	/**
+	 * @var integer -1 to 9, indicates zlib compression level: -1 = default level 6, 0 = no compression, 9 maximum compression
+	 */
+	protected $compressionLevel = -1;
+
+	/**
+	 * Construct this backend
+	 *
+	 * @param array Configuration options
+	 * @throws t3lib_cache_Exception if php redis module is not loaded
+	 * @author Christopher Hlubek <hlubek@networkteam.com>
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	public function __construct(array $options = array()) {
+		if (!extension_loaded('redis')) {
+			throw new t3lib_cache_Exception(
+				'The PHP extension "redis" must be installed and loaded in order to use the redis backend.',
+				1279462933
+			);
+		}
+
+		parent::__construct($options);
+
+		$this->initializeObject();
+	}
+
+	/**
+	 * Initializes the redis backend
+	 *
+	 * @param array Configuration options
+	 * @return void
+	 * @throws t3lib_cache_Exception if access to redis with password is denied or if database selection fails
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 */
+	protected function initializeObject() {
+		$this->redis = new Redis();
+
+		try {
+			$this->connected = $this->redis->connect($this->hostname, $this->port);
+		} catch (Exception $e) {
+			t3lib_div::sysLog('Unable to connect to redis server.', 'core', 3);
+		}
+
+		if ($this->connected) {
+			if (strlen($this->password)) {
+				$success = $this->redis->auth($this->password);
+				if (!$success) {
+					throw new t3lib_cache_Exception(
+						'The given password was not accepted by the redis server.',
+						1279765134
+					);
+				}
+			}
+
+			if ($this->database > 0) {
+				$success = $this->redis->select($this->database);
+				if (!$success) {
+					throw new t3lib_cache_Exception(
+						'The given database "' . $this->database . '" could not be selected.',
+						1279765144
+					);
+				}
+			}
+		}
+	}
+
+	/**
+	 * Setter for server hostname
+	 *
+	 * @param string Hostname
+	 * @return void
+	 * @author Christopher Hlubek <hlubek@networkteam.com>
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 * @api
+	 */
+	public function setHostname($hostname) {
+		$this->hostname = $hostname;
+	}
+
+	/**
+	 * Setter for server port
+	 *
+	 * @param integer Port
+	 * @return void
+	 * @author Christopher Hlubek <hlubek@networkteam.com>
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 * @api
+	 */
+	public function setPort($port) {
+		$this->port = $port;
+	}
+
+	/**
+	 * Setter for database number
+	 *
+	 * @param integer Database
+	 * @return void
+	 * @throws InvalidArgumentException if database number is not valid
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 * @api
+	 */
+	public function setDatabase($database) {
+		if (!is_integer($database)) {
+			throw new InvalidArgumentException(
+				'The specified database number is of type "' . gettype($database) . '" but an integer is expected.',
+				1279763057
+			);
+		}
+		if ($database < 0) {
+			throw new InvalidArgumentException(
+				'The specified database "' . $database. '" must be greater or equal than zero.',
+				1279763534
+			);
+		}
+
+		$this->database = $database;
+	}
+
+	/**
+	 * Setter for authentication password
+	 *
+	 * @param string Password
+	 * @return void
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 * @api
+	 */
+	public function setPassword($password) {
+		$this->password = $password;
+	}
+
+	/**
+	 * Enable data compression
+	 *
+	 * @param boolean TRUE to enable compression
+	 * @return void
+	 * @throws InvalidArgumentException if compression parameter is not of type boolean
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 * @api
+	 */
+	public function setCompression($compression) {
+		if (!is_bool($compression)) {
+			throw new InvalidArgumentException(
+				'The specified compression of type "' . gettype($compression) . '" but a boolean is expected.',
+				1289679153
+			);
+		}
+
+		$this->compression = $compression;
+	}
+
+	/**
+	 * Set data compression level.
+	 * If compression is enabled and this is not set,
+	 * gzcompress default level will be used.
+	 *
+	 * @param integer -1 to 9: Compression level
+	 * @return void
+	 * @throws InvalidArgumentException if compressionLevel parameter is not within allowed bounds
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 * @api
+	 */
+	public function setCompressionLevel($compressionLevel) {
+		if (!is_integer($compressionLevel)) {
+			throw new InvalidArgumentException(
+				'The specified compression of type "' . gettype($compressionLevel) . '" but an integer is expected.',
+				1289679154
+			);
+		}
+
+		if ($compressionLevel >= -1 && $compressionLevel <= 9) {
+			$this->compressionLevel = $compressionLevel;
+		} else {
+			throw new InvalidArgumentException(
+				'The specified compression level must be an integer between -1 and 9.',
+				1289679155
+			);
+		}
+	}
+
+	/**
+	 * Save data in the cache
+	 *
+	 * Scales O(1) with number of cache entries
+	 * Scales O(n) with number of tags
+	 *
+	 * @param string Identifier for this specific cache entry
+	 * @param string Data to be stored
+	 * @param array Tags to associate with this cache entry
+	 * @param integer Lifetime of this cache entry in seconds. If NULL is specified, default lifetime is used. "0" means unlimited lifetime.
+	 * @return void
+	 * @throws InvalidArgumentException if identifier is not valid
+	 * @throws t3lib_cache_Exception_InvalidData if data is not a string
+	 * @author Christopher Hlubek <hlubek@networkteam.com>
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 * @api
+	 */
+	public function set($entryIdentifier, $data, array $tags = array(), $lifetime = NULL) {
+		if (!is_string($entryIdentifier)) {
+			throw new InvalidArgumentException(
+				'The specified identifier is of type "' . gettype($entryIdentifier) . '" but a string is expected.',
+				1279470252
+			);
+		}
+		if (!is_string($data)) {
+			throw new t3lib_cache_Exception_InvalidData(
+				'The specified data is of type "' . gettype($data) . '" but a string is expected.',
+				1279469941
+			);
+		}
+
+		$lifetimeIsNull = is_null($lifetime);
+		$lifetimeIsInteger = is_integer($lifetime);
+
+		if (!$lifetimeIsNull && !$lifetimeIsInteger) {
+			throw new InvalidArgumentException(
+				'The specified lifetime is of type "' . gettype($lifetime) . '" but a string or NULL is expected.',
+				1279488008
+			);
+		}
+		if ($lifetimeIsInteger && $lifetime < 0) {
+			throw new InvalidArgumentException(
+				'The specified lifetime "' . $lifetime . '" must be greater or equal than zero.',
+				1279487573
+			);
+		}
+
+		if ($this->connected) {
+			$expiration = $lifetimeIsNull ? $this->defaultLifetime : $lifetime;
+			$expiration = $expiration === 0 ? self::FAKED_UNLIMITED_LIFETIME : $expiration;
+
+			if ($this->compression) {
+				$data = gzcompress($data, $this->compressionLevel);
+			}
+
+			$this->redis->setex(self::IDENTIFIER_DATA_PREFIX . $entryIdentifier, $expiration, $data);
+
+			$addTags = $tags;
+			$removeTags = array();
+			$existingTags = $this->redis->sMembers(self::IDENTIFIER_TAGS_PREFIX . $entryIdentifier);
+			if (!empty($existingTags)) {
+				$addTags = array_diff($tags, $existingTags);
+				$removeTags = array_diff($existingTags, $tags);
+			}
+
+			if (count($removeTags) > 0 || count($addTags) > 0) {
+				$queue = $this->redis->multi(Redis::PIPELINE);
+				foreach ($removeTags as $tag) {
+					$queue->sRemove(self::IDENTIFIER_TAGS_PREFIX . $entryIdentifier, $tag);
+					$queue->sRemove(self::TAG_IDENTIFIERS_PREFIX . $tag, $entryIdentifier);
+				}
+
+				foreach ($addTags as $tag) {
+					$queue->sAdd(self::IDENTIFIER_TAGS_PREFIX . $entryIdentifier, $tag);
+					$queue->sAdd(self::TAG_IDENTIFIERS_PREFIX . $tag, $entryIdentifier);
+				}
+				$queue->exec();
+			}
+		}
+	}
+
+	/**
+	 * Loads data from the cache.
+	 *
+	 * Scales O(1) with number of cache entries
+	 *
+	 * @param string An identifier which describes the cache entry to load
+	 * @return mixed The cache entry's content as a string or FALSE if the cache entry could not be loaded
+	 * @throws InvalidArgumentException if identifier is not a string
+	 * @author Christopher Hlubek <hlubek@networkteam.com>
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 * @api
+	 */
+	public function get($entryIdentifier) {
+		if (!is_string($entryIdentifier)) {
+			throw new InvalidArgumentException(
+				'The specified identifier is of type "' . gettype($entryIdentifier) . '" but a string is expected.',
+				1279470253
+			);
+		}
+
+		$storedEntry = FALSE;
+		if ($this->connected) {
+			$storedEntry = $this->redis->get(self::IDENTIFIER_DATA_PREFIX . $entryIdentifier);
+		}
+
+		if ($this->compression && strlen($storedEntry) > 0) {
+			$storedEntry = gzuncompress($storedEntry);
+		}
+
+		return $storedEntry;
+	}
+
+	/**
+	 * Checks if a cache entry with the specified identifier exists.
+	 *
+	 * Scales O(1) with number of cache entries
+	 *
+	 * @param string Identifier specifying the cache entry
+	 * @return boolean TRUE if such an entry exists, FALSE if not
+	 * @throws InvalidArgumentException if identifier is not a string
+	 * @author Christopher Hlubek <hlubek@networkteam.com>
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 * @api
+	 */
+	public function has($entryIdentifier) {
+		if (!is_string($entryIdentifier)) {
+			throw new InvalidArgumentException(
+				'The specified identifier is of type "' . gettype($entryIdentifier) . '" but a string is expected.',
+				1279470254
+			);
+		}
+		return $this->connected && $this->redis->exists(self::IDENTIFIER_DATA_PREFIX . $entryIdentifier);
+	}
+
+	/**
+	 * Removes all cache entries matching the specified identifier.
+	 *
+	 * Scales O(1) with number of cache entries
+	 * Scales O(n) with number of tags
+	 *
+	 * @param string Specifies the cache entry to remove
+	 * @return boolean TRUE if (at least) an entry could be removed or FALSE if no entry was found
+	 * @throws InvalidArgumentException if identifier is not a string
+	 * @author Christopher Hlubek <hlubek@networkteam.com>
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 * @api
+	 */
+	public function remove($entryIdentifier) {
+		if (!is_string($entryIdentifier)) {
+			throw new InvalidArgumentException(
+				'The specified identifier is of type "' . gettype($entryIdentifier) . '" but a string is expected.',
+				1279470255
+			);
+		}
+
+		$elementsDeleted = FALSE;
+		if ($this->connected) {
+			if ($this->redis->exists(self::IDENTIFIER_DATA_PREFIX . $entryIdentifier)) {
+				$assignedTags = $this->redis->sMembers(self::IDENTIFIER_TAGS_PREFIX . $entryIdentifier);
+
+				$queue = $this->redis->multi(Redis::PIPELINE);
+				foreach ($assignedTags as $tag) {
+					$queue->sRemove(self::TAG_IDENTIFIERS_PREFIX . $tag, $entryIdentifier);
+				}
+				$queue->delete(self::IDENTIFIER_DATA_PREFIX . $entryIdentifier, self::IDENTIFIER_TAGS_PREFIX . $entryIdentifier);
+				$queue->exec();
+				$elementsDeleted = TRUE;
+			}
+		}
+
+		return $elementsDeleted;
+	}
+
+	/**
+	 * Finds and returns all cache entry identifiers which are tagged by the
+	 * specified tag.
+	 *
+	 * Scales O(1) with number of cache entries
+	 * Scales O(n) with number of tag entries
+	 *
+	 * @param string The tag to search for
+	 * @return array An array of entries with all matching entries. An empty array if no entries matched
+	 * @throws InvalidArgumentException if tag is not a string
+	 * @author Christopher Hlubek <hlubek@networkteam.com>
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 * @api
+	 */
+	public function findIdentifiersByTag($tag) {
+		if (!is_string($tag)) {
+			throw new InvalidArgumentException(
+				'The specified tag is of type "' . gettype($tag) . '" but a string is expected.',
+				1279569759
+			);
+		}
+
+		$foundIdentifiers = array();
+		if ($this->connected) {
+			$foundIdentifiers = $this->redis->sMembers(self::TAG_IDENTIFIERS_PREFIX . $tag);
+		}
+
+		return $foundIdentifiers;
+	}
+
+	/**
+	 * Finds and returns all cache entry identifiers which are tagged
+	 * with all of the specified tags.
+	 *
+	 * Scales O(n) with number of tags
+	 *
+	 * @param array Array of tags to search for
+	 * @return array An array with identifiers of all matching entries. An empty array if no entries matched
+	 * @author Christopher Hlubek <hlubek@networkteam.com>
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 * @api
+	 */
+	public function findIdentifiersByTags(array $tags) {
+		$foundIdentifiers = array();
+
+		if ($this->connected) {
+			$tagsWithPrefix = array();
+			foreach ($tags as $tag) {
+				$tagsWithPrefix[] = self::TAG_IDENTIFIERS_PREFIX . $tag;
+			}
+			$foundIdentifiers = $this->redis->sInter($tagsWithPrefix);
+		}
+
+		return $foundIdentifiers;
+	}
+
+	/**
+	 * Removes all cache entries of this cache.
+	 *
+	 * Scales O(1) with number of cache entries
+	 *
+	 * @return void
+	 * @author Christopher Hlubek <hlubek@networkteam.com>
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 * @api
+	 */
+	public function flush() {
+		if ($this->connected) {
+			$this->redis->flushdb();
+		}
+	}
+
+	/**
+	 * Removes all cache entries of this cache which are tagged with the specified tag.
+	 *
+	 * Scales O(1) with number of cache entries
+	 * Scales O(n^2) with number of tag entries
+	 *
+	 * @param string Tag the entries must have
+	 * @return void
+	 * @throws InvalidArgumentException if identifier is not a string
+	 * @author Christopher Hlubek <hlubek@networkteam.com>
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 * @api
+	 */
+	public function flushByTag($tag) {
+		if (!is_string($tag)) {
+			throw new InvalidArgumentException(
+				'The specified tag is of type "' . gettype($tag) . '" but a string is expected.',
+				1279578078
+			);
+		}
+
+		if ($this->connected) {
+			$identifiers = $this->redis->sMembers(self::TAG_IDENTIFIERS_PREFIX . $tag);
+
+			if (count($identifiers) > 0) {
+				$this->removeIdentifierEntriesAndRelations($identifiers, array($tag));
+			}
+		}
+	}
+
+	/**
+	 * Removes all cache entries of this cache which are tagged with one of the specified tags.
+	 *
+	 * Scales O(1) with number of cache entries
+	 * Scales O(n^2) with number of tags
+	 *
+	 * @param array Tags the entries must have
+	 * @return void
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 * @api
+	 */
+	public function flushByTags(array $tags) {
+		if ($this->connected) {
+			$prefixedKeysToDelete = array();
+			foreach ($tags as $tag) {
+				$prefixedKeysToDelete[] = self::TAG_IDENTIFIERS_PREFIX . $tag;
+			}
+
+				// Get all identifiers tagged with at least one of the given tags
+			$identifiers = $this->redis->sUnion($prefixedKeysToDelete);
+
+			if (count($identifiers)) {
+				$this->removeIdentifierEntriesAndRelations($identifiers, $tags, $prefixedKeysToDelete);
+			}
+		}
+	}
+
+	/**
+	 * With the current internal structure, only the identifier to data entries
+	 * have a redis internal lifetime. If an entry expires, attached
+	 * identifier to tags and tag to identifiers entries will be left over.
+	 * This methods finds those entries and cleans them up.
+	 *
+	 * Scales O(n*m) with number of cache entries (n) and number of tags (m)
+	 *
+	 * @return void
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 * @author Christopher Hlubek <hlubek@networkteam.com>
+	 * @api
+	 */
+	public function collectGarbage() {
+		$identifierToTagsKeys = $this->redis->getKeys(self::IDENTIFIER_TAGS_PREFIX . '*');
+		foreach ($identifierToTagsKeys as $identifierToTagsKey) {
+			list(,$identifier) = explode(':', $identifierToTagsKey);
+				// Check if the data entry still exists
+			if (!$this->redis->exists(self::IDENTIFIER_DATA_PREFIX . $identifier)) {
+				$tagsToRemoveIdentifierFrom = $this->redis->sMembers($identifierToTagsKey);
+				$queue = $this->redis->multi(Redis::PIPELINE);
+				$queue->delete($identifierToTagsKey);
+				foreach ($tagsToRemoveIdentifierFrom as $tag) {
+					$queue->sRemove(self::TAG_IDENTIFIERS_PREFIX . $tag, $identifier);
+				}
+				$queue->exec();
+			}
+		}
+	}
+
+	/**
+	 * Helper method for flushByTag() and flushByTags()
+	 * Gets list of identifiers and tags and removes all relations of those tags
+	 *
+	 * Scales O(1) with number of cache entries
+	 * Scales O(n^2) with number of tags
+	 *
+	 * @param array List of identifiers to remove
+	 * @param array List of tags to be handled
+	 * @return void
+	 * @author Christian Kuhn <lolli@schwarzbu.ch>
+	 * @author Christopher Hlubek <hlubek@networkteam.com>
+	 */
+	protected function removeIdentifierEntriesAndRelations(array $identifiers, array $tags) {
+			// Set a temporary entry which holds all identifiers that need to be removed from
+			// the tag to identifiers sets
+		$uniqueTempKey = 'temp:' . uniqId();
+		$prefixedKeysToDelete = array($uniqueTempKey);
+
+		$prefixedIdentifierToTagsKeysToDelete = array();
+		foreach ($identifiers as $identifier) {
+			$prefixedKeysToDelete[] = self::IDENTIFIER_DATA_PREFIX . $identifier;
+			$prefixedIdentifierToTagsKeysToDelete[] = self::IDENTIFIER_TAGS_PREFIX . $identifier;
+		}
+		foreach ($tags as $tag) {
+			$prefixedKeysToDelete[] = self::TAG_IDENTIFIERS_PREFIX . $tag;
+		}
+
+		$tagToIdentifiersSetsToRemoveIdentifiersFrom = $this->redis->sUnion($prefixedIdentifierToTagsKeysToDelete);
+
+			// Remove the tag to identifier set of the given tags, they will be removed anyway
+		$tagToIdentifiersSetsToRemoveIdentifiersFrom = array_diff($tagToIdentifiersSetsToRemoveIdentifiersFrom, $tags);
+
+			// Diff all identifiers that must be removed from tag to identifiers sets off from a
+			// tag to identifiers set and store result in same tag to identifiers set again
+		$queue = $this->redis->multi(Redis::PIPELINE);
+		foreach ($identifiers as $identifier) {
+			$queue->sAdd($uniqueTempKey, $identifier);
+		}
+		foreach ($tagToIdentifiersSetsToRemoveIdentifiersFrom as $tagToIdentifiersSet) {
+			$queue->sDiffStore(
+				self::TAG_IDENTIFIERS_PREFIX . $tagToIdentifiersSet,
+				self::TAG_IDENTIFIERS_PREFIX . $tagToIdentifiersSet,
+				$uniqueTempKey
+			);
+		}
+
+		$queue->delete(array_merge($prefixedKeysToDelete, $prefixedIdentifierToTagsKeysToDelete));
+		$queue->exec();
+	}
+}
+?>
Index: t3lib/core_autoload.php
===================================================================
--- t3lib/core_autoload.php	(revision 9388)
+++ t3lib/core_autoload.php	(working copy)
@@ -95,6 +95,7 @@
 	't3lib_cache_backend_memcachedbackend' => PATH_t3lib . 'cache/backend/class.t3lib_cache_backend_memcachedbackend.php',
 	't3lib_cache_backend_nullbackend' => PATH_t3lib . 'cache/backend/class.t3lib_cache_backend_nullbackend.php',
 	't3lib_cache_backend_pdobackend' => PATH_t3lib . 'cache/backend/class.t3lib_cache_backend_pdobackend.php',
+	't3lib_cache_backend_redisbackend' => PATH_t3lib . 'cache/backend/class.t3lib_cache_backend_redisbackend.php',
 	't3lib_cache_backend_transientmemorybackend' => PATH_t3lib . 'cache/backend/class.t3lib_cache_backend_transientmemorybackend.php',
 	't3lib_cache_backend_backend' => PATH_t3lib . 'cache/backend/interfaces/interface.t3lib_cache_backend_backend.php',
 	't3lib_cache_backend_phpcapablebackend' => PATH_t3lib . 'cache/backend/interfaces/interface.t3lib_cache_backend_phpcapablebackend.php',
