Posted at

MemoryCacheにUpdateCallbackを設定するとAbsoluteExpirationよりキャッシュの寿命が延びる


前置き

System.Runtime.Caching の MemoryCache はキャッシュの寿命を AbsoluteExpiration or SlidingExpiration で設定できます。

ただし実際には期限が切れても即座にキャッシュが削除されるわけではありません。

実際にキャッシュが削除されるタイミングは次の2つです。


  1. 決められた polling 間隔 (30s) で期限切れのキャッシュを削除

  2. キャッシュを Get した時点ですでに期限切れの場合、値を返す前に削除 (AbsoluteExpiration のみ)

このタイミングの時のみキャッシュが削除され、設定していれば RemovedCallback が呼ばれます。

SlidingExpiration を設定している場合は期限切れでもキャッシュが実際に削除される前に Get すればまた寿命が延長されます。

AbsoluteExpiration の場合は polling を待たずとも Get した時に期限切れで削除されるので(RemovedCallback を考慮しなければ)設定した期限通りの時間だけキャッシュが生存します。

2 のコード: https://github.com/Microsoft/referencesource/blob/60a4f8b853f60a424e36c7bf60f9b5b5f1973ed1/System.Runtime.Caching/System/Caching/MemoryCacheStore.cs#L243


本論

UpdateCallback を設定した場合、上記の 2 が無効になります。

そのため実際に設定した期限よりも長くキャッシュが残ります。その時間は最大で polling 間隔 (30s) と同じです。


問題

実際に期待するキャッシュ時間よりも polling 間隔を待つだけの時間、キャッシュの寿命が長くなります。

特にキャッシュ時間と polling 間隔が変わらないあるいは短い場合は顕著な影響が発生することが考えられます。


解説

Set() で UpdateCallback を設定した場合、下記のように別の Set() が呼ばれます。

https://github.com/Microsoft/referencesource/blob/60a4f8b853f60a424e36c7bf60f9b5b5f1973ed1/System.Runtime.Caching/System/Caching/MemoryCache.cs#L574

この中で cache entry が作成されるのは次の場所からです。

https://github.com/Microsoft/referencesource/blob/60a4f8b853f60a424e36c7bf60f9b5b5f1973ed1/System.Runtime.Caching/System/Caching/MemoryCache.cs#L627

見てわかる通り cache entry が 2 つ作成されています。通常の cache entry と sentinel entry です。

Expiration を設定しているにもかかわらず cache entry は寿命が無限です。設定した Expiration は sentinel entry の方に設定されています。

sentinel entry が実際に削除された時、下記の sentinel entry の RemovedCallback が呼ばれ、そこから本来の UpdateCallback が呼び出されるわけです。

https://github.com/Microsoft/referencesource/blob/60a4f8b853f60a424e36c7bf60f9b5b5f1973ed1/System.Runtime.Caching/System/Caching/MemoryCache.cs#L98

つまり cache entry が削除されるにはまず sentinel entry が削除される必要があります。

ここで前置きのキャッシュが削除されるタイミング 2 を思い出します。

"Get 時にキャッシュが期限切れなら値を返す前に削除する"わけですが、Get するのは通常の cache entry。これは寿命が無限なので、MemoryCache に与えた AbsoluteExpiration を過ぎていても削除されません。

sentinel entry にアクセスする手段はありません。そのために削除タイミング 2 は期待できず、1 を待つしかありません。

結果として polling を待つだけの時間、キャッシュの寿命が延長されることになります。


回避方法

AbsoluteExpiration を使わず自前でキャッシュの寿命を管理すれば回避できます。