Skip to content

Commit 4762ca3

Browse files
committed
Fix: Race between layer and Lambda update (#5927, PR #7436)
2 parents bd301a9 + df2b150 commit 4762ca3

27 files changed

+371
-198
lines changed

.mypy.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ modules =
6868
scripts.post_deploy_tdr,
6969
scripts.find_in_snapshots,
7070
scripts.can_bundle,
71+
scripts.delete_older_function_versions,
7172
scripts.zenhub_to_github,
7273
azul.plugins.metadata.anvil.bundle,
7374
azul.plugins.metadata.anvil.schema,
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"""
2+
Delete all versions of a Lambda function prior to the specified one.
3+
"""
4+
import argparse
5+
import logging
6+
import sys
7+
8+
from azul import (
9+
R,
10+
config,
11+
)
12+
from azul.args import (
13+
AzulArgumentHelpFormatter,
14+
)
15+
from azul.lambdas import (
16+
LambdaFunctions,
17+
)
18+
from azul.logging import (
19+
configure_script_logging,
20+
)
21+
22+
log = logging.getLogger(__name__)
23+
24+
25+
def main(argv: list[str]):
26+
assert config.terraform_component == '', R(
27+
'This script cannot be run with a Terraform component selected',
28+
config.terraform_component)
29+
parser = argparse.ArgumentParser(description=__doc__,
30+
formatter_class=AzulArgumentHelpFormatter)
31+
parser.add_argument('--function-name', '-f',
32+
required=True,
33+
help='The name of the Lambda function.')
34+
parser.add_argument('--function-version', '-v',
35+
type=int,
36+
required=True,
37+
help='The Lambda function version to keep. Must be an '
38+
'integer.')
39+
args = parser.parse_args(argv)
40+
log.info('Deleting function %r versions older than %r',
41+
args.function_name, args.function_version)
42+
functions = LambdaFunctions()
43+
functions.delete_older_versions(args.function_name, args.function_version)
44+
45+
46+
if __name__ == '__main__':
47+
configure_script_logging(log)
48+
main(sys.argv[1:])

scripts/generate_openapi_document.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,14 @@ def main():
4242
sources=set())
4343
}
4444

45-
lambda_name = Path.cwd().name
46-
assert lambda_name in config.lambda_names(), lambda_name
45+
app_name = Path.cwd().name
46+
assert app_name in config.app_names(), app_name
4747

4848
# To create a normalized OpenAPI document, we patch any
4949
# deployment-specific variables that affect the document.
5050
with (
5151
patch_config('catalogs', catalogs),
52-
patch_config(f'{lambda_name}_function_name', f'azul-{lambda_name}-dev'),
52+
patch_config(f'{app_name}_function_name', f'azul-{app_name}-dev'),
5353
patch_config('enable_log_forwarding', False),
5454
patch_config('enable_replicas', True),
5555
patch_config('monitoring_email', '[email protected]')
@@ -58,10 +58,10 @@ def main():
5858
with patch.object(target=AzulChaliceApp,
5959
attribute='base_url',
6060
new=lambda_endpoint):
61-
app_module = load_app_module(lambda_name)
61+
app_module = load_app_module(app_name)
6262
assert app_module.app.base_url == lambda_endpoint
6363
app_spec = app_module.app.spec()
64-
doc_path = Path(config.project_root) / 'lambdas' / lambda_name / 'openapi.json'
64+
doc_path = Path(config.project_root) / 'lambdas' / app_name / 'openapi.json'
6565
with write_file_atomically(doc_path) as file:
6666
json.dump(app_spec, file, indent=4)
6767

scripts/manage_lambdas.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import logging
33

44
from azul.lambdas import (
5-
Lambdas,
5+
LambdaFunctions,
66
)
77
from azul.logging import (
88
configure_script_logging,
@@ -18,4 +18,5 @@
1818
group.add_argument('--disable', dest='enabled', action='store_false')
1919
args = parser.parse_args()
2020
assert args.enabled is not None
21-
Lambdas().manage_lambdas(args.enabled)
21+
functions = LambdaFunctions()
22+
functions.manage_lambdas(args.enabled)

scripts/reset_lambda_role.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
1+
"""
2+
Attempt to fix KMSAccessDeniedException when invoking a function.
3+
4+
See Troubleshooting section in README.md for details.
5+
"""
16
from azul.lambdas import (
2-
Lambdas,
7+
LambdaFunctions,
38
)
49

510

611
def main():
7-
Lambdas().reset_lambda_roles()
12+
functions = LambdaFunctions()
13+
functions.reset_lambda_roles()
814

915

1016
if __name__ == '__main__':

scripts/sell_unused_slots.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@
2727
aws,
2828
)
2929
from azul.lambdas import (
30-
Lambda,
31-
Lambdas,
30+
LambdaFunction,
31+
LambdaFunctions,
3232
)
3333
from azul.logging import (
3434
configure_script_logging,
@@ -65,17 +65,18 @@ def is_reindex_active(self) -> bool:
6565

6666
@classmethod
6767
@cache
68-
def _list_contribution_lambda_functions(cls) -> list[Lambda]:
68+
def _list_contribution_lambda_functions(cls) -> list[LambdaFunction]:
6969
"""
7070
Search Lambda functions for the names of contribution Lambdas.
7171
"""
72+
functions = LambdaFunctions()
7273
return [
73-
lambda_
74-
for lambda_ in Lambdas().list_lambdas()
75-
if lambda_.is_contribution_lambda
74+
function
75+
for function in functions.list_functions()
76+
if function.contributes
7677
]
7778

78-
def _lambda_invocation_counts(self) -> dict[Lambda, int]:
79+
def _lambda_invocation_counts(self) -> dict[LambdaFunction, int]:
7980
# FIXME: DeprecationWarning for datetime methods in Python 3.12
8081
# https://github.com/DataBiosphere/azul/issues/5953
8182
end = datetime.utcnow()
@@ -95,6 +96,9 @@ def _lambda_invocation_counts(self) -> dict[Lambda, int]:
9596
'Namespace': 'AWS/Lambda',
9697
'MetricName': 'Invocations',
9798
'Dimensions': [{
99+
# The 'FunctionName' dimension returns
100+
# aggregate metrics for all versions and
101+
# aliases of the function.
98102
'Name': 'FunctionName',
99103
'Value': lambda_.name
100104
}]

src/azul/__init__.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -743,7 +743,7 @@ def drs_endpoint(self) -> mutable_furl:
743743
else:
744744
return self.service_endpoint
745745

746-
def lambda_names(self) -> list[str]:
746+
def app_names(self) -> list[str]:
747747
return ['indexer', 'service']
748748

749749
@property
@@ -760,13 +760,15 @@ def indexer_function_name(self, handler_name: str | None = None):
760760
def service_function_name(self, handler_name: str | None = None):
761761
return self._function_name('service', handler_name)
762762

763-
def _function_name(self, lambda_name: str, handler_name: str | None):
763+
def _function_name(self, app_name: str, handler_name: str | None):
764764
if handler_name is None:
765-
return self.qualified_resource_name(lambda_name)
765+
return self.qualified_resource_name(app_name)
766766
else:
767767
# FIXME: Eliminate hardcoded separator
768768
# https://github.com/databiosphere/azul/issues/2964
769-
return self.qualified_resource_name(lambda_name, suffix='-' + handler_name)
769+
return self.qualified_resource_name(app_name, suffix='-' + handler_name)
770+
771+
active_function_alias_name = 'active'
770772

771773
qualifier_re = re.compile(r'[a-z][a-z0-9]{1,16}')
772774

src/azul/chalice.py

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,14 @@
2626
)
2727

2828
import attrs
29-
import chalice
3029
from chalice import (
3130
Chalice,
3231
ChaliceViewError,
3332
)
3433
from chalice.app import (
3534
BadRequestError,
3635
CaseInsensitiveMapping,
36+
EventSourceHandler,
3737
HeadersType,
3838
MultiDict,
3939
NotFoundError,
@@ -598,7 +598,7 @@ class metric_alarm(HandlerDecorator):
598598
period: int
599599

600600
def __call__(self, f):
601-
assert isinstance(f, chalice.app.EventSourceHandler), f
601+
assert isinstance(f, EventSourceHandler), f
602602
try:
603603
metric_alarms = getattr(f, 'metric_alarms')
604604
except AttributeError:
@@ -611,6 +611,14 @@ def __call__(self, f):
611611
def tf_resource_name(self) -> str:
612612
return f'{self.tf_function_resource_name}_{self.metric.name}'
613613

614+
@property
615+
def event_source_handlers(self) -> dict[str, EventSourceHandler]:
616+
return {
617+
handler_name: handler
618+
for handler_name, handler in self.handler_map.items()
619+
if isinstance(handler, EventSourceHandler)
620+
}
621+
614622
@property
615623
def metric_alarms(self) -> Iterator[metric_alarm]:
616624
for metric in LambdaMetric:
@@ -622,19 +630,16 @@ def metric_alarms(self) -> Iterator[metric_alarm]:
622630
threshold=0,
623631
period=60 * 60 if for_errors else 5 * 60)
624632
yield alarm.bind(self)
625-
for handler_name, handler in self.handler_map.items():
626-
if isinstance(handler, chalice.app.EventSourceHandler):
627-
try:
628-
metric_alarms = getattr(handler, 'metric_alarms')
629-
except AttributeError:
630-
metric_alarms = (
631-
self.metric_alarm(metric=metric,
632-
threshold=0,
633-
period=5 * 60)
634-
for metric in LambdaMetric
635-
)
636-
for metric_alarm in metric_alarms:
637-
yield metric_alarm.bind(self, handler_name)
633+
for handler_name, handler in self.event_source_handlers.items():
634+
try:
635+
metric_alarms = getattr(handler, 'metric_alarms')
636+
except AttributeError:
637+
metric_alarms = (
638+
self.metric_alarm(metric=metric, threshold=0, period=5 * 60)
639+
for metric in LambdaMetric
640+
)
641+
for metric_alarm in metric_alarms:
642+
yield metric_alarm.bind(self, handler_name)
638643

639644
# noinspection PyPep8Naming
640645
@attrs.frozen
@@ -650,20 +655,25 @@ class retry(HandlerDecorator):
650655
num_retries: int
651656

652657
def __call__(self, f):
653-
assert isinstance(f, chalice.app.EventSourceHandler), f
658+
assert isinstance(f, EventSourceHandler), f
654659
setattr(f, 'retry', self)
655660
return f
656661

657662
@property
658663
def retries(self) -> Iterator[retry]:
659-
for handler_name, handler in self.handler_map.items():
660-
if isinstance(handler, chalice.app.EventSourceHandler):
661-
try:
662-
retry = getattr(handler, 'retry')
663-
except AttributeError:
664-
pass
665-
else:
666-
yield retry.bind(self, handler_name)
664+
for handler_name, handler in self.event_source_handlers.items():
665+
try:
666+
retry = getattr(handler, 'retry')
667+
except AttributeError:
668+
pass
669+
else:
670+
yield retry.bind(self, handler_name)
671+
672+
@property
673+
def tf_function_resource_names(self) -> Iterator[str]:
674+
yield self.unqualified_app_name
675+
for handler_name in self.event_source_handlers:
676+
yield f'{self.unqualified_app_name}_{handler_name}'
667677

668678
def default_routes(self):
669679

src/azul/health.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ def description(self):
9393

9494
@attr.s(frozen=True, kw_only=True, auto_attribs=True)
9595
class HealthController(AppController):
96-
lambda_name: str
96+
app_name: str
9797

9898
@cached_property
9999
def storage_service(self):
@@ -134,7 +134,7 @@ def cached_health(self) -> JSON:
134134
self.app.catalog, config.default_catalog)
135135
else:
136136
try:
137-
cache = json.loads(self.storage_service.get(f'health/{self.lambda_name}'))
137+
cache = json.loads(self.storage_service.get(f'health/{self.app_name}'))
138138
except StorageObjectNotFound:
139139
raise NotFoundError('Cached health object does not exist')
140140
else:
@@ -148,7 +148,7 @@ def cached_health(self) -> JSON:
148148
def update_cache(self) -> None:
149149
assert self.app.catalog == config.default_catalog
150150
health_object = dict(time=time.time(), health=self._health.as_json_fast())
151-
self.storage_service.put(object_key=f'health/{self.lambda_name}',
151+
self.storage_service.put(object_key=f'health/{self.app_name}',
152152
data=json.dumps(health_object).encode())
153153

154154
@property
@@ -182,7 +182,7 @@ class does not examine any resources, only accessing the individual
182182

183183
@property
184184
def lambda_name(self):
185-
return self.controller.lambda_name
185+
return self.controller.app_name
186186

187187
def as_json(self, keys: Iterable[str]) -> JSON:
188188
keys = frozenset(keys)
@@ -200,9 +200,9 @@ def other_lambdas(self) -> JSON:
200200
Indicates whether the companion REST API responds to HTTP requests.
201201
"""
202202
response = {
203-
lambda_name: self._lambda(lambda_name)
204-
for lambda_name in config.lambda_names()
205-
if lambda_name != self.lambda_name
203+
app_name: self._lambda(app_name)
204+
for app_name in config.app_names()
205+
if app_name != self.lambda_name
206206
}
207207
return {
208208
'up': all(json_bool(v['up']) for v in response.values()),
@@ -333,7 +333,7 @@ class HealthApp(AzulChaliceApp):
333333

334334
@cached_property
335335
def health_controller(self) -> HealthController:
336-
return HealthController(app=self, lambda_name=self.unqualified_app_name)
336+
return HealthController(app=self, app_name=self.unqualified_app_name)
337337

338338
def default_routes(self):
339339
_routes = super().default_routes()

0 commit comments

Comments
 (0)