def _emit(self, level, msg, extended_info, *args):

        self._num += 1
        s = msg if len(args) == 0 else msg.format(*args)
        t = time.time()
        dt = datetime.fromtimestamp(t)
        s = LOG_FORMAT.format(dt.year, dt.month, dt.day, dt.hour, dt.minute,
                              dt.second,
                              str(dt.microsecond)[0:3], level, s)

        log_msg = s
        if extended_info not in [None, {}]:
            log_msg = "{}\n{}".format(s, json.dumps(extended_info, indent=3))

        if self._trigger_table is None:
            print(log_msg)
            return log_msg

        if self._cached_size + (len(log_msg) +
                                LOG_ENTRY_ADDITIONAL) > LOG_MAX_BATCH_SIZE:
            self.flush()

        self._cached_size += len(s) + LOG_ENTRY_ADDITIONAL

        if handlers.running_local(self._context) and str(
                os.getenv(ENV_SUPPRESS_LOG_STDOUT, False)).lower() != "true":
            print("> " + log_msg)
        self._buffer.append((long(t * 1000), log_msg, self._num))

        if len(self._buffer) >= self._buffer_size:
            self.flush()

        return s
Пример #2
0
    def _update(self, action_id, data):
        """
        Updates an item for the specified action id with the ata passed in as a dictionary
        :param action_id: Id of item to update
        :param data: dictionary containing fields to update
        :return:
        """
        resp = None
        old_item = None
        attributes = {}

        if handlers.running_local(self._context):
            resp = self._action_table.get_item_with_retries(
                Key={handlers.TASK_TR_ID: action_id}, ConsistentRead=True)
            old_item = resp.get("Item")

        for i in data:
            if data[i] is not None or "":
                attributes[i] = {"Action": "PUT", "Value": data[i]}
            else:
                attributes[i] = {"Action": "DELETE"}

        retries = 10
        while True:
            try:
                resp = self._action_table.update_item_with_retries(
                    Key={handlers.TASK_TR_ID: action_id},
                    AttributeUpdates=attributes,
                    Expected={
                        handlers.TASK_TR_ACTION: {
                            "ComparisonOperator": "NOT_NULL"
                        }
                    },
                    _expected_boto3_exceptions_=[
                        "ConditionalCheckFailedException"
                    ])
                break
            except Exception as ex:
                if "ConditionalCheckFailedException" in str(
                        ex) and retries > 0:
                    retries -= 1
                    sleep(1)
                    continue
                else:
                    raise Exception(
                        ER_STATUS_UPDATE.format(safe_json(data, indent=3),
                                                safe_json(resp, indent=3),
                                                str(ex)))

        if self._run_local:
            resp = self._action_table.get_item_with_retries(
                Key={handlers.TASK_TR_ID: action_id}, ConsistentRead=True)
            TaskTrackingTable._run_local_stream_event(
                os.getenv(handlers.ENV_ACTION_TRACKING_TABLE),
                "UPDATE",
                new_item=resp.get("Item"),
                old_item=old_item,
                context=self._context)
def start_completion_handler(context=None):
    with _get_logger(context=context) as logger:
        logger.info("execute_completion_handler")

        if not handlers.running_local(context):
            raise Exception("This action is only allowed if handler is running in local mode")

        result = CompletionHandler(event={}, context=context).handle_request()
        logger.info("Execute completion handler result is \n{}", safe_json(result, indent=3))
        return result
Пример #4
0
    def _handle_check_completion(self, task_item):
        self._logger.debug("Handling test for completion logic")
        if task_item.get(handlers.TASK_TR_RUN_LOCAL,
                         False) and not handlers.running_local(self._context):
            self._logger.debug("Item running in local mode skipped")
            return

        self.started_completion_checks += 1
        self._start_task_execution(
            task_item=task_item,
            action=handlers.HANDLER_ACTION_TEST_COMPLETION)
Пример #5
0
    def get_tasks_to_check_for_completion(self):

        waiting_for_completion_tasks = []

        args = {
            "IndexName": "WaitForCompletionTasks"
            # "FilterExpression": "#status = :waiting",
            # "ExpressionAttributeNames": {"#status": handlers.TASK_TR_STATUS},
            # "ExpressionAttributeValues": {":waiting": handlers.STATUS_WAIT_FOR_COMPLETION}
        }

        while True:
            resp = self._action_table.scan_with_retries(**args)

            not_longer_waiting = []
            for item in resp.get("Items", []):

                # only handle completion for tasks that are created in the same environment (local or lambda)
                running_local = handlers.running_local(self._context)
                local_running_task = item.get(handlers.TASK_TR_RUN_LOCAL,
                                              False)
                if running_local != local_running_task:
                    continue

                if item.get(
                        handlers.TASK_TR_STATUS) in NOT_LONGER_ACTIVE_STATUSES:
                    not_longer_waiting.append(item)
                    continue

                if item.get(handlers.TASK_TR_STATUS
                            ) == handlers.STATUS_WAIT_FOR_COMPLETION:
                    waiting_for_completion_tasks.append(item)

            if "LastEvaluatedKey" in resp:
                args["ExclusiveStartKey"] = resp["LastEvaluatedKey"]
            else:
                break

        # cleanup items
        for i in not_longer_waiting:
            self.update_task(
                i[handlers.TASK_TR_ID],
                status_data={handlers.TASK_TR_LAST_WAIT_COMPLETION: None})

        tasks_to_schedule_completion_for = []
        for i in waiting_for_completion_tasks:
            if not self._item_in_consistent_expected_state(i):
                self._logger.info(INF_SKIP_POSSIBLE_INCONSISTENT_ITEM,
                                  i[handlers.TASK_TR_ID],
                                  i[handlers.TASK_TR_ACTION])
                continue
            tasks_to_schedule_completion_for.append(i)

        return tasks_to_schedule_completion_for
    def handle_request(self):
        """
        Handles the cloudwatch rule timer event
        :return: Started tasks, if any, information
        """

        try:

            start = datetime.now()

            count = 0
            tracking_table = TaskTrackingTable(context=self._context,
                                               logger=self._logger)

            for task in tracking_table.get_tasks_to_check_for_completion():
                count += 1

                task_id = task[handlers.TASK_TR_ID]
                last_check_for_completion_time = datetime.now().isoformat()
                tracking_table.update_task(
                    task_id,
                    task=task.get(handlers.TASK_TR_NAME, None),
                    task_metrics=task.get(handlers.TASK_TR_METRICS, False),
                    status_data={
                        handlers.TASK_TR_LAST_WAIT_COMPLETION:
                        last_check_for_completion_time
                    })

                self._logger.debug("Task is {}", task)
                self._logger.info(INF_SET_COMPLETION_TASK_TIMER,
                                  task.get(handlers.TASK_TR_NAME, None),
                                  task_id, last_check_for_completion_time)

            running_time = float((datetime.now() - start).total_seconds())
            self._logger.info(INF_COMPLETION_ITEMS_SET, running_time, count)

            if count == 0 and not handlers.running_local(self._context):
                rule = handlers.disable_completion_cloudwatch_rule(
                    self._context)
                self._logger.info(INF_DISABLED_COMPLETION_TIMER, rule)

            return safe_dict({
                "datetime": datetime.now().isoformat(),
                "running-time": running_time,
                "tasks-to-check": count
            })

        except ValueError as ex:
            self._logger.error(ERR_COMPLETION_HANDLER, ex,
                               safe_json(self._event, indent=2))

        finally:
            self._logger.flush()
Пример #7
0
 def __init__(self, context=None, logger=None):
     """
     Initializes the instance
     """
     self._table = None
     self._client = None
     self._new_action_items = []
     self._context = context
     self._logger = logger
     self._s3_client = None
     self._account = None
     self._run_local = handlers.running_local(self._context)
     self._kms_client = None
 def __init__(self, context=None, logger=None):
     """
     Initializes the instance
     """
     self._table = None
     self._client = None
     self._new_action_items = []
     self._context = context
     self._logger = logger
     self._s3_client = None
     self._account = None
     self._run_local = handlers.running_local(self._context)
     self._resource_encryption_key = os.getenv(handlers.ENV_RESOURCE_ENCRYPTION_KEY, "")
     self._kms_client = None
Пример #9
0
    def is_handling_request(cls, event, context):

        # In simulation the handler is called directly when inserting or updating items in the table
        if handlers.running_local(context):
            return False

        if event.get("Records", [{}])[0].get("eventSource",
                                             "") != "aws:dynamodb":
            return False

        source_arn = event["Records"][0]["eventSourceARN"]
        table_name = source_arn.split("/")[1]
        return table_name in [
            os.getenv(handlers.ENV_ACTION_TRACKING_TABLE),
            os.getenv(handlers.ENV_CONCURRENCY_TABLE)
        ]
