Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions samcli/commands/local/cli_common/invoke_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ def __init__(
invoke_images: Optional[str] = None,
mount_symlinks: Optional[bool] = False,
no_mem_limit: Optional[bool] = False,
no_watch: Optional[bool] = False,
container_dns: Optional[Tuple[str]] = None,
function_logical_ids: Optional[Tuple[str, ...]] = None,
) -> None:
Expand Down Expand Up @@ -224,6 +225,7 @@ def __init__(

self._mount_symlinks: Optional[bool] = mount_symlinks
self._no_mem_limit = no_mem_limit
self._no_watch = no_watch

# Note(xinhol): despite self._function_provider and self._stacks are initialized as None
# they will be assigned with a non-None value in __enter__() and
Expand Down Expand Up @@ -261,6 +263,15 @@ def __enter__(self) -> "InvokeContext":
ContainersMode.COLD: [self._stacks],
}

# --no-watch only applies when --warm-containers is set (RefreshableSamFunctionProvider).
# SamFunctionProvider (cold mode) has no file watcher, so the flag is meaningless there.
if self._no_watch and self._containers_mode == ContainersMode.COLD:
self._no_watch = False
LOG.warning(
"--no-watch was supplied without --warm-containers; the flag has no effect "
"without warm containers and will be ignored."
)

# don't resolve the code URI immediately if we passed in docker vol by passing True for use_raw_codeuri
# this way at the end the code URI will get resolved against the basedir option
if self._docker_volume_basedir:
Expand All @@ -271,6 +282,9 @@ def __enter__(self) -> "InvokeContext":
if self._function_logical_ids:
_function_providers_kwargs["function_logical_ids"] = self._function_logical_ids

if self._no_watch and self._containers_mode == ContainersMode.WARM:
_function_providers_kwargs["no_watch"] = True

self._function_provider = _function_providers_class[self._containers_mode](
*_function_providers_args[self._containers_mode], **_function_providers_kwargs
)
Expand Down Expand Up @@ -580,6 +594,7 @@ def lambda_runtime(self) -> LambdaRuntime:
image_builder,
mount_symlinks=self._mount_symlinks,
no_mem_limit=self._no_mem_limit,
no_watch=self._no_watch,
),
ContainersMode.COLD: LambdaRuntime(
self._container_manager,
Expand Down
10 changes: 10 additions & 0 deletions samcli/commands/local/cli_common/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,16 @@ def warm_containers_common_options(f):
type=click.STRING,
multiple=False,
),
click.option(
"--no-watch",
Comment thread
roger-zhangg marked this conversation as resolved.
is_flag=True,
default=False,
help="Disable file watching for hot reload. Only applies when --warm-containers is set. "
"When enabled, local code or template changes will not restart the running container; "
"stop and rerun the command to pick up changes. Useful when file watching causes "
"high CPU/IO (e.g. Microsoft Defender on Windows, large monorepos, or projects "
"using auto-generated directories like .sandbox).",
),
]

# Reverse the list to maintain ordering of options in help text printed with --help
Expand Down
4 changes: 4 additions & 0 deletions samcli/commands/local/start_api/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ def cli(
terraform_plan_file,
ssl_cert_file,
ssl_key_file,
no_watch,
no_memory_limit,
container_dns,
):
Expand Down Expand Up @@ -183,6 +184,7 @@ def cli(
ssl_cert_file,
ssl_key_file,
no_memory_limit,
no_watch,
container_dns,
) # pragma: no cover

Expand Down Expand Up @@ -217,6 +219,7 @@ def do_cli( # pylint: disable=R0914
ssl_cert_file,
ssl_key_file,
no_mem_limit,
no_watch,
container_dns,
):
"""
Expand Down Expand Up @@ -264,6 +267,7 @@ def do_cli( # pylint: disable=R0914
invoke_images=processed_invoke_images,
add_host=add_host,
no_mem_limit=no_mem_limit,
no_watch=no_watch,
container_dns=container_dns,
) as invoke_context:
ssl_context = (ssl_cert_file, ssl_key_file) if ssl_cert_file else None
Expand Down
1 change: 1 addition & 0 deletions samcli/commands/local/start_api/core/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"container_dns",
"invoke_image",
"disable_authorizer",
"no_watch",
]

CONFIGURATION_OPTION_NAMES: List[str] = ["config_env", "config_file"] + SAVE_PARAMS_OPTIONS
Expand Down
4 changes: 4 additions & 0 deletions samcli/commands/local/start_lambda/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ def cli(
hook_name,
skip_prepare_infra,
terraform_plan_file,
no_watch,
no_memory_limit,
container_dns,
):
Expand Down Expand Up @@ -142,6 +143,7 @@ def cli(
invoke_image,
hook_name,
no_memory_limit,
no_watch,
container_dns,
) # pragma: no cover

Expand Down Expand Up @@ -173,6 +175,7 @@ def do_cli( # pylint: disable=R0914
invoke_image,
hook_name,
no_mem_limit,
no_watch,
container_dns,
):
"""
Expand Down Expand Up @@ -221,6 +224,7 @@ def do_cli( # pylint: disable=R0914
invoke_images=processed_invoke_images,
function_logical_ids=function_logical_ids,
no_mem_limit=no_mem_limit,
no_watch=no_watch,
container_dns=container_dns,
) as invoke_context:
service = LocalLambdaService(lambda_invoke_context=invoke_context, port=port, host=host)
Expand Down
1 change: 1 addition & 0 deletions samcli/commands/local/start_lambda/core/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"add_host",
"invoke_image",
"no_memory_limit",
"no_watch",
]

