Retry 기능 추가
This commit is contained in:
+174
@@ -0,0 +1,174 @@
|
|||||||
|
package com.eactive.eai.adapter.http.client.impl;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.NoRouteToHostException;
|
||||||
|
import java.net.SocketTimeoutException;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import org.apache.hc.client5.http.ConnectTimeoutException;
|
||||||
|
import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
|
||||||
|
import org.apache.hc.client5.http.config.RequestConfig;
|
||||||
|
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
|
||||||
|
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
|
||||||
|
import org.apache.hc.core5.http.protocol.HttpContext;
|
||||||
|
|
||||||
|
import com.eactive.eai.common.util.Logger;
|
||||||
|
|
||||||
|
|
||||||
|
public class DeadlineAwareRetryExecutor {
|
||||||
|
|
||||||
|
protected static Logger log = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||||
|
|
||||||
|
private final CloseableHttpClient httpClient;
|
||||||
|
private final int connectTimeoutMs;
|
||||||
|
private final int responseTimeoutMs;
|
||||||
|
|
||||||
|
public DeadlineAwareRetryExecutor(CloseableHttpClient httpClient,
|
||||||
|
int connectTimeoutMs,
|
||||||
|
int responseTimeoutMs) {
|
||||||
|
this.httpClient = httpClient;
|
||||||
|
this.connectTimeoutMs = connectTimeoutMs;
|
||||||
|
this.responseTimeoutMs = responseTimeoutMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CloseableHttpResponse execute(HttpUriRequestBase request, HttpContext context, RetryPolicy policy)
|
||||||
|
throws IOException {
|
||||||
|
|
||||||
|
// retry 비활성 → 단순 실행
|
||||||
|
if (policy.isDisabled()) {
|
||||||
|
applyTimeout(request, connectTimeoutMs, responseTimeoutMs);
|
||||||
|
return httpClient.execute(request, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
long totalDeadlineMs = connectTimeoutMs + responseTimeoutMs;
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
IOException lastException = null;
|
||||||
|
|
||||||
|
for (int attempt = 0; attempt <= policy.getMaxRetries(); attempt++) {
|
||||||
|
|
||||||
|
// ① 남은 시간 계산
|
||||||
|
long elapsed = System.currentTimeMillis() - startTime;
|
||||||
|
long remainingMs = totalDeadlineMs - elapsed;
|
||||||
|
|
||||||
|
if (remainingMs <= 0) {
|
||||||
|
throw new IOException(
|
||||||
|
String.format("전체 deadline 초과 [elapsed=%dms, deadline=%dms]",
|
||||||
|
elapsed, totalDeadlineMs),
|
||||||
|
lastException
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ② 이번 시도의 effective timeout = min(원래 설정, 남은 시간)
|
||||||
|
long effectiveConnect = Math.min(connectTimeoutMs, remainingMs);
|
||||||
|
long effectiveResponse = Math.min(responseTimeoutMs, remainingMs);
|
||||||
|
applyTimeout(request, effectiveConnect, effectiveResponse);
|
||||||
|
|
||||||
|
log.debug("attempt={}, remainingMs={}, effectiveConnect={}, effectiveResponse={}",
|
||||||
|
attempt + 1, remainingMs, effectiveConnect, effectiveResponse);
|
||||||
|
|
||||||
|
// ③ 요청 실행
|
||||||
|
CloseableHttpResponse response = null;
|
||||||
|
boolean doRetry = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
response = httpClient.execute(request, context);
|
||||||
|
int status = response.getCode();
|
||||||
|
|
||||||
|
if (!policy.shouldRetryOnStatus(request, status)) {
|
||||||
|
// 성공 또는 재시도 불필요 → 호출부에서 close 책임
|
||||||
|
log.debug("non-retryable response status={}, attempt={}", status, attempt + 1);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 재시도 대상
|
||||||
|
log.warn("retryable status={}, attempt={}/{}",
|
||||||
|
status, attempt + 1, policy.getMaxRetries());
|
||||||
|
lastException = new IOException("재시도 가능 상태코드: " + status);
|
||||||
|
doRetry = true;
|
||||||
|
|
||||||
|
} catch (ConnectTimeoutException e) {
|
||||||
|
if (!policy.shouldRetryOnException(e)) throw e;
|
||||||
|
log.warn("{} attempt={}/{}", e.getClass().getSimpleName(),
|
||||||
|
attempt + 1, policy.getMaxRetries());
|
||||||
|
lastException = e;
|
||||||
|
doRetry = true;
|
||||||
|
|
||||||
|
} catch (SocketTimeoutException e) {
|
||||||
|
if (!policy.shouldRetryOnException(e)) throw e;
|
||||||
|
log.warn("{} attempt={}/{}", e.getClass().getSimpleName(),
|
||||||
|
attempt + 1, policy.getMaxRetries());
|
||||||
|
lastException = e;
|
||||||
|
doRetry = true;
|
||||||
|
|
||||||
|
} catch (NoRouteToHostException e) {
|
||||||
|
if (!policy.shouldRetryOnException(e)) throw e;
|
||||||
|
log.warn("{} attempt={}/{}", e.getClass().getSimpleName(),
|
||||||
|
attempt + 1, policy.getMaxRetries());
|
||||||
|
lastException = e;
|
||||||
|
doRetry = true;
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
// 재시도 대상인 경우에만 여기서 close
|
||||||
|
// return된 response는 호출부에서 close 책임이므로 제외
|
||||||
|
if (doRetry && response != null) {
|
||||||
|
closeQuietly(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ④ 마지막 시도였으면 바로 종료
|
||||||
|
if (attempt >= policy.getMaxRetries()) break;
|
||||||
|
|
||||||
|
// ⑤ 백오프 대기 전 남은 시간 재확인
|
||||||
|
long remainingBeforeWait = totalDeadlineMs - (System.currentTimeMillis() - startTime);
|
||||||
|
if (remainingBeforeWait <= policy.getBackoffMs()) {
|
||||||
|
throw new IOException(
|
||||||
|
String.format("백오프 대기 시간 부족 [remaining=%dms, backoff=%dms]",
|
||||||
|
remainingBeforeWait, policy.getBackoffMs()),
|
||||||
|
lastException
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
log.debug("backoff sleep {}ms before attempt={}", policy.getBackoffMs(), attempt + 2);
|
||||||
|
Thread.sleep(policy.getBackoffMs());
|
||||||
|
} catch (InterruptedException ie) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new IOException("retry 대기 중 interrupt 발생", ie);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IOException(
|
||||||
|
String.format("최대 재시도 횟수 초과 [maxRetries=%d]", policy.getMaxRetries()),
|
||||||
|
lastException
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// private helpers
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
private void applyTimeout(HttpUriRequestBase request, long connectTimeoutMs, long responseTimeoutMs) {
|
||||||
|
RequestConfig existing = request.getConfig(); // configureHttpClient()에서 설정한 값
|
||||||
|
|
||||||
|
RequestConfig.Builder builder = (existing != null) ? RequestConfig.copy(existing) // HttpVersion 등 기존 설정 보존
|
||||||
|
: RequestConfig.custom();
|
||||||
|
|
||||||
|
RequestConfig config = builder.setConnectTimeout(connectTimeoutMs, TimeUnit.MILLISECONDS)
|
||||||
|
.setResponseTimeout(responseTimeoutMs, TimeUnit.MILLISECONDS).build();
|
||||||
|
|
||||||
|
request.setConfig(config);
|
||||||
|
log.info("RequestConfig = {}", config);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isSuccess(int status) {
|
||||||
|
return status >= 200 && status < 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void closeQuietly(CloseableHttpResponse response) {
|
||||||
|
try {
|
||||||
|
response.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.debug("response close 실패 (무시)", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
-4
@@ -12,12 +12,10 @@ import java.util.ArrayList;
|
|||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
@@ -31,12 +29,13 @@ import org.apache.commons.lang3.time.StopWatch;
|
|||||||
import org.apache.hc.client5.http.classic.HttpClient;
|
import org.apache.hc.client5.http.classic.HttpClient;
|
||||||
import org.apache.hc.client5.http.classic.methods.HttpDelete;
|
import org.apache.hc.client5.http.classic.methods.HttpDelete;
|
||||||
import org.apache.hc.client5.http.classic.methods.HttpGet;
|
import org.apache.hc.client5.http.classic.methods.HttpGet;
|
||||||
|
import org.apache.hc.client5.http.classic.methods.HttpPatch;
|
||||||
import org.apache.hc.client5.http.classic.methods.HttpPost;
|
import org.apache.hc.client5.http.classic.methods.HttpPost;
|
||||||
import org.apache.hc.client5.http.classic.methods.HttpPut;
|
import org.apache.hc.client5.http.classic.methods.HttpPut;
|
||||||
import org.apache.hc.client5.http.classic.methods.HttpPatch;
|
|
||||||
import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
|
import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
|
||||||
import org.apache.hc.client5.http.config.RequestConfig;
|
import org.apache.hc.client5.http.config.RequestConfig;
|
||||||
import org.apache.hc.client5.http.entity.UrlEncodedFormEntity;
|
import org.apache.hc.client5.http.entity.UrlEncodedFormEntity;
|
||||||
|
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
|
||||||
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
|
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
|
||||||
import org.apache.hc.core5.http.ContentType;
|
import org.apache.hc.core5.http.ContentType;
|
||||||
import org.apache.hc.core5.http.Header;
|
import org.apache.hc.core5.http.Header;
|
||||||
@@ -452,7 +451,17 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
|
|||||||
logger.debug("[method getRequestHeaders]" + method.getHeaders().toString());
|
logger.debug("[method getRequestHeaders]" + method.getHeaders().toString());
|
||||||
logger.debug("[method getURI]" + method.getPath());
|
logger.debug("[method getURI]" + method.getPath());
|
||||||
}
|
}
|
||||||
try (CloseableHttpResponse response = (CloseableHttpResponse) mclient.execute(method, context)) {
|
|
||||||
|
// executor 생성 (mclient, timeout은 기존 설정 그대로)
|
||||||
|
DeadlineAwareRetryExecutor executor =
|
||||||
|
new DeadlineAwareRetryExecutor((CloseableHttpClient)mclient,
|
||||||
|
vo.getConnectionTimeout(),
|
||||||
|
vo.getTimeout());
|
||||||
|
|
||||||
|
// Properties에서 RetryPolicy 로드
|
||||||
|
RetryPolicy policy = RetryPolicy.from(prop); // 설정 로드 방식에 맞게
|
||||||
|
try (CloseableHttpResponse response = (CloseableHttpResponse) executor.execute(method, context, policy)) {
|
||||||
|
// try (CloseableHttpResponse response = (CloseableHttpResponse) mclient.execute(method, context)) {
|
||||||
status = response.getCode();
|
status = response.getCode();
|
||||||
|
|
||||||
//responseMessage = EntityUtils.toByteArray(response.getEntity());
|
//responseMessage = EntityUtils.toByteArray(response.getEntity());
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
package com.eactive.eai.adapter.http.client.impl;
|
||||||
|
|
||||||
|
import java.net.NoRouteToHostException;
|
||||||
|
import java.net.SocketTimeoutException;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Properties;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.apache.hc.client5.http.ConnectTimeoutException;
|
||||||
|
import org.apache.hc.core5.http.HttpRequest;
|
||||||
|
|
||||||
|
import com.eactive.eai.common.util.Logger;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public class RetryPolicy {
|
||||||
|
public static final String RETRY_MAX_RETRIES = "RETRY_MAX_RETRIES";
|
||||||
|
public static final String RETRY_BACKOFF_MS = "RETRY_BACKOFF_MS";
|
||||||
|
public static final String RETRY_STATUS_CODES = "RETRY_STATUS_CODES";
|
||||||
|
public static final String RETRY_ON_CONNECT_FAIL = "RETRY_ON_CONNECT_FAIL";
|
||||||
|
public static final String RETRY_ON_TIMEOUT = "RETRY_ON_TIMEOUT";
|
||||||
|
public static final String RETRY_IDEMPOTENT_ONLY = "RETRY_IDEMPOTENT_ONLY";
|
||||||
|
|
||||||
|
protected static Logger log = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||||
|
private int maxRetries;
|
||||||
|
private long backoffMs;
|
||||||
|
private Set<Integer> retryCodes;
|
||||||
|
private boolean retryOnConnectFail;
|
||||||
|
private boolean retryOnTimeout;
|
||||||
|
private boolean idempotentOnly;
|
||||||
|
private static Set<String> IDEMPOTENT_METHODS;
|
||||||
|
static {
|
||||||
|
Set<String> methods = new HashSet<>();
|
||||||
|
methods.add("GET");
|
||||||
|
methods.add("HEAD");
|
||||||
|
methods.add("OPTIONS");
|
||||||
|
// methods.add("PUT");
|
||||||
|
// methods.add("DELETE");
|
||||||
|
IDEMPOTENT_METHODS = Collections.unmodifiableSet(methods);
|
||||||
|
}
|
||||||
|
|
||||||
|
private RetryPolicy(int maxRetries, long backoffMs, Set<Integer> retryCodes, boolean retryOnConnectFail,
|
||||||
|
boolean retryOnTimeout, boolean idempotentOnly) {
|
||||||
|
this.maxRetries = maxRetries;
|
||||||
|
this.backoffMs = backoffMs;
|
||||||
|
this.retryCodes = Collections.unmodifiableSet(retryCodes);
|
||||||
|
this.retryOnConnectFail = retryOnConnectFail;
|
||||||
|
this.retryOnTimeout = retryOnTimeout;
|
||||||
|
this.idempotentOnly = idempotentOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RetryPolicy from(Properties props) {
|
||||||
|
Set<Integer> codes = new HashSet<>();
|
||||||
|
String statusCodes = props.getProperty(RETRY_STATUS_CODES);
|
||||||
|
if (statusCodes != null && !statusCodes.trim().isEmpty()) {
|
||||||
|
for (String code : statusCodes.split(",")) {
|
||||||
|
String trimmed = code.trim();
|
||||||
|
if (!trimmed.isEmpty()) {
|
||||||
|
codes.add(Integer.parseInt(trimmed));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RetryPolicy(getInt(props, RETRY_MAX_RETRIES, 0), getLong(props, RETRY_BACKOFF_MS, 1000L), codes,
|
||||||
|
getBoolean(props, RETRY_ON_CONNECT_FAIL, false), getBoolean(props, RETRY_ON_TIMEOUT, false),
|
||||||
|
getBoolean(props, RETRY_IDEMPOTENT_ONLY, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// Properties 파싱 헬퍼 (키 누락 / 타입 오류 방어)
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
|
||||||
|
private static int getInt(Properties props, String key, int defaultValue) {
|
||||||
|
String value = props.getProperty(key);
|
||||||
|
if (value == null || value.trim().isEmpty())
|
||||||
|
return defaultValue;
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(value.trim());
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
log.warn("RetryPolicy 설정값 파싱 실패 [key={}, value={}] → 기본값({}) 사용", key, value, defaultValue);
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long getLong(Properties props, String key, long defaultValue) {
|
||||||
|
String value = props.getProperty(key);
|
||||||
|
if (value == null || value.trim().isEmpty())
|
||||||
|
return defaultValue;
|
||||||
|
try {
|
||||||
|
return Long.parseLong(value.trim());
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
log.warn("RetryPolicy 설정값 파싱 실패 [key={}, value={}] → 기본값({}) 사용", key, value, defaultValue);
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// "Y"/"y" → true, 그 외 → false
|
||||||
|
private static boolean getBoolean(Properties props, String key, boolean defaultValue) {
|
||||||
|
String value = props.getProperty(key);
|
||||||
|
if (value == null || value.trim().isEmpty())
|
||||||
|
return defaultValue;
|
||||||
|
return "Y".equalsIgnoreCase(value.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDisabled() {
|
||||||
|
return maxRetries <= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean shouldRetryOnStatus(HttpRequest request, int status) {
|
||||||
|
if (idempotentOnly && !isIdempotent(request)) return false;
|
||||||
|
return retryCodes.contains(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean shouldRetryOnException(Exception e) {
|
||||||
|
if (e instanceof ConnectTimeoutException) return retryOnConnectFail;
|
||||||
|
if (e instanceof SocketTimeoutException) return retryOnTimeout;
|
||||||
|
if (e instanceof NoRouteToHostException) return retryOnConnectFail;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isIdempotent(HttpRequest request) {
|
||||||
|
return IDEMPOTENT_METHODS.contains(request.getMethod());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user