Пример #10
0
    def _leave_waiting_list(self, task_id, concurrency_key):
        """
        Subtracts 1 from waiting list counter for the specified concurrency key and returns new value. If the counter reaches 0
        then the entry for the concurrency key is removed
        :param concurrency_key: Concurrency key for counter
        :return: Updated counter
        """

        # make a consistent read of the task
        self.tracking_table.get_task_item(task_id)

        if not handlers.running_local(self._context):

            resp = self.concurrency_table.update_item_with_retries(
                Key={CONCURRENCY_ID: concurrency_key},
                UpdateExpression="ADD InstanceCount :min_one SET RunNext=:run",
                ExpressionAttributeValues={
                    ":min_one": -1,
                    ":run": True
                },
                ReturnValues="UPDATED_NEW")
            count = max(0, int(resp["Attributes"].get(ACTIVE_INSTANCES, 0)))

            # remove entry if no more waiting items for this key
            if count == 0:
                self.concurrency_table.delete_item_with_retries(
                    Key={CONCURRENCY_ID: concurrency_key})
        else:
            resp = self.concurrency_table.get_item_with_retries(
                Key={CONCURRENCY_ID: concurrency_key})
            count = resp.get("Item", {}).get(ACTIVE_INSTANCES, 0)
            TaskTrackingTable._run_local_stream_event(
                os.getenv(handlers.ENV_CONCURRENCY_TABLE), "UPDATE", {
                    "ConcurrencyId": concurrency_key,
                    "InstanceCount": count
                }, {
                    "ConcurrencyId": concurrency_key,
                    "InstanceCount": count + 1
                }, self._context)

        return count
Пример #11
0
    def _enter_waiting_list(self, concurrency_key):
        """
        Adds 1 to waiting list counter for the specified concurrency key and returns new value
        :param concurrency_key: Concurrency key for counter
        :return: Updated counter
        """

        # update/read counter for the concurrency key
        if not handlers.running_local(self._context):
            resp = self.concurrency_table.update_item_with_retries(
                Key={CONCURRENCY_ID: concurrency_key},
                UpdateExpression="ADD InstanceCount :one SET RunNext=:run",
                ExpressionAttributeValues={
                    ":one": 1,
                    ":run": False
                },
                ReturnValues="UPDATED_NEW")
            return int(resp["Attributes"].get("InstanceCount", 0))
        else:
            resp = self.concurrency_table.get_item_with_retries(
                Key={CONCURRENCY_ID: concurrency_key})
            return resp.get("Item", {}).get(ACTIVE_INSTANCES, 0)
