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

com.fizzgate.proxy.RpcInstanceServiceImpl Maven / Gradle / Ivy

/*
 *  Copyright (C) 2021 the original author or authors.
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see .
 */
package com.fizzgate.proxy;

import org.apache.logging.log4j.ThreadContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.ReactiveStringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import com.fizzgate.config.AggregateRedisConfig;
import com.fizzgate.util.Consts;
import com.fizzgate.util.JacksonUtils;
import com.fizzgate.util.ReactorUtils;

import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Supplier;

/**
 * RPC instance service implementation, get all config from redis cache when init and listen on redis channel for change
 *
 * @author zhongjie
 */
@Service
public class RpcInstanceServiceImpl implements RpcInstanceService {
    private static final Logger LOGGER = LoggerFactory.getLogger(RpcInstanceServiceImpl.class);
    /**
     *  redis rpc service change channel
     */
    private static final String RPC_SERVICE_CHANNEL = "fizz_rpc_service_channel";
    /**
     * redis rpc service info hash key
     */
    private static final String RPC_SERVICE_HASH_KEY = "fizz_rpc_service";

    /**
     * key pattern of {@link #serviceToInstancesMap}, {@link #serviceToLoadBalanceTypeMap} and {@link #serviceToCountMap}
     * {rpc type}-{service name}
     */
    private static final String SERVICE_KEY_PATTERN = "%s-%s";

    private static final Byte LOAD_BALANCE_TYPE_ROUND_ROBIN = 1;
    private static final Byte LOAD_BALANCE_TYPE_RANDOM = 2;

    private static Map> serviceToInstancesMap = new ConcurrentHashMap<>(32);
    private static Map serviceToLoadBalanceTypeMap = new ConcurrentHashMap<>(32);
    private static Map idToRpcServiceMap = new ConcurrentHashMap<>(32);
    private static Map serviceToCountMap = new ConcurrentHashMap<>(32);

    @Resource(name = AggregateRedisConfig.AGGREGATE_REACTIVE_REDIS_TEMPLATE)
    private ReactiveStringRedisTemplate redisTemplate;

    @PostConstruct
    public void init() throws Throwable {
        this.init(this::lsnRpcServiceChange);
    }

    @Override
    public void refreshLocalCache() throws Throwable {
        this.init(null);
    }

    private void init(Supplier> doAfterLoadCache) throws Throwable {
        Map> serviceToInstancesMapTmp = new ConcurrentHashMap<>(32);
        Map serviceToLoadBalanceTypeMapTmp = new ConcurrentHashMap<>(32);
        Map idToRpcServiceMapTmp = new ConcurrentHashMap<>(32);
        Map serviceToCountMapTmp = new ConcurrentHashMap<>(32);

        final Throwable[] throwable = new Throwable[1];
        Throwable error = Mono.just(Objects.requireNonNull(redisTemplate.opsForHash().entries(RPC_SERVICE_HASH_KEY)
                .defaultIfEmpty(new AbstractMap.SimpleEntry<>(ReactorUtils.OBJ, ReactorUtils.OBJ)).onErrorStop().doOnError(t -> LOGGER.info(null, t))
                .concatMap(e -> {
                    Object k = e.getKey();
                    if (k == ReactorUtils.OBJ) {
                        return Flux.just(e);
                    }
                    Object v = e.getValue();
                    // LOGGER.info(k.toString() + Consts.S.COLON + v.toString(), LogService.BIZ_ID, k.toString());
                    ThreadContext.put(Consts.TRACE_ID, k.toString());
                    LOGGER.info(k.toString() + Consts.S.COLON + v.toString());
                    String json = (String) v;
                    try {
                        RpcService rpcService = JacksonUtils.readValue(json, RpcService.class);
                        this.updateLocalCache(rpcService, serviceToInstancesMapTmp, serviceToLoadBalanceTypeMapTmp,
                                idToRpcServiceMapTmp, serviceToCountMapTmp);
                        return Flux.just(e);
                    } catch (Throwable t) {
                        throwable[0] = t;
                        LOGGER.info(json, t);
                        return Flux.error(t);
                    }
                }).blockLast())).flatMap(
                e -> {
                    if (throwable[0] != null) {
                        return Mono.error(throwable[0]);
                    }

                    if (doAfterLoadCache != null) {
                        return doAfterLoadCache.get();
                    } else {
                        return Mono.just(ReactorUtils.EMPTY_THROWABLE);
                    }
                }
        ).block();
        if (error != ReactorUtils.EMPTY_THROWABLE) {
            assert error != null;
            throw error;
        }

        serviceToInstancesMap = serviceToInstancesMapTmp;
        serviceToLoadBalanceTypeMap = serviceToLoadBalanceTypeMapTmp;
        idToRpcServiceMap = idToRpcServiceMapTmp;
        serviceToCountMap = serviceToCountMapTmp;
    }

