123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947 |
- """Core disk and file backed cache API.
-
- """
-
- import codecs
- import contextlib as cl
- import errno
- import functools as ft
- import io
- import os
- import os.path as op
- import pickletools
- import sqlite3
- import struct
- import sys
- import threading
- import time
- import warnings
- import zlib
-
- if sys.hexversion < 0x03000000:
- import cPickle as pickle # pylint: disable=import-error
- # ISSUE #25 Fix for http://bugs.python.org/issue10211
- from cStringIO import StringIO as BytesIO # pylint: disable=import-error
- TextType = unicode # pylint: disable=invalid-name,undefined-variable
- BytesType = str
- INT_TYPES = int, long # pylint: disable=undefined-variable
- range = xrange # pylint: disable=redefined-builtin,invalid-name,undefined-variable
- io_open = io.open # pylint: disable=invalid-name
- else:
- import pickle
- from io import BytesIO # pylint: disable=ungrouped-imports
- TextType = str
- BytesType = bytes
- INT_TYPES = (int,)
- io_open = open # pylint: disable=invalid-name
-
- try:
- WindowsError
- except NameError:
- class WindowsError(Exception):
- "Windows error place-holder on platforms without support."
- pass
-
- class Constant(tuple):
- "Pretty display of immutable constant."
- def __new__(cls, name):
- return tuple.__new__(cls, (name,))
-
- def __repr__(self):
- return '%s' % self[0]
-
- DBNAME = 'cache.db'
- ENOVAL = Constant('ENOVAL')
- UNKNOWN = Constant('UNKNOWN')
-
- MODE_NONE = 0
- MODE_RAW = 1
- MODE_BINARY = 2
- MODE_TEXT = 3
- MODE_PICKLE = 4
-
- DEFAULT_SETTINGS = {
- u'statistics': 0, # False
- u'tag_index': 0, # False
- u'eviction_policy': u'least-recently-stored',
- u'size_limit': 2 ** 30, # 1gb
- u'cull_limit': 10,
- u'sqlite_auto_vacuum': 1, # FULL
- u'sqlite_cache_size': 2 ** 13, # 8,192 pages
- u'sqlite_journal_mode': u'wal',
- u'sqlite_mmap_size': 2 ** 26, # 64mb
- u'sqlite_synchronous': 1, # NORMAL
- u'disk_min_file_size': 2 ** 15, # 32kb
- u'disk_pickle_protocol': pickle.HIGHEST_PROTOCOL,
- }
-
- METADATA = {
- u'count': 0,
- u'size': 0,
- u'hits': 0,
- u'misses': 0,
- }
-
- EVICTION_POLICY = {
- 'none': {
- 'init': None,
- 'get': None,
- 'cull': None,
- },
- 'least-recently-stored': {
- 'init': (
- 'CREATE INDEX IF NOT EXISTS Cache_store_time ON'
- ' Cache (store_time)'
- ),
- 'get': None,
- 'cull': 'SELECT {fields} FROM Cache ORDER BY store_time LIMIT ?',
- },
- 'least-recently-used': {
- 'init': (
- 'CREATE INDEX IF NOT EXISTS Cache_access_time ON'
- ' Cache (access_time)'
- ),
- 'get': 'access_time = {now}',
- 'cull': 'SELECT {fields} FROM Cache ORDER BY access_time LIMIT ?',
- },
- 'least-frequently-used': {
- 'init': (
- 'CREATE INDEX IF NOT EXISTS Cache_access_count ON'
- ' Cache (access_count)'
- ),
- 'get': 'access_count = access_count + 1',
- 'cull': 'SELECT {fields} FROM Cache ORDER BY access_count LIMIT ?',
- },
- }
-
-
- class Disk(object):
- "Cache key and value serialization for SQLite database and files."
- def __init__(self, directory, min_file_size=0, pickle_protocol=0):
- """Initialize disk instance.
-
- :param str directory: directory path
- :param int min_file_size: minimum size for file use
- :param int pickle_protocol: pickle protocol for serialization
-
- """
- self._directory = directory
- self.min_file_size = min_file_size
- self.pickle_protocol = pickle_protocol
-
-
- def hash(self, key):
- """Compute portable hash for `key`.
-
- :param key: key to hash
- :return: hash value
-
- """
- mask = 0xFFFFFFFF
- disk_key, _ = self.put(key)
- type_disk_key = type(disk_key)
-
- if type_disk_key is sqlite3.Binary:
- return zlib.adler32(disk_key) & mask
- elif type_disk_key is TextType:
- return zlib.adler32(disk_key.encode('utf-8')) & mask # pylint: disable=no-member
- elif type_disk_key in INT_TYPES:
- return disk_key % mask
- else:
- assert type_disk_key is float
- return zlib.adler32(struct.pack('!d', disk_key)) & mask
-
-
- def put(self, key):
- """Convert `key` to fields key and raw for Cache table.
-
- :param key: key to convert
- :return: (database key, raw boolean) pair
-
- """
- # pylint: disable=bad-continuation,unidiomatic-typecheck
- type_key = type(key)
-
- if type_key is BytesType:
- return sqlite3.Binary(key), True
- elif ((type_key is TextType)
- or (type_key in INT_TYPES
- and -9223372036854775808 <= key <= 9223372036854775807)
- or (type_key is float)):
- return key, True
- else:
- data = pickle.dumps(key, protocol=self.pickle_protocol)
- result = pickletools.optimize(data)
- return sqlite3.Binary(result), False
-
-
- def get(self, key, raw):
- """Convert fields `key` and `raw` from Cache table to key.
-
- :param key: database key to convert
- :param bool raw: flag indicating raw database storage
- :return: corresponding Python key
-
- """
- # pylint: disable=no-self-use,unidiomatic-typecheck
- if raw:
- return BytesType(key) if type(key) is sqlite3.Binary else key
- else:
- return pickle.load(BytesIO(key))
-
-
- def store(self, value, read, key=UNKNOWN):
- """Convert `value` to fields size, mode, filename, and value for Cache
- table.
-
- :param value: value to convert
- :param bool read: True when value is file-like object
- :param key: key for item (default UNKNOWN)
- :return: (size, mode, filename, value) tuple for Cache table
-
- """
- # pylint: disable=unidiomatic-typecheck
- type_value = type(value)
- min_file_size = self.min_file_size
-
- if ((type_value is TextType and len(value) < min_file_size)
- or (type_value in INT_TYPES
- and -9223372036854775808 <= value <= 9223372036854775807)
- or (type_value is float)):
- return 0, MODE_RAW, None, value
- elif type_value is BytesType:
- if len(value) < min_file_size:
- return 0, MODE_RAW, None, sqlite3.Binary(value)
- else:
- filename, full_path = self.filename(key, value)
-
- with open(full_path, 'wb') as writer:
- writer.write(value)
-
- return len(value), MODE_BINARY, filename, None
- elif type_value is TextType:
- filename, full_path = self.filename(key, value)
-
- with io_open(full_path, 'w', encoding='UTF-8') as writer:
- writer.write(value)
-
- size = op.getsize(full_path)
- return size, MODE_TEXT, filename, None
- elif read:
- size = 0
- reader = ft.partial(value.read, 2 ** 22)
- filename, full_path = self.filename(key, value)
-
- with open(full_path, 'wb') as writer:
- for chunk in iter(reader, b''):
- size += len(chunk)
- writer.write(chunk)
-
- return size, MODE_BINARY, filename, None
- else:
- result = pickle.dumps(value, protocol=self.pickle_protocol)
-
- if len(result) < min_file_size:
- return 0, MODE_PICKLE, None, sqlite3.Binary(result)
- else:
- filename, full_path = self.filename(key, value)
-
- with open(full_path, 'wb') as writer:
- writer.write(result)
-
- return len(result), MODE_PICKLE, filename, None
-
-
- def fetch(self, mode, filename, value, read):
- """Convert fields `mode`, `filename`, and `value` from Cache table to
- value.
-
- :param int mode: value mode raw, binary, text, or pickle
- :param str filename: filename of corresponding value
- :param value: database value
- :param bool read: when True, return an open file handle
- :return: corresponding Python value
-
- """
- # pylint: disable=no-self-use,unidiomatic-typecheck
- if mode == MODE_RAW:
- return BytesType(value) if type(value) is sqlite3.Binary else value
- elif mode == MODE_BINARY:
- if read:
- return open(op.join(self._directory, filename), 'rb')
- else:
- with open(op.join(self._directory, filename), 'rb') as reader:
- return reader.read()
- elif mode == MODE_TEXT:
- full_path = op.join(self._directory, filename)
- with io_open(full_path, 'r', encoding='UTF-8') as reader:
- return reader.read()
- elif mode == MODE_PICKLE:
- if value is None:
- with open(op.join(self._directory, filename), 'rb') as reader:
- return pickle.load(reader)
- else:
- return pickle.load(BytesIO(value))
-
-
- def filename(self, key=UNKNOWN, value=UNKNOWN):
- """Return filename and full-path tuple for file storage.
-
- Filename will be a randomly generated 28 character hexadecimal string
- with ".val" suffixed. Two levels of sub-directories will be used to
- reduce the size of directories. On older filesystems, lookups in
- directories with many files may be slow.
-
- The default implementation ignores the `key` and `value` parameters.
-
- In some scenarios, for example :meth:`Cache.push
- <diskcache.Cache.push>`, the `key` or `value` may not be known when the
- item is stored in the cache.
-
- :param key: key for item (default UNKNOWN)
- :param value: value for item (default UNKNOWN)
-
- """
- # pylint: disable=unused-argument
- hex_name = codecs.encode(os.urandom(16), 'hex').decode('utf-8')
- sub_dir = op.join(hex_name[:2], hex_name[2:4])
- name = hex_name[4:] + '.val'
- directory = op.join(self._directory, sub_dir)
-
- try:
- os.makedirs(directory)
- except OSError as error:
- if error.errno != errno.EEXIST:
- raise
-
- filename = op.join(sub_dir, name)
- full_path = op.join(self._directory, filename)
- return filename, full_path
-
-
- def remove(self, filename):
- """Remove a file given by `filename`.
-
- This method is cross-thread and cross-process safe. If an "error no
- entry" occurs, it is suppressed.
-
- :param str filename: relative path to file
-
- """
- full_path = op.join(self._directory, filename)
-
- try:
- os.remove(full_path)
- except WindowsError:
- pass
- except OSError as error:
- if error.errno != errno.ENOENT:
- # ENOENT may occur if two caches attempt to delete the same
- # file at the same time.
- raise
-
-
- class Timeout(Exception):
- "Database timeout expired."
- pass
-
-
- class UnknownFileWarning(UserWarning):
- "Warning used by Cache.check for unknown files."
- pass
-
-
- class EmptyDirWarning(UserWarning):
- "Warning used by Cache.check for empty directories."
- pass
-
-
- class Cache(object):
- "Disk and file backed cache."
- # pylint: disable=bad-continuation
- def __init__(self, directory, timeout=60, disk=Disk, **settings):
- """Initialize cache instance.
-
- :param str directory: cache directory
- :param float timeout: SQLite connection timeout
- :param disk: Disk type or subclass for serialization
- :param settings: any of DEFAULT_SETTINGS
-
- """
- try:
- assert issubclass(disk, Disk)
- except (TypeError, AssertionError):
- raise ValueError('disk must subclass diskcache.Disk')
-
- self._directory = directory
- self._timeout = 0 # Manually handle retries during initialization.
- self._local = threading.local()
-
- if not op.isdir(directory):
- try:
- os.makedirs(directory, 0o755)
- except OSError as error:
- if error.errno != errno.EEXIST:
- raise EnvironmentError(
- error.errno,
- 'Cache directory "%s" does not exist'
- ' and could not be created' % self._directory
- )
-
- sql = self._sql_retry
-
- # Setup Settings table.
-
- try:
- current_settings = dict(sql(
- 'SELECT key, value FROM Settings'
- ).fetchall())
- except sqlite3.OperationalError:
- current_settings = {}
-
- sets = DEFAULT_SETTINGS.copy()
- sets.update(current_settings)
- sets.update(settings)
-
- for key in METADATA:
- sets.pop(key, None)
-
- # Chance to set pragmas before any tables are created.
-
- for key, value in sorted(sets.items()):
- if key.startswith('sqlite_'):
- self.reset(key, value, update=False)
-
- sql('CREATE TABLE IF NOT EXISTS Settings ('
- ' key TEXT NOT NULL UNIQUE,'
- ' value)'
- )
-
- # Setup Disk object (must happen after settings initialized).
-
- kwargs = {
- key[5:]: value for key, value in sets.items()
- if key.startswith('disk_')
- }
- self._disk = disk(directory, **kwargs)
-
- # Set cached attributes: updates settings and sets pragmas.
-
- for key, value in sets.items():
- query = 'INSERT OR REPLACE INTO Settings VALUES (?, ?)'
- sql(query, (key, value))
- self.reset(key, value)
-
- for key, value in METADATA.items():
- query = 'INSERT OR IGNORE INTO Settings VALUES (?, ?)'
- sql(query, (key, value))
- self.reset(key)
-
- (self._page_size,), = sql('PRAGMA page_size').fetchall()
-
- # Setup Cache table.
-
- sql('CREATE TABLE IF NOT EXISTS Cache ('
- ' rowid INTEGER PRIMARY KEY,'
- ' key BLOB,'
- ' raw INTEGER,'
- ' store_time REAL,'
- ' expire_time REAL,'
- ' access_time REAL,'
- ' access_count INTEGER DEFAULT 0,'
- ' tag BLOB,'
- ' size INTEGER DEFAULT 0,'
- ' mode INTEGER DEFAULT 0,'
- ' filename TEXT,'
- ' value BLOB)'
- )
-
- sql('CREATE UNIQUE INDEX IF NOT EXISTS Cache_key_raw ON'
- ' Cache(key, raw)'
- )
-
- sql('CREATE INDEX IF NOT EXISTS Cache_expire_time ON'
- ' Cache (expire_time)'
- )
-
- query = EVICTION_POLICY[self.eviction_policy]['init']
-
- if query is not None:
- sql(query)
-
- # Use triggers to keep Metadata updated.
-
- sql('CREATE TRIGGER IF NOT EXISTS Settings_count_insert'
- ' AFTER INSERT ON Cache FOR EACH ROW BEGIN'
- ' UPDATE Settings SET value = value + 1'
- ' WHERE key = "count"; END'
- )
-
- sql('CREATE TRIGGER IF NOT EXISTS Settings_count_delete'
- ' AFTER DELETE ON Cache FOR EACH ROW BEGIN'
- ' UPDATE Settings SET value = value - 1'
- ' WHERE key = "count"; END'
- )
-
- sql('CREATE TRIGGER IF NOT EXISTS Settings_size_insert'
- ' AFTER INSERT ON Cache FOR EACH ROW BEGIN'
- ' UPDATE Settings SET value = value + NEW.size'
- ' WHERE key = "size"; END'
- )
-
- sql('CREATE TRIGGER IF NOT EXISTS Settings_size_update'
- ' AFTER UPDATE ON Cache FOR EACH ROW BEGIN'
- ' UPDATE Settings'
- ' SET value = value + NEW.size - OLD.size'
- ' WHERE key = "size"; END'
- )
-
- sql('CREATE TRIGGER IF NOT EXISTS Settings_size_delete'
- ' AFTER DELETE ON Cache FOR EACH ROW BEGIN'
- ' UPDATE Settings SET value = value - OLD.size'
- ' WHERE key = "size"; END'
- )
-
- # Create tag index if requested.
-
- if self.tag_index: # pylint: disable=no-member
- self.create_tag_index()
- else:
- self.drop_tag_index()
-
- # Close and re-open database connection with given timeout.
-
- self.close()
- self._timeout = timeout
- self._sql # pylint: disable=pointless-statement
-
-
- @property
- def directory(self):
- """Cache directory."""
- return self._directory
-
-
- @property
- def timeout(self):
- """SQLite connection timeout value in seconds."""
- return self._timeout
-
-
- @property
- def disk(self):
- """Disk used for serialization."""
- return self._disk
-
-
- @property
- def _con(self):
- # Check process ID to support process forking. If the process
- # ID changes, close the connection and update the process ID.
-
- local_pid = getattr(self._local, 'pid', None)
- pid = os.getpid()
-
- if local_pid != pid:
- self.close()
- self._local.pid = pid
-
- con = getattr(self._local, 'con', None)
-
- if con is None:
- con = self._local.con = sqlite3.connect(
- op.join(self._directory, DBNAME),
- timeout=self._timeout,
- isolation_level=None,
- )
-
- # Some SQLite pragmas work on a per-connection basis so
- # query the Settings table and reset the pragmas. The
- # Settings table may not exist so catch and ignore the
- # OperationalError that may occur.
-
- try:
- select = 'SELECT key, value FROM Settings'
- settings = con.execute(select).fetchall()
- except sqlite3.OperationalError:
- pass
- else:
- for key, value in settings:
- if key.startswith('sqlite_'):
- self.reset(key, value, update=False)
-
- return con
-
-
- @property
- def _sql(self):
- return self._con.execute
-
-
- @property
- def _sql_retry(self):
- sql = self._sql
-
- # 2018-11-01 GrantJ - Some SQLite builds/versions handle
- # the SQLITE_BUSY return value and connection parameter
- # "timeout" differently. For a more reliable duration,
- # manually retry the statement for 60 seconds. Only used
- # by statements which modify the database and do not use
- # a transaction (like those in ``__init__`` or ``reset``).
- # See Issue #85 for and tests/issue_85.py for more details.
-
- def _execute_with_retry(statement, *args, **kwargs):
- start = time.time()
- while True:
- try:
- return sql(statement, *args, **kwargs)
- except sqlite3.OperationalError as exc:
- if str(exc) != 'database is locked':
- raise
- diff = time.time() - start
- if diff > 60:
- raise
- time.sleep(0.001)
-
- return _execute_with_retry
-
-
- @cl.contextmanager
- def _transact(self, filename=None):
- sql = self._sql
- filenames = []
- _disk_remove = self._disk.remove
-
- try:
- sql('BEGIN IMMEDIATE')
- except sqlite3.OperationalError:
- if filename is not None:
- _disk_remove(filename)
- raise Timeout
-
- try:
- yield sql, filenames.append
- except BaseException:
- sql('ROLLBACK')
- raise
- else:
- sql('COMMIT')
- for name in filenames:
- if name is not None:
- _disk_remove(name)
-
-
- def set(self, key, value, expire=None, read=False, tag=None):
- """Set `key` and `value` item in cache.
-
- When `read` is `True`, `value` should be a file-like object opened
- for reading in binary mode.
-
- :param key: key for item
- :param value: value for item
- :param float expire: seconds until item expires
- (default None, no expiry)
- :param bool read: read value as bytes from file (default False)
- :param str tag: text to associate with key (default None)
- :return: True if item was set
- :raises Timeout: if database timeout expires
-
- """
- now = time.time()
- db_key, raw = self._disk.put(key)
- expire_time = None if expire is None else now + expire
- size, mode, filename, db_value = self._disk.store(value, read, key=key)
- columns = (expire_time, tag, size, mode, filename, db_value)
-
- # The order of SELECT, UPDATE, and INSERT is important below.
- #
- # Typical cache usage pattern is:
- #
- # value = cache.get(key)
- # if value is None:
- # value = expensive_calculation()
- # cache.set(key, value)
- #
- # Cache.get does not evict expired keys to avoid writes during lookups.
- # Commonly used/expired keys will therefore remain in the cache making
- # an UPDATE the preferred path.
- #
- # The alternative is to assume the key is not present by first trying
- # to INSERT and then handling the IntegrityError that occurs from
- # violating the UNIQUE constraint. This optimistic approach was
- # rejected based on the common cache usage pattern.
- #
- # INSERT OR REPLACE aka UPSERT is not used because the old filename may
- # need cleanup.
-
- with self._transact(filename) as (sql, cleanup):
- rows = sql(
- 'SELECT rowid, filename FROM Cache'
- ' WHERE key = ? AND raw = ?',
- (db_key, raw),
- ).fetchall()
-
- if rows:
- (rowid, old_filename), = rows
- cleanup(old_filename)
- self._row_update(rowid, now, columns)
- else:
- self._row_insert(db_key, raw, now, columns)
-
- self._cull(now, sql, cleanup)
-
- return True
-
-
- __setitem__ = set
-
-
- def _row_update(self, rowid, now, columns):
- sql = self._sql
- expire_time, tag, size, mode, filename, value = columns
- sql('UPDATE Cache SET'
- ' store_time = ?,'
- ' expire_time = ?,'
- ' access_time = ?,'
- ' access_count = ?,'
- ' tag = ?,'
- ' size = ?,'
- ' mode = ?,'
- ' filename = ?,'
- ' value = ?'
- ' WHERE rowid = ?', (
- now, # store_time
- expire_time,
- now, # access_time
- 0, # access_count
- tag,
- size,
- mode,
- filename,
- value,
- rowid,
- ),
- )
-
-
- def _row_insert(self, key, raw, now, columns):
- sql = self._sql
- expire_time, tag, size, mode, filename, value = columns
- sql('INSERT INTO Cache('
- ' key, raw, store_time, expire_time, access_time,'
- ' access_count, tag, size, mode, filename, value'
- ') VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', (
- key,
- raw,
- now, # store_time
- expire_time,
- now, # access_time
- 0, # access_count
- tag,
- size,
- mode,
- filename,
- value,
- ),
- )
-
-
- def _cull(self, now, sql, cleanup, limit=None):
- cull_limit = self.cull_limit if limit is None else limit
-
- if cull_limit == 0:
- return
-
- # Evict expired keys.
-
- select_expired_template = (
- 'SELECT %s FROM Cache'
- ' WHERE expire_time IS NOT NULL AND expire_time < ?'
- ' ORDER BY expire_time LIMIT ?'
- )
-
- select_expired = select_expired_template % 'filename'
- rows = sql(select_expired, (now, cull_limit)).fetchall()
-
- if rows:
- delete_expired = (
- 'DELETE FROM Cache WHERE rowid IN (%s)'
- % (select_expired_template % 'rowid')
- )
- sql(delete_expired, (now, cull_limit))
-
- for filename, in rows:
- cleanup(filename)
-
- cull_limit -= len(rows)
-
- if cull_limit == 0:
- return
-
- # Evict keys by policy.
-
- select_policy = EVICTION_POLICY[self.eviction_policy]['cull']
-
- if select_policy is None or self.volume() < self.size_limit:
- return
-
- select_filename = select_policy.format(fields='filename', now=now)
- rows = sql(select_filename, (cull_limit,)).fetchall()
-
- if rows:
- delete = (
- 'DELETE FROM Cache WHERE rowid IN (%s)'
- % (select_policy.format(fields='rowid', now=now))
- )
- sql(delete, (cull_limit,))
-
- for filename, in rows:
- cleanup(filename)
-
-
- def add(self, key, value, expire=None, read=False, tag=None):
- """Add `key` and `value` item to cache.
-
- Similar to `set`, but only add to cache if key not present.
-
- Operation is atomic. Only one concurrent add operation for a given key
- will succeed.
-
- When `read` is `True`, `value` should be a file-like object opened
- for reading in binary mode.
-
- :param key: key for item
- :param value: value for item
- :param float expire: seconds until the key expires
- (default None, no expiry)
- :param bool read: read value as bytes from file (default False)
- :param str tag: text to associate with key (default None)
- :return: True if item was added
- :raises Timeout: if database timeout expires
-
- """
- now = time.time()
- db_key, raw = self._disk.put(key)
- expire_time = None if expire is None else now + expire
- size, mode, filename, db_value = self._disk.store(value, read, key=key)
- columns = (expire_time, tag, size, mode, filename, db_value)
-
- with self._transact(filename) as (sql, cleanup):
- rows = sql(
- 'SELECT rowid, filename, expire_time FROM Cache'
- ' WHERE key = ? AND raw = ?',
- (db_key, raw),
- ).fetchall()
-
- if rows:
- (rowid, old_filename, old_expire_time), = rows
-
- if old_expire_time is None or old_expire_time > now:
- cleanup(filename)
- return False
-
- cleanup(old_filename)
- self._row_update(rowid, now, columns)
- else:
- self._row_insert(db_key, raw, now, columns)
-
- self._cull(now, sql, cleanup)
-
- return True
-
-
- def incr(self, key, delta=1, default=0):
- """Increment value by delta for item with key.
-
- If key is missing and default is None then raise KeyError. Else if key
- is missing and default is not None then use default for value.
-
- Operation is atomic. All concurrent increment operations will be
- counted individually.
-
- Assumes value may be stored in a SQLite column. Most builds that target
- machines with 64-bit pointer widths will support 64-bit signed
- integers.
-
- :param key: key for item
- :param int delta: amount to increment (default 1)
- :param int default: value if key is missing (default None)
- :return: new value for item
- :raises KeyError: if key is not found and default is None
- :raises Timeout: if database timeout expires
-
- """
- now = time.time()
- db_key, raw = self._disk.put(key)
- select = (
- 'SELECT rowid, expire_time, filename, value FROM Cache'
- ' WHERE key = ? AND raw = ?'
- )
-
- with self._transact() as (sql, cleanup):
- rows = sql(select, (db_key, raw)).fetchall()
-
- if not rows:
- if default is None:
- raise KeyError(key)
-
- value = default + delta
- columns = (None, None) + self._disk.store(value, False, key=key)
- self._row_insert(db_key, raw, now, columns)
- self._cull(now, sql, cleanup)
- return value
-
- (rowid, expire_time, filename, value), = rows
-
- if expire_time is not None and expire_time < now:
- if default is None:
- raise KeyError(key)
-
- value = default + delta
- columns = (None, None) + self._disk.store(value, False, key=key)
- self._row_update(rowid, now, columns)
- self._cull(now, sql, cleanup)
- cleanup(filename)
- return value
-
- value += delta
-
- columns = 'store_time = ?, value = ?'
- update_column = EVICTION_POLICY[self.eviction_policy]['get']
-
- if update_column is not None:
- columns += ', ' + update_column.format(now=now)
-
- update = 'UPDATE Cache SET %s WHERE rowid = ?' % columns
- sql(update, (now, value, rowid))
-
- return value
-
-
- def decr(self, key, delta=1, default=0):
- """Decrement value by delta for item with key.
-
- If key is missing and default is None then raise KeyError. Else if key
- is missing and default is not None then use default for value.
-
- Operation is atomic. All concurrent decrement operations will be
- counted individually.
-
- Unlike Memcached, negative values are supported. Value may be
- decremented below zero.
-
- Assumes value may be stored in a SQLite column. Most builds that target
- machines with 64-bit pointer widths will support 64-bit signed
- integers.
-
- :param key: key for item
- :param int delta: amount to decrement (default 1)
- :param int default: value if key is missing (default 0)
- :return: new value for item
- :raises KeyError: if key is not found and default is None
- :raises Timeout: if database timeout expires
-
- """
- return self.incr(key, -delta, default)
-
-
- def get(self, key, default=None, read=False, expire_time=False, tag=False):
- """Retrieve value from cache. If `key` is missing, return `default`.
-
- :param key: key for item
- :param default: value to return if key is missing (default None)
- :param bool read: if True, return file handle to value
- (default False)
- :param bool expire_time: if True, return expire_time in tuple
- (default False)
- :param bool tag: if True, return tag in tuple (default False)
- :return: value for item or default if key not found
- :raises Timeout: if database timeout expires
-
- """
- db_key, raw = self._disk.put(key)
- update_column = EVICTION_POLICY[self.eviction_policy]['get']
- select = (
- 'SELECT rowid, expire_time, tag, mode, filename, value'
- ' FROM Cache WHERE key = ? AND raw = ?'
- ' AND (expire_time IS NULL OR expire_time > ?)'
- )
-
- if expire_time and tag:
- default = (default, None, None)
- elif expire_time or tag:
- default = (default, None)
-
- if not self.statistics and update_column is None:
- # Fast path, no transaction necessary.
-
- rows = self._sql(select, (db_key, raw, time.time())).fetchall()
-
- if not rows:
- return default
-
- (rowid, db_expire_time, db_tag, mode, filename, db_value), = rows
-
- try:
- value = self._disk.fetch(mode, filename, db_value, read)
- except IOError:
- # Key was deleted before we could retrieve result.
- return default
-
- else: # Slow path, transaction required.
- cache_hit = (
- 'UPDATE Settings SET value = value + 1 WHERE key = "hits"'
- )
- cache_miss = (
- 'UPDATE Settings SET value = value + 1 WHERE key = "misses"'
- )
-
- with self._transact() as (sql, _):
- rows = sql(select, (db_key, raw, time.time())).fetchall()
-
- if not rows:
- if self.statistics:
- sql(cache_miss)
- return default
-
- (rowid, db_expire_time, db_tag,
- mode, filename, db_value), = rows
-
- try:
- value = self._disk.fetch(mode, filename, db_value, read)
- except IOError as error:
- if error.errno == errno.ENOENT:
- # Key was deleted before we could retrieve result.
- if self.statistics:
- sql(cache_miss)
- return default
- else:
- raise
-
- if self.statistics:
- sql(cache_hit)
-
- now = time.time()
- update = 'UPDATE Cache SET %s WHERE rowid = ?'
-
- if update_column is not None:
- sql(update % update_column.format(now=now), (rowid,))
-
- if expire_time and tag:
- return (value, db_expire_time, db_tag)
- elif expire_time:
- return (value, db_expire_time)
- elif tag:
- return (value, db_tag)
- else:
- return value
-
-
- def __getitem__(self, key):
- """Return corresponding value for `key` from cache.
-
- :param key: key matching item
- :return: corresponding value
- :raises KeyError: if key is not found
- :raises Timeout: if database timeout expires
-
- """
- value = self.get(key, default=ENOVAL)
- if value is ENOVAL:
- raise KeyError(key)
- return value
-
-
- def read(self, key):
- """Return file handle value corresponding to `key` from cache.
-
- :param key: key matching item
- :return: file open for reading in binary mode
- :raises KeyError: if key is not found
- :raises Timeout: if database timeout expires
-
- """
- handle = self.get(key, default=ENOVAL, read=True)
- if handle is ENOVAL:
- raise KeyError(key)
- return handle
-
-
- def __contains__(self, key):
- """Return `True` if `key` matching item is found in cache.
-
- :param key: key matching item
- :return: True if key matching item
-
- """
- sql = self._sql
- db_key, raw = self._disk.put(key)
- select = (
- 'SELECT rowid FROM Cache'
- ' WHERE key = ? AND raw = ?'
- ' AND (expire_time IS NULL OR expire_time > ?)'
- )
-
- rows = sql(select, (db_key, raw, time.time())).fetchall()
-
- return bool(rows)
-
-
- def pop(self, key, default=None, expire_time=False, tag=False):
- """Remove corresponding item for `key` from cache and return value.
-
- If `key` is missing, return `default`.
-
- Operation is atomic. Concurrent operations will be serialized.
-
- :param key: key for item
- :param default: value to return if key is missing (default None)
- :param bool expire_time: if True, return expire_time in tuple
- (default False)
- :param bool tag: if True, return tag in tuple (default False)
- :return: value for item or default if key not found
- :raises Timeout: if database timeout expires
-
- """
- db_key, raw = self._disk.put(key)
- select = (
- 'SELECT rowid, expire_time, tag, mode, filename, value'
- ' FROM Cache WHERE key = ? AND raw = ?'
- ' AND (expire_time IS NULL OR expire_time > ?)'
- )
-
- if expire_time and tag:
- default = default, None, None
- elif expire_time or tag:
- default = default, None
-
- with self._transact() as (sql, _):
- rows = sql(select, (db_key, raw, time.time())).fetchall()
-
- if not rows:
- return default
-
- (rowid, db_expire_time, db_tag, mode, filename, db_value), = rows
-
- sql('DELETE FROM Cache WHERE rowid = ?', (rowid,))
-
- try:
- value = self._disk.fetch(mode, filename, db_value, False)
- except IOError as error:
- if error.errno == errno.ENOENT:
- # Key was deleted before we could retrieve result.
- return default
- else:
- raise
- finally:
- if filename is not None:
- self._disk.remove(filename)
-
- if expire_time and tag:
- return value, db_expire_time, db_tag
- elif expire_time:
- return value, db_expire_time
- elif tag:
- return value, db_tag
- else:
- return value
-
-
- def __delitem__(self, key):
- """Delete corresponding item for `key` from cache.
-
- :param key: key matching item
- :raises KeyError: if key is not found
- :raises Timeout: if database timeout expires
-
- """
- db_key, raw = self._disk.put(key)
-
- with self._transact() as (sql, cleanup):
- rows = sql(
- 'SELECT rowid, filename FROM Cache'
- ' WHERE key = ? AND raw = ?'
- ' AND (expire_time IS NULL OR expire_time > ?)',
- (db_key, raw, time.time()),
- ).fetchall()
-
- if not rows:
- raise KeyError(key)
-
- (rowid, filename), = rows
- sql('DELETE FROM Cache WHERE rowid = ?', (rowid,))
- cleanup(filename)
-
- return True
-
-
- def delete(self, key):
- """Delete corresponding item for `key` from cache.
-
- Missing keys are ignored.
-
- :param key: key matching item
- :return: True if item was deleted
- :raises Timeout: if database timeout expires
-
- """
- try:
- return self.__delitem__(key)
- except KeyError:
- return False
-
-
- def push(self, value, prefix=None, side='back', expire=None, read=False,
- tag=None):
- """Push `value` onto `side` of queue identified by `prefix` in cache.
-
- When prefix is None, integer keys are used. Otherwise, string keys are
- used in the format "prefix-integer". Integer starts at 500 trillion.
-
- Defaults to pushing value on back of queue. Set side to 'front' to push
- value on front of queue. Side must be one of 'back' or 'front'.
-
- Operation is atomic. Concurrent operations will be serialized.
-
- When `read` is `True`, `value` should be a file-like object opened
- for reading in binary mode.
-
- See also `Cache.pull`.
-
- >>> cache = Cache('/tmp/test')
- >>> _ = cache.clear()
- >>> print(cache.push('first value'))
- 500000000000000
- >>> cache.get(500000000000000)
- 'first value'
- >>> print(cache.push('second value'))
- 500000000000001
- >>> print(cache.push('third value', side='front'))
- 499999999999999
- >>> cache.push(1234, prefix='userids')
- 'userids-500000000000000'
-
- :param value: value for item
- :param str prefix: key prefix (default None, key is integer)
- :param str side: either 'back' or 'front' (default 'back')
- :param float expire: seconds until the key expires
- (default None, no expiry)
- :param bool read: read value as bytes from file (default False)
- :param str tag: text to associate with key (default None)
- :return: key for item in cache
- :raises Timeout: if database timeout expires
-
- """
- if prefix is None:
- min_key = 0
- max_key = 999999999999999
- else:
- min_key = prefix + '-000000000000000'
- max_key = prefix + '-999999999999999'
-
- now = time.time()
- raw = True
- expire_time = None if expire is None else now + expire
- size, mode, filename, db_value = self._disk.store(value, read)
- columns = (expire_time, tag, size, mode, filename, db_value)
- order = {'back': 'DESC', 'front': 'ASC'}
- select = (
- 'SELECT key FROM Cache'
- ' WHERE ? < key AND key < ? AND raw = ?'
- ' ORDER BY key %s LIMIT 1'
- ) % order[side]
-
- with self._transact(filename) as (sql, cleanup):
- rows = sql(select, (min_key, max_key, raw)).fetchall()
-
- if rows:
- (key,), = rows
-
- if prefix is not None:
- num = int(key[(key.rfind('-') + 1):])
- else:
- num = key
-
- if side == 'back':
- num += 1
- else:
- assert side == 'front'
- num -= 1
- else:
- num = 500000000000000
-
- if prefix is not None:
- db_key = '{0}-{1:015d}'.format(prefix, num)
- else:
- db_key = num
-
- self._row_insert(db_key, raw, now, columns)
- self._cull(now, sql, cleanup)
-
- return db_key
-
-
- def pull(self, prefix=None, default=(None, None), side='front',
- expire_time=False, tag=False):
- """Pull key and value item pair from `side` of queue in cache.
-
- When prefix is None, integer keys are used. Otherwise, string keys are
- used in the format "prefix-integer". Integer starts at 500 trillion.
-
- If queue is empty, return default.
-
- Defaults to pulling key and value item pairs from front of queue. Set
- side to 'back' to pull from back of queue. Side must be one of 'front'
- or 'back'.
-
- Operation is atomic. Concurrent operations will be serialized.
-
- See also `Cache.push` and `Cache.get`.
-
- >>> cache = Cache('/tmp/test')
- >>> _ = cache.clear()
- >>> cache.pull()
- (None, None)
- >>> for letter in 'abc':
- ... print(cache.push(letter))
- 500000000000000
- 500000000000001
- 500000000000002
- >>> key, value = cache.pull()
- >>> print(key)
- 500000000000000
- >>> value
- 'a'
- >>> _, value = cache.pull(side='back')
- >>> value
- 'c'
- >>> cache.push(1234, 'userids')
- 'userids-500000000000000'
- >>> _, value = cache.pull('userids')
- >>> value
- 1234
-
- :param str prefix: key prefix (default None, key is integer)
- :param default: value to return if key is missing
- (default (None, None))
- :param str side: either 'front' or 'back' (default 'front')
- :param bool expire_time: if True, return expire_time in tuple
- (default False)
- :param bool tag: if True, return tag in tuple (default False)
- :return: key and value item pair or default if queue is empty
- :raises Timeout: if database timeout expires
-
- """
- if prefix is None:
- min_key = 0
- max_key = 999999999999999
- else:
- min_key = prefix + '-000000000000000'
- max_key = prefix + '-999999999999999'
-
- order = {'front': 'ASC', 'back': 'DESC'}
- select = (
- 'SELECT rowid, key, expire_time, tag, mode, filename, value'
- ' FROM Cache WHERE ? < key AND key < ? AND raw = 1'
- ' ORDER BY key %s LIMIT 1'
- ) % order[side]
-
- if expire_time and tag:
- default = default, None, None
- elif expire_time or tag:
- default = default, None
-
- while True:
- with self._transact() as (sql, cleanup):
- rows = sql(select, (min_key, max_key)).fetchall()
-
- if not rows:
- return default
-
- (rowid, key, db_expire, db_tag, mode, name, db_value), = rows
-
- sql('DELETE FROM Cache WHERE rowid = ?', (rowid,))
-
- if db_expire is not None and db_expire < time.time():
- cleanup(name)
- else:
- break
-
- try:
- value = self._disk.fetch(mode, name, db_value, False)
- except IOError as error:
- if error.errno == errno.ENOENT:
- # Key was deleted before we could retrieve result.
- return default
- else:
- raise
- finally:
- if name is not None:
- self._disk.remove(name)
-
- if expire_time and tag:
- return (key, value), db_expire, db_tag
- elif expire_time:
- return (key, value), db_expire
- elif tag:
- return (key, value), db_tag
- else:
- return key, value
-
-
- def check(self, fix=False):
- """Check database and file system consistency.
-
- Intended for use in testing and post-mortem error analysis.
-
- While checking the Cache table for consistency, a writer lock is held
- on the database. The lock blocks other cache clients from writing to
- the database. For caches with many file references, the lock may be
- held for a long time. For example, local benchmarking shows that a
- cache with 1,000 file references takes ~60ms to check.
-
- :param bool fix: correct inconsistencies
- :return: list of warnings
- :raises Timeout: if database timeout expires
-
- """
- # pylint: disable=access-member-before-definition,W0201
- with warnings.catch_warnings(record=True) as warns:
- sql = self._sql
-
- # Check integrity of database.
-
- rows = sql('PRAGMA integrity_check').fetchall()
-
- if len(rows) != 1 or rows[0][0] != u'ok':
- for message, in rows:
- warnings.warn(message)
-
- if fix:
- sql('VACUUM')
-
- with self._transact() as (sql, _):
-
- # Check Cache.filename against file system.
-
- filenames = set()
- select = (
- 'SELECT rowid, size, filename FROM Cache'
- ' WHERE filename IS NOT NULL'
- )
-
- rows = sql(select).fetchall()
-
- for rowid, size, filename in rows:
- full_path = op.join(self._directory, filename)
- filenames.add(full_path)
-
- if op.exists(full_path):
- real_size = op.getsize(full_path)
-
- if size != real_size:
- message = 'wrong file size: %s, %d != %d'
- args = full_path, real_size, size
- warnings.warn(message % args)
-
- if fix:
- sql('UPDATE Cache SET size = ?'
- ' WHERE rowid = ?',
- (real_size, rowid),
- )
-
- continue
-
- warnings.warn('file not found: %s' % full_path)
-
- if fix:
- sql('DELETE FROM Cache WHERE rowid = ?', (rowid,))
-
- # Check file system against Cache.filename.
-
- for dirpath, _, files in os.walk(self._directory):
- paths = [op.join(dirpath, filename) for filename in files]
- error = set(paths) - filenames
-
- for full_path in error:
- if DBNAME in full_path:
- continue
-
- message = 'unknown file: %s' % full_path
- warnings.warn(message, UnknownFileWarning)
-
- if fix:
- os.remove(full_path)
-
- # Check for empty directories.
-
- for dirpath, dirs, files in os.walk(self._directory):
- if not (dirs or files):
- message = 'empty directory: %s' % dirpath
- warnings.warn(message, EmptyDirWarning)
-
- if fix:
- os.rmdir(dirpath)
-
- # Check Settings.count against count of Cache rows.
-
- self.reset('count')
- (count,), = sql('SELECT COUNT(key) FROM Cache').fetchall()
-
- if self.count != count:
- message = 'Settings.count != COUNT(Cache.key); %d != %d'
- warnings.warn(message % (self.count, count))
-
- if fix:
- sql('UPDATE Settings SET value = ? WHERE key = ?',
- (count, 'count'),
- )
-
- # Check Settings.size against sum of Cache.size column.
-
- self.reset('size')
- select_size = 'SELECT COALESCE(SUM(size), 0) FROM Cache'
- (size,), = sql(select_size).fetchall()
-
- if self.size != size:
- message = 'Settings.size != SUM(Cache.size); %d != %d'
- warnings.warn(message % (self.size, size))
-
- if fix:
- sql('UPDATE Settings SET value = ? WHERE key =?',
- (size, 'size'),
- )
-
- return warns
-
-
- def create_tag_index(self):
- """Create tag index on cache database.
-
- It is better to initialize cache with `tag_index=True` than use this.
-
- :raises Timeout: if database timeout expires
-
- """
- sql = self._sql
- sql('CREATE INDEX IF NOT EXISTS Cache_tag_rowid ON Cache(tag, rowid)')
- self.reset('tag_index', 1)
-
-
- def drop_tag_index(self):
- """Drop tag index on cache database.
-
- :raises Timeout: if database timeout expires
-
- """
- sql = self._sql
- sql('DROP INDEX IF EXISTS Cache_tag_rowid')
- self.reset('tag_index', 0)
-
-
- def evict(self, tag):
- """Remove items with matching `tag` from cache.
-
- Removing items is an iterative process. In each iteration, a subset of
- items is removed. Concurrent writes may occur between iterations.
-
- If a :exc:`Timeout` occurs, the first element of the exception's
- `args` attribute will be the number of items removed before the
- exception occurred.
-
- :param str tag: tag identifying items
- :return: count of rows removed
- :raises Timeout: if database timeout expires
-
- """
- select = (
- 'SELECT rowid, filename FROM Cache'
- ' WHERE tag = ? AND rowid > ?'
- ' ORDER BY rowid LIMIT ?'
- )
- args = [tag, 0, 100]
- return self._select_delete(select, args, arg_index=1)
-
-
- def expire(self, now=None):
- """Remove expired items from cache.
-
- Removing items is an iterative process. In each iteration, a subset of
- items is removed. Concurrent writes may occur between iterations.
-
- If a :exc:`Timeout` occurs, the first element of the exception's
- `args` attribute will be the number of items removed before the
- exception occurred.
-
- :param float now: current time (default None, ``time.time()`` used)
- :return: count of items removed
- :raises Timeout: if database timeout expires
-
- """
- select = (
- 'SELECT rowid, expire_time, filename FROM Cache'
- ' WHERE ? < expire_time AND expire_time < ?'
- ' ORDER BY expire_time LIMIT ?'
- )
- args = [0, now or time.time(), 100]
- return self._select_delete(select, args, row_index=1)
-
-
- def cull(self):
- """Cull items from cache until volume is less than size limit.
-
- Removing items is an iterative process. In each iteration, a subset of
- items is removed. Concurrent writes may occur between iterations.
-
- If a :exc:`Timeout` occurs, the first element of the exception's
- `args` attribute will be the number of items removed before the
- exception occurred.
-
- :return: count of items removed
- :raises Timeout: if database timeout expires
-
- """
- now = time.time()
-
- # Remove expired items.
-
- count = self.expire(now)
-
- # Remove items by policy.
-
- select_policy = EVICTION_POLICY[self.eviction_policy]['cull']
-
- if select_policy is None:
- return
-
- select_filename = select_policy.format(fields='filename', now=now)
-
- try:
- while self.volume() > self.size_limit:
- with self._transact() as (sql, cleanup):
- rows = sql(select_filename, (10,)).fetchall()
-
- if not rows:
- break
-
- count += len(rows)
- delete = (
- 'DELETE FROM Cache WHERE rowid IN (%s)'
- % select_policy.format(fields='rowid', now=now)
- )
- sql(delete, (10,))
-
- for filename, in rows:
- cleanup(filename)
- except Timeout:
- raise Timeout(count)
-
- return count
-
-
- def clear(self):
- """Remove all items from cache.
-
- Removing items is an iterative process. In each iteration, a subset of
- items is removed. Concurrent writes may occur between iterations.
-
- If a :exc:`Timeout` occurs, the first element of the exception's
- `args` attribute will be the number of items removed before the
- exception occurred.
-
- :return: count of rows removed
- :raises Timeout: if database timeout expires
-
- """
- select = (
- 'SELECT rowid, filename FROM Cache'
- ' WHERE rowid > ?'
- ' ORDER BY rowid LIMIT ?'
- )
- args = [0, 100]
- return self._select_delete(select, args)
-
-
- def _select_delete(self, select, args, row_index=0, arg_index=0):
- count = 0
- delete = 'DELETE FROM Cache WHERE rowid IN (%s)'
-
- try:
- while True:
- with self._transact() as (sql, cleanup):
- rows = sql(select, args).fetchall()
-
- if not rows:
- break
-
- count += len(rows)
- sql(delete % ','.join(str(row[0]) for row in rows))
-
- for row in rows:
- args[arg_index] = row[row_index]
- cleanup(row[-1])
-
- except Timeout:
- raise Timeout(count)
-
- return count
-
-
- def iterkeys(self, reverse=False):
- """Iterate Cache keys in database sort order.
-
- >>> cache = Cache('/tmp/diskcache')
- >>> _ = cache.clear()
- >>> for key in [4, 1, 3, 0, 2]:
- ... cache[key] = key
- >>> list(cache.iterkeys())
- [0, 1, 2, 3, 4]
- >>> list(cache.iterkeys(reverse=True))
- [4, 3, 2, 1, 0]
-
- :param bool reverse: reverse sort order (default False)
- :return: iterator of Cache keys
-
- """
- sql = self._sql
- limit = 100
- _disk_get = self._disk.get
-
- if reverse:
- select = (
- 'SELECT key, raw FROM Cache'
- ' ORDER BY key DESC, raw DESC LIMIT 1'
- )
- iterate = (
- 'SELECT key, raw FROM Cache'
- ' WHERE key = ? AND raw < ? OR key < ?'
- ' ORDER BY key DESC, raw DESC LIMIT ?'
- )
- else:
- select = (
- 'SELECT key, raw FROM Cache'
- ' ORDER BY key ASC, raw ASC LIMIT 1'
- )
- iterate = (
- 'SELECT key, raw FROM Cache'
- ' WHERE key = ? AND raw > ? OR key > ?'
- ' ORDER BY key ASC, raw ASC LIMIT ?'
- )
-
- row = sql(select).fetchall()
-
- if row:
- (key, raw), = row
- else:
- return
-
- yield _disk_get(key, raw)
-
- while True:
- rows = sql(iterate, (key, raw, key, limit)).fetchall()
-
- if not rows:
- break
-
- for key, raw in rows:
- yield _disk_get(key, raw)
-
-
- def _iter(self, ascending=True):
- sql = self._sql
- rows = sql('SELECT MAX(rowid) FROM Cache').fetchall()
- (max_rowid,), = rows
- yield # Signal ready.
-
- if max_rowid is None:
- return
-
- bound = max_rowid + 1
- limit = 100
- _disk_get = self._disk.get
- rowid = 0 if ascending else bound
- select = (
- 'SELECT rowid, key, raw FROM Cache'
- ' WHERE ? < rowid AND rowid < ?'
- ' ORDER BY rowid %s LIMIT ?'
- ) % ('ASC' if ascending else 'DESC')
-
- while True:
- if ascending:
- args = (rowid, bound, limit)
- else:
- args = (0, rowid, limit)
-
- rows = sql(select, args).fetchall()
-
- if not rows:
- break
-
- for rowid, key, raw in rows:
- yield _disk_get(key, raw)
-
-
- def __iter__(self):
- "Iterate keys in cache including expired items."
- iterator = self._iter()
- next(iterator)
- return iterator
-
-
- def __reversed__(self):
- "Reverse iterate keys in cache including expired items."
- iterator = self._iter(ascending=False)
- next(iterator)
- return iterator
-
-
- def stats(self, enable=True, reset=False):
- """Return cache statistics hits and misses.
-
- :param bool enable: enable collecting statistics (default True)
- :param bool reset: reset hits and misses to 0 (default False)
- :return: (hits, misses)
-
- """
- # pylint: disable=E0203,W0201
- result = (self.reset('hits'), self.reset('misses'))
-
- if reset:
- self.reset('hits', 0)
- self.reset('misses', 0)
-
- self.reset('statistics', enable)
-
- return result
-
-
- def volume(self):
- """Return estimated total size of cache on disk.
-
- :return: size in bytes
-
- """
- (page_count,), = self._sql('PRAGMA page_count').fetchall()
- total_size = self._page_size * page_count + self.reset('size')
- return total_size
-
-
- def close(self):
- """Close database connection.
-
- """
- con = getattr(self._local, 'con', None)
-
- if con is None:
- return
-
- con.close()
-
- try:
- delattr(self._local, 'con')
- except AttributeError:
- pass
-
-
- def __enter__(self):
- return self
-
-
- def __exit__(self, *exception):
- self.close()
-
-
- def __len__(self):
- "Count of items in cache including expired items."
- return self.reset('count')
-
-
- def __getstate__(self):
- return (self.directory, self.timeout, type(self.disk))
-
-
- def __setstate__(self, state):
- self.__init__(*state)
-
-
- def reset(self, key, value=ENOVAL, update=True):
- """Reset `key` and `value` item from Settings table.
-
- Use `reset` to update the value of Cache settings correctly. Cache
- settings are stored in the Settings table of the SQLite database. If
- `update` is ``False`` then no attempt is made to update the database.
-
- If `value` is not given, it is reloaded from the Settings
- table. Otherwise, the Settings table is updated.
-
- Settings with the ``disk_`` prefix correspond to Disk
- attributes. Updating the value will change the unprefixed attribute on
- the associated Disk instance.
-
- Settings with the ``sqlite_`` prefix correspond to SQLite
- pragmas. Updating the value will execute the corresponding PRAGMA
- statement.
-
- SQLite PRAGMA statements may be executed before the Settings table
- exists in the database by setting `update` to ``False``.
-
- :param str key: Settings key for item
- :param value: value for item (optional)
- :param bool update: update database Settings table (default True)
- :return: updated value for item
- :raises Timeout: if database timeout expires
-
- """
- sql = self._sql
- sql_retry = self._sql_retry
-
- if value is ENOVAL:
- select = 'SELECT value FROM Settings WHERE key = ?'
- (value,), = sql_retry(select, (key,)).fetchall()
- setattr(self, key, value)
- return value
-
- if update:
- statement = 'UPDATE Settings SET value = ? WHERE key = ?'
- sql_retry(statement, (value, key))
-
- if key.startswith('sqlite_'):
- pragma = key[7:]
-
- # 2016-02-17 GrantJ - PRAGMA and isolation_level=None
- # don't always play nicely together. Retry setting the
- # PRAGMA. I think some PRAGMA statements expect to
- # immediately take an EXCLUSIVE lock on the database. I
- # can't find any documentation for this but without the
- # retry, stress will intermittently fail with multiple
- # processes.
-
- # 2018-11-05 GrantJ - Avoid setting pragma values that
- # are already set. Pragma settings like auto_vacuum and
- # journal_mode can take a long time or may not work after
- # tables have been created.
-
- start = time.time()
- while True:
- try:
- try:
- (old_value,), = sql('PRAGMA %s' % (pragma)).fetchall()
- update = old_value != value
- except ValueError:
- update = True
- if update:
- sql('PRAGMA %s = %s' % (pragma, value)).fetchall()
- break
- except sqlite3.OperationalError as exc:
- if str(exc) != 'database is locked':
- raise
- diff = time.time() - start
- if diff > 60:
- raise
- time.sleep(0.001)
- elif key.startswith('disk_'):
- attr = key[5:]
- setattr(self._disk, attr, value)
-
- setattr(self, key, value)
- return value
|