yii\web\DbSession Causing Dirty Reads

All,

I have an issue where if a route is called more than once per second, the database updates from the first call don’t finish before the second call is made.

Took a while to diagnose, but the issue only occurs when using yii\web\DbSession. Reverting to standard session handling fixes the issue. Problem is, I need yii\web\DbSession for load balancing.

I’ve researched but can’t find much on this problem. Can anyone help please?

Thank you.

David

DbSession does not support session locking. Usually it is good thing (you can handle multiple concurrent requests using the same session ID) unless you’re storing really important data in session.

Thanks for your reply, and for the very interesting article.

Would there be a way to add session locking to DbSession? Or does a better solution exist?

I’m not sure if there is existing DbSession with session locking, but it should be quite easy to implement using mutex - acquire lock on session start (with some timeout) and release it on session close.

Thanks again – I’m learning a lot this morning!

Have implemented the following by adapting this StackOverflow answer:

config/web.php

'session' => [
    'class' => 'app\components\DbSessionMutex'
],
'mutex' => [
    'class' => 'yii\mutex\MysqlMutex'
]

components/DbSessionMutex.php

namespace app\components;

use Yii;
use yii\web\DbSession;

class DbSessionMutex extends DbSession
{
    public function init()
    {
        parent::init();
        Yii::$app->mutex->acquire('session-mutex', 10);
    }

    public function close()
    {
        parent::close();
        Yii::$app->mutex->release('session-mutex');
    }
}

Would this be a good way to go? Performance is a concern.

I would rather acquire mutex inside of open() instead of init(). And you should do this before parent::open(). Otherwise it looks fine, but you should test it - I never tried it myself :P.

Using mutex itself should not create any noticeable performance regressions. With session locking you will not be able to handle concurrent request using the same session ID, but this is what you want.

Outstanding. Testing looks promising so far.

Thank you very much for all your help. Very appreciated.

All the best,

David

Also note that you’re not checking if lock is actually acquired. You may try to throw exception if lock cannot be acquired, but it may be an overkill - probably processing without lock should be better choice than displaying Error 500 to user.
And mutex ID should be unique for each session - it may be better to use readSession() and writeSession() for mutex checks:

public function readSession($id)  {
    if (!Yii::$app->mutex->acquire('session-mutex:' . $id, 10)) {
        // throw exception?
    }

    parent::readSession();
}

Just following up on this. I tried using readSession($id) as suggested, but I get the following error:

session_start(): Failed to read session data: user (path: )

I’m not entirely sure why I can’t extend this method without causing an error. It is marked as @internal in the comments but I still don’t see why I can’t extend it.

Anyway, current solution is this:

namespace app\components;

use Yii;
use yii\web\DbSession;

/**
 * Moves session data into a database table to facilitate load balancing.
 * Uses a mutex to prevent dirty reads on the database (stock duplication bug).
 * https://forum.yiiframework.com/t/yii-web-dbsession-causing-dirty-reads/126817
 */
class DbSessionMutex extends DbSession
{
    public $mutex;

    public function open()
    {
        $this->mutex = 'session-mutex-' . session_id();
        Yii::$app->mutex->acquire($this->mutex, 10);
        parent::open();
    }

    public function close()
    {
        parent::close();
        Yii::$app->mutex->release($this->mutex);
    }
}