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
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
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)
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()
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
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) ]
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
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)
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())
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
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)