All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.axoniq.axonserver.connector.impl.HeartbeatMonitor Maven / Gradle / Ivy

/*
 * Copyright (c) 2020. AxonIQ
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.axoniq.axonserver.connector.impl;

import io.axoniq.axonserver.connector.AxonServerException;
import io.axoniq.axonserver.connector.ErrorCategory;
import io.axoniq.axonserver.connector.ReplyChannel;
import io.axoniq.axonserver.grpc.control.Heartbeat;
import io.axoniq.axonserver.grpc.control.PlatformInboundInstruction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.Clock;
import java.time.Instant;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

/**
 * Monitor dealing with all the logic around sending out heartbeats.
 */
public class HeartbeatMonitor {

    private static final Logger logger = LoggerFactory.getLogger(HeartbeatMonitor.class);

    private static final PlatformInboundInstruction HEARTBEAT_MESSAGE =
            PlatformInboundInstruction.newBuilder().setHeartbeat(Heartbeat.getDefaultInstance()).build();

    static Clock clock = Clock.systemUTC();

    private final ScheduledExecutorService executor;
    private final HeartbeatSender sender;
    private final Runnable onHeartbeatMissed;

    private final AtomicLong nextHeartbeatDeadline = new AtomicLong();
    private final AtomicLong nextHeartbeat = new AtomicLong();
    private final AtomicLong timeout = new AtomicLong(Long.MAX_VALUE);
    private final AtomicLong interval = new AtomicLong(Long.MAX_VALUE);
    private final AtomicInteger taskId = new AtomicInteger();

    /**
     * Constructs a {@link HeartbeatMonitor}.
     *
     * @param executor          the {@link ScheduledExecutorService} used to schedule operations to validate if a
     *                          heartbeat should be send with the given {@code heartbeatSender}
     * @param heartbeatSender   the {@link HeartbeatSender} used to send heartbeats with
     * @param onHeartbeatMissed operation to perform if a heartbeat has been missed. Can be used to force a reconnect of
     *                          a channel for example
     */
    public HeartbeatMonitor(ScheduledExecutorService executor,
                            HeartbeatSender heartbeatSender,
                            Runnable onHeartbeatMissed) {
        this.executor = executor;
        this.sender = heartbeatSender;
        this.onHeartbeatMissed = onHeartbeatMissed;
    }

    /**
     * Turn on heartbeat sending by this {@link HeartbeatMonitor}.
     *
     * @param interval the interval at which heartbeats occur. Will use a minimal value of {@code 500} milliseconds to
     *                 reschedule heartbeat validation
     * @param timeout  the timeout within which this monitor expects responses to the dispatched heartbeats. If this
     *                 timeout is hit, the given {@code onHeartbeatMissed} on construction will be called
     * @param timeUnit the {@link TimeUnit} used to define in which time frame both the given {@code interval} and
     *                 {@code timeout} reside. Will be used to change both values to their representative in
     *                 milliseconds
     */
    public void enableHeartbeat(long interval, long timeout, TimeUnit timeUnit) {
        this.interval.set(timeUnit.toMillis(interval));
        this.timeout.set(timeUnit.toMillis(timeout));
        long now = clock.millis();
        nextHeartbeat.set(now);
        nextHeartbeatDeadline.set(now + timeUnit.toMillis(timeout));
        int task = taskId.incrementAndGet();
        executor.execute(() -> checkAndReschedule(task));
    }

    /**
     * Turn off heartbeat sending by this {@link HeartbeatMonitor}.
     */
    public void disableHeartbeat() {
        this.interval.set(Long.MAX_VALUE);
        this.nextHeartbeatDeadline.set(Long.MAX_VALUE);
        taskId.incrementAndGet();
    }

    private void checkAndReschedule(int task) {
        if (task == taskId.get()) {
            // heartbeats should not be considered valid when a change was made
            checkBeat();
            long delay = Math.min(interval.get(), 1000);
            logger.debug("Heartbeat status checked. Scheduling next heartbeat verification in {}ms", delay);
            executor.schedule(() -> checkAndReschedule(task), delay, TimeUnit.MILLISECONDS);
        }
    }

    /**
     * Pause the process of sending out heartbeats by this monitor.
     */
    public void pause() {
        long currentInterval = this.interval.get();
        long currentTimeout = this.timeout.get();
        if (currentInterval != Long.MAX_VALUE || currentTimeout != Long.MAX_VALUE) {
            taskId.incrementAndGet();
        }
    }

    /**
     * Resume the process of sending out heartbeats by this monitor.
     */
    public void resume() {
        long currentInterval = this.interval.get();
        long currentTimeout = this.timeout.get();
        if (currentInterval != Long.MAX_VALUE && currentTimeout != Long.MAX_VALUE) {
            enableHeartbeat(currentInterval, currentTimeout, TimeUnit.MILLISECONDS);
        }
    }

    private void checkBeat() {
        long now = clock.millis();
        long nextDeadline = nextHeartbeatDeadline.get();
        if (nextDeadline <= now) {
            logger.info("Did not receive heartbeat acknowledgement within {}ms", this.timeout.get());
            onHeartbeatMissed.run();
            nextHeartbeatDeadline.compareAndSet(nextDeadline, now + this.interval.get());
        }
        if (planNextBeat(now, this.interval.get())) {
            long currentInterval = this.interval.get();
            long beatTimeout = this.timeout.get();
            sender.sendHeartbeat().whenComplete((r, e) -> {

                boolean success = e == null
                        || (e instanceof AxonServerException && ((AxonServerException) e).getErrorCategory() == ErrorCategory.UNSUPPORTED_INSTRUCTION);
                // if AxonServer indicates it doesn't know this instruction, we have at least reached it.
                // We can assume the connection is alive
                if (success) {
                    if (currentInterval != Long.MAX_VALUE) {
                        long newDeadline = nextHeartbeatDeadline.updateAndGet(
                                currentDeadline -> Math.max(now + beatTimeout + currentInterval, currentDeadline)
                        );
                        if (logger.isDebugEnabled()) {
                            logger.debug("Heartbeat Acknowledgement received. Extending deadline to {}",
                                         Instant.ofEpochMilli(newDeadline));
                        }
                    } else if (logger.isDebugEnabled()) {
                        logger.debug("Heartbeat Acknowledgment received.");
                    }
                }
            });
        }
    }

    /**
     * Calculates the time for the next heartbeat based on the most recent heartbeat time and the interval, indicating
     * whether the time has been reached to send a heartbeat message.
     *
     * @param currentTime the current timestamp
     * @param interval    the interval at which heartbeats occur
     * @return whether or not the time for a new heartbeat has elapsed
     */
    private boolean planNextBeat(long currentTime, long interval) {
        return nextHeartbeat.getAndAccumulate(
                interval, (next, currentInterval) -> next <= currentTime ? currentTime + currentInterval : next
        ) <= currentTime;
    }

    /**
     * Handler of {@link PlatformInboundInstruction} requesting a heartbeat from this connector. The given {@link
     * ReplyChannel} is used to send responding heartbeat message with.
     *
     * @param reply the {@link ReplyChannel} to send a heartbeat reply message over
     */
    public void handleIncomingBeat(ReplyChannel reply) {
        long now = clock.millis();
        long currentInterval = this.interval.get();
        // receiving a heartbeat from Server is equivalent to receiving an acknowledgement
        planNextBeat(now, currentInterval);
        nextHeartbeatDeadline.updateAndGet(current -> Math.max(now + currentInterval, current));
        try {
            reply.send(HEARTBEAT_MESSAGE);
        } finally {
            reply.complete();
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy