Esempio n. 1
0
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()
Esempio n. 2
0
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)