package com.cyfonly.thriftj.failover;

import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.cyfonly.thriftj.pool.ThriftServer;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.EvictingQueue;


/**
 * failover 策略
 * @author yunfeng.cheng
 * @create 2016-11-19
 * @param <T>
 */
public class FailoverStrategy<T> {
	
	private final Logger logger = LoggerFactory.getLogger(getClass());
	
	private static final int DEFAULT_FAIL_COUNT = 10;
	private static final long DEFAULT_FAIL_DURATION = TimeUnit.MINUTES.toMillis(1);
	private static final long DEFAULT_RECOVER_DURATION = TimeUnit.MINUTES.toMillis(1);
	
	private final long failDuration;
	private final Cache<T, Boolean> failedList;
	private final LoadingCache<T, EvictingQueue<Long>> failCountMap;
	
	/**
	 * 使用默认 failover 策略
	 */
	public FailoverStrategy() {
		this(DEFAULT_FAIL_COUNT, DEFAULT_FAIL_DURATION, DEFAULT_RECOVER_DURATION);
	}
	
	/**
	 * 自定义 failover 策略
	 * @param failCount 失败次数
	 * @param failDuration 失效持续时间
	 * @param recoverDuration 恢复持续时间
	 */
	public FailoverStrategy(final int failCount, long failDuration, long recoverDuration) {
		this.failDuration = failDuration;
		this.failedList = CacheBuilder.newBuilder().weakKeys().expireAfterWrite(recoverDuration, TimeUnit.MILLISECONDS).build();
		this.failCountMap = CacheBuilder.newBuilder().weakKeys().build(new CacheLoader<T, EvictingQueue<Long>>() {
			@Override
			public EvictingQueue<Long> load(T key) throws Exception {
				return EvictingQueue.create(failCount);
			}
		});
	}
	
	public void fail(T object) {
		logger.info("Server {}:{} failed.", ((ThriftServer)object).getHost(),((ThriftServer)object).getPort());
		boolean addToFail = false;
		try {
			EvictingQueue<Long> evictingQueue = failCountMap.get(object);
			synchronized (evictingQueue) {
				evictingQueue.add(System.currentTimeMillis());
				if (evictingQueue.remainingCapacity() == 0 && evictingQueue.element() >= (System.currentTimeMillis() - failDuration)) {
					addToFail = true;
				}
			}
		} catch (ExecutionException e) {
			logger.error("Ops.", e);
		}
		if (addToFail) {
			failedList.put(object, Boolean.TRUE);
			logger.info("Server {}:{} failed. Add to fail list.", ((ThriftServer)object).getHost(), ((ThriftServer)object).getPort());
		}
	}
	
	public Set<T> getFailed() {
		return failedList.asMap().keySet();
	}
}