    @Override
    public String getInstance(RpcTypeEnum rpcTypeEnum, String service) {
        Byte loadBalanceType = serviceToLoadBalanceTypeMap.get(this.getServiceKey(rpcTypeEnum.getType(), service));
        if (LOAD_BALANCE_TYPE_RANDOM.equals(loadBalanceType)) {
            LOGGER.debug("type:{} service:{} get instance random", rpcTypeEnum, service);
            return this.getInstanceRandom(rpcTypeEnum, service);
        } else {
            LOGGER.debug("type:{} service:{} get instance round-robin", rpcTypeEnum, service);
            return this.getInstanceRoundRobin(rpcTypeEnum, service);
        }
    }

    private String getInstanceRandom(RpcTypeEnum rpcTypeEnum, String service) {
        List instanceList = this.getAllInstance(rpcTypeEnum, service);
        if (CollectionUtils.isEmpty(instanceList)) {
            return null;
        }
        if (instanceList.size() == 1) {
            return instanceList.get(0);
        }

        return instanceList.get(ThreadLocalRandom.current().nextInt(instanceList.size()));
    }

    private String getInstanceRoundRobin(RpcTypeEnum rpcTypeEnum, String service) {
        List instanceList = this.getAllInstance(rpcTypeEnum, service);
        if (CollectionUtils.isEmpty(instanceList)) {
            return null;
        }
        if (instanceList.size() == 1) {
            return instanceList.get(0);
        }

        long currentCount = serviceToCountMap.computeIfAbsent(this.getServiceKey(rpcTypeEnum.getType(), service),
                it -> new AtomicLong()).getAndIncrement();
        return instanceList.get((int)currentCount % instanceList.size());
    }

    private List getAllInstance(RpcTypeEnum rpcTypeEnum, String service) {
        return serviceToInstancesMap.get(this.getServiceKey(rpcTypeEnum.getType(), service));
    }

    private Mono lsnRpcServiceChange() {
        final Throwable[] throwable = new Throwable[1];
        final boolean[] b = {false};
        redisTemplate.listenToChannel(RPC_SERVICE_CHANNEL).doOnError(t -> {
            throwable[0] = t;
            b[0] = false;
            LOGGER.error("lsn " + RPC_SERVICE_CHANNEL, t);
        }).doOnSubscribe(
                s -> {
                    b[0] = true;
                    LOGGER.info("success to lsn on " + RPC_SERVICE_CHANNEL);
                }
        ).doOnNext(msg -> {
            String json = msg.getMessage();
            // LOGGER.info(json, LogService.BIZ_ID, "rpc" + System.currentTimeMillis());
            ThreadContext.put(Consts.TRACE_ID, "rpc" + System.currentTimeMillis());
            LOGGER.info(json);
            try {
                RpcService rpcService = JacksonUtils.readValue(json, RpcService.class);
                this.updateLocalCache(rpcService, serviceToInstancesMap, serviceToLoadBalanceTypeMap, idToRpcServiceMap,
                        serviceToCountMap);
            } catch (Throwable t) {
                LOGGER.info(json, t);
            }
        }).subscribe();
        Throwable t = throwable[0];
        while (!b[0]) {
            if (t != null) {
                return Mono.error(t);
            } else {
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    return Mono.error(e);
                }
            }
        }
        return Mono.just(ReactorUtils.EMPTY_THROWABLE);
    }

    private void updateLocalCache(RpcService rpcService, Map> serviceToInstancesMap,
                                  Map serviceToLoadBalanceTypeMap, Map idToRpcServiceMap,
                                  Map serviceToCountMap) {
        if (rpcService.getType() == null) {
            // historical gRPC data type and loadBalanceType is null, here set default value
            rpcService.setType(RpcTypeEnum.gRPC.getType());
            rpcService.setLoadBalanceType(LOAD_BALANCE_TYPE_ROUND_ROBIN);
        }

        if (rpcService.getIsDeleted() == RpcService.DELETED) {
            RpcService removedRpcService = idToRpcServiceMap.remove(rpcService.getId());
            LOGGER.info("remove {}", removedRpcService);
            if (removedRpcService != null) {
                serviceToInstancesMap.remove(this.getServiceKey(rpcService));
                serviceToLoadBalanceTypeMap.remove(this.getServiceKey(rpcService));
                serviceToCountMap.remove(this.getServiceKey(rpcService));
            }
        } else {
            RpcService existRpcService = idToRpcServiceMap.get(rpcService.getId());
            idToRpcServiceMap.put(rpcService.getId(), rpcService);
            if (existRpcService == null) {
                LOGGER.info("add {}", rpcService);
            } else {
                LOGGER.info("update {} with {}", existRpcService, rpcService);
                serviceToInstancesMap.remove(this.getServiceKey(existRpcService));
                serviceToLoadBalanceTypeMap.remove(this.getServiceKey(existRpcService));
                serviceToCountMap.remove(this.getServiceKey(existRpcService));
            }
            serviceToInstancesMap.put(this.getServiceKey(rpcService), rpcService.getInstance() == null ? Collections.emptyList() :
                    Arrays.asList(rpcService.getInstance().split(",")));
            serviceToLoadBalanceTypeMap.put(this.getServiceKey(rpcService), rpcService.getLoadBalanceType());
        }
    }

    private String getServiceKey(RpcService rpcService) {
        return String.format(SERVICE_KEY_PATTERN, rpcService.getType(), rpcService.getService());
    }

    private String getServiceKey(Byte type, String service) {
        return String.format(SERVICE_KEY_PATTERN, type, service);
    }

    static class RpcService {
        private static final int DELETED = 1;
        private Long id;
        private Integer isDeleted;
        private String service;
        private String instance;
        /**
         * RPC type: 2-gRPC 3-HTTP
         */
        private Byte type;
        /**
         * load balance type: 1-round-robin 2-random
         */
        private Byte loadBalanceType;

        @Override
        public String toString() {
            return JacksonUtils.writeValueAsString(this);
        }

        public Long getId() {
            return id;
        }

        public void setId(Long id) {
            this.id = id;
        }

        public Integer getIsDeleted() {
            return isDeleted;
        }

        public void setDeleted(Integer isDeleted) {
            this.isDeleted = isDeleted;
        }

        public String getService() {
            return service;
        }

        public void setService(String service) {
            this.service = service;
        }

        public String getInstance() {
            return instance;
        }

        public void setInstance(String instance) {
            this.instance = instance;
        }

        public Byte getType() {
            return type;
        }

        public void setType(Byte type) {
            this.type = type;
        }

        public Byte getLoadBalanceType() {
            return loadBalanceType;
        }

        public void setLoadBalanceType(Byte loadBalanceType) {
            this.loadBalanceType = loadBalanceType;
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy