def setUp(self, PatternMatchingEventHandlerMock, ObserverMock): self.on_change = Mock() self.watchdog_observer_mock = Mock() ObserverMock.return_value = self.watchdog_observer_mock self.handler_mock = Mock() PatternMatchingEventHandlerMock.return_value = self.handler_mock self.observer = FileObserver(self.on_change) self.observer._watch_dog_observed_paths = { "parent_path1": ["parent_path1/path1", "parent_path1/path2"], "parent_path2": ["parent_path2/path3"], } self.observer._observed_paths = { "parent_path1/path1": "1234", "parent_path1/path2": "4567", "parent_path2/path3": "7890", } self._parent1_watcher_mock = Mock() self._parent2_watcher_mock = Mock() self.observer._observed_watches = { "parent_path1": self._parent1_watcher_mock, "parent_path2": self._parent2_watcher_mock, }
def setUp(self, PatternMatchingEventHandlerMock, ObserverMock): self.on_change = Mock() self.watchdog_observer_mock = Mock() self.watcher_mock = Mock() self.watchdog_observer_mock.schedule.return_value = self.watcher_mock ObserverMock.return_value = self.watchdog_observer_mock self._PatternMatchingEventHandlerMock = PatternMatchingEventHandlerMock self.handler_mock = Mock() self._PatternMatchingEventHandlerMock.return_value = self.handler_mock self.observer = FileObserver(self.on_change)
def __init__(self, container_manager, image_builder): """ Initialize the Local Lambda runtime Parameters ---------- container_manager samcli.local.docker.manager.ContainerManager 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. """ self._containers = {} # used to observe any code change in case if warm containers is enabled to support the reloading self._observed_paths = {} self._observer = FileObserver(self._on_code_change) super().__init__(container_manager, image_builder)
class FileObserver_stop(TestCase): @patch("samcli.lib.utils.file_observer.Observer") @patch("samcli.lib.utils.file_observer.PatternMatchingEventHandler") def setUp(self, PatternMatchingEventHandlerMock, ObserverMock): self.on_change = Mock() self.watchdog_observer_mock = Mock() ObserverMock.return_value = self.watchdog_observer_mock self.handler_mock = Mock() PatternMatchingEventHandlerMock.return_value = self.handler_mock self.observer = FileObserver(self.on_change) self.observer._watch_dog_observed_paths = { "parent_path1": ["parent_path1/path1", "parent_path1/path2"], "parent_path2": ["parent_path2/path3"], } self.observer._observed_paths = { "parent_path1/path1": "1234", "parent_path1/path2": "4567", "parent_path2/path3": "7890", } self._parent1_watcher_mock = Mock() self._parent2_watcher_mock = Mock() self.observer._observed_watches = { "parent_path1": self._parent1_watcher_mock, "parent_path2": self._parent2_watcher_mock, } def test_stop_started_observer_successfully(self): self.watchdog_observer_mock.is_alive.return_value = True self.observer.stop() self.watchdog_observer_mock.stop.assert_called_with() def test_stop_non_started_observer_does_not_call_watchdog_observer(self): self.watchdog_observer_mock.is_alive.return_value = False self.observer.stop() self.watchdog_observer_mock.stop.assert_not_called()
class FileObserver_on_change(TestCase): @patch("samcli.lib.utils.file_observer.Observer") @patch("samcli.lib.utils.file_observer.PatternMatchingEventHandler") def setUp(self, PatternMatchingEventHandlerMock, ObserverMock): self.on_change = Mock() self.watchdog_observer_mock = Mock() ObserverMock.return_value = self.watchdog_observer_mock self.handler_mock = Mock() PatternMatchingEventHandlerMock.return_value = self.handler_mock self.observer = FileObserver(self.on_change) self.observer._watch_dog_observed_paths = { "parent_path1": ["parent_path1/path1", "parent_path1/path2"], "parent_path2": ["parent_path2/path3"], } self.observer._observed_paths = { "parent_path1/path1": "1234", "parent_path1/path2": "4567", "parent_path2/path3": "7890", } self._parent1_watcher_mock = Mock() self._parent2_watcher_mock = Mock() self.observer._observed_watches = { "parent_path1": self._parent1_watcher_mock, "parent_path2": self._parent2_watcher_mock, } @patch("samcli.lib.utils.file_observer.Path") @patch("samcli.lib.utils.file_observer.calculate_checksum") def test_modification_event_got_fired_for_sub_path_and_check_sum_changed( self, calculate_checksum_mock, PathMock): event = Mock() event.src_path = "parent_path1/path1/sub_path" path_mock = Mock() PathMock.return_value = path_mock calculate_checksum_mock.side_effect = ["123456543"] path_mock.exists.return_value = True self.observer.on_change(event) self.assertEqual( self.observer._observed_paths, { "parent_path1/path1": "123456543", "parent_path1/path2": "4567", "parent_path2/path3": "7890", }, ) self.on_change.assert_called_once_with(["parent_path1/path1"]) @patch("samcli.lib.utils.file_observer.Path") @patch("samcli.lib.utils.file_observer.calculate_checksum") def test_modification_event_got_fired_for_sub_path_and_check_sum_is_not_changed( self, calculate_checksum_mock, PathMock): event = Mock() event.src_path = "parent_path1/path1/sub_path" path_mock = Mock() PathMock.return_value = path_mock calculate_checksum_mock.side_effect = ["1234", "4567"] path_mock.exists.return_value = True self.observer.on_change(event) self.assertEqual( self.observer._observed_paths, { "parent_path1/path1": "1234", "parent_path1/path2": "4567", "parent_path2/path3": "7890", }, ) self.on_change.assert_not_called() @patch("samcli.lib.utils.file_observer.Path") @patch("samcli.lib.utils.file_observer.calculate_checksum") def test_modification_event_got_fired_for_path_got_deleted( self, calculate_checksum_mock, PathMock): event = Mock() event.src_path = "parent_path1/path1/sub_path" path_mock = Mock() PathMock.return_value = path_mock calculate_checksum_mock.return_value = "4567" path_mock.exists.side_effect = [False, True] self.observer.on_change(event) self.assertEqual( self.observer._observed_paths, { "parent_path1/path2": "4567", "parent_path2/path3": "7890", }, ) self.on_change.assert_called_once_with(["parent_path1/path1"])
class FileObserver_watch(TestCase): @patch("samcli.lib.utils.file_observer.Observer") @patch("samcli.lib.utils.file_observer.PatternMatchingEventHandler") def setUp(self, PatternMatchingEventHandlerMock, ObserverMock): self.on_change = Mock() self.watchdog_observer_mock = Mock() self.watcher_mock = Mock() self.watchdog_observer_mock.schedule.return_value = self.watcher_mock ObserverMock.return_value = self.watchdog_observer_mock self._PatternMatchingEventHandlerMock = PatternMatchingEventHandlerMock self.handler_mock = Mock() self._PatternMatchingEventHandlerMock.return_value = self.handler_mock self.observer = FileObserver(self.on_change) def test_init_successfully(self): self.assertEqual(self.observer._observed_paths, {}) self.assertEqual(self.observer._observed_watches, {}) self.assertEqual(self.observer._watch_dog_observed_paths, {}) self.assertEqual(self.observer._observer, self.watchdog_observer_mock) self._PatternMatchingEventHandlerMock.assert_called_with( patterns=["*"], ignore_patterns=[], ignore_directories=False) self.assertEqual(self.observer._code_modification_handler, self.handler_mock) self.assertEqual(self.observer._input_on_change, self.on_change) @patch("samcli.lib.utils.file_observer.Path") @patch("samcli.lib.utils.file_observer.calculate_checksum") def test_path_get_watched_successfully(self, calculate_checksum_mock, PathMock): path_str = "path" parent_path = "parent_path" check_sum = "1234565432" path_mock = Mock() PathMock.return_value = path_mock path_mock.parent = parent_path path_mock.exists.return_value = True calculate_checksum_mock.return_value = check_sum self.observer.watch(path_str) self.assertEqual(self.observer._observed_paths, {path_str: check_sum}) self.assertEqual( self.observer._watch_dog_observed_paths, { parent_path: [path_str], path_str: [path_str] }, ) self.assertEqual( self.observer._observed_watches, { parent_path: self.watcher_mock, path_str: self.watcher_mock, }, ) self.assertEqual( self.watchdog_observer_mock.schedule.call_args_list, [ call(self.handler_mock, path_str, recursive=True), call(self.handler_mock, parent_path, recursive=False), ], ) @patch("samcli.lib.utils.file_observer.Path") @patch("samcli.lib.utils.file_observer.calculate_checksum") def test_parent_path_get_watched_only_one_time(self, calculate_checksum_mock, PathMock): path1_str = "path1" path2_str = "path2" parent_path = "parent_path" check_sum = "1234565432" path_mock = Mock() PathMock.return_value = path_mock path_mock.parent = parent_path path_mock.exists.return_value = True calculate_checksum_mock.return_value = check_sum self.observer.watch(path1_str) self.observer.watch(path2_str) self.assertEqual( self.observer._observed_paths, { path1_str: check_sum, path2_str: check_sum, }, ) self.assertEqual( self.observer._watch_dog_observed_paths, { parent_path: [path1_str, path2_str], path1_str: [path1_str], path2_str: [path2_str] }, ) self.assertEqual( self.observer._observed_watches, { parent_path: self.watcher_mock, path1_str: self.watcher_mock, path2_str: self.watcher_mock, }, ) self.assertEqual( self.watchdog_observer_mock.schedule.call_args_list, [ call(self.handler_mock, path1_str, recursive=True), call(self.handler_mock, parent_path, recursive=False), call(self.handler_mock, path2_str, recursive=True), ], ) @patch("samcli.lib.utils.file_observer.Path") def test_raise_FileObserverException_if_watched_path_is_not_exist( self, PathMock): path_str = "path" path_mock = Mock() PathMock.return_value = path_mock path_mock.exists.return_value = False with self.assertRaises(FileObserverException): self.observer.watch(path_str)
class FileObserver_unwatch(TestCase): @patch("samcli.lib.utils.file_observer.Observer") @patch("samcli.lib.utils.file_observer.PatternMatchingEventHandler") def setUp(self, PatternMatchingEventHandlerMock, ObserverMock): self.on_change = Mock() self.watchdog_observer_mock = Mock() ObserverMock.return_value = self.watchdog_observer_mock self.handler_mock = Mock() PatternMatchingEventHandlerMock.return_value = self.handler_mock self.observer = FileObserver(self.on_change) self.observer._watch_dog_observed_paths = { "parent_path1": ["path1", "path2"], "parent_path2": ["path3"], } self.observer._observed_paths = { "path1": "1234", "path2": "4567", "path3": "7890", } self._parent1_watcher_mock = Mock() self._parent2_watcher_mock = Mock() self.observer._observed_watches = { "parent_path1": self._parent1_watcher_mock, "parent_path2": self._parent2_watcher_mock, } @patch("samcli.lib.utils.file_observer.Path") def test_path_get_unwatched_successfully(self, PathMock): path_str = "path1" parent_path = "parent_path1" path_mock = Mock() PathMock.return_value = path_mock path_mock.parent = parent_path path_mock.exists.return_value = True self.observer.unwatch(path_str) self.assertEqual( self.observer._observed_paths, { "path2": "4567", "path3": "7890", }, ) self.assertEqual( self.observer._watch_dog_observed_paths, { "parent_path1": ["path2"], "parent_path2": ["path3"], }, ) self.assertEqual( self.observer._observed_watches, { "parent_path1": self._parent1_watcher_mock, "parent_path2": self._parent2_watcher_mock, }, ) self.watchdog_observer_mock.unschedule.assert_not_called() @patch("samcli.lib.utils.file_observer.Path") def test_parent_path_get_unwatched_successfully(self, PathMock): path_str = "path3" parent_path = "parent_path2" path_mock = Mock() PathMock.return_value = path_mock path_mock.parent = parent_path path_mock.exists.return_value = True self.observer.unwatch(path_str) self.assertEqual( self.observer._observed_paths, { "path1": "1234", "path2": "4567", }, ) self.assertEqual( self.observer._watch_dog_observed_paths, { "parent_path1": ["path1", "path2"], }, ) self.assertEqual( self.observer._observed_watches, { "parent_path1": self._parent1_watcher_mock, }, ) self.watchdog_observer_mock.unschedule.assert_called_with( self._parent2_watcher_mock)
class WarmLambdaRuntime(LambdaRuntime): """ This class extends the LambdaRuntime class to add the Warm containers feature. This class handles the warm containers life cycle. """ def __init__(self, container_manager, image_builder): """ Initialize the Local Lambda runtime Parameters ---------- container_manager samcli.local.docker.manager.ContainerManager 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. """ self._containers = {} # used to observe any code change in case if warm containers is enabled to support the reloading self._observed_paths = {} self._observer = FileObserver(self._on_code_change) super().__init__(container_manager, image_builder) def create(self, function_config, debug_context=None, event=None): """ Create a new Container for the passed function, then store it in a dictionary using the function name, so it can be retrieved later and used in the other functions. Make sure to use the debug_context only if the function_config.name equals debug_context.debug-function or the warm_containers option is disabled Parameters ---------- function_config FunctionConfig Configuration of the function to create a new Container for it. debug_context DebugContext Debugging context for the function (includes port, args, and path) event str input event passed to Lambda function Returns ------- Container the created container """ # reuse the cached container if it is created container = self._containers.get(function_config.name, None) if container and container.is_created(): LOG.info( "Reuse the created warm container for Lambda function '%s'", function_config.name) return container # debug_context should be used only if the function name is the one defined # in debug-function option if debug_context and debug_context.debug_function != function_config.name: LOG.debug( "Disable the debugging for Lambda Function %s, as the passed debug function is %s", function_config.name, debug_context.debug_function, ) debug_context = None container = super().create(function_config, debug_context, None) self._containers[function_config.name] = container self._add_function_to_observer(function_config) return container def _on_invoke_done(self, container): """ Cleanup the created resources, just before the invoke function ends. In warm containers, the running containers will be closed just before the end of te command execution, so no action is done here Parameters ---------- container: Container The current running container """ def _add_function_to_observer(self, function_config): """ observe the function code path, so the function container be reload if its code got changed. Parameters ---------- function_config: FunctionConfig Configuration of the function to create a new Container for it. """ code_paths = [function_config.code_abs_path] if function_config.layers: code_paths += [layer.codeuri for layer in function_config.layers] for code_path in code_paths: functions = self._observed_paths.get(code_path, []) functions += [function_config] self._observed_paths[code_path] = functions self._observer.watch(code_path) self._observer.start() def _configure_interrupt(self, function_name, timeout, container, is_debugging): """ When a Lambda function is executing, we setup certain interrupt handlers to stop the execution. Usually, we setup a function timeout interrupt to kill the container after timeout expires. If debugging though, we don't enforce a timeout. But we setup a SIGINT interrupt to catch Ctrl+C and terminate the container. Parameters ---------- function_name: str Name of the function we are running timeout: int Timeout in seconds container: samcli.local.docker.container.Container Instance of a container to terminate is_debugging: bool Are we debugging? Returns ------- threading.Timer Timer object, if we setup a timer. None otherwise """ def timer_handler(): # NOTE: This handler runs in a separate thread. So don't try to mutate any non-thread-safe data structures LOG.info("Function '%s' timed out after %d seconds", function_name, timeout) def signal_handler(sig, frame): # NOTE: This handler runs in a separate thread. So don't try to mutate any non-thread-safe data structures LOG.info("Execution of function %s was interrupted", function_name) if is_debugging: LOG.debug("Setting up SIGTERM interrupt handler") signal.signal(signal.SIGTERM, signal_handler) return None # Start a timer, we'll use this to abort the function if it runs beyond the specified timeout LOG.debug("Starting a timer for %s seconds for function '%s'", timeout, function_name) timer = threading.Timer(timeout, timer_handler, ()) timer.start() return timer def clean_running_containers_and_related_resources(self): """ Clean the running containers, the decompressed code dirs, and stop the created observer """ LOG.debug("Terminating all running warm containers") for function_name, container in self._containers.items(): LOG.debug( "Terminate running warm container for Lambda Function '%s'", function_name) self._container_manager.stop(container) self._clean_decompressed_paths() self._observer.stop() def _on_code_change(self, paths): """ Handles the lambda function code change event. it determines if there is a real change in the code by comparing the checksum of the code path before and after the event. Parameters ---------- paths: list of str the paths of the code that got changed """ for path in paths: functions = self._observed_paths.get(path, []).copy() for function_config in functions: function_name = function_config.name LOG.info( "Lambda Function '%s' code has been changed, terminate its warm container. " "The new container will be created in lazy mode", function_name, ) container = self._containers.get(function_name, None) if container: self._container_manager.stop(container) self._containers.pop(function_name, None) self._remove_observed_lambda_function(function_config) def _remove_observed_lambda_function(self, function_config): """ remove the lambda function's code paths from the observed paths Parameters ---------- function_config: FunctionConfig Configuration of the function to invoke """ code_paths = [function_config.code_abs_path] if function_config.layers: code_paths += [layer.codeuri for layer in function_config.layers] for path in code_paths: functions = self._observed_paths.get(path, []) if function_config in functions: functions.remove(function_config) if not functions: self._observed_paths.pop(path, None) self._observer.unwatch(path)