Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding support for Redis Cluster & Username & TlsOptions #62

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 78 additions & 27 deletions src/Cm/RedisSession/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,12 @@
* - Detects inactive waiting processes to prevent false-positives in concurrency throttling.
*/

use Cm\RedisSession\Handler\ClusterConfigInterface;
use Cm\RedisSession\Handler\ConfigInterface;
use Cm\RedisSession\Handler\ConfigSentinelPasswordInterface;
use Cm\RedisSession\Handler\LoggerInterface;
use Cm\RedisSession\Handler\TlsOptionsConfigInterface;
use Cm\RedisSession\Handler\UsernameConfigInterface;

class Handler implements \SessionHandlerInterface
{
Expand Down Expand Up @@ -162,10 +165,20 @@ class Handler implements \SessionHandlerInterface
const DEFAULT_LIFETIME = 60;

/**
* @var \Credis_Client
* @var \Credis_Client|\Credis_ClusterClient
*/
protected $_redis;

/**
* @var bool
*/
protected readonly bool $_usePipeline;

/**
* @var bool
*/
protected readonly bool $_useCluster;

/**
* @var int
*/
Expand Down Expand Up @@ -278,10 +291,12 @@ public function __construct(ConfigInterface $config, LoggerInterface $logger, $r
$host = $this->config->getHost() ?: self::DEFAULT_HOST;
$port = $this->config->getPort() ?: self::DEFAULT_PORT;
$pass = $this->config->getPassword() ?: null;
$username = $this->config instanceof UsernameConfigInterface ? $this->config->getUsername() : null;
$timeout = $this->config->getTimeout() ?: self::DEFAULT_TIMEOUT;
$retries = $this->config->getRetries() ?: self::DEFAULT_RETRIES;
$persistent = $this->config->getPersistentIdentifier() ?: '';
$this->_dbNum = $this->config->getDatabase() ?: self::DEFAULT_DATABASE;
$tlsOptions = $this->config instanceof TlsOptionsConfigInterface ? $this->config->getTlsOptions() : null;

// General config
$this->_readOnly = $readOnly;
Expand All @@ -307,6 +322,8 @@ public function __construct(ConfigInterface $config, LoggerInterface $logger, $r

// Connect and authenticate
if ($sentinelServers && $sentinelMaster) {
$this->_usePipeline = true;
$this->_useCluster = false;
$servers = preg_split('/\s*,\s*/', trim($sentinelServers), -1, PREG_SPLIT_NO_EMPTY);
$sentinel = NULL;
$exception = NULL;
Expand All @@ -322,35 +339,35 @@ public function __construct(ConfigInterface $config, LoggerInterface $logger, $r
} catch (\CredisException $e) {
// Prevent throwing exception if Sentinel has no password set (error messages are different between redis 5 and redis 6)
if ($e->getCode() !== 0 || (
strpos($e->getMessage(), 'ERR Client sent AUTH, but no password is set') === false &&
strpos($e->getMessage(), 'ERR Client sent AUTH, but no password is set') === false &&
strpos($e->getMessage(), 'ERR AUTH <password> called without any password configured for the default user. Are you sure your configuration is correct?') === false)
) {
throw $e;
}
}
}

$sentinel = new \Credis_Sentinel($sentinelClient);
$sentinel
->setClientTimeout($timeout)
->setClientPersistent($persistent);
$redisMaster = $sentinel->getMasterClient($sentinelMaster);
if ($pass) $redisMaster->auth($pass);
if ($pass) $redisMaster->auth($pass, $username);

// Verify connected server is actually master as per Sentinel client spec
if ($sentinelVerifyMaster) {
$roleData = $redisMaster->role();
if ( ! $roleData || $roleData[0] != 'master') {
usleep(100000); // Sleep 100ms and try again
$redisMaster = $sentinel->getMasterClient($sentinelMaster);
if ($pass) $redisMaster->auth($pass);
if ($pass) $redisMaster->auth($pass, $username);
$roleData = $redisMaster->role();
if ( ! $roleData || $roleData[0] != 'master') {
throw new \Exception('Unable to determine master redis server.');
}
}
}
if ($this->_dbNum || $persistent) $redisMaster->select(0);
if (($this->_dbNum || $persistent) && !$this->_useCluster) $redisMaster->select(0);

$this->_redis = $redisMaster;
break 2;
Expand All @@ -366,13 +383,38 @@ public function __construct(ConfigInterface $config, LoggerInterface $logger, $r
}
}
else {
$this->_redis = new \Credis_Client($host, $port, $timeout, $persistent, 0, $pass);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Too bad this isn't using dependency injection here for creating \Credis_Client object. :-(

if (($config instanceof ClusterConfigInterface) && ($config->isCluster())) {
$this->_redis = new \Credis_ClusterClient(
$config->getClusterName(),
$config->getClusterSeeds(),
$timeout,
0,
$config->getClusterUsePersistentConnection(),
$pass,
$username,
$tlsOptions,
);
$this->_usePipeline = false;
$this->_useCluster = true;
} else {
$this->_redis = new \Credis_Client(
$host,
$port,
$timeout,
$persistent,
0,
$pass,
$username,
$tlsOptions
);
$this->_usePipeline = true;
$this->_useCluster = false;
}
$this->_redis->setMaxConnectRetries($retries);
if ($this->hasConnection() == false) {
throw new ConnectionFailedException('Unable to connect to Redis');
}
}

// Destructor order cannot be predicted
$this->_redis->setCloseOnDestruct(false);
$this->_log(
Expand Down Expand Up @@ -459,7 +501,7 @@ public function read($sessionId)
$timeStart = microtime(true);
$this->_log(sprintf("Attempting to take lock on ID %s", $sessionId));

$this->_redis->select($this->_dbNum);
if (!$this->_useCluster) $this->_redis->select($this->_dbNum);
while ($this->_useLocking && !$this->_readOnly)
{
// Increment lock value for this session and retrieve the new value
Expand Down Expand Up @@ -639,18 +681,19 @@ public function read($sessionId)
);
}
}

// Set session data and expiration
$this->_redis->pipeline();
if ($this->_usePipeline) {
// 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();

if ($this->_usePipeline) {
$this->_redis->exec();
}
// Reset flag in case of multiple session read/write operations
$this->_sessionWritten = false;

return $sessionData ? (string) $this->_decodeData($sessionData) : '';
}

Expand All @@ -673,7 +716,7 @@ public function write($sessionId, $sessionData)

// Do not overwrite the session if it is locked by another pid
try {
if($this->_dbNum) $this->_redis->select($this->_dbNum); // Prevent conflicts with other connections?
if ($this->_dbNum && !$this->_useCluster) $this->_redis->select($this->_dbNum); // Prevent conflicts with other connections?

if ( ! $this->_useLocking
|| ( ! ($pid = $this->_redis->hGet('sess_'.$sessionId, 'pid')) || $pid == $this->_getPid())
Expand Down Expand Up @@ -711,10 +754,14 @@ public function write($sessionId, $sessionData)
public function destroy($sessionId)
{
$this->_log(sprintf("Destroying ID %s", $sessionId));
$this->_redis->pipeline();
if($this->_dbNum) $this->_redis->select($this->_dbNum);
if ($this->_usePipeline) {
$this->_redis->pipeline();
}
if ($this->_dbNum && !$this->_useCluster) $this->_redis->select($this->_dbNum);
$this->_redis->unlink(self::SESSION_PREFIX.$sessionId);
$this->_redis->exec();
if ($this->_usePipeline) {
$this->_redis->exec();
}
return true;
}

Expand Down Expand Up @@ -832,7 +879,7 @@ protected function _encodeData($data)
case 'lz4': $data = lz4_compress($data); $prefix = ':l4:'; break;
case 'gzip': $data = gzcompress($data, 1); break;
}
if($data) {
if ($data) {
$data = $prefix.$data;
$this->_log(
sprintf(
Expand Down Expand Up @@ -880,15 +927,19 @@ protected function _decodeData($data)
protected function _writeRawSession($id, $data, $lifetime)
{
$sessionId = 'sess_' . $id;
$this->_redis->pipeline()
->select($this->_dbNum)
->hMSet($sessionId, array(
if ($this->_usePipeline) {
$this->_redis->pipeline();
}
if (!$this->_useCluster) $this->_redis->select($this->_dbNum);
$this->_redis->hMSet($sessionId, array(
'data' => $this->_encodeData($data),
'lock' => 0, // 0 so that next lock attempt will get 1
))
->hIncrBy($sessionId, 'writes', 1)
->expire($sessionId, min((int)$lifetime, (int)$this->_maxLifetime))
->exec();
));
$this->_redis->hIncrBy($sessionId, 'writes', 1);
$this->_redis->expire($sessionId, min((int)$lifetime, (int)$this->_maxLifetime));
if ($this->_usePipeline) {
$this->_redis->exec();
}
}

/**
Expand Down
62 changes: 62 additions & 0 deletions src/Cm/RedisSession/Handler/ClusterConfigInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php
/*
==New BSD License==

Copyright (c) 2013, Colin Mollenhour
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* The name of Colin Mollenhour may not be used to endorse or promote products
derived from this software without specific prior written permission.
* Redistributions in any form must not change the Cm_RedisSession namespace.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
namespace Cm\RedisSession\Handler;

interface ClusterConfigInterface extends ConfigInterface
{
/**
* Is this a cluster?
*
* @return bool
*/
public function isCluster() : bool;

/**
* Optional name for cluster as read in redis.ini
*
* @return bool
*/
public function getClusterName() : ?string;

/**
* Seeds for cluster
*
* @return bool
*/
public function getClusterSeeds() : ?array;

/**
* Should we use persistent connection?
*
* @return bool
*/
public function getClusterUsePersistentConnection() : bool;
}
41 changes: 41 additions & 0 deletions src/Cm/RedisSession/Handler/TlsOptionsConfigInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php
/*
==New BSD License==

Copyright (c) 2013, Colin Mollenhour
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* The name of Colin Mollenhour may not be used to endorse or promote products
derived from this software without specific prior written permission.
* Redistributions in any form must not change the Cm_RedisSession namespace.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
namespace Cm\RedisSession\Handler;

interface TlsOptionsConfigInterface extends ConfigInterface
{
/**
* Get optional TLS options
*
* @return array|null
*/
public function getTlsOptions() : ?array;
}
41 changes: 41 additions & 0 deletions src/Cm/RedisSession/Handler/UsernameConfigInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php
/*
==New BSD License==

Copyright (c) 2013, Colin Mollenhour
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* The name of Colin Mollenhour may not be used to endorse or promote products
derived from this software without specific prior written permission.
* Redistributions in any form must not change the Cm_RedisSession namespace.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
namespace Cm\RedisSession\Handler;

interface UsernameConfigInterface extends ConfigInterface
{
/**
* Get optional username
*
* @return string|null
*/
public function getUsername() : ?string;
}