-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: warm containers #2383
feat: warm containers #2383
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,3 +15,4 @@ requests==2.23.0 | |
serverlessrepo==0.1.10 | ||
aws_lambda_builders==1.1.0 | ||
tomlkit==0.7.0 | ||
watchdog==0.10.3 | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,23 +1,38 @@ | ||
""" | ||
Reads CLI arguments and performs necessary preparation to be able to run the function | ||
""" | ||
|
||
import errno | ||
import json | ||
import logging | ||
import os | ||
from enum import Enum | ||
from pathlib import Path | ||
|
||
import samcli.lib.utils.osutils as osutils | ||
from samcli.lib.utils.async_utils import AsyncContext | ||
from samcli.lib.utils.stream_writer import StreamWriter | ||
from samcli.commands.local.lib.local_lambda import LocalLambdaRunner | ||
from samcli.commands.local.lib.debug_context import DebugContext | ||
from samcli.local.lambdafn.runtime import LambdaRuntime | ||
from samcli.local.lambdafn.runtime import LambdaRuntime, WarmLambdaRuntime | ||
from samcli.local.docker.lambda_image import LambdaImage | ||
from samcli.local.docker.manager import ContainerManager | ||
from samcli.commands._utils.template import get_template_data, TemplateNotFoundException, TemplateFailedParsingException | ||
from samcli.local.layers.layer_downloader import LayerDownloader | ||
from samcli.lib.providers.sam_function_provider import SamFunctionProvider | ||
from .user_exceptions import InvokeContextException, DebugContextException | ||
from ...exceptions import ContainersInitializationException | ||
|
||
LOG = logging.getLogger(__name__) | ||
|
||
|
||
class ContainersInitializationMode(Enum): | ||
EAGER = "EAGER" | ||
LAZY = "LAZY" | ||
|
||
|
||
class ContainersMode(Enum): | ||
WARM = "WARM" | ||
COLD = "COLD" | ||
|
||
|
||
class InvokeContext: | ||
|
@@ -54,6 +69,8 @@ def __init__( | |
force_image_build=None, | ||
aws_region=None, | ||
aws_profile=None, | ||
warm_container_initialization_mode=None, | ||
debug_function=None, | ||
): | ||
""" | ||
Initialize the context | ||
|
@@ -91,6 +108,14 @@ def __init__( | |
Whether or not to force build the image | ||
aws_region str | ||
AWS region to use | ||
warm_container_initialization_mode str | ||
Specifies how SAM cli manages the containers when using start-api or start_lambda. | ||
Two modes are available: | ||
"EAGER": Containers for every function are loaded at startup and persist between invocations. | ||
"LAZY": Containers are only loaded when the function is first invoked and persist for additional invocations | ||
debug_function str | ||
The Lambda function logicalId that will have the debugging options enabled in case of warm containers | ||
option is enabled | ||
""" | ||
self._template_file = template_file | ||
self._function_identifier = function_identifier | ||
|
@@ -109,6 +134,15 @@ def __init__( | |
self._aws_region = aws_region | ||
self._aws_profile = aws_profile | ||
|
||
self._containers_mode = ContainersMode.COLD | ||
self._containers_initializing_mode = ContainersInitializationMode.LAZY | ||
|
||
if warm_container_initialization_mode: | ||
self._containers_mode = ContainersMode.WARM | ||
self._containers_initializing_mode = ContainersInitializationMode(warm_container_initialization_mode) | ||
|
||
self._debug_function = debug_function | ||
|
||
self._template_dict = None | ||
self._function_provider = None | ||
self._env_vars_value = None | ||
|
@@ -117,6 +151,9 @@ def __init__( | |
self._debug_context = None | ||
self._layers_downloader = None | ||
self._container_manager = None | ||
self._lambda_runtimes = None | ||
|
||
self._local_lambda_runner = None | ||
|
||
def __enter__(self): | ||
""" | ||
|
@@ -133,8 +170,27 @@ def __enter__(self): | |
self._container_env_vars_value = self._get_env_vars_value(self._container_env_vars_file) | ||
self._log_file_handle = self._setup_log_file(self._log_file) | ||
|
||
# in case of warm containers && debugging is enabled && if debug-function property is not provided, so | ||
# if the provided template only contains one lambda function, so debug-function will be set to this function | ||
# if the template contains multiple functions, a warning message "that the debugging option will be ignored" | ||
# will be printed | ||
if self._containers_mode == ContainersMode.WARM and self._debug_ports and not self._debug_function: | ||
if len(self._function_provider.functions) == 1: | ||
self._debug_function = list(self._function_provider.functions.keys())[0] | ||
else: | ||
LOG.info( | ||
"Warning: you supplied debugging options but you did not specify the --debug-function option." | ||
" To specify which function you want to debug, please use the --debug-function <function-name>" | ||
) | ||
# skipp the debugging | ||
self._debug_ports = None | ||
|
||
self._debug_context = self._get_debug_context( | ||
self._debug_ports, self._debug_args, self._debugger_path, self._container_env_vars_value | ||
self._debug_ports, | ||
self._debug_args, | ||
self._debugger_path, | ||
self._container_env_vars_value, | ||
self._debug_function, | ||
) | ||
|
||
self._container_manager = self._get_container_manager(self._docker_network, self._skip_pull_image) | ||
|
@@ -144,17 +200,56 @@ def __enter__(self): | |
"Running AWS SAM projects locally requires Docker. Have you got it installed and running?" | ||
) | ||
|
||
# initialize all lambda function containers upfront | ||
if self._containers_initializing_mode == ContainersInitializationMode.EAGER: | ||
self._initialize_all_functions_containers() | ||
|
||
return self | ||
|
||
def __exit__(self, *args): | ||
""" | ||
Cleanup any necessary opened files | ||
Cleanup any necessary opened resources | ||
""" | ||
|
||
if self._log_file_handle: | ||
self._log_file_handle.close() | ||
self._log_file_handle = None | ||
|
||
if self._containers_mode == ContainersMode.WARM: | ||
self._clean_running_containers_and_related_resources() | ||
|
||
def _initialize_all_functions_containers(self): | ||
""" | ||
Create and run a container for each available lambda function | ||
""" | ||
LOG.info("Initializing the lambda functions containers.") | ||
|
||
def initialize_function_container(function): | ||
function_config = self.local_lambda_runner.get_invoke_config(function) | ||
self.lambda_runtime.run(None, function_config, self._debug_context, None) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we be 'running' or There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we can use running, as it also the same name use in docker to run the container |
||
|
||
try: | ||
async_context = AsyncContext() | ||
for function in self._function_provider.get_all(): | ||
async_context.add_async_task(initialize_function_container, function) | ||
|
||
async_context.run_async(default_executor=False) | ||
LOG.info("Containers Initialization is done.") | ||
except KeyboardInterrupt: | ||
mndeveci marked this conversation as resolved.
Show resolved
Hide resolved
|
||
LOG.debug("Ctrl+C was pressed. Aborting containers initialization") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there any cleanup we need to be doing here? Do we need to cancel the async tasks here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
self._clean_running_containers_and_related_resources() | ||
raise | ||
except Exception as ex: | ||
LOG.error("Lambda functions containers initialization failed because of %s", ex) | ||
self._clean_running_containers_and_related_resources() | ||
raise ContainersInitializationException("Lambda functions containers initialization failed") from ex | ||
|
||
def _clean_running_containers_and_related_resources(self): | ||
""" | ||
Clean the running containers and any other related open resources | ||
""" | ||
self.lambda_runtime.clean_running_containers_and_related_resources() | ||
|
||
@property | ||
def function_name(self): | ||
""" | ||
|
@@ -183,6 +278,18 @@ def function_name(self): | |
"Possible options in your template: {}".format(all_function_names) | ||
) | ||
|
||
@property | ||
def lambda_runtime(self): | ||
if not self._lambda_runtimes: | ||
layer_downloader = LayerDownloader(self._layer_cache_basedir, self.get_cwd()) | ||
image_builder = LambdaImage(layer_downloader, self._skip_pull_image, self._force_image_build) | ||
self._lambda_runtimes = { | ||
ContainersMode.WARM: WarmLambdaRuntime(self._container_manager, image_builder), | ||
ContainersMode.COLD: LambdaRuntime(self._container_manager, image_builder), | ||
} | ||
|
||
return self._lambda_runtimes[self._containers_mode] | ||
|
||
@property | ||
def local_lambda_runner(self): | ||
""" | ||
|
@@ -191,20 +298,19 @@ def local_lambda_runner(self): | |
:return samcli.commands.local.lib.local_lambda.LocalLambdaRunner: Runner configured to run Lambda functions | ||
locally | ||
""" | ||
if self._local_lambda_runner: | ||
return self._local_lambda_runner | ||
|
||
layer_downloader = LayerDownloader(self._layer_cache_basedir, self.get_cwd()) | ||
image_builder = LambdaImage(layer_downloader, self._skip_pull_image, self._force_image_build) | ||
|
||
lambda_runtime = LambdaRuntime(self._container_manager, image_builder) | ||
return LocalLambdaRunner( | ||
local_runtime=lambda_runtime, | ||
self._local_lambda_runner = LocalLambdaRunner( | ||
local_runtime=self.lambda_runtime, | ||
function_provider=self._function_provider, | ||
cwd=self.get_cwd(), | ||
aws_profile=self._aws_profile, | ||
aws_region=self._aws_region, | ||
env_vars_values=self._env_vars_value, | ||
debug_context=self._debug_context, | ||
) | ||
return self._local_lambda_runner | ||
|
||
@property | ||
def stdout(self): | ||
|
@@ -322,7 +428,7 @@ def _setup_log_file(log_file): | |
return open(log_file, "wb") | ||
|
||
@staticmethod | ||
def _get_debug_context(debug_ports, debug_args, debugger_path, container_env_vars): | ||
def _get_debug_context(debug_ports, debug_args, debugger_path, container_env_vars, debug_function=None): | ||
""" | ||
Creates a DebugContext if the InvokeContext is in a debugging mode | ||
|
||
|
@@ -336,6 +442,9 @@ def _get_debug_context(debug_ports, debug_args, debugger_path, container_env_var | |
Path to the directory of the debugger to mount on Docker | ||
container_env_vars dict | ||
Dictionary containing debugging based environmental variables. | ||
debug_function str | ||
The Lambda function logicalId that will have the debugging options enabled in case of warm containers | ||
option is enabled | ||
|
||
Returns | ||
------- | ||
|
@@ -364,6 +473,7 @@ def _get_debug_context(debug_ports, debug_args, debugger_path, container_env_var | |
debug_ports=debug_ports, | ||
debug_args=debug_args, | ||
debugger_path=debugger_path, | ||
debug_function=debug_function, | ||
container_env_vars=container_env_vars, | ||
) | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,7 @@ | |
import click | ||
|
||
from samcli.commands._utils.options import template_click_option, docker_click_options, parameter_override_click_option | ||
from samcli.commands.local.cli_common.invoke_context import ContainersInitializationMode | ||
|
||
|
||
def get_application_dir(): | ||
|
@@ -140,3 +141,32 @@ def invoke_common_options(f): | |
option(f) | ||
|
||
return f | ||
|
||
|
||
def warm_containers_common_options(f): | ||
""" | ||
Warm containers related CLI options shared by "local start-api" and "local start_lambda" commands | ||
|
||
:param f: Callback passed by Click | ||
""" | ||
|
||
warm_containers_options = [ | ||
click.option( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if we should abstract Choices based on Enums into something more concrete. Doesn't look like click supports it out of the box. Not blocking on this but something that came to mind reviewing. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not sure if I got what do you mean. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So we are duplicating the values in line 156 There is a feature request out to click for this: pallets/click#605 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
"--warm-containers", | ||
help="Optional. Specifies how AWS SAM CLI manages containers for each function.", | ||
type=click.Choice(ContainersInitializationMode.__members__, case_sensitive=False), | ||
), | ||
click.option( | ||
"--debug-function", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a limitation of only being able to debug a single function in "warm" container mode? Will this flag limit our ability to allow customers to debug multiple functions in the future? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, only one function will be enabled for debugging in the warm/lazy container mode. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm thinking out loud, but wondering if it would make sense to only have a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @hoffa .. there is also another option to let the customer provides multiple debugging ports in a map format (like the parameter-overrides). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I understand this will limit to only one function now. I am more curious if this walks us into a one way door that would make supporting multiple much more difficult later. @hoffa A customer shouldn't have to drop into another tool for this. Yes they could There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it is not a one door, we can consider this parameter to support the use case if customers want to debug only one function, and if there is a need later to debug multiple functions we can add another parameter (these parameters will be only applicable if warm-containers is enabled) |
||
help="Optional. Specifies the Lambda Function logicalId to apply debug options to when" | ||
" --warm-containers is specified ", | ||
type=click.STRING, | ||
multiple=False, | ||
), | ||
] | ||
|
||
# Reverse the list to maintain ordering of options in help text printed with --help | ||
for option in reversed(warm_containers_options): | ||
option(f) | ||
|
||
return f |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -104,7 +104,7 @@ def invoke(self, function_name, event, stdout=None, stderr=None): | |
f"ImageUri not provided for Function: {function_name} of PackageType: {function.packagetype}" | ||
) | ||
LOG.info("Invoking Container created from %s", function.imageuri) | ||
config = self._get_invoke_config(function) | ||
config = self.get_invoke_config(function) | ||
|
||
# Invoke the function | ||
try: | ||
|
@@ -135,7 +135,7 @@ def is_debugging(self): | |
""" | ||
return bool(self.debug_context) | ||
|
||
def _get_invoke_config(self, function): | ||
def get_invoke_config(self, function): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Curious: Is this no longer a private method? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, It is called in InvokeContext to get the FunctionConfig to send it as part of containers initialization in Runtime. |
||
""" | ||
Returns invoke configuration to pass to Lambda Runtime to invoke the given function | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How does watchdog affect what we need installed for customers? Looking at their github repo, it says you need xcode installed for Mac: https://github.com/gorakhargosh/watchdog#dependencies.
There is no
.whl
for this package in pypi as well. How does this dependency impact MSI generation and possible the pyinstaller work that is ongoing for a new way to install linux? We have had issues with libraries written in C before with our installers.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
regarding the xcode dependency, I think it should not be a problem, as we use use brew to install SAM, and brew depends on xcode as well.
I tested the MSI generation, and the pyinstaller, and both work fine, so I do not think adding watchdog will affect the release process.