package com.ctrip.framework.apollo.portal.component;
import com.ctrip.framework.apollo.common.exception.ServiceException;
import com.ctrip.framework.apollo.core.MetaDomainConsts;
import com.ctrip.framework.apollo.core.dto.ServiceDTO;
import com.ctrip.framework.apollo.core.enums.Env;
import com.ctrip.framework.apollo.portal.constant.CatEventType;
import com.ctrip.framework.apollo.tracer.Tracer;
import com.ctrip.framework.apollo.tracer.spi.Transaction;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.HttpHostConnectException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.DefaultUriTemplateHandler;
import org.springframework.web.util.UriTemplateHandler;
import java.net.SocketTimeoutException;
import java.util.List;
import javax.annotation.PostConstruct;
/**
* 封装RestTemplate. admin server集群在某些机器宕机或者超时的情况下轮询重试
*/
@Component
public class RetryableRestTemplate {
private Logger logger = LoggerFactory.getLogger(RetryableRestTemplate.class);
private UriTemplateHandler uriTemplateHandler = new DefaultUriTemplateHandler();
private RestTemplate restTemplate;
@Autowired
private RestTemplateFactory restTemplateFactory;
@Autowired
private AdminServiceAddressLocator adminServiceAddressLocator;
@PostConstruct
private void postConstruct() {
restTemplate = restTemplateFactory.getObject();
}
public <T> T get(Env env, String path, Class<T> responseType, Object... urlVariables)
throws RestClientException {
return execute(HttpMethod.GET, env, path, null, responseType, urlVariables);
}
public <T> ResponseEntity<T> get(Env env, String path, ParameterizedTypeReference<T> reference,
Object... uriVariables)
throws RestClientException {
return exchangeGet(env, path, reference, uriVariables);
}
public <T> T post(Env env, String path, Object request, Class<T> responseType, Object... uriVariables)
throws RestClientException {
return execute(HttpMethod.POST, env, path, request, responseType, uriVariables);
}
public void put(Env env, String path, Object request, Object... urlVariables) throws RestClientException {
execute(HttpMethod.PUT, env, path, request, null, urlVariables);
}
public void delete(Env env, String path, Object... urlVariables) throws RestClientException {
execute(HttpMethod.DELETE, env, path, null, null, urlVariables);
}
private <T> T execute(HttpMethod method, Env env, String path, Object request, Class<T> responseType,
Object... uriVariables) {
if (path.startsWith("/")) {
path = path.substring(1, path.length());
}
String uri = uriTemplateHandler.expand(path, uriVariables).getPath();
Transaction ct = Tracer.newTransaction("AdminAPI", uri);
ct.addData("Env", env);
List<ServiceDTO> services = getAdminServices(env, ct);
for (ServiceDTO serviceDTO : services) {
try {
T result = doExecute(method, serviceDTO, path, request, responseType, uriVariables);
ct.setStatus(Transaction.SUCCESS);
ct.complete();
return result;
} catch (Throwable t) {
logger.error("Http request failed, uri: {}, method: {}", uri, method, t);
Tracer.logError(t);
if (canRetry(t, method)) {
Tracer.logEvent(CatEventType.API_RETRY, uri);
} else {//biz exception rethrow
ct.setStatus(t);
ct.complete();
throw t;
}
}
}
//all admin server down
ServiceException e =
new ServiceException(String.format("Admin servers are unresponsive. meta server address: %s, admin servers: %s",
MetaDomainConsts.getDomain(env), services));
ct.setStatus(e);
ct.complete();
throw e;
}
private <T> ResponseEntity<T> exchangeGet(Env env, String path, ParameterizedTypeReference<T> reference,
Object... uriVariables) {
if (path.startsWith("/")) {
path = path.substring(1, path.length());
}
String uri = uriTemplateHandler.expand(path, uriVariables).getPath();
Transaction ct = Tracer.newTransaction("AdminAPI", uri);
ct.addData("Env", env);
List<ServiceDTO> services = getAdminServices(env, ct);
for (ServiceDTO serviceDTO : services) {
try {
ResponseEntity<T> result =
restTemplate.exchange(parseHost(serviceDTO) + path, HttpMethod.GET, null, reference, uriVariables);
ct.setStatus(Transaction.SUCCESS);
ct.complete();
return result;
} catch (Throwable t) {
logger.error("Http request failed, uri: {}, method: {}", uri, HttpMethod.GET, t);
Tracer.logError(t);
if (canRetry(t, HttpMethod.GET)) {
Tracer.logEvent(CatEventType.API_RETRY, uri);
} else {// biz exception rethrow
ct.setStatus(t);
ct.complete();
throw t;
}
}
}
//all admin server down
ServiceException e =
new ServiceException(String.format("Admin servers are unresponsive. meta server address: %s, admin servers: %s",
MetaDomainConsts.getDomain(env), services));
ct.setStatus(e);
ct.complete();
throw e;
}
private List<ServiceDTO> getAdminServices(Env env, Transaction ct) {
List<ServiceDTO> services = adminServiceAddressLocator.getServiceList(env);
if (CollectionUtils.isEmpty(services)) {
ServiceException e = new ServiceException(String.format("No available admin server."
+ " Maybe because of meta server down or all admin server down. "
+ "Meta server address: %s",
MetaDomainConsts.getDomain(env)));
ct.setStatus(e);
ct.complete();
throw e;
}
return services;
}
private <T> T doExecute(HttpMethod method, ServiceDTO service, String path, Object request,
Class<T> responseType,
Object... uriVariables) {
T result = null;
switch (method) {
case GET:
result = restTemplate.getForObject(parseHost(service) + path, responseType, uriVariables);
break;
case POST:
result =
restTemplate.postForEntity(parseHost(service) + path, request, responseType, uriVariables).getBody();
break;
case PUT:
restTemplate.put(parseHost(service) + path, request, uriVariables);
break;
case DELETE:
restTemplate.delete(parseHost(service) + path, uriVariables);
break;
default:
throw new UnsupportedOperationException(String.format("unsupported http method(method=%s)", method));
}
return result;
}
private String parseHost(ServiceDTO serviceAddress) {
return serviceAddress.getHomepageUrl() + "/";
}
//post,delete,put请求在admin server处理超时情况下不重试
private boolean canRetry(Throwable e, HttpMethod method) {
Throwable nestedException = e.getCause();
if (method == HttpMethod.GET) {
return nestedException instanceof SocketTimeoutException
|| nestedException instanceof HttpHostConnectException
|| nestedException instanceof ConnectTimeoutException;
} else {
return nestedException instanceof HttpHostConnectException
|| nestedException instanceof ConnectTimeoutException;
}
}
}