async handleSyncedCallback()

in functions/source/faz-handler/lib/core/autoscale-handler.js [528:892]


    async handleSyncedCallback() {
        const instanceId = this._requestInfo.instanceId,
            interval = this._requestInfo.interval;

        let parameters = {},
            primaryIp,
            isPrimary = false,
            lifecycleShouldAbandon = false;

        let primaryChanged = false;

        parameters.instanceId = instanceId;
        parameters.scalingGroupName = this.scalingGroupName;
        // get selfinstance
        this._selfInstance =
            this._selfInstance || (await this.platform.describeInstance(parameters));

        // handle hb monitor
        // get self health check
        this._selfHealthCheck =
            this._selfHealthCheck ||
            (await this.platform.getInstanceHealthCheck(
                {
                    instanceId: this._selfInstance.instanceId
                },
                interval
            ));
        // if self is already out-of-sync, skip the monitoring logics
        if (this._selfHealthCheck && !this._selfHealthCheck.inSync) {
            return {};
        }
        // get primary instance monitoring
        await this.retrievePrimary();

        // get the current primary instance info for comparisons later
        let primaryInstanceId = (this._primaryRecord && this._primaryRecord.instanceId) || null;
        let primaryElectionVote = (this._primaryRecord && this._primaryRecord.voteState) || null;

        if (
            this._primaryInfo &&
            this._selfInstance.instanceId === this._primaryInfo.instanceId &&
            this.scalingGroupName === this.primaryScalingGroupName
        ) {
            // this instance is the current primary, skip checking primary election
            // use primary health check result as self health check result
            isPrimary = true;
            this._selfHealthCheck = this._primaryHealthCheck;
        } else if (this._selfHealthCheck && !this._selfHealthCheck.healthy) {
            // this instance isn't the current primary
            // if this instance is unhealth, skip primary election check
        } else if (
            !(this._primaryInfo && this._primaryHealthCheck && this._primaryHealthCheck.healthy)
        ) {
            // this instance isn't the current primary
            // if no primary or primary is unhealthy, try to run a primary election or check if a
            // primary election is running then wait for it to end
            // promiseEmitter to handle the primary election process by periodically check:
            // 1. if there is a running election, then waits for its final
            // 2. if there isn't a running election, then runs an election and complete it
            let promiseEmitter = this.checkPrimaryElection.bind(this),
                // validator set a condition to determine if the fgt needs to keep waiting or not.
                validator = primaryInfo => {
                    // i am the new primary, don't wait, continue to finalize the election.
                    // should return true to end the waiting.
                    if (
                        primaryInfo &&
                        primaryInfo.primaryPrivateIpAddress ===
                            this._selfInstance.primaryPrivateIpAddress
                    ) {
                        isPrimary = true;
                        return true;
                    } else if (this._primaryRecord && this._primaryRecord.voteState === 'pending') {
                        // i am not the new primary
                        // if no wait for primary election, I could become a headless instance
                        // may allow any instance of secondary role to come up without primary.
                        // They will receive the new primary ip on one of their following
                        // heartbeat sync callback
                        if (this._settings['primary-election-no-wait'] === 'true') {
                            return true;
                        } else {
                            // the new primary hasn't come up to finalize the election,
                            // I should keep on waiting.
                            // should return false to continue.
                            this._primaryRecord = null; // clear the primary record cache
                            return false;
                        }
                    } else if (this._primaryRecord && this._primaryRecord.voteState === 'done') {
                        // if the primary election is final, then no need to wait.
                        // should return true to end the waiting.
                        return true;
                    } else {
                        // no primary elected yet
                        // entering this syncedCallback function means i am already insync so
                        // i used to be assigned a primary.
                        // if i am not in the primary scaling group then I can't start a new
                        // election.
                        // i stay as is and hoping for someone in the primary scaling group
                        // triggers a primary election. Then I will be notified at some point.
                        if (this.scalingGroupName !== this.primaryScalingGroupName) {
                            return true;
                        } else {
                            // for new instance or instance in the primary scaling group
                            // they should keep on waiting
                            return false;
                        }
                    }
                },
                // counter to set a time based condition to end this waiting. If script execution
                // time is close to its timeout (6 seconds - abount 1 inteval + 1 second), ends the
                // waiting to allow for the rest of logic to run
                counter = () => {
                    // eslint-disable-line no-unused-vars
                    if (Date.now() < process.env.SCRIPT_EXECUTION_EXPIRE_TIME - 6000) {
                        return false;
                    }
                    this.logger.warn('script execution is about to expire');
                    return true;
                };

            try {
                this._primaryInfo = await CoreFunctions.waitFor(
                    promiseEmitter,
                    validator,
                    5000,
                    counter
                );
                // after new primary is elected, get the new primary healthcheck
                // there are two possible results here:
                // 1. a new instance comes up and becomes the new primary instance, its healthcheck won't
                // exist yet because this instance isn't added to monitor.
                //   1.1. in this case, the instance will be added to monitor.
                // 2. an existing secondary instance becomes the new primary, its healthcheck exists
                // because the instance in under monitoring.
                //   2.1. in this case, the instance will take actions based on its healthcheck
                //        result.
                this._primaryHealthCheck = null; // invalidate the primary health check object
                // reload the primary health check object
                await this.retrievePrimary();
            } catch (error) {
                // if error occurs, check who is holding a primary election, if it is this instance,
                // terminates this election. then continue
                await this.retrievePrimary(null, true);

                if (
                    this._primaryRecord.instanceId === this._selfInstance.instanceId &&
                    this._primaryRecord.scalingGroupName === this._selfInstance.scalingGroupName
                ) {
                    await this.platform.removePrimaryRecord();
                }
                await this.removeInstance(this._selfInstance);
                throw new Error(
                    'Failed to determine the primary instance within ' +
                        `${process.env.SCRIPT_EXECUTION_EXPIRE_TIME} seconds. This instance is unable` +
                        ' to bootstrap. Please report this to administrators.'
                );
            }
        }

        // check if myself is under health check monitoring
        // (primary instance itself may have got its healthcheck result in some code blocks above)
        this._selfHealthCheck =
            this._selfHealthCheck ||
            (await this.platform.getInstanceHealthCheck(
                {
                    instanceId: this._selfInstance.instanceId
                },
                interval
            ));

        // if this instance is the primary instance and the primary election record is still pending, it will
        // finalize the primary election.
        if (
            this._primaryInfo &&
            this._selfInstance.instanceId === this._primaryInfo.instanceId &&
            this.scalingGroupName === this.primaryScalingGroupName &&
            this._primaryRecord &&
            this._primaryRecord.voteState === 'pending'
        ) {
            isPrimary = true;
            if (
                !this._selfHealthCheck ||
                (this._selfHealthCheck && this._selfHealthCheck.healthy)
            ) {
                // if election couldn't be finalized, remove the current election so someone else
                // could start another election
                if (!(await this.platform.finalizePrimaryElection())) {
                    await this.platform.removePrimaryRecord();
                    this._primaryRecord = null;
                    lifecycleShouldAbandon = true;
                } else {
                    // primary election is finalized and now I am the new primary role
                    this._primaryRecord = null;
                    await this.retrievePrimary();
                    // update tags
                    await this.platform.updateHAAPRoleTag(this._selfInstance.instanceId);
                    // if enable nat gw, update route
                    if (this._settings['enable-integrated-egress-nat-gateway'] === 'true') {
                        await this.updateRouteForEgressNATGateway(this._selfInstance);
                    }
                }
            }
        }

        // check whether primary role has changed or not
        let currentPrimaryInstanceId =
            (this._primaryRecord && this._primaryRecord.instanceId) || null;
        let currentPrimaryElectionVote =
            (this._primaryRecord && this._primaryRecord.voteState) || null;

        // no primary instance (election done) before, but have a primary (election done) now
        // or
        // had a pending primary election (pending) before, but now primary election is done
        // no matter the primary instance id has changed or not.
        if (primaryElectionVote !== 'done' && currentPrimaryElectionVote === 'done') {
            primaryChanged = true;
        }

        // had a primary (election done) before, have a primary (election done) now,
        // but they have different instance id
        if (
            primaryElectionVote === 'done' &&
            currentPrimaryElectionVote === 'done' &&
            primaryInstanceId !== currentPrimaryInstanceId
        ) {
            primaryChanged = true;
        }

        // if primary role has changed and this is the new primary role,
        if (primaryChanged && isPrimary) {
            // update primary/secondary tags
            await this.platform.updateHAAPRoleTag(this._selfInstance.instanceId);
            // if enable nat gw, update route
            if (this._settings['enable-integrated-egress-nat-gateway'] === 'true') {
                await this.updateRouteForEgressNATGateway(this._selfInstance);
            }
        }

        this.logger.info(
            `currentPrimaryInstanceId: ${currentPrimaryInstanceId}, ` +
                `currentPrimaryElectionVote: ${currentPrimaryElectionVote}, ` +
                `primaryChanged: ${primaryChanged}`
        );

        // if no self healthcheck record found, this instance not under monitor. It's about the
        // time to add it to monitor. should make sure its all lifecycle actions are complete
        // while starting to monitor it.
        // if this instance is not the primary, still add it to monitor but leave its primary unknown.
        // if there's a primary instance, add the monitor record using this primary regardless
        // the primary health status.
        if (!this._selfHealthCheck) {
            // check if a lifecycle event waiting
            await this.completeGetConfigLifecycleAction(
                this._selfInstance.instanceId,
                !lifecycleShouldAbandon
            );

            // trigger device pre-authorization (via fortianalyzer device manager)
            if (this._settings['enable-fortianalyzer-integration'] === 'true') {
                await this.authorizeDevice();
            }

            primaryIp = this._primaryInfo ? this._primaryInfo.primaryPrivateIpAddress : null;
            // if a secondary instance finds primary is pending, don't update primary ip to the health check record
            if (
                !isPrimary &&
                this._primaryRecord &&
                this._primaryRecord.voteState === 'pending' &&
                this._settings['primary-election-no-wait'] === 'true'
            ) {
                primaryIp = null;
            }
            await this.addInstanceToMonitor(this._selfInstance, interval, primaryIp);
            let logMessagPrimaryIp =
                !primaryIp && this._settings['primary-election-no-wait'] === 'true'
                    ? ' without primary ip)'
                    : ` master-ip: ${primaryIp})`;
            this.logger.info(
                `instance (id:${this._selfInstance.instanceId}, ` +
                    `${logMessagPrimaryIp} is added to monitor at timestamp: ${Date.now()}.`
            );
            // if this newly come-up instance is the new primary, save its instance id as the
            // default password into settings because all other instance will sync password from
            // the primary there's a case if users never changed the primary's password, when the
            // primary was torn-down, there will be no way to retrieve this original password.
            // so in this case, should keep track of the update of default password.
            if (
                this._primaryInfo &&
                this._selfInstance.instanceId === this._primaryInfo.instanceId &&
                this.scalingGroupName === this.primaryScalingGroupName
            ) {
                await this.platform.setSettingItem(
                    'fortigate-default-password',
                    this._selfInstance.instanceId,
                    'default password comes from the new elected primary.',
                    false,
                    false
                );
            }
            return primaryIp
                ? {
                      'master-ip': this._primaryInfo.primaryPrivateIpAddress
                  }
                : '';
        } else if (this._selfHealthCheck && this._selfHealthCheck.healthy) {
            // this instance is already in monitor. if the primary has changed (i.e.: the current
            // primary is different from the one this instance is holding), and the new primary
            // is in a healthy state now, notify it by sending the new primary ip to it.

            // if no primary presents (reasons: waiting for the pending primary instance to become
            // in-service; the primary has been purged but no new primary is elected yet.)
            // keep the calling instance 'in-sync'. don't update its master-ip.

            primaryIp =
                this._primaryInfo && this._primaryHealthCheck && this._primaryHealthCheck.healthy
                    ? this._primaryInfo.primaryPrivateIpAddress
                    : this._selfHealthCheck.primaryIp;
            let now = Date.now();
            await this.platform.updateInstanceHealthCheck(
                this._selfHealthCheck,
                interval,
                primaryIp,
                now
            );
            this.logger.info(
                `hb record updated on (timestamp: ${now}, instance id:` +
                    `${this._selfInstance.instanceId}, ` +
                    `ip: ${this._selfInstance.primaryPrivateIpAddress}) health check ` +
                    `(${this._selfHealthCheck.healthy ? 'healthy' : 'unhealthy'}, ` +
                    `heartBeatLossCount: ${this._selfHealthCheck.heartBeatLossCount}, ` +
                    `nextHeartBeatTime: ${this._selfHealthCheck.nextHeartBeatTime}` +
                    `syncState: ${this._selfHealthCheck.syncState}, master-ip: ${primaryIp}).`
            );
            return primaryIp &&
                this._selfHealthCheck &&
                this._selfHealthCheck.primaryIp !== primaryIp
                ? {
                      'master-ip': this._primaryInfo.primaryPrivateIpAddress
                  }
                : '';
        } else {
            this.logger.info(
                'instance is unhealthy. need to remove it. healthcheck record:',
                JSON.stringify(this._selfHealthCheck)
            );
            // for unhealthy instances, fail this instance
            // if it is previously on 'in-sync' state, mark it as 'out-of-sync' so script will stop
            // keeping it in sync and stop doing any other logics for it any longer.
            if (this._selfHealthCheck && this._selfHealthCheck.inSync) {
                // change its sync state to 'out of sync' by updating it state one last time
                await this.platform.updateInstanceHealthCheck(
                    this._selfHealthCheck,
                    interval,
                    this._selfHealthCheck.primaryIp,
                    Date.now(),
                    true
                );
                // terminate it from autoscaling group
                await this.removeInstance(this._selfInstance);
            }
            // for unhealthy instances, keep responding with action 'shutdown'
            return {
                action: 'shutdown'
            };
        }
    }