Пример #12
0
    def _start_task_execution(self,
                              task_item,
                              action=handlers.HANDLER_ACTION_EXECUTE):
        """
        Creates an instance of the lambda function that executes the tasks action. It first checks is the action has specific memory
        requirements and based on this it creates a copy of this instance or one configured for the required memory. All
        information for executing the action is passed in the event.
        :param task_item: Task item for which action is executed
        :return:
        """

        try:

            self._logger.debug(
                "Entering start_task_execution ({}) with task {}", action,
                safe_json(task_item, indent=3))

            # Create event for execution of the action and set its action so that is picked up by the execution handler
            event = {i: task_item.get(i) for i in task_item}
            event[handlers.HANDLER_EVENT_ACTION] = action

            self._logger.debug(DEBUG_ACTION,
                               task_item[handlers.TASK_TR_ACTION],
                               task_item[handlers.TASK_TR_NAME],
                               task_item[handlers.TASK_TR_ID])

            self._logger.debug(
                DEBUG_ACTION_PARAMETERS,
                safe_json(task_item.get(handlers.TASK_TR_PARAMETERS, {}),
                          indent=3))

            # get memory allocation for executing the task
            lambda_size = handlers.TASK_TR_COMPLETION_SIZE \
                if action == handlers.HANDLER_ACTION_TEST_COMPLETION \
                else handlers.TASK_TR_EXECUTE_SIZE

            execute_lambda_size = task_item.get(lambda_size,
                                                actions.ACTION_SIZE_STANDARD)

            if execute_lambda_size == actions.ACTION_USE_ECS:
                ecs_memory = task_item.get(
                    handlers.TASK_EXECUTE_ECS_MEMORY
                    if action == handlers.HANDLER_ACTION_EXECUTE else
                    handlers.TASK_COMPLETION_ECS_MEMORY, None)
            else:
                ecs_memory = None

            if not handlers.running_local(self._context):

                self._logger.debug(DEBUG_MEMORY_SIZE, execute_lambda_size)

                if execute_lambda_size != actions.ACTION_USE_ECS:

                    # create event payload
                    payload = str.encode(safe_json(event))

                    # determine which lambda to execute on
                    function_name = "{}-{}-{}".format(
                        os.getenv(handlers.ENV_STACK_NAME),
                        os.getenv(handlers.ENV_LAMBDA_NAME),
                        execute_lambda_size)

                    self._logger.debug(
                        "Running execution of task on lambda function {}",
                        function_name)

                    self._logger.debug(DEBUG_LAMBDA_FUNCTION_, function_name,
                                       payload)
                    # start lambda function
                    lambda_client = boto_retry.get_client_with_retries(
                        "lambda", ["invoke"],
                        context=self._context,
                        logger=self._logger)
                    resp = lambda_client.invoke_with_retries(
                        FunctionName=function_name,
                        InvocationType="Event",
                        LogType="None",
                        Payload=payload)

                    task_info = {
                        "id": task_item[handlers.TASK_TR_ID],
                        "task": task_item[handlers.TASK_TR_NAME],
                        "action": task_item[handlers.TASK_TR_ACTION],
                        "payload": payload,
                        "status-code": resp["StatusCode"]
                    }

                    self._logger.debug(DEBUG_LAMBDA,
                                       safe_json(task_info, indent=2))
                    self.invoked_lambda_functions.append(task_info)
                else:
                    # run as ECS job
                    ecs_args = {
                        "subnets": os.getenv('AWSVPC_SUBNETS'),
                        "securitygroups": os.getenv('AWSVPC_SECURITYGROUPS'),
                        "assignpublicip": os.getenv('AWSVPC_ASSIGNPUBLICIP'),
                        handlers.HANDLER_EVENT_ACTION: action,
                        handlers.TASK_NAME: task_item[handlers.TASK_TR_NAME],
                        handlers.TASK_TR_ID: task_item[handlers.TASK_TR_ID]
                    }

                    self._logger.debug(DEBUG_RUNNING_ECS_TASK, action,
                                       task_item[handlers.TASK_TR_NAME])
                    handlers.run_as_ecs_job(ecs_args,
                                            ecs_memory_size=ecs_memory,
                                            context=self._context,
                                            logger=self._logger)

            else:
                lambda_handler(event, self._context)

            ResultNotifications(context=self._context,
                                logger=self._logger).publish_started(task_item)

        except Exception as ex:
            self._logger.error(ERR_RUNNING_TASK, task_item, str(ex),
                               full_stack())
Пример #13
0
    def __init__(self, event, context, logger=None, tracking_store=None):
        def log_stream_name():

            classname = self.__class__.__name__
            dt = datetime.utcnow()

            account = self._event.get(handlers.HANDLER_SELECT_ARGUMENTS,
                                      {}).get(handlers.HANDLER_EVENT_ACCOUNT,
                                              "")
            regions = self._event.get(handlers.HANDLER_SELECT_ARGUMENTS,
                                      {}).get(handlers.HANDLER_EVENT_REGIONS,
                                              [])

            if account is not None and len(regions) > 0:
                account_and_region = "-".join([account, regions[0]]) + "-"

            else:
                region = ""

                if self.sub_task is not None:
                    account = ""
                    if self._this_account:
                        if len(self._accounts) == 0:
                            account = os.getenv(
                                handlers.ENV_OPS_AUTOMATOR_ACCOUNT)
                    elif len(self._accounts) == 1:
                        account = self._accounts[0]

                    region = self._regions[0] if len(
                        self._regions) == 1 else ""

                if account != "":
                    if region not in ["", None]:
                        account_and_region = "-".join([account, region]) + "-"
                    else:
                        account_and_region = account
                else:
                    account_and_region = ""

            return LOG_STREAM.format(classname, self.task[handlers.TASK_NAME],
                                     account_and_region, dt.year, dt.month,
                                     dt.day)

        self._context = context
        self._event = event
        self.task = event[handlers.HANDLER_EVENT_TASK]
        self.sub_task = event.get(handlers.HANDLER_EVENT_SUB_TASK, None)
        self.use_custom_select = event.get(
            handlers.HANDLER_EVENT_CUSTOM_SELECT, True)

        # the job id is used to correlate all generated tasks for the selected resources
        self.task_group = self._event.get(handlers.HANDLER_EVENT_TASK_GROUP,
                                          None)
        if self.task_group is None:
            self.task_group = str(uuid.uuid4())

        debug = event[handlers.HANDLER_EVENT_TASK].get(handlers.TASK_DEBUG,
                                                       False)
        if logger is None:
            self._logger = QueuedLogger(logstream=log_stream_name(),
                                        context=context,
                                        buffersize=50 if debug else 20,
                                        debug=debug)
        else:
            self._logger = logger

        self._sts = None

        self.select_args = event.get(handlers.HANDLER_SELECT_ARGUMENTS, {})
        self.task_dt = event[handlers.HANDLER_EVENT_TASK_DT]

        self.action_properties = actions.get_action_properties(
            self.task[handlers.TASK_ACTION])
        self.action_class = actions.get_action_class(
            self.task[handlers.TASK_ACTION])
        self.task_parameters = self.task.get(handlers.TASK_PARAMETERS, {})
        self.metrics = self.task.get(handlers.TASK_METRICS, False)

        self.service = self.action_properties[actions.ACTION_SERVICE]
        self.keep_tags = self.action_properties.get(
            actions.ACTION_KEEP_RESOURCE_TAGS, True)

        self.source = self._event.get(handlers.HANDLER_EVENT_SOURCE,
                                      handlers.UNKNOWN_SOURCE)
        self.run_local = handlers.running_local(self._context)
        self._timer = None
        self._timeout_event = self._timeout_event = threading.Event()

        self.aggregation_level = self.action_properties.get(
            actions.ACTION_AGGREGATION, actions.ACTION_AGGREGATION_RESOURCE)
        if self.aggregation_level is not None and isinstance(
                self.aggregation_level, types.FunctionType):
            self.aggregation_level = self.aggregation_level(
                self.task_parameters)

        self.batch_size = self.action_properties.get(actions.ACTION_BATCH_SIZE)
        if self.batch_size is not None and isinstance(self.batch_size,
                                                      types.FunctionType):
            self.batch_size = self.batch_size(self.task_parameters)

        self.actions_tracking = TaskTrackingTable(
            self._context,
            logger=self._logger) if tracking_store is None else tracking_store
