Help us understand the problem. What is going on with this article?

DoctrineCacheBundleが非推奨になったのでSymfonyのCacheを使うようにした

概要

Symfonyのアプリケーションで、キャッシュをする際に DoctrineCacheBundle を使っていたが、
doctrine-bundle が2.0系にバージョンアップしたタイミングで、
DoctrineCacheBundle が非推奨になっていたので、
SymfonyのCacheで実装したのと、その際に クリアキャッシュで少しハマった話。

環境

  • Docker
    • Symfonyアプリケーションが動作するWebコンテナ(:443, :80)
      • Apache 2.4.41
      • PHP 7.3.9
      • Symfony 4.4.3
      • PHPUnit 8.5.2
    • DBコンテナ(:3306)
      • MySQL 5.6
    • Memcachedコンテナ(:11211)

実装方法

doctrine-bundle パッケージが 2.0系にバージョンアップしたタイミングで非推奨になった。

https://github.com/doctrine/DoctrineBundle/blob/2.0.6/UPGRADE-2.0.md#deprecation-of-doctrinecachebundle

Deprecation of DoctrineCacheBundle
Configuring caches through DoctrineCacheBundle is no longer possible. Please use symfony/cache through the pool type or configure your cache services manually and use the service type.

Symfony/cache を使えと書いてあるので今回はそれに従って修正した :pencil:

実装としてはこんな感じ

# framework.yaml
framework:
    cache:
        pools:
            app_memcached:
                public: true
                adapter: cache.adapter.memcached
                provider: app_memcached_provider

HogeというクラスにキャッシュをDIさせる

# services.yaml
parameters:
    memcached_servers:
        - 'memcached://memcached:11211' # 環境によって複数台書いたり

services:
    app_memcached_provider:
        class: \Memcached
        factory: ['Symfony\Component\Cache\Adapter\MemcachedAdapter', 'createConnection']
        arguments:
            - "%memcached_servers%"

    hoge:
        class: Hoge
        arguments:
            - "@app_memcached"

使い方はこんな感じ(関数名とか動作内容はテキトー)

..
class Hoge
{
    /** @var CacheInterface */
    private $cache;

    public function __construct(
        CacheInterface $cache
    ) {
        $this->cache = $cache;
    }

    /**
     * キャッシュされていればtrue キャッシュされていなければ保存してfalseを返す
     * @param string $key
     * @return bool
     */
    public function isCached(string $key): bool
    {
        /** @var CacheItem $item */
        $item = $this->cache->getItem($key);

        if ($item->isHit()) {
            return true;
        }

        $item->set(true);
        $item->expiresAfter(3600);
        $this->cache->save($item);
        return false;
    }

これを検証するために書いたテスト。いたってシンプル。

..
class HogeTest extends WebTestCase
{
    use KernelTestTrait;

    public function setUp(): void
    {
        static::bootKernel();

        // mt_srand(1); // この乱数のシード値を設定するとテストが落ちる。なぜでしょう。
        $cache = $this->getContainer()->get('app_memcached');
        $cache->clear(); // テストの前にはキャッシュをクリアさせる
    }

    public function test_キャッシュが既にされていればTrueがかえること()
    {
        $hoge = $this->getContainer()->get('hoge');
        // 一旦キャッシュさせる
        $hoge->isCached('key');
        $this->assertTrue($hoge->isCached('key'));
    }

    public function test_キャッシュしてなければ新規にキャッシュされてFalseがかえること()
    {
        $hoge = $this->getContainer()->get('hoge');
        $this->assertFalse($hoge->isCached('key'));
    }
}

コメントに書いたように、SymfonyのCacheを使うようにしたことによって、ハマったというのが、

この キャッシュをクリアする前に乱数のシード値を設定しているとキャッシュがクリアされない という問題。

イメージでは、 $cache->clear() を実行すれば、キャッシュ内は全てクリアされるものだと思っていたが、

Symfony\Component\Cache\Traits\AbstractTrait.php を確認すると、

https://github.com/symfony/symfony/blob/v4.4.3/src/Symfony/Component/Cache/Traits/AbstractTrait.php#L112-L136

どうやら ネームスペースを更新することで実質キャッシュクリア をしている模様。 実質。

versioningIsEnabled が TRUE である場合

そして、その ネームスペースの更新は乱数を元に生成されていること

つまり、キャッシュをクリアする前に乱数のシード値を固定値で設定してしまうと、その後 $cache->clear() をしても、同じネームスペースが作成されるため、実質キャッシュクリアができておらず、前のテストで実行したキャッシュが残っており、意図した挙動とならない。

Symfonyキャッシュクリアのイメージ

image_symfony_cache.png

自分の場合は、テストデータを作成するパッケージが内部で乱数のシード値を設定していたために発生していた。

実際にキャッシュの中身を確認すると、ネームスペースがキャッシュのキーに含まれていることが分かる。

telnet memcached 11211
..
Connected to memcached.
Escape character is '^]'.

stats items
STAT items:1:number 16
..
stats cachedump 1 100
ITEM ivGyRuQzLa%3AXiCMZ%3Akey [4 b; 1580539034 s]
..
get ivGyRuQzLa%3AXiCMZ%3Akey
VALUE ivGyRuQzLa%3AXiCMZ%3Akey 0 4
b:1;
END

キャッシュのキーが ivGyRuQzLa:XiCMZ:key で 間の XiCMZ がネームスペースのバージョン。

もちろん $cache->clear() してもexpiredしない限り残り続けている。

回避策としては、

  • キャッシュクリアの前に、乱数の初期化をする
    • mt_srand()
  • もしくは、テストで検証したいキャッシュのキーが分かっているのであれば、明示的に消してからテストを実行する
    • $cache->delete('key')
    • 意図せず別のキーでキャッシュされてしまい、他のテストに影響を及ぼす可能性はある

他の人はどうやっているのか気になるところ。

  

今回、MemcachedAdapterでしか動作確認してませんが、RedisAdapterも同様に AbstractAdapterを継承しているので、clearの挙動は同じかもです。

そもそも、キャッシュの部分を検証したい内容に含めるかどうか(モックで代用する)とか考えてもいいかもしれない。

akkiihs
https://twitter.com/akkiihs
https://github.com/akkiihs
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away