12
12
# language governing permissions and limitations under the License.
13
13
"""Cryptographic materials provider that uses a provider store to obtain cryptographic materials."""
14
14
from collections import OrderedDict
15
+ from enum import Enum
15
16
import logging
16
- from threading import RLock
17
+ from threading import Lock , RLock
17
18
import time
18
19
19
20
import attr
34
35
35
36
__all__ = ('MostRecentProvider' ,)
36
37
_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
37
49
38
50
39
51
def _min_capacity_validator (instance , attribute , value ):
@@ -139,7 +151,7 @@ def __init__(
139
151
def __attrs_post_init__ (self ):
140
152
# type: () -> None
141
153
"""Initialize the cache."""
142
- self ._lock = RLock ()
154
+ self ._lock = Lock ()
143
155
self ._cache = BasicCache (1000 )
144
156
self .refresh ()
145
157
@@ -165,6 +177,26 @@ def decryption_materials(self, encryption_context):
165
177
166
178
return provider .decryption_materials (encryption_context )
167
179
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
+
168
200
def _can_use_current (self ):
169
201
# type: () -> bool
170
202
"""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):
187
219
self ._version = version
188
220
self ._last_updated = time .time ()
189
221
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
+
190
280
def encryption_materials (self , encryption_context ):
191
281
# type: (EncryptionContext) -> CryptographicMaterials
192
282
"""Return encryption materials.
@@ -195,22 +285,22 @@ def encryption_materials(self, encryption_context):
195
285
:type encryption_context: dynamodb_encryption_sdk.structures.EncryptionContext
196
286
:raises AttributeError: if no encryption materials are available
197
287
"""
198
- if self ._can_use_current ():
199
- return self ._cache .get (self ._version )
200
- # TODO: handle key errors
288
+ ttl_action = self ._ttl_action ()
201
289
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
206
295
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 )
214
304
215
305
self ._cache .put (actual_version , provider )
216
306
self ._set_most_recent_version (actual_version )
@@ -223,4 +313,4 @@ def refresh(self):
223
313
with self ._lock :
224
314
self ._cache .clear ()
225
315
self ._version = None # type: int
226
- self ._last_updated = None # type: CryptographicMaterialsProvider
316
+ self ._last_updated = None # type: float
0 commit comments