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

[BUG]: Redis Cache/Storage Adapter race condition #16269

Open
Aphexx opened this issue Jan 18, 2023 · 4 comments
Open

[BUG]: Redis Cache/Storage Adapter race condition #16269

Aphexx opened this issue Jan 18, 2023 · 4 comments
Assignees
Labels
5.0 The issues we want to solve in the 5.0 release enhancement Enhancement to the framework

Comments

@Aphexx
Copy link

Aphexx commented Jan 18, 2023

Describe the bug
\Phalcon\Cache\Adapter\Redis has race condition that returns false in cases when value was deleted on redis server after this->has(key) check and before this->doGet(key). Also this->has(key) making excessive request to redis server.

if (true !== this->has(key)) {

To Reproduce

        $redisCache = new \Phalcon\Cache\Adapter\Redis(new \Phalcon\Storage\SerializerFactory(), [
            'index' => 3,
            'persistent' => false,
            'host' =>  '/tmp/redis-sock/redis.sock',
            'port' => null,
            'lifetime' => 3600,
            'prefix' => '_c_'
        ]);
        for($i=0;$i<200000;$i++) {
            $v = $redisCache->get('test2', 'default_value');
            if($v === 'default_value') {
                //correct behavior
                var_dump(['OK', $i, $v]);
                $redisCache->set('test2', 'value', 1);
            }else if($v === 'value') {
                //correct behavior
            } else {
                //incorrect behaviour. Must never get here
                var_dump(['ERROR', $i, $v]);
                die;
            }
        }

Will eventualy return false :
image

Expected behavior
Must return "value" if value exists in cache and "default_value" in case cache miss..

Solution
\Redis->get() returns bool false in case key not exists in Redis. We must remove this->has(key) request from \Phalcon\Cache\Adapter\Redis::get(), and add check for false. This will return $defaultValue for non existing keys, and will not create excessive request via this->has(key)

    public function get(string $key, $defaultValue =null) {
        $v = $this->doGet($key);
        if($v === false){
            return $defaultValue;
        }
        return $this->getUnserializedData($v, $defaultValue);
    }

Details

  • Phalcon version: 5.1.4
  • PHP Version: 8.1.14
  • Operating System: Linux
  • Installation type: pecl
  • Zephir version (if any): no
  • Server: Nginx
@Aphexx Aphexx added bug A bug report status: unverified Unverified labels Jan 18, 2023
@Aphexx
Copy link
Author

Aphexx commented Jan 18, 2023

Other cache adapters can be optimized replacing this->has(key) check in get() with adapter-specific check. For example apcu:

    public function get(string $key, $defaultValue=null) {
        $success = null;
        $v = apcu_fetch($this->getPrefixedKey($key), $success);
        if(!$success) {
            return $defaultValue;
        }
        return $v;
    }

libmemcached (in current implementation actually doing 2 requests in $cache->get($key))

    public function get(string $key, $defaultValue=null) {
        $connection = $this->adapter();
        $v = $connection->get(key);
        if(\Memcached::RES_NOTFOUND  === $connection->getResultCode()) {
            return $defaultValue;
        }
        return $v;
    }

@niden
Copy link
Member

niden commented Jan 18, 2023

What do we do if the actual stored value is false ?

$key = 'somekey';

$value = $this->doGet($key); // value is `false`
if (false === $value) {
    return $defaultValue; // null
}

return $value;

The reason we did it that way was just for the above scenario.

Thoughts?

@Aphexx
Copy link
Author

Aphexx commented Jan 20, 2023

Redis is not supporting storing boolean or int types, it stores everything as string. If the actual value will be false, it will be stored like "b:0;" string (depending of serializer adapter) and further passed to this->getUnserializedData and unserialized to php false.

Answering your question @niden, it will work properly with actual false value:

$key = 'somekey';

$serialized_value = $this->doGet($key); // if stored value is `false` will return string "b:0;" 
if (false === $value) {
    return $defaultValue; // null
}
$value = $this->getUnserializedData($serialized_value, $defaultValue); // "b:0;" will be unserialized to false
return $value;

@niden
Copy link
Member

niden commented Jan 20, 2023

That I did not know :)

Thank you for reporting this. I will get on it shortly.

@niden niden self-assigned this Jan 20, 2023
@niden niden added enhancement Enhancement to the framework 5.0 The issues we want to solve in the 5.0 release and removed bug A bug report status: unverified Unverified labels Jan 20, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
5.0 The issues we want to solve in the 5.0 release enhancement Enhancement to the framework
Projects
Status: In Progress
Development

No branches or pull requests

2 participants