symfony: Using nested cache contracts can cause deadlock between processes

Symfony version(s) affected: 5.2.*

Description Using nested cache contracts can cause deadlock between processes

How to reproduce test-deadlock.php:

#!/usr/bin/env php
<?php

require __DIR__.'/vendor/autoload.php';
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Cache\Adapter\MemcachedAdapter;
use Symfony\Contracts\Cache\ItemInterface;

class TestCommand extends Command {
    protected static $defaultName = 'test' ;

    private $keys = [
        'a' => ["abc_5", "def_4"],
        'b' => ["fgh_17", "ijk_21"],
    ];

    public function __construct() {
        $this->cache = new MemcachedAdapter(MemcachedAdapter::createConnection('memcached://localhost'));
        parent::__construct();
    }

    protected function configure() {
        $this->addArgument('key_set', InputArgument::REQUIRED);
    }

    public function execute(InputInterface $input, OutputInterface $output) {
        $key_set = $input->getArgument('key_set');
        $value = $this->cache->get($this->keys[$key_set][0], function (ItemInterface $item) use ($key_set) {
            sleep(10);
            $value2 = $this->cache->get($this->keys[$key_set][1], function (ItemInterface $item) {
                sleep(10);
                $item->expiresAfter(1);
                return 1;
            });
            $item->expiresAfter(1);
            return 1;
        });
        $output->writeln("All done");
        return 0;
    }
}

$app = new Application();
$app->add(new TestCommand());
$app->run();

composer.json:

{
    "require": {
        "symfony/console": "5.2.*",
        "symfony/framework-bundle": "5.2.*"
    }
}

memcached daemon should be running on localhost run composer install if you then run the scripts sequentially, php test-deadlock.php test a php test-deadlock.php test b then they exit correctly, after 20 seconds each But if run them in parallel in different terminals, they will never finish. If we look at the locks when the scripts are running, we will see something like this:

$ lslocks | grep php
php             32014 FLOCK 21.6K READ* 0     0   0 /.../vendor/symfony/cache/Adapter/PdoAdapter.php
php             32018 FLOCK 21.6K WRITE 0     0   0 /.../vendor/symfony/cache/Adapter/PdoAdapter.php
php             32018 FLOCK  1.9K READ* 0     0   0 /.../vendor/symfony/cache/Adapter/Psr16Adapter.php
php             32014 FLOCK  1.9K WRITE 0     0   0 /.../vendor/symfony/cache/Adapter/Psr16Adapter.php

Possible Solution Please make using flock() in cache contract optional, or add to the documentation that using nested cache contracts is dangerous. Additional context

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Comments: 21 (19 by maintainers)

Commits related to this issue

Most upvoted comments

This is not supposed to happen, because a process cannot dead-lock itself. Please share a reproducer if you can!

I’m not sure we can fix this in Symfony itself. What you need to do is to call LockRegistry::setFiles([]) from within your subprocesses to disable locking in them.