Skip to content

Commit c4bc8a2

Browse files
authored
feat(local): add --no-watch flag to disable hot reload (#8196)
* first debug version * add --no-watch flag * add no-watch flag * black lint fix * feedback * chore: black formatting for runtime.py post-merge Apply black formatting after merge of develop into hot-swap-fix. * chore: post-review cleanups for --no-watch - Add no_watch docstring entry to RefreshableSamFunctionProvider. - Initialize self._observer = None up front in RefreshableSamFunctionProvider so the type annotation is unconditional and the no-watch branch is implied. - Drop incidental whitespace-only hunks (blank line in invoke_context.__enter__, blank line in SingletonFileObserver.__init__) flagged during review. * add LE to no-watch test
1 parent faf5ec8 commit c4bc8a2

16 files changed

Lines changed: 355 additions & 21 deletions

File tree

samcli/commands/local/cli_common/invoke_context.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ def __init__(
102102
invoke_images: Optional[str] = None,
103103
mount_symlinks: Optional[bool] = False,
104104
no_mem_limit: Optional[bool] = False,
105+
no_watch: Optional[bool] = False,
105106
container_dns: Optional[Tuple[str]] = None,
106107
function_logical_ids: Optional[Tuple[str, ...]] = None,
107108
language_extensions: Optional[bool] = None,
@@ -226,6 +227,7 @@ def __init__(
226227

227228
self._mount_symlinks: Optional[bool] = mount_symlinks
228229
self._no_mem_limit = no_mem_limit
230+
self._no_watch = no_watch
229231

230232
self._language_extensions_enabled = resolve_language_extensions_enabled(language_extensions)
231233

@@ -265,6 +267,15 @@ def __enter__(self) -> "InvokeContext":
265267
ContainersMode.COLD: [self._stacks],
266268
}
267269

270+
# --no-watch only applies when --warm-containers is set (RefreshableSamFunctionProvider).
271+
# SamFunctionProvider (cold mode) has no file watcher, so the flag is meaningless there.
272+
if self._no_watch and self._containers_mode == ContainersMode.COLD:
273+
self._no_watch = False
274+
LOG.warning(
275+
"--no-watch was supplied without --warm-containers; the flag has no effect "
276+
"without warm containers and will be ignored."
277+
)
278+
268279
# don't resolve the code URI immediately if we passed in docker vol by passing True for use_raw_codeuri
269280
# this way at the end the code URI will get resolved against the basedir option
270281
if self._docker_volume_basedir:
@@ -275,6 +286,9 @@ def __enter__(self) -> "InvokeContext":
275286
if self._function_logical_ids:
276287
_function_providers_kwargs["function_logical_ids"] = self._function_logical_ids
277288

289+
if self._no_watch and self._containers_mode == ContainersMode.WARM:
290+
_function_providers_kwargs["no_watch"] = True
291+
278292
if self._containers_mode == ContainersMode.WARM:
279293
_function_providers_kwargs["language_extensions_enabled"] = self._language_extensions_enabled
280294

@@ -596,6 +610,7 @@ def lambda_runtime(self) -> LambdaRuntime:
596610
image_builder,
597611
mount_symlinks=self._mount_symlinks,
598612
no_mem_limit=self._no_mem_limit,
613+
no_watch=self._no_watch,
599614
),
600615
ContainersMode.COLD: LambdaRuntime(
601616
self._container_manager,

samcli/commands/local/cli_common/options.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,16 @@ def warm_containers_common_options(f):
268268
type=click.STRING,
269269
multiple=False,
270270
),
271+
click.option(
272+
"--no-watch",
273+
is_flag=True,
274+
default=False,
275+
help="Disable file watching for hot reload. Only applies when --warm-containers is set. "
276+
"When enabled, local code or template changes will not restart the running container; "
277+
"stop and rerun the command to pick up changes. Useful when file watching causes "
278+
"high CPU/IO (e.g. Microsoft Defender on Windows, large monorepos, or projects "
279+
"using auto-generated directories like .sandbox).",
280+
),
271281
]
272282

273283
# Reverse the list to maintain ordering of options in help text printed with --help

samcli/commands/local/start_api/cli.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ def cli(
148148
language_extensions,
149149
ssl_cert_file,
150150
ssl_key_file,
151+
no_watch,
151152
no_memory_limit,
152153
container_dns,
153154
):
@@ -187,6 +188,7 @@ def cli(
187188
ssl_cert_file,
188189
ssl_key_file,
189190
no_memory_limit,
191+
no_watch,
190192
container_dns,
191193
) # pragma: no cover
192194

