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

io.mantisrx.extensions.dynamodb.DynamoDBMasterMonitor Maven / Gradle / Ivy

/*
 * Copyright 2024 Netflix, Inc.
 *
 * 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.mantisrx.extensions.dynamodb;

import com.amazonaws.services.dynamodbv2.AmazonDynamoDBLockClient;
import com.amazonaws.services.dynamodbv2.LockItem;
import io.mantisrx.common.metrics.Counter;
import io.mantisrx.common.metrics.Metrics;
import io.mantisrx.common.metrics.MetricsRegistry;
import io.mantisrx.server.core.BaseService;
import io.mantisrx.server.core.json.DefaultObjectMapper;
import io.mantisrx.server.core.master.MasterDescription;
import io.mantisrx.server.core.master.MasterMonitor;
import io.mantisrx.shaded.com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.Optional;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import rx.Observable;
import rx.subjects.BehaviorSubject;


@Slf4j
public class DynamoDBMasterMonitor extends BaseService implements MasterMonitor {

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


    private final ThreadFactory monitorThreadFactory = r -> {
        Thread thread = new Thread(r);
        thread.setName("dynamodb-monitor-" + System.currentTimeMillis());
        thread.setDaemon(true); // allow JVM to shutdown if monitor is still running
        thread.setPriority(Thread.NORM_PRIORITY);
        thread.setUncaughtExceptionHandler((t, e) -> logger.error("thread: {} failed with {}", t.getName(), e.getMessage(), e) );
        return thread;
    };
    private final ScheduledExecutorService leaderMonitor =
            Executors.newScheduledThreadPool(1, monitorThreadFactory);

    // Assuming your lock client's options are in a variable named options
    private final AmazonDynamoDBLockClient lockClient;

    private final String partitionKey;

    private final Duration pollInterval;

    private final Duration gracefulShutdown;

    private final BehaviorSubject masterSubject;

    private final ObjectMapper jsonMapper = DefaultObjectMapper.getInstance();

    private final Metrics metrics;

    private final Counter noLockPresentCounter;
    private final Counter lockDecodeFailedCounter;
    private final Counter nullNextLeaderCounter;

    /**
     * Creates a MasterMonitor backed by DynamoDB. This should be used if you are using a {@link DynamoDBLeaderElector}
     */
    public DynamoDBMasterMonitor() {
        this(DynamoDBClientSingleton.getLockClient(),
            DynamoDBClientSingleton.getPartitionKey(),
            Duration.parse(DynamoDBClientSingleton.getDynamoDBConf().getDynamoDBLeaderHeartbeatDuration()),
            Duration.parse(DynamoDBClientSingleton.getDynamoDBConf().getDynamoDBMonitorGracefulShutdownDuration()));
    }

    public DynamoDBMasterMonitor(
            AmazonDynamoDBLockClient lockClient,
            String partitionKey,
            Duration pollInterval,
            Duration gracefulShutdown) {
        masterSubject = BehaviorSubject.create(MasterDescription.MASTER_NULL);
        this.lockClient = lockClient;
        this.partitionKey = partitionKey;
        this.pollInterval = pollInterval;
        this.gracefulShutdown = gracefulShutdown;

        Metrics m = new Metrics.Builder()
            .id("DynamoDBMasterMonitor")
            .addCounter("no_lock_present")
            .addCounter("lock_decode_failed")
            .addCounter("null_next_leader")
            .build();
        this.metrics = MetricsRegistry.getInstance().registerAndGet(m);

        this.noLockPresentCounter = metrics.getCounter("no_lock_present");
        this.lockDecodeFailedCounter = metrics.getCounter("lock_decode_failed");
        this.nullNextLeaderCounter = metrics.getCounter("null_next_leader");
    }

    @Override
    @SuppressWarnings("FutureReturnValueIgnored")
    public void start() {
        leaderMonitor.scheduleAtFixedRate(
                this::getCurrentLeader, 0, pollInterval.toMillis(), TimeUnit.MILLISECONDS);
    }

    @Override
    public void shutdown() {
        logger.info("close the lock client");
        try {
            lockClient.close();
        } catch (IOException e) {
            logger.error("error closing the dynamodb lock client", e);
        }

        try {
            final boolean isTerminated =
                    leaderMonitor.awaitTermination(gracefulShutdown.toMillis(), TimeUnit.MILLISECONDS);
            if (!isTerminated) {
                leaderMonitor.shutdownNow();
            }
        } catch (InterruptedException e) {
            logger.error("error timeout waiting on leader monitor to terminate executor", e);
        }
        logger.info("leader monitor shutdown");
    }

    @SuppressWarnings("FutureReturnValueIgnored")
    private void getCurrentLeader() {
        logger.info("attempting leader lookup");
        final Optional optionalLock = lockClient.getLock(partitionKey, Optional.empty());
        final MasterDescription nextDescription;
        if (optionalLock.isPresent()) {
            final LockItem lock = optionalLock.get();
            nextDescription = lock.getData().map(this::bytesToMaster).orElse(null);
        } else {
            nextDescription = null;
            logger.warn("no leader found");
            this.noLockPresentCounter.increment();
        }

        if (nextDescription != null) {
            updateLeader(nextDescription);
        } else {
            this.nullNextLeaderCounter.increment();
        }
    }

    private void updateLeader(@Nullable MasterDescription nextDescription) {
        final MasterDescription prev = Optional.ofNullable(masterSubject.getValue()).orElse(MasterDescription.MASTER_NULL);
        final MasterDescription next = (nextDescription == null) ? MasterDescription.MASTER_NULL : nextDescription;
        if (!prev.equals(next)) {
            logger.info("leader changer information previous {} and next {}", prev.getHostname(), next.getHostname());
            masterSubject.onNext(next);
        }
    }

    private MasterDescription bytesToMaster(ByteBuffer data) {
        // It is possible that the underlying buffer is read more than once,
        // so if the offset of the buffer is at the end, rewind, so we can read it.
        if (!data.hasRemaining()) {
            data.rewind();
        }
        final byte[] bytes = new byte[data.remaining()];
        data.get(bytes);
        try {
            return jsonMapper.readValue(bytes, MasterDescription.class);
        } catch (IOException e) {
            logger.error("unable to parse master description bytes: {}", data, e);
            this.lockDecodeFailedCounter.increment();
        }
        return MasterDescription.MASTER_NULL;
    }

    @Override
    public Observable getMasterObservable() {
        return masterSubject;
    }

    /**
     * Returns the latest master if there's one. If there has been no master in recent history, then
     * this return null.
     *
     * @return Latest description of the master
     */
    @Override
    @Nullable
    public MasterDescription getLatestMaster() {
        return Optional.ofNullable(masterSubject.getValue()).orElse(MasterDescription.MASTER_NULL);
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy