Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: 비동기 스케쥴링 간 스케쥴러 레이스컨디션 #44

Merged
merged 3 commits into from
Nov 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 5 additions & 38 deletions src/main/java/io/codef/api/CodefExecutorManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,49 +6,21 @@
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

// 실행기 관리를 위한 클래스
public class CodefExecutorManager implements AutoCloseable {
private final ScheduledExecutorService scheduler;
public class CodefExecutorManager {

private final Executor virtualThreadExecutor;

private CodefExecutorManager(ScheduledExecutorService scheduler, Executor virtualThreadExecutor) {
this.scheduler = scheduler;
private CodefExecutorManager(Executor virtualThreadExecutor) {
this.virtualThreadExecutor = virtualThreadExecutor;
}

public static CodefExecutorManager create() {
return new CodefExecutorManager(
Executors.newScheduledThreadPool(1),
Executors.newThreadPerTaskExecutor(Thread.ofVirtual().factory())
);
}

public CompletableFuture<EasyCodefResponse> scheduleRequest(
EasyCodefRequest request,
long delayMs,
SingleReqFacade facade
) {
return scheduleDelayedExecution(delayMs)
.thenComposeAsync(
ignored -> executeRequest(request, facade),
virtualThreadExecutor
);
}

private CompletableFuture<Void> scheduleDelayedExecution(long delayMs) {
CompletableFuture<Void> future = new CompletableFuture<>();
scheduler.schedule(
() -> future.complete(null),
delayMs,
TimeUnit.MILLISECONDS
);
return future;
Executors.newThreadPerTaskExecutor(Thread.ofVirtual().factory()));
}

private CompletableFuture<EasyCodefResponse> executeRequest(
public CompletableFuture<EasyCodefResponse> executeRequest(
EasyCodefRequest request,
SingleReqFacade facade
) {
Expand All @@ -57,9 +29,4 @@ private CompletableFuture<EasyCodefResponse> executeRequest(
virtualThreadExecutor
);
}

@Override
public void close() {
scheduler.shutdown();
}
}
105 changes: 74 additions & 31 deletions src/main/java/io/codef/api/ResponseHandler.java
Original file line number Diff line number Diff line change
@@ -1,62 +1,83 @@
package io.codef.api;

import static io.codef.api.dto.EasyCodefRequest.ACCESS_TOKEN;
import static io.codef.api.dto.EasyCodefResponse.DATA;
import static io.codef.api.dto.EasyCodefResponse.RESULT;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import io.codef.api.constants.CodefResponseCode;
import io.codef.api.dto.EasyCodefResponse;
import io.codef.api.error.CodefError;
import io.codef.api.error.CodefException;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.ParseException;
import org.apache.hc.core5.http.io.entity.EntityUtils;

import java.io.IOException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.ParseException;
import org.apache.hc.core5.http.io.entity.EntityUtils;

import static io.codef.api.dto.EasyCodefRequest.ACCESS_TOKEN;
import static io.codef.api.dto.EasyCodefResponse.DATA;
import static io.codef.api.dto.EasyCodefResponse.RESULT;

public class ResponseHandler {

private static final String UTF_8 = StandardCharsets.UTF_8.toString();

public static boolean isSuccessResponse(EasyCodefResponse response) {
return CodefResponseCode.CF_00000.equals(response.code());
}

public static boolean isAddAuthResponse(EasyCodefResponse response) {
return CodefResponseCode.CF_03002.equals(response.code());
}

public static boolean isAddAuthExceedResponse(EasyCodefResponse response) {
return CodefResponseCode.CF_12872.equals(response.code());
}

public static boolean isFailureResponse(EasyCodefResponse response) {
return !isSuccessResponse(response) && !isAddAuthResponse(response);
}

/**
* 토큰 응답 처리
*/
public String handleTokenResponse(ClassicHttpResponse response) throws CodefException {
return handleHttpResponse(
response,
this::parseAccessToken,
CodefError.OAUTH_UNAUTHORIZED,
CodefError.OAUTH_INTERNAL_ERROR,
false
response,
this::parseAccessToken,
CodefError.OAUTH_UNAUTHORIZED,
CodefError.OAUTH_INTERNAL_ERROR,
false
);
}

/**
* 상품 응답 처리
*/
public EasyCodefResponse handleProductResponse(ClassicHttpResponse response) throws CodefException {
public EasyCodefResponse handleProductResponse(ClassicHttpResponse response)
throws CodefException {
return handleHttpResponse(
response,
this::parseProductResponse,
CodefError.CODEF_API_UNAUTHORIZED,
CodefError.CODEF_API_SERVER_ERROR,
true
response,
this::parseProductResponse,
CodefError.CODEF_API_UNAUTHORIZED,
CodefError.CODEF_API_SERVER_ERROR,
true
);
}

/**
* 공통 HTTP 응답 처리 로직
*/
private <T> T handleHttpResponse(
ClassicHttpResponse response,
Function<String, T> parser,
CodefError unauthorizedError,
CodefError defaultError,
boolean requireUrlDecoding
ClassicHttpResponse response,
Function<String, T> parser,
CodefError unauthorizedError,
CodefError defaultError,
boolean requireUrlDecoding
) throws CodefException {
String responseBody = extractResponseBody(response, requireUrlDecoding);

Expand All @@ -70,7 +91,8 @@ private <T> T handleHttpResponse(
/**
* HTTP 응답 본문 추출
*/
private String extractResponseBody(ClassicHttpResponse response, boolean requiresDecoding) throws CodefException {
private String extractResponseBody(ClassicHttpResponse response, boolean requiresDecoding)
throws CodefException {
try {
String responseBody = EntityUtils.toString(response.getEntity());
return requiresDecoding ? URLDecoder.decode(responseBody, UTF_8) : responseBody;
Expand Down Expand Up @@ -99,17 +121,38 @@ private EasyCodefResponse parseProductResponse(String responseBody) throws Codef
try {
JSONObject jsonResponse = JSON.parseObject(responseBody);

EasyCodefResponse.Result result = Optional.ofNullable(jsonResponse.getJSONObject(RESULT))
EasyCodefResponse.Result result = parseResult(jsonResponse);
Object data = parseData(jsonResponse);

return new EasyCodefResponse(result, data);
} catch (Exception exception) {
throw CodefException.of(CodefError.PARSE_ERROR, exception);
}
}

private EasyCodefResponse.Result parseResult(JSONObject jsonResponse) throws CodefException {
return Optional.ofNullable(jsonResponse.getJSONObject(RESULT))
.map(object -> object.to(EasyCodefResponse.Result.class))
.orElseThrow(() -> CodefException.from(CodefError.PARSE_ERROR));
}

Object data = Optional.ofNullable(jsonResponse.getJSONObject(DATA))
private Object parseData(JSONObject jsonResponse) throws CodefException {
try {
return parseObjectData(jsonResponse);
} catch (Exception e) {
return parseArrayData(jsonResponse);
}
}

private Object parseObjectData(JSONObject jsonResponse) throws CodefException {
return Optional.ofNullable(jsonResponse.getJSONObject(DATA))
.map(obj -> obj.to(Object.class))
.orElseThrow(() -> CodefException.from(CodefError.PARSE_ERROR));
}

return new EasyCodefResponse(result, data);
} catch (Exception e) {
throw CodefException.of(CodefError.PARSE_ERROR, e);
}
private List<?> parseArrayData(JSONObject jsonResponse) throws CodefException {
return Optional.ofNullable(jsonResponse.getJSONArray(DATA))
.map(obj -> obj.to(List.class))
.orElseThrow(() -> CodefException.from(CodefError.PARSE_ERROR));
}
}
101 changes: 101 additions & 0 deletions src/main/java/io/codef/api/ResponseLogger.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package io.codef.api;

import com.alibaba.fastjson2.JSON;
import io.codef.api.dto.EasyCodefResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;
import java.util.Optional;

import static io.codef.api.constants.CodefResponseCode.*;

public class ResponseLogger {
private static final Logger log = LoggerFactory.getLogger(ResponseLogger.class);

private ResponseLogger() {
}


public static void logResponseStatus(EasyCodefResponse response) {
logBasicInfo(response);

switch (response.code()) {
case CF_00000:
logSuccessResponse();
break;

case CF_03002:
logAddAuthRequired(response);
break;

case CF_12872:
logAuthExceeded();
break;

default:
logError(response);
break;
}
}

public static void logStatusSummary(List<EasyCodefResponse> responses) {
long successCount = responses.stream().filter(ResponseHandler::isSuccessResponse).count();
long addAuthCount = responses.stream().filter(ResponseHandler::isAddAuthResponse).count();
long failureCount = responses.stream().filter(ResponseHandler::isFailureResponse).count();

log.info("Total Responses Count = {}\n", responses.size());
logStatus(successCount, addAuthCount, failureCount);
}

private static void logBasicInfo(EasyCodefResponse response) {
log.info("Response Status Code : {}", response.code());
log.info("Transaction Id : {}", response.transactionId());
}

private static void logSuccessResponse() {
log.info("Successfully returned Value from Codef API\n");
}

private static void logAddAuthRequired(EasyCodefResponse response) {
Object data = response.data();
String addAuthMethod = JSON.parseObject(data.toString()).getString("method");
log.warn("Additional authentication required | method : {}\n", addAuthMethod);
}

private static void logAuthExceeded() {
log.error("Retry limit for additional authentication exceeded. "
+ "Please restart the process from the initial request.\n");
}

private static void logError(EasyCodefResponse response) {
log.error("Failed to get proper scrapping response. Check the Error errorMessage And StatusCode");
log.error("> message : {}", response.result().message());
log.error("> extraMessage : {}", response.result().extraMessage());
}

private static void logStatus(
long successCount,
long addAuthCount,
long failureCount
) {
Optional.of(successCount)
.filter(ResponseLogger::isExist)
.ifPresent(count -> log.info("Success Response Status [ {} ] Count : {}", CF_00000, count));

Optional.of(addAuthCount)
.filter(ResponseLogger::isExist)
.ifPresent(count -> log.info("AddAuth Response Status [ {} ] Count : {}", CF_03002, count));

Optional.of(failureCount)
.filter(ResponseLogger::isExist)
.ifPresentOrElse(
count -> log.warn("Failure Response Status [ Else ] Count : {}\n", count),
() -> log.info("No Failure Responses\n")
);
}

private static boolean isExist(Long count) {
return count > 0;
}
}
Loading