Skip to content

Commit e4ac0c1

Browse files
authored
Adding cache level refreshNow functionality (#47)
1 parent 359b7df commit e4ac0c1

File tree

4 files changed

+80
-1
lines changed

4 files changed

+80
-1
lines changed

src/aws_secretsmanager_caching/cache/items.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
# pylint: disable=super-with-arguments
1515

1616
import threading
17+
import time
1718
from abc import ABCMeta, abstractmethod
1819
from copy import deepcopy
1920
from datetime import datetime, timedelta, timezone
@@ -24,7 +25,8 @@
2425

2526
class SecretCacheObject: # pylint: disable=too-many-instance-attributes
2627
"""Secret cache object that handles the common refresh logic."""
27-
28+
# Jitter max for refresh now
29+
FORCE_REFRESH_JITTER_SLEEP = 5000
2830
__metaclass__ = ABCMeta
2931

3032
def __init__(self, config, client, secret_id):
@@ -121,6 +123,26 @@ def get_secret_value(self, version_stage=None):
121123
if not value and self._exception:
122124
raise self._exception
123125
return deepcopy(value)
126+
127+
def refresh_secret_now(self):
128+
"""Force a refresh of the cached secret.
129+
:rtype: None
130+
:return: None
131+
"""
132+
self._refresh_needed = True
133+
134+
# Generate a random number to have a sleep jitter to not get stuck in a retry loop
135+
sleep = randint(int(self.FORCE_REFRESH_JITTER_SLEEP / 2), self.FORCE_REFRESH_JITTER_SLEEP + 1)
136+
137+
if self._exception is not None:
138+
current_time_millis = int(datetime.now(timezone.utc).timestamp() * 1000)
139+
exception_sleep = self._next_retry_time - current_time_millis
140+
sleep = max(exception_sleep, sleep)
141+
142+
# Divide by 1000 for millis
143+
time.sleep(sleep / 1000)
144+
145+
self._execute_refresh()
124146

125147
def _get_result(self):
126148
"""Get the stored result using a hook if present"""

src/aws_secretsmanager_caching/secret_cache.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,11 @@ def get_secret_binary(self, secret_id, version_stage=None):
9999
if secret is None:
100100
return secret
101101
return secret.get("SecretBinary")
102+
103+
def refresh_secret_now(self, secret_id):
104+
"""Immediately refresh the secret in the cache.
105+
106+
:type secret_id: str
107+
:param secret_id: The secret identifier
108+
"""
109+
self._get_cached_secret(secret_id).refresh_secret_now()

test/unit/test_aws_secretsmanager_caching.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,31 @@ def test_get_secret_binary_no_versions(self):
169169
cache = SecretCache(client=self.get_client())
170170
self.assertIsNone(cache.get_secret_binary('test'))
171171

172+
def test_refresh_secret_now(self):
173+
secret = 'mysecret'
174+
response = {}
175+
versions = {
176+
'01234567890123456789012345678901': ['AWSCURRENT']
177+
}
178+
version_response = {'SecretString': secret}
179+
cache = SecretCache(client=self.get_client(response,
180+
versions,
181+
version_response))
182+
secret = cache._get_cached_secret('test')
183+
self.assertIsNotNone(secret)
184+
185+
old_refresh_time = secret._next_refresh_time
186+
187+
secret = cache._get_cached_secret('test')
188+
self.assertTrue(old_refresh_time == secret._next_refresh_time)
189+
190+
cache.refresh_secret_now('test')
191+
192+
secret = cache._get_cached_secret('test')
193+
194+
new_refresh_time = secret._next_refresh_time
195+
self.assertTrue(new_refresh_time > old_refresh_time)
196+
172197
def test_get_secret_string_exception(self):
173198
client = botocore.session.get_session().create_client(
174199
'secretsmanager', region_name='us-west-2')

test/unit/test_items.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,30 @@ def test_simple_2(self):
5050
sco._exception = Exception("test")
5151
self.assertRaises(Exception, sco.get_secret_value)
5252

53+
def test_refresh_now(self):
54+
config = SecretCacheConfig()
55+
56+
client_mock = Mock()
57+
client_mock.describe_secret = Mock()
58+
client_mock.describe_secret.return_value = "test"
59+
secret_cache_item = SecretCacheItem(config, client_mock, None)
60+
secret_cache_item._next_refresh_time = datetime.now(timezone.utc) + timedelta(days=30)
61+
secret_cache_item._refresh_needed = False
62+
self.assertFalse(secret_cache_item._is_refresh_needed())
63+
64+
old_refresh_time = secret_cache_item._next_refresh_time
65+
self.assertTrue(old_refresh_time > datetime.now(timezone.utc) + timedelta(days=29))
66+
67+
secret_cache_item.refresh_secret_now()
68+
new_refresh_time = secret_cache_item._next_refresh_time
69+
70+
ttl = config.secret_refresh_interval
71+
72+
# New refresh time will use the ttl and will be less than the old refresh time that was artificially set a month ahead
73+
# The new refresh time will be between now + ttl and now + (ttl / 2) if the secret was immediately refreshed
74+
self.assertTrue(new_refresh_time < old_refresh_time and new_refresh_time < datetime.now(timezone.utc) + timedelta(ttl))
75+
76+
5377
def test_datetime_fix_is_refresh_needed(self):
5478
secret_cached_object = TestSecretCacheObject.TestObject(SecretCacheConfig(), None, None)
5579

0 commit comments

Comments
 (0)