1

I have a simple custom session storage where I want to save how many pages user has visited. I make an Ajax request to controller action on every page load which updates the custom foo session. Environment is local (Magento 2.4.3-p2), sessions are stored in DB, caches disabled: block_html, full_page.

The issue is that the session update is very incosistent:

  • sometimes the initial foo session data is not being set on page load (sessions cleared)
  • when initial foo data is set it then sometimes doesn't get updated on next page load (it should increment the pages visited)

Foo/Bar/etc/di.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Foo\Bar\Model\Session">
        <arguments>
            <argument name="storage" xsi:type="object">Foo\Bar\Model\Session\Storage</argument>
        </arguments>
    </type>
</config>

Foo/Bar/Model/Session.php

<?php

namespace Foo\Bar\Model;

class Session extends \Magento\Framework\Session\SessionManager
{
    /**
     * @var \Magento\Framework\Event\ManagerInterface
     */
    protected \Magento\Framework\Event\ManagerInterface $_eventManager;
    
    /**
     * @param \Magento\Framework\App\Request\Http                    $request
     * @param \Magento\Framework\Session\SidResolverInterface        $sidResolver
     * @param \Magento\Framework\Session\Config\ConfigInterface      $sessionConfig
     * @param \Magento\Framework\Session\SaveHandlerInterface        $saveHandler
     * @param \Magento\Framework\Session\ValidatorInterface          $validator
     * @param \Magento\Framework\Session\StorageInterface            $storage
     * @param \Magento\Framework\Stdlib\CookieManagerInterface       $cookieManager
     * @param \Magento\Framework\Stdlib\Cookie\CookieMetadataFactory $cookieMetadataFactory
     * @param \Magento\Framework\App\State                           $appState
     * @param \Magento\Framework\Event\ManagerInterface              $eventManager
     * @param \Magento\Framework\Session\SessionStartChecker|null    $sessionStartChecker
     */
    public function __construct(
        \Magento\Framework\App\Request\Http                    $request,
        \Magento\Framework\Session\SidResolverInterface        $sidResolver,
        \Magento\Framework\Session\Config\ConfigInterface      $sessionConfig,
        \Magento\Framework\Session\SaveHandlerInterface        $saveHandler,
        \Magento\Framework\Session\ValidatorInterface          $validator,
        \Magento\Framework\Session\StorageInterface            $storage,
        \Magento\Framework\Stdlib\CookieManagerInterface       $cookieManager,
        \Magento\Framework\Stdlib\Cookie\CookieMetadataFactory $cookieMetadataFactory,
        \Magento\Framework\App\State                           $appState,
        \Magento\Framework\Event\ManagerInterface              $eventManager,
        \Magento\Framework\Session\SessionStartChecker         $sessionStartChecker = null
    ) {
        $this->_eventManager = $eventManager;
        
        parent::__construct(
            $request,
            $sidResolver,
            $sessionConfig,
            $saveHandler,
            $validator,
            $storage,
            $cookieManager,
            $cookieMetadataFactory,
            $appState,
            $sessionStartChecker
        );
        
        $this->_eventManager->dispatch(
            'foo_session_init',
            ['foo_session' => $this]
        );
    }
    
    /**
     * @return void
     */
    public function setPagesVisitedData()
    {
        $pagesVisited = $this->getPagesVisited();
        
        if (empty($pagesVisited)) {
            $pagesVisited = 1;
        } else {
            $pagesVisited = (int) $pagesVisited + 1;
        }
        
        $this->setPagesVisited($pagesVisited);
    }
}

Foo/Bar/Model/Session/Storage.php

<?php

namespace Foo\Bar\Model\Session;

class Storage extends \Magento\Framework\Session\Storage
{
    /**
     * @param \Magento\Customer\Model\Config\Share       $configShare
     * @param \Magento\Store\Model\StoreManagerInterface $storeManager
     * @param string                                     $namespace
     * @param array                                      $data
     *
     * @throws \Magento\Framework\Exception\LocalizedException
     */
    public function __construct(
        \Magento\Customer\Model\Config\Share       $configShare,
        \Magento\Store\Model\StoreManagerInterface $storeManager,
        string                                     $namespace = 'foo',
        array                                      $data = []
    ) {
        if ($configShare->isWebsiteScope()) {
            $namespace .= '_' . $storeManager->getWebsite()->getCode();
        }
        
        parent::__construct($namespace, $data);
    }
}

Foo/Bar/Model/Controller/Index.php

<?php

namespace Foo\Bar\Controller;

class Index extends \Magento\Framework\App\Action\Action implements \Magento\Framework\App\Action\HttpPostActionInterface
{
    /**
     * @var \Foo\Bar\Model\Session
     */
    protected \Foo\Bar\Model\Session $_fooSession;
    
    /**
     * @param \Magento\Framework\App\Action\Context $context
     * @param \Foo\Bar\Model\Session                $fooSession
     */
    public function __construct(
        \Magento\Framework\App\Action\Context $context,
        \Foo\Bar\Model\Session                $fooSession
    
    ) {
        $this->_fooSession = $fooSession;
        
        parent::__construct($context);
    }
    
    /**
     * @return void
     */
    public function execute()
    {
        $this->_fooSession->setPagesVisitedData();
    }
}

Edit: Similar issue reported but I'm updating the session from Ajax so it's not related: Magento 2 custom sessions not consistent

1 Answer 1

2

It turns out the issue is within race conditions which occur if disable_locking is set to 1 for Redis session cache. Solution for me is to set it to 0 within app/etc/env.php:

'session' => [
    'save' => 'redis',
    'redis' => [
        ...
        'disable_locking' => 0 // Enables session locking
    ]
]

This will have some impact on performance but will prevent data loss or inconsistency when reading/writing session values.

This is one of many great explanations that describes what happens with multiple clients session interactions:

As you add more threads/clients/apps, things can go merrily and terribly wrong when multiple writers attempt modifying the data without coordination (i.e.g. locking)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.