@@ -222,6 +224,7 @@ def do_cli( # pylint: disable=R0914
222224
ssl_cert_file,
223225
ssl_key_file,
224226
no_mem_limit,
227+
no_watch,
225228
container_dns,
226229
):
227230
"""
@@ -270,6 +273,7 @@ def do_cli( # pylint: disable=R0914
270273
add_host=add_host,
271274
language_extensions=language_extensions,
272275
no_mem_limit=no_mem_limit,
276+
no_watch=no_watch,
273277
container_dns=container_dns,
274278
) as invoke_context:
275279
ssl_context = (ssl_cert_file, ssl_key_file) if ssl_cert_file else None

samcli/commands/local/start_api/core/options.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"container_dns",
4646
"invoke_image",
4747
"disable_authorizer",
48+
"no_watch",
4849
]
4950

5051
CONFIGURATION_OPTION_NAMES: List[str] = ["config_env", "config_file"] + SAVE_PARAMS_OPTIONS

samcli/commands/local/start_lambda/cli.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ def cli(
109109
hook_name,
110110
skip_prepare_infra,
111111
terraform_plan_file,
112+
no_watch,
112113
language_extensions,
113114
no_memory_limit,
114115
container_dns,
@@ -146,6 +147,7 @@ def cli(
146147
hook_name,
147148
language_extensions,
148149
no_memory_limit,
150+
no_watch,
149151
container_dns,
150152
) # pragma: no cover
151153

@@ -178,6 +180,7 @@ def do_cli( # pylint: disable=R0914
178180
hook_name,
179181
language_extensions,
180182
no_mem_limit,
183+
no_watch,
181184
container_dns,
182185
):
183186
"""
@@ -227,6 +230,7 @@ def do_cli( # pylint: disable=R0914
227230
function_logical_ids=function_logical_ids,
228231
language_extensions=language_extensions,
229232
no_mem_limit=no_mem_limit,
233+
no_watch=no_watch,
230234
container_dns=container_dns,
231235
) as invoke_context:
232236
service = LocalLambdaService(lambda_invoke_context=invoke_context, port=port, host=host)

samcli/commands/local/start_lambda/core/options.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"add_host",
4141
"invoke_image",
4242
"no_memory_limit",
43+
"no_watch",
4344
]
4445

