thirose’s blog

openstackやpythonなどなど

キャッシュのデータを削除しても参照できてしまう。

今回のケースはpythonでsimplecacheを使っていて、キャッシュを消しても消えてないという事象に遭遇したので原因を調べてまとめてみました。

from werkzeug.contrib.cache import SimpleCache
import threading
import time
import datetime


class TestThread(threading.Thread):
    def __init__(self, number, sleep_time):
        super(TestThread, self).__init__()
        self.number = number
        self.sleep_time = sleep_time

    def run(self):
        for i in range(self.number):
            print('{}: thread-0: {}'.format(i, cache.get('key1')))
            if i == 1:
                cache.delete('key1')
                print('cache delete')
                cache.set('key1', 'test-data2')
            time.sleep(self.sleep_time)

def sub_thread(number, sleep_time, no):
    for i in range(number):
        print('{}: thread-{}: {}'.format(i, no, cache.get('key1')))
        time.sleep(sleep_time)

if __name__ == '__main__':
    cache = SimpleCache(10)
    cache.set('key1', 'test-data1')
    thread_main = TestThread(3, 2)
    thread_main.start()
    for i in range(1, 5):
        thread_sub = threading.Thread(target=sub_thread, name="th", args=(3, 2, i))
        thread_sub.start()

これを何回か実行してたらたしかに残ってた

$ python sample.py
0: thread-0: test-data1
0: thread-1: test-data1
0: thread-2: test-data1
0: thread-3: test-data1
0: thread-4: test-data1
1: thread-1: test-data1
1: thread-0: test-data1
cache delete
1: thread-3: test-data1
1: thread-2: test-data1
1: thread-4: test-data1
2: thread-1: test-data2
2: thread-0: test-data2
2: thread-3: test-data2
2: thread-2: test-data2
2: thread-4: test-data2

こういった場合は、素直にredisとか使うのが早そうだなと思い、redisに変更

import redis
import threading
import time
import datetime


class TestThread(threading.Thread):
    def __init__(self, number, sleep_time):
        super(TestThread, self).__init__()
        self.number = number
        self.sleep_time = sleep_time

    def run(self):
        for i in range(self.number):
            print('{}: thread-0: {}'.format(i, cache.get('key1')))
            if i == 1:
                cache.delete('key1')
                print('cache delete')
                cache.set('key1', 'test-data2')
            time.sleep(self.sleep_time)

def sub_thread(number, sleep_time, no):
    for i in range(number):
        print('{}: thread-{}: {}'.format(i, no, cache.get('key1')))
        time.sleep(sleep_time)

if __name__ == '__main__':
    cache = redis.StrictRedis(host='localhost', port=6379, db=0)
    cache.set('key1', 'test-data1')
    thread_main = TestThread(3, 2)
    thread_main.start()
    for i in range(1, 5):
        thread_sub = threading.Thread(target=sub_thread, name="th", args=(3, 2, i))
        thread_sub.start()

このように変更することで、deleteされてからの挙動は、

$ python sample-redis.py
0: thread-0: b'test-data1'
0: thread-1: b'test-data1'
0: thread-2: b'test-data1'
0: thread-3: b'test-data1'
0: thread-4: b'test-data1'
1: thread-3: b'test-data1'
1: thread-0: b'test-data1'
1: thread-1: b'test-data1'
cache delete
1: thread-2: None
1: thread-4: None
2: thread-2: b'test-data2'
2: thread-3: b'test-data2'
2: thread-1: b'test-data2'
2: thread-0: b'test-data2'
2: thread-4: b'test-data2'

のように、登録される前かと思いますが、消されたデータを参照するようなことはなくすことはできました。 原因はなんだよ?って思って調べてたら、しっかりドキュメントにも、以下のように書かれてました。

class werkzeug.contrib.cache.SimpleCache(threshold=500, default_timeout=300) Simple memory cache for single process environments. This class exists mainly for the development server and is not 100% thread safe. It tries to use as many atomic operations as possible and no locks for simplicity but it could happen under heavy load that keys are added multiple times.

Parameters:
threshold – the maximum number of items the cache stores before it starts deleting some. default_timeout – the default timeout that is used if no timeout is specified on set(). A timeout of 0 indicates that the cache never expires.

引用元: http://werkzeug.pocoo.org/docs/0.14/contrib/cache/

thread saveじゃないなら仕方ないね... しっかりドキュメント読みましょうということですね。 開発環境以外で一時的に保持したいデータがあるなら、simplecacheじゃなくて別のものを使うようにしよう。