ARTIFACT_LOCATION_OPTIONS: List[str] = [
Expand Down
26 changes: 18 additions & 8 deletions samcli/lib/providers/sam_function_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -893,6 +893,7 @@ def __init__(
use_raw_codeuri: bool = False,
ignore_code_extraction_warnings: bool = False,
function_logical_ids: Optional[Tuple[str, ...]] = None,
no_watch: Optional[bool] = False,
) -> None:
"""
Initialize the class with SAM template data. The SAM template passed to this provider is assumed
Expand All @@ -909,6 +910,8 @@ def __init__(
Note(xinhol): use_raw_codeuri is temporary to fix a bug, and will be removed for a permanent solution.
:param bool ignore_code_extraction_warnings: Ignores Log warnings
:param tuple function_logical_ids: Optional tuple of function logical IDs to filter by
:param bool no_watch: If True, skip creating the FileObserver entirely. The provider will not
detect template changes and stack/function refreshes will not be triggered.
"""

# Store function_logical_ids before calling super().__init__
Expand All @@ -930,9 +933,13 @@ def __init__(
self.parent_templates_paths.append(stack.location)

self.is_changed = False
self._observer = FileObserver(self._set_templates_changed)
self._observer.start()
self._watch_stack_templates(stacks)

self._observer: Optional[FileObserver] = None
# Only initialize file watcher when --no-watch is not set
if not no_watch:
self._observer = FileObserver(self._set_templates_changed)
self._observer.start()
self._watch_stack_templates(stacks)

@property
def stacks(self) -> List[Stack]:
Expand Down Expand Up @@ -995,15 +1002,17 @@ def _set_templates_changed(self, paths: List[str]) -> None:
", ".join(paths),
)
self.is_changed = True
for stack in self._stacks:
self._observer.unwatch(stack.location)
if self._observer:
for stack in self._stacks:
self._observer.unwatch(stack.location)

def _watch_stack_templates(self, stacks: List[Stack]) -> None:
"""
initialize the list of stack template watchers
"""
for stack in stacks:
self._observer.watch(stack.location)
if self._observer:
for stack in stacks:
self._observer.watch(stack.location)

def _refresh_loaded_functions(self) -> None:
"""
Expand Down Expand Up @@ -1039,4 +1048,5 @@ def stop_observer(self) -> None:
"""
Stop Observing.
"""
self._observer.stop()
if self._observer:
self._observer.stop()
20 changes: 20 additions & 0 deletions samcli/lib/utils/file_observer.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,11 +182,22 @@ def _on_change(self, resources: List[str], package_type: str) -> None:
package_type: str
determine if the changed resource is a source code path or an image name
"""
LOG.debug(
"Acquiring lock for _on_change to process %s %s resource changes: %s",
len(resources),
package_type,
resources,
)
with self._watch_lock:
changed_functions: List[FunctionConfig] = []
for resource in resources:
if self._observed_functions[package_type].get(resource, None):
changed_functions += self._observed_functions[package_type][resource]
LOG.debug(
"Acquired lock and processing %s changed functions from %s resources",
len(changed_functions),
len(resources),
)
self._input_on_change(changed_functions)

def watch(self, function_config: FunctionConfig) -> None:
Expand All @@ -203,9 +214,18 @@ def watch(self, function_config: FunctionConfig) -> None:
ObserverException:
if not able to observe the input function source path/image
"""
LOG.debug(
"Acquiring lock for watch to observe %s function: %s", function_config.packagetype, function_config.name
)
with self._watch_lock:
if self.get_resources.get(function_config.packagetype, None):
resources = self.get_resources[function_config.packagetype](function_config)
LOG.debug(
"Acquired lock for watch, observing %s resources for function %s: %s",
len(resources),
function_config.name,
resources,
)
for resource in resources:
functions = self._observed_functions[function_config.packagetype].get(resource, [])
functions += [function_config]
Expand Down
35 changes: 25 additions & 10 deletions samcli/local/lambdafn/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,9 @@ class WarmLambdaRuntime(LambdaRuntime):
warm containers life cycle.
"""

def __init__(self, container_manager, image_builder, observer=None, mount_symlinks=False, no_mem_limit=False):
def __init__(
self, container_manager, image_builder, observer=None, mount_symlinks=False, no_mem_limit=False, no_watch=False
):
"""
Initialize the Local Lambda runtime

Expand All @@ -536,14 +538,24 @@ def __init__(self, container_manager, image_builder, observer=None, mount_symlin
Instance of the ContainerManager class that can run a local Docker container
image_builder samcli.local.docker.lambda_image.LambdaImage
Instance of the LambdaImage class that can create am image
warm_containers bool
Determines if the warm containers is enabled or not.
observer
Optional observer for file watching
mount_symlinks bool
Optional. True if symlinks should be mounted in the container
no_mem_limit bool
Optional. True if memory limit should be disabled
Comment thread
roger-zhangg marked this conversation as resolved.
no_watch bool
Optional. True if file watching should be disabled
"""
self._function_configs = {}
self._containers = {}
self._no_watch = no_watch
self._container_lock = threading.Lock() # Thread-safe container creation

self._observer = observer if observer else LambdaFunctionObserver(self._on_code_change)
if no_watch:
self._observer = None
else:
self._observer = observer if observer else LambdaFunctionObserver(self._on_code_change)

super().__init__(container_manager, image_builder, mount_symlinks=mount_symlinks, no_mem_limit=no_mem_limit)

Expand Down Expand Up @@ -604,17 +616,18 @@ def create(
if container:
self._container_manager.stop(container)
self._containers.pop(function_path, None)
if exist_function_config:
if exist_function_config and self._observer is not None:
self._observer.unwatch(exist_function_config)
container = None

# Reuse existing container if available and compatible
elif container and container.is_created():
return container

# Create new container
self._observer.watch(function_config)
self._observer.start()
# Create new container; only watch/start observer when file watching is enabled
if self._observer is not None:
self._observer.watch(function_config)
self._observer.start()

container = super().create(
function_config,
Expand Down Expand Up @@ -702,7 +715,8 @@ def clean_running_containers_and_related_resources(self):
self._function_configs.clear()

self._clean_decompressed_paths()
self._observer.stop()
if self._observer is not None:
self._observer.stop()

def _on_code_change(self, functions):
"""
Expand All @@ -723,7 +737,8 @@ def _on_code_change(self, functions):
function_full_path,
resource,
)
self._observer.unwatch(function_config)
if self._observer is not None:
self._observer.unwatch(function_config)
self._function_configs.pop(function_full_path, None)
container = self._containers.get(function_full_path, None)
if container:
Expand Down
Loading
Loading