Skip to content

Commit b405026

Browse files
committed
add fuzzy ttl period to MostRecentProvider to avoid cache contention and version explosion
1 parent d10a97f commit b405026

File tree

1 file changed

+107
-17
lines changed

1 file changed

+107
-17
lines changed

src/dynamodb_encryption_sdk/material_providers/most_recent.py

Lines changed: 107 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@
1212
# language governing permissions and limitations under the License.
1313
"""Cryptographic materials provider that uses a provider store to obtain cryptographic materials."""
1414
from collections import OrderedDict
15+
from enum import Enum
1516
import logging
16-
from threading import RLock
17+
from threading import Lock, RLock
1718
import time
1819

1920
import attr
@@ -34,6 +35,17 @@
3435

3536
__all__ = ('MostRecentProvider',)
3637
_LOGGER = logging.getLogger(LOGGER_NAME)
38+
#: Grace period during which we will return the latest local materials. This allows multiple
39+
#: threads to be using this same provider without risking lock contention or many threads
40+
#: all attempting to create new versions simultaneously.
41+
_GRACE_PERIOD = 0.5
42+
43+
44+
class TtlActions(Enum):
45+
"""Actions that can be taken based on the version TTl state."""
46+
EXPIRED = 0
47+
GRACE_PERIOD = 1
48+
LIVE = 2
3749

3850

3951
def _min_capacity_validator(instance, attribute, value):
@@ -139,7 +151,7 @@ def __init__(
139151
def __attrs_post_init__(self):
140152
# type: () -> None
141153
"""Initialize the cache."""
142-
self._lock = RLock()
154+
self._lock = Lock()
143155
self._cache = BasicCache(1000)
144156
self.refresh()
145157

@@ -165,6 +177,26 @@ def decryption_materials(self, encryption_context):
165177

166178
return provider.decryption_materials(encryption_context)
167179

180+
def _ttl_action(self):
181+
# type: () -> bool
182+
"""Determine the correct action to take based on the local resources and TTL.
183+
184+
:returns: decision
185+
:rtype: TtlActions
186+
"""
187+
if self._version is None:
188+
return TtlActions.EXPIRED
189+
190+
time_since_updated = time.time() - self._last_updated
191+
192+
if time_since_updated < self._version_ttl:
193+
return TtlActions.LIVE
194+
195+
elif time_since_updated < self._version_ttl + _GRACE_PERIOD:
196+
return TtlActions.GRACE_PERIOD
197+
198+
return TtlActions.EXPIRED
199+
168200
def _can_use_current(self):
169201
# type: () -> bool
170202
"""Determine if we can use the current known max version without asking the provider store.
@@ -187,6 +219,64 @@ def _set_most_recent_version(self, version):
187219
self._version = version
188220
self._last_updated = time.time()
189221

222+
def _get_max_version(self):
223+
# type: () -> int
224+
"""Ask the provider store for the most recent version of this material.
225+
226+
:returns: Latest version in the provider store (0 if not found)
227+
:rtype: int
228+
"""
229+
try:
230+
return self._provider_store.max_version(self._material_name)
231+
except NoKnownVersionError:
232+
return 0
233+
234+
def _get_provider(self, version):
235+
# type: (int) -> CryptographicMaterialsProvider
236+
"""Ask the provider for a specific version of this material.
237+
238+
:param int version: Version to request
239+
:returns: Cryptographic materials provider for the requested version
240+
:rtype: CryptographicMaterialsProvider
241+
:raises AttributeError: if provider could not locate version
242+
"""
243+
try:
244+
return self._provider_store.get_or_create_provider(self._material_name, version)
245+
except InvalidVersionError:
246+
_LOGGER.exception('Unable to get encryption materials from provider store.')
247+
raise AttributeError('No encryption materials available')
248+
249+
def _get_most_recent_version(self, allow_local):
250+
# type: (bool) -> Tuple[int, CryptographicMaterialsProvider]
251+
"""Get the most recent version of the provider.
252+
253+
If allowing local and we cannot obtain the lock, just return the most recent local
254+
version. Otherwise, wait for the lock and ask the provider store for the most recent
255+
version of the provider.
256+
257+
:param bool allow_local: Should we allow returning the local version if we cannot obtain the lock?
258+
:returns: version and corresponding cryptographic materials provider
259+
:rtype: tuple containing int and CryptographicMaterialsProvider
260+
"""
261+
acquired = self._lock.acquire(blocking=not allow_local)
262+
263+
if not acquired:
264+
# We failed to acquire the lock.
265+
# If blocking, we will never reach this point.
266+
# If not blocking, we want whatever the latest local version is.
267+
version = self._version
268+
return version, self._cache.get(version)
269+
270+
try:
271+
version = self._get_max_version()
272+
provider = self._get_provider(version)
273+
actual_version = self._provider_store.version_from_material_description(provider._material_description)
274+
# TODO: ^ should we promote material description from hidden?
275+
finally:
276+
self._lock.release()
277+
278+
return actual_version, provider
279+
190280
def encryption_materials(self, encryption_context):
191281
# type: (EncryptionContext) -> CryptographicMaterials
192282
"""Return encryption materials.
@@ -195,22 +285,22 @@ def encryption_materials(self, encryption_context):
195285
:type encryption_context: dynamodb_encryption_sdk.structures.EncryptionContext
196286
:raises AttributeError: if no encryption materials are available
197287
"""
198-
if self._can_use_current():
199-
return self._cache.get(self._version)
200-
# TODO: handle key errors
288+
ttl_action = self._ttl_action()
201289

202-
try:
203-
version = self._provider_store.max_version(self._material_name)
204-
except NoKnownVersionError:
205-
version = 0
290+
if ttl_action is TtlActions.LIVE:
291+
try:
292+
return self._cache.get(self._version)
293+
except KeyError:
294+
ttl_action = TtlActions.EXPIRED
206295

207-
try:
208-
provider = self._provider_store.get_or_create_provider(self._material_name, version)
209-
except InvalidVersionError:
210-
_LOGGER.exception('Unable to get encryption materials from provider store.')
211-
raise AttributeError('No encryption materials available')
212-
actual_version = self._provider_store.version_from_material_description(provider._material_description)
213-
# TODO: ^ should we promote material description from hidden?
296+
if ttl_action is TtlActions.GRACE_PERIOD:
297+
# Just get the latest local version if we cannot acquire the lock.
298+
allow_local = True
299+
else:
300+
# Block until we can acquire the lock.
301+
allow_local = False
302+
303+
actual_version, provider = self._get_most_recent_version(allow_local)
214304

215305
self._cache.put(actual_version, provider)
216306
self._set_most_recent_version(actual_version)
@@ -223,4 +313,4 @@ def refresh(self):
223313
with self._lock:
224314
self._cache.clear()
225315
self._version = None # type: int
226-
self._last_updated = None # type: CryptographicMaterialsProvider
316+
self._last_updated = None # type: float

0 commit comments

Comments
 (0)