def call_me_back_send(delay=None):
    delay = delay if delay is not None else max(next_call_delay() + 1, 2)
    client = ctx["sqs.client"]

    log.log(
        log.NOTICE,
        "Sending SQS 'CallMeBack' message (delay=%d) to Main lambda queue: %s"
        % (delay, ctx["MainSQSQueue"]))
    if (misc.is_sam_local()):
        log.debug("Should have sent a call_me_back()")
        return

    caller_function = inspect.currentframe().f_back
    while caller_function.f_code.co_name in ["record_subsegment", "__call__"
                                             ]:  # Skip X-Ray wrappers
        caller_function = caller_function.f_back

    response = client.send_message(QueueUrl=ctx["MainSQSQueue"],
                                   DelaySeconds=int(delay),
                                   MessageBody=json.dumps({
                                       "SQSHeartBeat": {
                                           "Reason":
                                           "NEED_UPDATE",
                                           "CallerFunction":
                                           caller_function.f_code.co_name,
                                           "LambdaFunction":
                                           ctx["FunctionName"]
                                       }
                                   }))
def is_called_too_early():
    global ctx
    delay = Cfg.get_duration_secs("app.run_period")
    delta = sqs.seconds_since_last_call()
    if delta != -1 and delta < delay:
        if misc.is_sam_local():
            log.warning("is_called_too_early disabled because running in SAM!")
            return False
        log.log(log.NOTICE, "Called too early (now=%s, delay=%s => delta_seconds=%s)..." %
                (ctx["now"], delay, delta)) 
        return True
    return False
def init(context, with_kvtable=True, with_predefined_configuration=True):
    global _init
    _init = {}
    _init["context"] = context
    _init["all_configs"] = [{
        "source": "Built-in defaults",
        "config": {},
        "metas" : {}
        }]
    if with_kvtable:
        _init["configuration_table"] = kvtable.KVTable(context, context["ConfigurationTable"])
        _init["configuration_table"].reread_table()
    _init["with_kvtable"] = with_kvtable
    _init["active_parameter_set"] = None
    register({
             "config.dump_configuration,Stable" : {
                 "DefaultValue": "0",
                 "Format"      : "Bool",
                 "Description" : """Display all relevant configuration parameters in CloudWatch logs.

    Used for debugging purpose.
                 """
             },
             "config.loaded_files,Stable" : {
                 "DefaultValue" : "internal:predefined.config.yaml;internal:custom.config.yaml",
                 "Format"       : "StringList",
                 "Description"  : """A semi-column separated list of URL to load as configuration.

Upon startup, CloneSquad will load the listed files in sequence and stack them allowing override between layers.

The default contains a reference to the empty internal file 'custom.config.yaml'. Users that intend to embed
customization directly inside the Lambda delivery should override this file with their own configuration. See 
[Customizing the Lambda package](#customizing-the-lambda-package).

This key is evaluated again after each URL parsing meaning that a layer can redefine the 'config.loaded_files' to load further
YAML files.
                 """
             },
             "config.max_file_hierarchy_depth" : 10,
             "config.active_parameter_set,Stable": {
                 "DefaultValue": "",
                 "Format"      : "String",
                 "Description" : """Defines the parameter set to activate.

See [Parameter sets](#parameter-sets) documentation.
                 """
             },
             "config.default_ttl": 0
    })

    # Load extra configuration from specified URLs
    xray_recorder.begin_subsegment("config.init:load_files")
    files_to_load = get_list("config.loaded_files", default=[]) if with_predefined_configuration else []
    if "ConfigurationURL" in context:
        files_to_load.extend(context["ConfigurationURL"].split(";"))
    if misc.is_sam_local():
        # For debugging purpose. Ability to override config when in SAM local
        resource_file = "internal:sam.local.config.yaml" 
        log.info("Reading local resource file %s..." % resource_file)
        files_to_load.append(resource_file)


    loaded_files = []
    i = 0
    while i < len(files_to_load):
        f = files_to_load[i]
        if f == "": 
            i += 1
            continue

        try:
            c = yaml.safe_load(misc.get_url(f))
            if c is None: c = {}
            loaded_files.append({
                    "source": f,
                    "config": c
                })
            if "config.loaded_files" in c:
                files_to_load.extend(c["config.loaded_files"].split(";"))
            if i > get_int("config.max_file_hierarchy_depth"):
                log.warning("Too much config file loads (%s)!! Stopping here!" % loaded_files) 
        except Exception as e:
            log.warning("Failed to load and/or parse config file '%s'! (Notice: It will be safely ignored!)" % f)
        i += 1
    _init["loaded_files"] = loaded_files
    xray_recorder.end_subsegment()

    builtin_config = _init["all_configs"][0]["config"]
    for cfg in _get_config_layers(reverse=True):
        c = cfg["config"]
        if "config.active_parameter_set" in c:
            if c == builtin_config and isinstance(c, dict):
                _init["active_parameter_set"] = c["config.active_parameter_set"]["DefaultValue"]
            else:
                _init["active_parameter_set"] = c["config.active_parameter_set"]
            break
    _parameterset_sanity_check()

    register({
        "config.ignored_warning_keys,Stable" : {
            "DefaultValue": "",
            "Format"      : "StringList",
            "Description" : """A list of config keys that are generating a warning on usage, to disable them.

Typical usage is to avoid the 'WARNING' Cloudwatch Alarm to trigger when using a non-Stable configuration key.

    Ex: ec2.schedule.key1;ec2.schedule.key2

    Remember that using non-stable configuration keys, is creating risk as semantic and/or existence could change 
    from CloneSquad version to version!
            """
            }
        })
    _init["ignored_warning_keys"] = get_list_of_dict("config.ignored_warning_keys")
def main_handler_entrypoint(event, context):
    """

    Parameters
    ----------
    event: dict, required

    context: object, required
        Lambda Context runtime methods and attributes

        Context doc: https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html

    Returns
    ------

    """

    #print(Dbg.pprint(event))

    ctx["now"] = misc.utc_now()
    ctx["FunctionName"] = "Main"

    init()

    if Cfg.get_int("app.disable") != 0 and not misc.is_sam_local():
        log.warning("Application disabled due to 'app.disable' key")
        return

    no_is_called_too_early = False
    # Manage Spot interruption as fast as we can
    if sqs.process_sqs_records(event, function=ec2_schedule.manage_spot_notification, function_arg=ctx):
        log.info("Managed Spot Interruption SQS record!")
        # Force to run now disregarding `app.run_period` as we have at least one Spot instance to 
        #   remove from target groups immediatly
        no_is_called_too_early = True
    
    # Check that we are not called too early
    #   Note: We peform a direct read to the KVTable to spare initialization time when the
    #   Lambda is called too early
    ctx["main.last_call_date"] = ctx["o_ec2"].get_state("main.last_call_date", direct=True)
    if ctx["main.last_call_date"] is None or ctx["main.last_call_date"] == "": 
        ctx["main.last_call_date"] = str(misc.epoch())

    if not no_is_called_too_early and is_called_too_early():
        log.log(log.NOTICE, "Called too early by: %s" % event)
        notify.do_not_notify = True
        sqs.process_sqs_records(event)
        sqs.call_me_back_send()
        return

    log.debug("Load prerequisites.")
    load_prerequisites(["o_state", "o_notify", "o_ec2", "o_cloudwatch", "o_targetgroup", "o_ec2_schedule", "o_scheduler", "o_rds"])

    # Remember 'now' as the last execution date
    ctx["o_ec2"].set_state("main.last_call_date", value=ctx["now"], TTL=Cfg.get_duration_secs("app.default_ttl"))

    Cfg.dump()

    # Perform actions:
    log.debug("Main processing.")
    ctx["o_targetgroup"].manage_targetgroup()
    ctx["o_ec2_schedule"].schedule_instances()
    ctx["o_ec2_schedule"].stop_drained_instances()
    ctx["o_cloudwatch"].configure_alarms()
    ctx["o_rds"].manage_subfleet_rds()
    ctx["o_ec2_schedule"].prepare_metrics()

    ctx["o_cloudwatch"].send_metrics()
    ctx["o_cloudwatch"].configure_dashboard()

    # If we got woke up by SNS, acknowledge the message(s) now
    sqs.process_sqs_records(event)

    ctx["o_notify"].notify_user_arn_resources()

    # Call me back if needed
    sqs.call_me_back_send()
        ctx["StateTable"]      = "CloneSquad-%s%s-State" % (ctx["GroupName"], ctx["VariantNumber"])
        ctx["EventTable"]         = "CloneSquad-%s%s-EventLog" % (ctx["GroupName"], ctx["VariantNumber"])
        ctx["LongTermEventTable"] = "CloneSquad-%s%s-EventLog-LongTerm" % (ctx["GroupName"], ctx["VariantNumber"])
        ctx["SchedulerTable"]     = "CloneSquad-%s%s-Scheduler" % (ctx["GroupName"], ctx["VariantNumber"])
        ctx["MainSQSQueue"]       = "https://sqs.%s.amazonaws.com/%s/CloneSquad-Main-%s" % (ctx["AWS_DEFAULT_REGION"], account_id, ctx["GroupName"])
        ctx["InteractSQSUrl"]     = "https://sqs.%s.amazonaws.com/%s/CloneSquad-Interact-%s" % (ctx["AWS_DEFAULT_REGION"], account_id, ctx["GroupName"])
        ctx["CloudWatchEventRoleArn"] = "arn:aws:iam::%s:role/CloneSquad-%s-CWRole" % (account_id, ctx["GroupName"])
        ctx["GenericInsufficientDataActions_SNSTopicArn"] = "arn:aws:sns:%s:%s:CloneSquad-CloudWatchAlarm-InsufficientData-%s" % (ctx["AWS_DEFAULT_REGION"], account_id, ctx["GroupName"])
        ctx["GenericOkActions_SNSTopicArn"] = "arn:aws:sns:%s:%s:CloneSquad-CloudWatchAlarm-Ok-%s"  % (ctx["AWS_DEFAULT_REGION"], account_id, ctx["GroupName"])
        ctx["ScaleUp_SNSTopicArn"] =  "arn:aws:sns:%s:%s:CloneSquad-CloudWatchAlarm-ScaleUp-%s" % (ctx["AWS_DEFAULT_REGION"], account_id, ctx["GroupName"])
        ctx["InteractLambdaArn"]  = "arn:aws:lambda:%s:%s:function:CloneSquad-Interact-%s" % (ctx["AWS_DEFAULT_REGION"], account_id, ctx["GroupName"])
        ctx["AWS_LAMBDA_LOG_GROUP_NAME"] = "/aws/lambda/CloneSquad-Main-%s" % ctx["GroupName"]


# Special treatment while started from SMA invoke loval
if misc.is_sam_local() or __name__ == '__main__':
    fix_sam_bugs()
    print("SAM Local Environment:")
    for env in os.environ:
        print("%s=%s" % (env, os.environ[env]))

def initialize_clients(clients, context):
    global ctx
    log.debug("Initialize clients.")
    ctx["cwd"]     = os.getcwd()
    config = Config(
       retries = {
       'max_attempts': 5,
       'mode': 'standard'
       })
    for c in clients: