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

tech.ydb.core.impl.pool.EndpointPool Maven / Gradle / Ivy

package tech.ydb.core.impl.pool;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import javax.annotation.Nullable;

import com.google.common.annotations.VisibleForTesting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import tech.ydb.core.grpc.BalancingSettings;

/**
 * @author Nikolay Perfilov
 * @author Kirill Kurdyukov
 */
public final class EndpointPool {
    private static final Logger logger = LoggerFactory.getLogger(EndpointPool.class);

    // Maximum percent of endpoints pessimized by transport errors to start recheck
    private static final long DISCOVERY_PESSIMIZATION_THRESHOLD = 50;

    private final BalancingSettings balancingSettings;
    private final ReadWriteLock recordsLock = new ReentrantReadWriteLock();

    private List records = new ArrayList<>();
    private Map recordsByNodeId = new HashMap<>();
    private Map recordsByEndpoint = new HashMap<>();

    private boolean needToRunDiscovery = false;
    // Number of endpoints with best load factor (priority)
    private int bestEndpointsCount = -1;

    public EndpointPool(BalancingSettings balancingSettings) {
        logger.debug("Creating endpoint pool with balancing settings policy: {}", balancingSettings.getPolicy());
        this.balancingSettings = balancingSettings;
    }

    @Nullable
    public EndpointRecord getEndpoint(@Nullable Integer preferredNodeID) {
        recordsLock.readLock().lock();
        try {
            if (preferredNodeID != null) {
                PriorityEndpoint knownEndpoint = recordsByNodeId.get(preferredNodeID);
                if (knownEndpoint != null) {
                    return knownEndpoint.record;
                }
            }
            if (bestEndpointsCount > 0) {
                // returns value in range [0, n)
                int idx = ThreadLocalRandom.current().nextInt(bestEndpointsCount);
                return records.get(idx).record;
            } else {
                return null;
            }
        } finally {
            recordsLock.readLock().unlock();
        }
    }

    // Sets new endpoints, returns removed
    public List setNewState(String selfLocation, List endpoints) {
        PriorityPicker picker = PriorityPicker.from(balancingSettings, selfLocation, endpoints);

        Map newRecordsByEndpoint = new HashMap<>();
        Map newRecordsByNodeId = new HashMap<>();
        List newRecords = new ArrayList<>();
        int newBestEndpointsCount = 0;
        int bestPriority = Integer.MAX_VALUE;

        logger.debug("init new state with {} endpoints", endpoints.size());
        for (EndpointRecord endpoint : endpoints) {
            int priority = picker.getEndpointPriority(endpoint.getLocation());

            PriorityEndpoint entry = new PriorityEndpoint(endpoint, priority);
            String hostAndPort = endpoint.getHostAndPort();

            if (!newRecordsByEndpoint.containsKey(hostAndPort)) {
                logger.debug("added endpoint {}", endpoint);
                newRecordsByEndpoint.put(hostAndPort, entry);
                if (endpoint.getNodeId() != 0) {
                    newRecordsByNodeId.put(endpoint.getNodeId(), entry);
                }
                newRecords.add(entry);

                if (priority < bestPriority) {
                    bestPriority = priority;
                    newBestEndpointsCount = 0;
                }
                if (priority == bestPriority) {
                    newBestEndpointsCount += 1;
                }
            } else {
                logger.warn("duplicate endpoint {}", endpoint.getHostAndPort());
            }
        }

        newRecords.sort(PriorityEndpoint.COMPARATOR);
        List removed = new ArrayList<>();

        recordsLock.writeLock().lock();
        try {
            for (PriorityEndpoint entry : records) {
                if (!newRecordsByEndpoint.containsKey(entry.record.getHostAndPort())) {
                    removed.add(entry.record);
                }
            }

            records = newRecords;
            recordsByNodeId = newRecordsByNodeId;
            recordsByEndpoint = newRecordsByEndpoint;
            bestEndpointsCount = newBestEndpointsCount;
            needToRunDiscovery = false;
        } finally {
            recordsLock.writeLock().unlock();
        }
        return removed;
    }

    public void pessimizeEndpoint(EndpointRecord endpoint) {
        if (endpoint == null) {
            return;
        }

        PriorityEndpoint knownEndpoint = recordsByEndpoint.get(endpoint.getHostAndPort());
        if (knownEndpoint == null) {
            return;
        }

        if (knownEndpoint.isPessimized()) {
            logger.trace("Endpoint {} is already pessimized", endpoint);
            return;
        }

        recordsLock.writeLock().lock();
        try {
            knownEndpoint.pessimize();

            records.sort(PriorityEndpoint.COMPARATOR);

            long bestPriority = records.get(0).priority;
            int newBestCount = 0;
            int newPessimizedCount = 0;
            for (PriorityEndpoint record: records) {
                if (record.getPriority() == bestPriority) {
                    newBestCount++;
                }
                if (record.isPessimized()) {
                    newPessimizedCount++;
                }
            }

            bestEndpointsCount = newBestCount;
            needToRunDiscovery = 100 * newPessimizedCount > records.size() * DISCOVERY_PESSIMIZATION_THRESHOLD;
            if (needToRunDiscovery) {
                logger.debug("launching discovery due to pessimization threshold is exceeded: {}/{} is more than {}",
                        newPessimizedCount, records.size(), DISCOVERY_PESSIMIZATION_THRESHOLD);
            }

            logger.trace("Endpoint {} was pessimized. New pessimization ratio: {}/{}",
                    endpoint, newPessimizedCount, records.size());
        } finally {
            recordsLock.writeLock().unlock();
        }
    }

    public boolean needToRunDiscovery() {
        return needToRunDiscovery;
    }

    @VisibleForTesting
    static class PriorityEndpoint {
        static final Comparator COMPARATOR = Comparator
                .comparingLong(PriorityEndpoint::getPriority)
                .thenComparing(e -> e.record.getHostAndPort());

        private final EndpointRecord record;
        private long priority;

        PriorityEndpoint(EndpointRecord record, long priority) {
            this.record = record;
            this.priority = priority;
        }

        public long getPriority() {
            return this.priority;
        }

        public EndpointRecord getEndpoint() {
            return this.record;
        }

        public void pessimize() {
            this.priority = Long.MAX_VALUE;
        }

        public boolean isPessimized() {
            return priority == Long.MAX_VALUE;
        }
    }

    @VisibleForTesting
    Map getEndpointsByNodeId() {
        return this.recordsByNodeId;
    }

    @VisibleForTesting
    List getRecords() {
        return this.records;
    }

    @VisibleForTesting
    int getBestEndpointCount() {
        return bestEndpointsCount;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy