Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/c7n/cache.py: 39%
106 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-08 06:51 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-08 06:51 +0000
1# Copyright The Cloud Custodian Authors.
2# SPDX-License-Identifier: Apache-2.0
3"""Provide basic caching services to avoid extraneous queries over
4multiple policies on the same resource type.
5"""
6import pickle # nosec nosemgrep
8from datetime import datetime, timedelta
9import os
10import logging
11import sqlite3
13log = logging.getLogger('custodian.cache')
15CACHE_NOTIFY = False
18def factory(config):
20 global CACHE_NOTIFY
22 if not config:
23 return NullCache(None)
25 if not config.cache or not config.cache_period:
26 if not CACHE_NOTIFY:
27 log.debug("Disabling cache")
28 CACHE_NOTIFY = True
29 return NullCache(config)
30 elif config.cache == 'memory':
31 if not CACHE_NOTIFY:
32 log.debug("Using in-memory cache")
33 CACHE_NOTIFY = True
34 return InMemoryCache(config)
35 return SqlKvCache(config)
38class Cache:
40 def __init__(self, config):
41 self.config = config
43 def load(self):
44 return False
46 def get(self, key):
47 pass
49 def save(self, key, data):
50 pass
52 def size(self):
53 return 0
55 def close(self):
56 pass
58 def __enter__(self):
59 self.load()
60 return self
62 def __exit__(self, exc_type, exc_val, exc_tb):
63 self.close()
66class NullCache(Cache):
67 pass
70class InMemoryCache(Cache):
71 # Running in a temporary environment, so keep as a cache.
73 __shared_state = {}
75 def __init__(self, config):
76 super().__init__(config)
77 self.data = self.__shared_state
79 def load(self):
80 return True
82 def get(self, key):
83 return self.data.get(encode(key))
85 def save(self, key, data):
86 self.data[encode(key)] = data
88 def size(self):
89 return sum(map(len, self.data.values()))
92def encode(key):
93 return pickle.dumps(key, protocol=pickle.HIGHEST_PROTOCOL) # nosemgrep
96def resolve_path(path):
97 return os.path.abspath(
98 os.path.expanduser(
99 os.path.expandvars(path)))
102class SqlKvCache(Cache):
104 create_table = """
105 create table if not exists c7n_cache (
106 key blob primary key,
107 value blob,
108 create_date timestamp
109 )
110 """
112 def __init__(self, config):
113 super().__init__(config)
114 self.cache_period = config.cache_period
115 self.cache_path = resolve_path(config.cache)
116 self.conn = None
118 def init(self):
119 # migration from pickle cache file
120 if os.path.exists(self.cache_path):
121 with open(self.cache_path, 'rb') as fh:
122 header = fh.read(15)
123 if header != b'SQLite format 3':
124 log.debug('removing old cache file')
125 os.remove(self.cache_path)
126 elif not os.path.exists(os.path.dirname(self.cache_path)):
127 # parent directory creation
128 os.makedirs(os.path.dirname(self.cache_path))
129 self.conn = sqlite3.connect(self.cache_path)
130 self.conn.execute(self.create_table)
131 with self.conn as cursor:
132 result = cursor.execute(
133 'delete from c7n_cache where create_date < ?',
134 [datetime.utcnow() - timedelta(minutes=self.cache_period)])
135 if result.rowcount:
136 log.debug('expired %d stale cache entries', result.rowcount)
138 def load(self):
139 if not self.conn:
140 self.init()
141 return True
143 def get(self, key):
144 with self.conn as cursor:
145 r = cursor.execute(
146 'select value, create_date from c7n_cache where key = ?',
147 [sqlite3.Binary(encode(key))]
148 )
149 row = r.fetchone()
150 if row is None:
151 return None
152 value, create_date = row
153 create_date = sqlite3.converters['TIMESTAMP'](create_date.encode('utf8'))
154 if (datetime.utcnow() - create_date).total_seconds() / 60.0 > self.cache_period:
155 return None
156 return pickle.loads(value) # nosec nosemgrep
158 def save(self, key, data, timestamp=None):
159 with self.conn as cursor:
160 timestamp = timestamp or datetime.utcnow()
161 cursor.execute(
162 'replace into c7n_cache (key, value, create_date) values (?, ?, ?)',
163 (sqlite3.Binary(encode(key)), sqlite3.Binary(encode(data)), timestamp))
165 def size(self):
166 return os.path.exists(self.cache_path) and os.path.getsize(self.cache_path) or 0
168 def close(self):
169 if self.conn:
170 self.conn.close()
171 self.conn = None