4546
ARTIFACT_LOCATION_OPTIONS: List[str] = [

samcli/lib/providers/sam_function_provider.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -893,6 +893,7 @@ def __init__(
893893
use_raw_codeuri: bool = False,
894894
ignore_code_extraction_warnings: bool = False,
895895
function_logical_ids: Optional[Tuple[str, ...]] = None,
896+
no_watch: Optional[bool] = False,
896897
language_extensions_enabled: bool = False,
897898
) -> None:
898899
"""
@@ -910,6 +911,8 @@ def __init__(
910911
Note(xinhol): use_raw_codeuri is temporary to fix a bug, and will be removed for a permanent solution.
911912
:param bool ignore_code_extraction_warnings: Ignores Log warnings
912913
:param tuple function_logical_ids: Optional tuple of function logical IDs to filter by
914+
:param bool no_watch: If True, skip creating the FileObserver entirely. The provider will not
915+
detect template changes and stack/function refreshes will not be triggered.
913916
"""
914917

915918
# Store function_logical_ids before calling super().__init__
@@ -932,9 +935,13 @@ def __init__(
932935
self.parent_templates_paths.append(stack.location)
933936

934937
self.is_changed = False
935-
self._observer = FileObserver(self._set_templates_changed)
936-
self._observer.start()
937-
self._watch_stack_templates(stacks)
938+
939+
self._observer: Optional[FileObserver] = None
940+
# Only initialize file watcher when --no-watch is not set
941+
if not no_watch:
942+
self._observer = FileObserver(self._set_templates_changed)
943+
self._observer.start()
944+
self._watch_stack_templates(stacks)
938945

939946
@property
940947
def stacks(self) -> List[Stack]:
@@ -997,15 +1004,17 @@ def _set_templates_changed(self, paths: List[str]) -> None:
9971004
", ".join(paths),
9981005
)
9991006
self.is_changed = True
1000-
for stack in self._stacks:
1001-
self._observer.unwatch(stack.location)
1007+
if self._observer:
1008+
for stack in self._stacks:
1009+
self._observer.unwatch(stack.location)
10021010

10031011
def _watch_stack_templates(self, stacks: List[Stack]) -> None:
10041012
"""
10051013
initialize the list of stack template watchers
10061014
"""
1007-
for stack in stacks:
1008-
self._observer.watch(stack.location)
1015+
if self._observer:
1016+
for stack in stacks:
1017+
self._observer.watch(stack.location)
10091018

10101019
def _refresh_loaded_functions(self) -> None:
10111020
"""
@@ -1042,4 +1051,5 @@ def stop_observer(self) -> None:
10421051
"""
10431052
Stop Observing.
10441053
"""
1045-
self._observer.stop()
1054+
if self._observer:
1055+
self._observer.stop()

samcli/lib/utils/file_observer.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,11 +182,22 @@ def _on_change(self, resources: List[str], package_type: str) -> None:
182182
package_type: str
183183
determine if the changed resource is a source code path or an image name
184184
"""
185+
LOG.debug(
186+
"Acquiring lock for _on_change to process %s %s resource changes: %s",
187+
len(resources),
188+
package_type,
189+
resources,
190+
)
185191
with self._watch_lock:
186192
changed_functions: List[FunctionConfig] = []
187193
for resource in resources:
188194
if self._observed_functions[package_type].get(resource, None):
189195
changed_functions += self._observed_functions[package_type][resource]
196+
LOG.debug(
197+
"Acquired lock and processing %s changed functions from %s resources",
198+
len(changed_functions),
199+
len(resources),
200+
)
190201
self._input_on_change(changed_functions)
191202

192203
def watch(self, function_config: FunctionConfig) -> None:
@@ -203,9 +214,18 @@ def watch(self, function_config: FunctionConfig) -> None:
203214
ObserverException:
204215
if not able to observe the input function source path/image
205216
"""
217+
LOG.debug(
218+
"Acquiring lock for watch to observe %s function: %s", function_config.packagetype, function_config.name
219+
)
206220
with self._watch_lock:
207221
if self.get_resources.get(function_config.packagetype, None):
208222
resources = self.get_resources[function_config.packagetype](function_config)
223+
LOG.debug(
224+
"Acquired lock for watch, observing %s resources for function %s: %s",
225+
len(resources),
226+
function_config.name,
227+
resources,
228+
)
209229
for resource in resources:
210230
functions = self._observed_functions[function_config.packagetype].get(resource, [])
211231
functions += [function_config]

samcli/local/lambdafn/runtime.py

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -526,7 +526,9 @@ class WarmLambdaRuntime(LambdaRuntime):
526526
warm containers life cycle.
527527
"""
528528

529-
def __init__(self, container_manager, image_builder, observer=None, mount_symlinks=False, no_mem_limit=False):
529+
def __init__(
530+
self, container_manager, image_builder, observer=None, mount_symlinks=False, no_mem_limit=False, no_watch=False
531+
):
530532
"""
531533
Initialize the Local Lambda runtime
532534
@@ -536,14 +538,24 @@ def __init__(self, container_manager, image_builder, observer=None, mount_symlin
536538
Instance of the ContainerManager class that can run a local Docker container
537539
image_builder samcli.local.docker.lambda_image.LambdaImage
538540
Instance of the LambdaImage class that can create am image
539-
warm_containers bool
540-
Determines if the warm containers is enabled or not.
541+
observer
542+
Optional observer for file watching
543+
mount_symlinks bool
544+
Optional. True if symlinks should be mounted in the container
545+
no_mem_limit bool
546+
Optional. True if memory limit should be disabled
547+
no_watch bool
548+
Optional. True if file watching should be disabled
541549
"""
542550
self._function_configs = {}
543551
self._containers = {}
552+
self._no_watch = no_watch
544553
self._container_lock = threading.Lock() # Thread-safe container creation
545554

546-
self._observer = observer if observer else LambdaFunctionObserver(self._on_code_change)
555+
if no_watch:
556+
self._observer = None
557+
else:
558+
self._observer = observer if observer else LambdaFunctionObserver(self._on_code_change)
547559

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

@@ -604,17 +616,18 @@ def create(
604616
if container:
605617
self._container_manager.stop(container)
606618
self._containers.pop(function_path, None)
607-
if exist_function_config:
619+
if exist_function_config and self._observer is not None:
608620
self._observer.unwatch(exist_function_config)
609621
container = None
610622

611623
# Reuse existing container if available and compatible
612624
elif container and container.is_created():
613625
return container
614626

615-
# Create new container
616-
self._observer.watch(function_config)
617-
self._observer.start()
627+
# Create new container; only watch/start observer when file watching is enabled
628+
if self._observer is not None:
629+
self._observer.watch(function_config)
630+
self._observer.start()
618631

619632
container = super().create(
620633
function_config,
@@ -702,7 +715,8 @@ def clean_running_containers_and_related_resources(self):
702715
self._function_configs.clear()
703716

704717
self._clean_decompressed_paths()
705-
self._observer.stop()
718+
if self._observer is not None:
719+
self._observer.stop()
706720

707721
def _on_code_change(self, functions):
708722
"""
@@ -723,7 +737,8 @@ def _on_code_change(self, functions):
723737
function_full_path,
724738
resource,
725739
)
726-
self._observer.unwatch(function_config)
740+
if self._observer is not None:
741+
self._observer.unwatch(function_config)
727742
self._function_configs.pop(function_full_path, None)
728743
container = self._containers.get(function_full_path, None)
729744
if container:

0 commit comments

Comments
 (0)