概要
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
)
- Symfonyアプリケーションが動作するWebコンテナ(
実装方法
doctrine-bundle
パッケージが 2.0系にバージョンアップしたタイミングで非推奨になった。
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 を使えと書いてあるので今回はそれに従って修正した
実装としてはこんな感じ
# 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
を確認すると、
どうやら ネームスペースを更新することで実質キャッシュクリア をしている模様。 実質。
※ versioningIsEnabled
が TRUE である場合
そして、その ネームスペースの更新は乱数を元に生成されていること 。
つまり、キャッシュをクリアする前に乱数のシード値を固定値で設定してしまうと、その後 $cache->clear()
をしても、同じネームスペースが作成されるため、実質キャッシュクリアができておらず、前のテストで実行したキャッシュが残っており、意図した挙動とならない。
Symfonyキャッシュクリアのイメージ
自分の場合は、テストデータを作成するパッケージが内部で乱数のシード値を設定していたために発生していた。
実際にキャッシュの中身を確認すると、ネームスペースがキャッシュのキーに含まれていることが分かる。
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の挙動は同じかもです。
そもそも、キャッシュの部分を検証したい内容に含めるかどうか(モックで代用する)とか考えてもいいかもしれない。