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

io.hekate.election.internal.DefaultElectionService Maven / Gradle / Ivy

There is a newer version: 4.1.3
Show newest version
/*
 * Copyright 2020 The Hekate Project
 *
 * The Hekate Project licenses this file to you 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.hekate.election.internal;

import io.hekate.cluster.event.ClusterEventType;
import io.hekate.core.Hekate;
import io.hekate.core.HekateException;
import io.hekate.core.internal.util.ArgAssert;
import io.hekate.core.internal.util.ConfigCheck;
import io.hekate.core.internal.util.HekateThreadFactory;
import io.hekate.core.jmx.JmxService;
import io.hekate.core.report.ConfigReporter;
import io.hekate.core.service.ConfigurationContext;
import io.hekate.core.service.CoreService;
import io.hekate.core.service.DependencyContext;
import io.hekate.core.service.InitializationContext;
import io.hekate.election.Candidate;
import io.hekate.election.CandidateConfig;
import io.hekate.election.CandidateConfigProvider;
import io.hekate.election.ElectionService;
import io.hekate.election.ElectionServiceFactory;
import io.hekate.election.LeaderFuture;
import io.hekate.lock.DistributedLock;
import io.hekate.lock.LockConfigProvider;
import io.hekate.lock.LockRegionConfig;
import io.hekate.lock.LockService;
import io.hekate.util.StateGuard;
import io.hekate.util.async.Waiting;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static io.hekate.core.internal.util.StreamUtils.nullSafe;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static java.util.stream.Collectors.toList;

public class DefaultElectionService implements ElectionService, CoreService, LockConfigProvider {
    private static final Logger log = LoggerFactory.getLogger(DefaultElectionService.class);

    private static final boolean DEBUG = log.isDebugEnabled();

    private static final String ELECTION_LOCK_REGION = "hekate.election";

    private final StateGuard guard = new StateGuard(ElectionService.class);

    private final List candidateConfigs = new ArrayList<>();

    private final Map candidates = new HashMap<>();

    private JmxService jmx;

    private LockService locks;

    public DefaultElectionService(ElectionServiceFactory factory) {
        ArgAssert.notNull(factory, "Factory");

        nullSafe(factory.getCandidates()).forEach(candidateConfigs::add);

        nullSafe(factory.getConfigProviders()).forEach(provider ->
            nullSafe(provider.configureElection()).forEach(candidateConfigs::add)
        );
    }

    @Override
    public void resolve(DependencyContext ctx) {
        locks = ctx.require(LockService.class);

        jmx = ctx.optional(JmxService.class);
    }

    @Override
    public void configure(ConfigurationContext ctx) {
        // Collect configurations from providers.
        nullSafe(ctx.findComponents(CandidateConfigProvider.class)).forEach(provider ->
            nullSafe(provider.configureElection()).forEach(candidateConfigs::add)
        );

        // Validate configs.
        ConfigCheck check = ConfigCheck.get(CandidateConfig.class);

        Set uniqueGroups = new HashSet<>();

        candidateConfigs.forEach(cfg -> {
            check.notEmpty(cfg.getGroup(), "group");
            check.validSysName(cfg.getGroup(), "group");
            check.notNull(cfg.getCandidate(), "candidate");

            String group = cfg.getGroup().trim();

            check.unique(group, uniqueGroups, "group");

            uniqueGroups.add(group);
        });
    }

    @Override
    public Collection configureLocking() {
        if (candidateConfigs.isEmpty()) {
            return emptyList();
        }

        return singletonList(new LockRegionConfig(ELECTION_LOCK_REGION));
    }

    @Override
    public void initialize(InitializationContext ctx) throws HekateException {
        if (DEBUG) {
            log.debug("Initializing...");
        }

        guard.becomeInitialized(() -> {
            if (!candidateConfigs.isEmpty()) {
                // Register candidates.
                candidateConfigs.forEach(cfg ->
                    registerCandidate(cfg, ctx.hekate())
                );

                // Register JMX for candidates.
                if (jmx != null) {
                    for (CandidateHandler candidate : candidates.values()) {
                        jmx.register(candidate, candidate.group());
                    }
                }

                // Initialize handlers after we join the cluster.
                ctx.cluster().addListener(
                    event -> guard.withReadLockIfInitialized(() ->
                        candidates.values().forEach(CandidateHandler::initialize)
                    ),
                    ClusterEventType.JOIN
                );
            }
        });

        if (DEBUG) {
            log.debug("Initialized.");
        }
    }

    @Override
    public void report(ConfigReporter report) {
        guard.withReadLockIfInitialized(() -> {
            if (!candidates.isEmpty()) {
                report.section("election", election ->
                    election.section("candidates", candidates ->
                        this.candidates.values().forEach(h ->
                            candidates.section("candidate", candidate -> {
                                candidate.value("group", h.group());
                                candidate.value("candidate", h.candidate());
                            })
                        )
                    )
                );
            }
        });
    }

    @Override
    public void preTerminate() throws HekateException {
        Waiting done = guard.becomeTerminating(() ->
            candidates.values().stream()
                .map(CandidateHandler::terminate)
                .collect(toList())
        );

        done.awaitUninterruptedly();
    }

    @Override
    public void terminate() throws HekateException {
        // Actual termination of handlers happens in postTerminate() in order to make sure that worker threads are terminated after
        // the lock service termination. Otherwise it can lead to RejectedExecutionException if lock service tries to process async lock
        // events while election service is already terminated.
    }

    @Override
    public void postTerminate() throws HekateException {
        if (DEBUG) {
            log.debug("Terminating...");
        }

        Waiting done = guard.becomeTerminated(() -> {
            List waiting = candidates.values().stream()
                .map(CandidateHandler::shutdown)
                .collect(toList());

            candidates.clear();

            return waiting;
        });

        done.awaitUninterruptedly();

        if (DEBUG) {
            log.debug("Terminated.");
        }
    }

    @Override
    public LeaderFuture leader(String group) {
        return candidate(group).leaderFuture();
    }

    private CandidateHandler candidate(String group) {
        ArgAssert.notNull(group, "Group name");

        return guard.withReadLockAndStateCheck(() ->
            candidates.computeIfAbsent(group, missing -> {
                throw new IllegalArgumentException("Unknown group [name=" + missing + ']');
            })
        );
    }

    private void registerCandidate(CandidateConfig cfg, Hekate hekate) {
        assert guard.isWriteLocked() : "Thread must hold a write lock.";

        if (DEBUG) {
            log.debug("Registering new candidate [config={}]", cfg);
        }

        // Prepare configuration values.
        String group = cfg.getGroup().trim();
        Candidate candidate = cfg.getCandidate();

        // Election lock.
        DistributedLock lock = locks.region(ELECTION_LOCK_REGION).get(group);

        // Election thread.
        ExecutorService worker = Executors.newSingleThreadExecutor(new HekateThreadFactory("Election-" + group));

        // Register candidate.
        CandidateHandler handler = new CandidateHandler(
            group,
            candidate,
            worker,
            lock,
            hekate.localNode(),
            hekate
        );

        candidates.put(group, handler);
    }

    @Override
    public String toString() {
        return ElectionService.class.getSimpleName();
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy