Skip to content

Commit 854fb8d

Browse files
authored
Apply caching when handling parameters (#790)
Fixes: #781 ## Why? When generating the pipeline definition, it needs to know whether or not empty targets are allowed, a config that is managed from the adfconfig.yml. To support this, the Target instances fetch the configuration value from parameter store. When hundreds of targets are initialized, it will attempt to fetch this parameter way too often. Especially considering the value is not likely to change. ## What? Apply caching on the fetch, put, delete operations in the Parameter Store class. Such that all ADF components that need to perform lookups frequently can benefit from this enhancement.
1 parent d4c7291 commit 854fb8d

File tree

5 files changed

+313
-66
lines changed

5 files changed

+313
-66
lines changed

src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/shared/python/cache.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,7 @@ def get(self, key):
2323

2424
def add(self, key, value):
2525
self._stash[key] = value
26+
27+
def remove(self, key):
28+
if key in self._stash:
29+
del self._stash[key]

src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/shared/python/parameter_store.py

Lines changed: 44 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@
77
from botocore.config import Config
88

99
# ADF imports
10+
from cache import Cache
1011
from errors import ParameterNotFoundError
1112
from paginator import paginator
1213
from logger import configure_logger
1314

1415
LOGGER = configure_logger(__name__)
15-
PARAMETER_DESCRIPTION = 'DO NOT EDIT - Used by The AWS Deployment Framework'
16-
PARAMETER_PREFIX = '/adf'
16+
PARAMETER_DESCRIPTION = "DO NOT EDIT - Used by The AWS Deployment Framework"
17+
PARAMETER_PREFIX = "/adf"
1718
SSM_CONFIG = Config(
1819
retries={
1920
"max_attempts": 10,
@@ -22,99 +23,104 @@
2223

2324

2425
class ParameterStore:
25-
"""Class used for modeling Parameters
26-
"""
26+
"""Class used for modeling Parameters"""
2727

28-
def __init__(self, region, role):
29-
self.client = role.client('ssm', region_name=region, config=SSM_CONFIG)
28+
def __init__(self, region, role, cache=None):
29+
self.cache = cache or Cache()
30+
self.client = role.client("ssm", region_name=region, config=SSM_CONFIG)
3031

31-
def put_parameter(self, name, value, tier='Standard'):
32-
"""Puts a Parameter into Parameter Store
33-
"""
32+
def put_parameter(self, name, value, tier="Standard"):
33+
"""Puts a Parameter into Parameter Store"""
3434
try:
3535
current_value = self.fetch_parameter(name)
3636
assert current_value == value
3737
LOGGER.debug(
38-
'No need to update parameter %s with value %s since they '
39-
'are the same',
38+
"No need to update parameter %s with value %s since they "
39+
"are the same",
4040
ParameterStore._build_param_name(name),
4141
value,
4242
)
4343
except (ParameterNotFoundError, AssertionError):
4444
param_name = ParameterStore._build_param_name(name)
4545
LOGGER.debug(
46-
'Putting SSM Parameter %s with value %s',
46+
"Putting SSM Parameter %s with value %s",
4747
param_name,
4848
value,
4949
)
5050
self.client.put_parameter(
5151
Name=param_name,
5252
Description=PARAMETER_DESCRIPTION,
5353
Value=value,
54-
Type='String',
54+
Type="String",
5555
Overwrite=True,
56-
Tier=tier
56+
Tier=tier,
5757
)
58+
self.cache.add(param_name, value)
5859

5960
def delete_parameter(self, name):
6061
param_name = ParameterStore._build_param_name(name)
6162
try:
62-
LOGGER.debug('Deleting Parameter %s', param_name)
63+
LOGGER.debug("Deleting Parameter %s", param_name)
64+
self.cache.remove(param_name)
6365
self.client.delete_parameter(
6466
Name=param_name,
6567
)
6668
except self.client.exceptions.ParameterNotFound:
6769
LOGGER.debug(
68-
'Attempted to delete Parameter %s but it was not found',
70+
"Attempted to delete Parameter %s but it was not found",
6971
param_name,
7072
)
7173

7274
def fetch_parameters_by_path(self, path):
73-
"""Gets a Parameter(s) by Path from Parameter Store (Recursively)
74-
"""
75+
"""Gets a Parameter(s) by Path from Parameter Store (Recursively)"""
7576
param_path = ParameterStore._build_param_name(path)
7677
try:
7778
LOGGER.debug(
78-
'Fetching Parameters from path %s',
79+
"Fetching Parameters from path %s",
7980
param_path,
8081
)
8182
return paginator(
8283
self.client.get_parameters_by_path,
8384
Path=param_path,
8485
Recursive=True,
85-
WithDecryption=False
86+
WithDecryption=False,
8687
)
8788
except self.client.exceptions.ParameterNotFound as error:
8889
raise ParameterNotFoundError(
89-
f'Parameter Path {param_path} Not Found',
90+
f"Parameter Path {param_path} Not Found",
9091
) from error
9192

9293
@staticmethod
9394
def _build_param_name(name, adf_only=True):
94-
slash_name = name if name.startswith('/') else f"/{name}"
95-
add_prefix = (
96-
adf_only
97-
and not slash_name.startswith(f"{PARAMETER_PREFIX}/")
98-
)
99-
param_prefix = PARAMETER_PREFIX if add_prefix else ''
95+
slash_name = name if name.startswith("/") else f"/{name}"
96+
add_prefix = adf_only and not slash_name.startswith(f"{PARAMETER_PREFIX}/")
97+
param_prefix = PARAMETER_PREFIX if add_prefix else ""
10098
return f"{param_prefix}{slash_name}"
10199

102100
def fetch_parameter(self, name, with_decryption=False, adf_only=True):
103-
"""Gets a Parameter from Parameter Store (Returns the Value)
104-
"""
101+
"""Gets a Parameter from Parameter Store (Returns the Value)"""
105102
param_name = ParameterStore._build_param_name(name, adf_only)
103+
if self.cache.exists(param_name):
104+
LOGGER.debug("Reading Parameter from Cache: %s", param_name)
105+
cached_value = self.cache.get(param_name)
106+
if isinstance(cached_value, ParameterNotFoundError):
107+
raise cached_value
108+
return cached_value
106109
try:
107-
LOGGER.debug('Fetching Parameter %s', param_name)
110+
LOGGER.debug("Fetching Parameter %s", param_name)
108111
response = self.client.get_parameter(
109-
Name=param_name,
110-
WithDecryption=with_decryption
112+
Name=param_name, WithDecryption=with_decryption
111113
)
112-
return response['Parameter']['Value']
114+
fetched_value = response["Parameter"]["Value"]
115+
self.cache.add(param_name, fetched_value)
116+
return fetched_value
113117
except self.client.exceptions.ParameterNotFound as error:
114-
LOGGER.debug('Parameter %s not found', param_name)
115-
raise ParameterNotFoundError(
116-
f'Parameter {param_name} Not Found',
117-
) from error
118+
LOGGER.debug("Parameter %s not found", param_name)
119+
not_found = ParameterNotFoundError(
120+
f"Parameter {param_name} Not Found",
121+
)
122+
self.cache.add(param_name, not_found)
123+
raise not_found from error
118124

119125
def fetch_parameter_accept_not_found(
120126
self,
@@ -131,5 +137,5 @@ def fetch_parameter_accept_not_found(
131137
try:
132138
return self.fetch_parameter(name, with_decryption, adf_only)
133139
except ParameterNotFoundError:
134-
LOGGER.debug('Using default instead: %s', default_value)
140+
LOGGER.debug("Using default instead: %s", default_value)
135141
return default_value

src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/shared/python/target.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,6 @@ def __init__(
156156
).lower() == "enabled"
157157
)
158158

159-
160159
@staticmethod
161160
def _account_is_active(account):
162161
return bool(account.get("Status") == "ACTIVE")

src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/shared/python/tests/test_cache.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,52 @@ def test_get(cls):
3232
assert cls.get('my_key') == 'my_value'
3333
assert cls.get('true_key') is True
3434
assert cls.get('false_key') is False
35+
36+
37+
def test_remove_existing_key(cls):
38+
# Arrange
39+
cls.add("test_key", "test_value")
40+
41+
# Act
42+
cls.remove("test_key")
43+
44+
# Assert
45+
assert cls.exists("test_key") is False
46+
assert cls.get("test_key") is None
47+
48+
49+
def test_remove_non_existing_key(cls):
50+
# Arrange
51+
cls.remove("non_existing_key")
52+
53+
# Assert
54+
assert cls.exists("non_existing_key") is False
55+
assert cls.get("non_existing_key") is None
56+
57+
58+
def test_remove_and_read(cls):
59+
# Arrange
60+
cls.add("test_key", "test_value")
61+
62+
# Act
63+
cls.remove("test_key")
64+
cls.add("test_key", "new_value")
65+
66+
# Assert
67+
assert cls.exists("test_key") is True
68+
assert cls.get("test_key") == "new_value"
69+
70+
71+
def test_remove_multiple_keys(cls):
72+
# Arrange
73+
cls.add("key1", "value1")
74+
cls.add("key2", "value2")
75+
76+
# Act
77+
cls.remove("key1")
78+
79+
# Assert
80+
assert cls.exists("key1") is False
81+
assert cls.exists("key2") is True
82+
assert cls.get("key1") is None
83+
assert cls.get("key2") == "value2"

0 commit comments

Comments
 (0)