Пример #14
0
    def handle_request(self, use_custom_select=True):
        """
        Handled the cloudwatch rule timer event
        :return: Started tasks, if any, information
        """
        try:

            self._logger.info("Handling CloudWatch event {}",
                              safe_json(self._event, indent=3))

            result = []
            start = datetime.now()

            dt = self._event_time()
            config_task = None

            source_resource_tags = None

            try:

                # for all events tasks in configuration
                for config_task in TaskConfiguration(
                        context=self._context,
                        logger=self._logger).get_tasks():

                    self._logger.debug_enabled = config_task.get(
                        handlers.TASK_DEBUG, False)

                    if not self._event_triggers_task(task=config_task):
                        continue

                    # tasks that can react to events with a wider resource scope than the actual resource causing the event may
                    # have a filter that can is used to filter based on the tags of the resource
                    event_source_tag_filter = config_task.get(
                        handlers.TASK_EVENT_SOURCE_TAG_FILTER, None)
                    if event_source_tag_filter is not None:
                        if source_resource_tags is None:
                            # get the tags for the source resource of the event
                            session = services.get_session(
                                self._role_executing_triggered_task,
                                logger=self._logger)
                            if session is None:
                                self._logger.error(
                                    ERR_NO_SESSION_FOR_GETTING_TAGS)
                                continue
                            try:
                                source_resource_tags = self._source_resource_tags(
                                    session, config_task)
                            except Exception as ex:
                                self._logger.error(
                                    ERR_GETTING_EVENT_SOURCE_RESOURCE_TAGS, ex)
                                continue

                            self._logger.debug(
                                "Tags for event source resource are  {}",
                                source_resource_tags)

                        # apply filter to source resource tags
                        if not TagFilterExpression(
                                event_source_tag_filter).is_match(
                                    source_resource_tags):
                            self._logger.debug(
                                "Tags of source resource do not match tag filter {}",
                                event_source_tag_filter)
                            continue

                    task_name = config_task[handlers.TASK_NAME]
                    result.append(task_name)

                    select_parameters = self._select_parameters(
                        self._event_name(), config_task)
                    if select_parameters is None:
                        continue

                    self._logger.debug(DEBUG_EVENT, task_name,
                                       self._event_name(), select_parameters,
                                       self._event_account(),
                                       self._event_region(),
                                       safe_json(config_task, indent=3))

                    # create an event for lambda function that scans for resources for this task
                    lambda_event = {
                        handlers.HANDLER_EVENT_ACTION:
                        handlers.HANDLER_ACTION_SELECT_RESOURCES,
                        handlers.HANDLER_EVENT_CUSTOM_SELECT:
                        use_custom_select,
                        handlers.HANDLER_SELECT_ARGUMENTS: {
                            handlers.HANDLER_EVENT_REGIONS:
                            [self._event_region()],
                            handlers.HANDLER_EVENT_ACCOUNT:
                            self._event_account(),
                            handlers.HANDLER_EVENT_RESOURCE_NAME:
                            config_task[handlers.TASK_RESOURCE_TYPE],
                        },
                        handlers.HANDLER_EVENT_SOURCE:
                        "{}:{}:{}".format(self._handled_event_source,
                                          self._handled_detail_type,
                                          self._event_name()),
                        handlers.HANDLER_EVENT_TASK:
                        config_task,
                        handlers.HANDLER_EVENT_TASK_DT:
                        dt
                    }

                    for i in select_parameters:
                        lambda_event[handlers.HANDLER_SELECT_ARGUMENTS][
                            i] = select_parameters[i]

                    if self._event_resources() is not None:
                        self._logger.debug(
                            DEBUG_EVENT_RESOURCES,
                            safe_json(self._event_resources(), indent=3))
                        lambda_event[
                            handlers.
                            HANDLER_SELECT_RESOURCES] = self._event_resources(
                            )

                    if not handlers.running_local(self._context):
                        # start lambda function to scan for task resources
                        payload = str.encode(safe_json(lambda_event))
                        client = get_client_with_retries("lambda", ["invoke"],
                                                         context=self._context,
                                                         logger=self._logger)
                        client.invoke_with_retries(
                            FunctionName=self._context.function_name,
                            InvocationType="Event",
                            LogType="None",
                            Payload=payload)
                    else:
                        # or if not running in lambda environment pass event to main task handler
                        lambda_handler(lambda_event, None)

                return safe_dict({
                    "datetime":
                    datetime.now().isoformat(),
                    "running-time": (datetime.now() - start).total_seconds(),
                    "event-datetime":
                    dt,
                    "started-tasks":
                    result
                })

            except ValueError as ex:
                self._logger.error(ERR_HANDLING_EVENT_IN_BASE_HANDLER, ex,
                                   safe_json(config_task, indent=2))

        finally:
            self._logger.flush()
    def _handle_task_execution(self):
        def execute_timed_out():
            """
            Function is called when the handling of the request times out
            :return:
            """
            time_used = int(
                int(os.getenv(handlers.ENV_LAMBDA_TIMEOUT)) -
                self._context.get_remaining_time_in_millis() / 1000)
            self._logger.error(ERR_EXECUTION_NOT_COMPLETED, time_used)

            if self.action_properties.get(actions.ACTION_EXECUTE_SIZE,
                                          None) is not None:
                self._logger.error(ERR_TIMEOUT, self.task)

            self._timeout_event.set()
            self._logger.flush()
            self._timer.cancel()

        def handle_metrics(result):
            self._logger.info(
                INF_SENDING_METRICS_DATA,
                "enabled" if allow_send_metrics() else "disabled")
            if allow_send_metrics():
                try:
                    result_data = result if isinstance(
                        result, dict) else json.loads(result)
                    if actions.METRICS_DATA in result_data:
                        send_metrics_data(
                            metrics_data=result_data[actions.METRICS_DATA],
                            logger=self._logger)
                except Exception as ex:
                    self._logger.warning(WARN_METRICS_DATA, str(ex))

        self._logger.info(INF_ACTION, self.action, self.action_id, self.task,
                          safe_json(self.action_parameters, indent=3))
        if not handlers.running_local(self._context):
            self._logger.info(INF_LAMBDA_MEMORY, self._context.function_name,
                              self._context.memory_limit_in_mb)

        self._logger.debug("Setting task state to {}", handlers.STATUS_STARTED)
        self._action_tracking.update_task(self.action_id,
                                          self.task,
                                          task_metrics=self.metrics,
                                          status=handlers.STATUS_STARTED)

        start = time.time()

        return_data = {
            "task": self.task,
            "action": self.action,
            "id": self.action_id,
            "dryrun": self.dryrun,
        }

        if self._context is not None:
            execution_time_left = (
                self._context.get_remaining_time_in_millis() /
                1000.00) - EXECUTE_TIME_REMAINING
            self._timer = threading.Timer(execution_time_left,
                                          execute_timed_out)
            self._timer.start()

        try:
            self._logger.debug("Start executing task")
            action_result = self._action_instance.execute()
            if isinstance(action_result, str):
                action_result = json.loads(action_result)
        finally:
            if self._timer is not None:
                self._timer.cancel()
                if self._timeout_event.is_set():
                    raise Exception("Timeout execution action")

        if not self._action_instance.properties.get(actions.ACTION_INTERNAL,
                                                    False):
            handle_metrics(action_result)

        execution_time = int(time.time() - start)
        self._logger.debug("Task needs{}completion",
                           " no" if not self.has_completion else " ")
        if not self.has_completion or self.dryrun:

            self._logger.debug("Setting state of task to {} ",
                               handlers.STATUS_COMPLETED)
            self._action_tracking.update_task(
                action_id=self.action_id,
                task=self.task,
                task_metrics=self.metrics,
                status=handlers.STATUS_COMPLETED,
                status_data={
                    handlers.TASK_TR_STARTED_TS: int(start),
                    handlers.TASK_TR_RESULT: action_result,
                    handlers.TASK_TR_EXECUTION_TIME: str(execution_time),
                    handlers.TASK_TR_EXECUTION_LOGSTREAM:
                    self.execution_log_stream
                })
            # noinspection PyBroadException
            try:
                self._logger.info(INF_ACTION_RESULT, execution_time,
                                  safe_json(action_result, indent=3))
            except Exception:
                self._logger.info(INF_ACTION_RESULT, execution_time,
                                  str(action_result))

        else:
            # the action has a method for testing completion of the task, set the status to waiting and store the result
            # of the execution that started the action as start result that will be passed to the completion method together
            self._logger.debug("Setting state of task to {} ",
                               handlers.STATUS_WAIT_FOR_COMPLETION)
            self._action_tracking.update_task(
                action_id=self.action_id,
                task=self.task,
                task_metrics=self.metrics,
                status=handlers.STATUS_WAIT_FOR_COMPLETION,
                status_data={
                    handlers.TASK_TR_LAST_WAIT_COMPLETION:
                    datetime.now().isoformat(),
                    handlers.TASK_TR_STARTED_TS:
                    int(start),
                    handlers.TASK_TR_START_RESULT:
                    action_result,
                    handlers.TASK_TR_START_EXECUTION_TIME:
                    str(execution_time),
                    handlers.TASK_TR_EXECUTION_LOGSTREAM:
                    self.execution_log_stream
                })

            self._logger.info(INF_STARTED_AND_WAITING_FOR_COMPLETION,
                              safe_json(action_result, indent=3))

            if not handlers.running_local(self._context):
                rule = handlers.enable_completion_cloudwatch_rule(
                    self._context)
                self._logger.info(INF_RULE_ENABLED, rule)
            else:
                self._logger.info(INF_SIMULATION_MODE_NO_RULE_ENABLED)

        # no exception from action
        return_data.update({
            "result":
            handlers.STATUS_WAIT_FOR_COMPLETION
            if self.has_completion else handlers.STATUS_COMPLETED,
            "action-result":
            str(action_result),
            "datetime":
            datetime.now().isoformat(),
            "running-time":
            str(execution_time),
            "task-group":
            self._event[handlers.TASK_TR_GROUP],
            "task-id":
            self._event[handlers.TASK_TR_ID]
        })

        return safe_dict(return_data)