Merge pull request '[견적배치] estimateSyncJob 만성 QSP timeout — chunk 분할 송신 + 스케줄 슬롯 04:20→05:20 (bomJob 과 스왑)' (#498) from dev into dev-deploy
Reviewed-on: #498
This commit is contained in:
commit
c374ec3a82
@ -163,9 +163,11 @@ public class JobLauncherController {
|
||||
/**
|
||||
* BOM 아이템 동기화 배치
|
||||
*
|
||||
|
||||
* [2026-05-13] 04:20 으로 이동 (estimateSyncJob 과 시간 스왑).
|
||||
* 사유: estimateSyncJob 이 만성 timeout 으로 길어질 수 있어, 새벽 마지막 슬롯(05:20)으로 옮김.
|
||||
* BOM 은 GET 단일 호출(0.7초 수준)이라 슬롯 영향 無.
|
||||
*/
|
||||
@Scheduled(cron = "0 20 05 * * *")
|
||||
@Scheduled(cron = "0 20 04 * * *")
|
||||
public String bomJob() throws JobInstanceAlreadyCompleteException, JobExecutionAlreadyRunningException, JobParametersInvalidException, JobRestartException {
|
||||
|
||||
return executeScheduledJob("bomJob");
|
||||
@ -246,9 +248,12 @@ public class JobLauncherController {
|
||||
/**
|
||||
* 견적서 전송 동기화 배치
|
||||
*
|
||||
|
||||
* [2026-05-13] 05:20 으로 이동 (bomJob 과 시간 스왑).
|
||||
* 사유: 만성 QSP timeout 으로 chunk 분할 송신해도 길어질 수 있어, 새벽 마지막 슬롯에 배치.
|
||||
* 다음 스케줄(23:50 storeAdditionalJob)까지 18시간 버퍼 확보 → 후속 job 침범 위험 제거.
|
||||
* chunk 분할 로직은 EstimateSyncConfiguration 참고.
|
||||
*/
|
||||
@Scheduled(cron = "0 20 04 * * *")
|
||||
@Scheduled(cron = "0 20 05 * * *")
|
||||
public String estimateSyncJob() throws JobInstanceAlreadyCompleteException, JobExecutionAlreadyRunningException, JobParametersInvalidException, JobRestartException {
|
||||
|
||||
return executeScheduledJob("estimateSyncJob");
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
package com.interplug.qcast.batch.estimate;
|
||||
|
||||
import com.interplug.qcast.biz.estimate.EstimateService;
|
||||
import com.interplug.qcast.biz.estimate.dto.EstimateSendRequest;
|
||||
import com.interplug.qcast.biz.estimate.dto.EstimateSendResponse;
|
||||
import com.interplug.qcast.biz.estimate.dto.EstimateSyncResponse;
|
||||
import com.interplug.qcast.biz.estimate.dto.PlanSyncResponse;
|
||||
import com.interplug.qcast.util.InterfaceQsp;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.batch.core.Job;
|
||||
import org.springframework.batch.core.JobExecutionListener;
|
||||
import org.springframework.batch.core.Step;
|
||||
@ -24,17 +27,24 @@ import org.springframework.http.HttpMethod;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
|
||||
/** Plan 확정 Item 마스터 동기화 배치 */
|
||||
@Slf4j
|
||||
@Configuration
|
||||
public class EstimateSyncConfiguration implements JobExecutionListener {
|
||||
private final EstimateService estimateService;
|
||||
|
||||
private final InterfaceQsp interfaceQsp;
|
||||
|
||||
EstimateSyncResponse estimateSyncResponse;
|
||||
|
||||
@Value("${qsp.estimate-sync-batch-url}")
|
||||
private String qspInterfaceUrl;
|
||||
|
||||
// [2026-05-13] QSP 견적 동기화 chunk 크기 — 만성 120s timeout 회피 위해 분할 송신
|
||||
@Value("${qsp.estimate-sync-chunk-size:50}")
|
||||
private int estimateSyncChunkSize;
|
||||
|
||||
// [2026-05-13] 총 송신 시간 상한 (밀리초). 초과 시 남은 chunk 중단하여 다음 스케줄 job (04:50 materialJob) 침범 방지.
|
||||
@Value("${qsp.estimate-sync-total-timeout-ms:1500000}")
|
||||
private long estimateSyncTotalTimeoutMs;
|
||||
|
||||
public EstimateSyncConfiguration(EstimateService estimateService, InterfaceQsp interfaceQsp) {
|
||||
this.estimateService = estimateService;
|
||||
this.interfaceQsp = interfaceQsp;
|
||||
@ -59,15 +69,94 @@ public class EstimateSyncConfiguration implements JobExecutionListener {
|
||||
@Bean
|
||||
@StepScope
|
||||
public ListItemReader<PlanSyncResponse> estimateSyncReader() throws Exception {
|
||||
this.estimateSyncResponse =
|
||||
interfaceQsp.callApiData(
|
||||
HttpMethod.POST,
|
||||
qspInterfaceUrl,
|
||||
estimateService.selectEstimateSyncFailList(),
|
||||
EstimateSyncResponse.class);
|
||||
return (estimateSyncResponse != null)
|
||||
? new ListItemReader<>(estimateSyncResponse.getSuccessList())
|
||||
: new ListItemReader<>(Collections.emptyList());
|
||||
// [2026-05-13] 만성 timeout 회피: 전체 fail list 를 한 번에 POST 하지 않고 chunk 단위로 분할 송신.
|
||||
// 각 chunk 는 try/catch — 일부 chunk 실패해도 다음 chunk 진행.
|
||||
// 모든 chunk 실패 시에만 throw 하여 Step FAILED.
|
||||
// 총 elapsed 가 상한 초과 시 남은 chunk 중단 (다음 스케줄 job 침범 방지).
|
||||
EstimateSendRequest fullRequest = estimateService.selectEstimateSyncFailList();
|
||||
List<EstimateSendResponse> quoteList =
|
||||
(fullRequest != null) ? fullRequest.getQuoteList() : null;
|
||||
|
||||
if (quoteList == null || quoteList.isEmpty()) {
|
||||
log.info("QSP 견적 동기화: 송신할 fail list 없음");
|
||||
return new ListItemReader<>(Collections.emptyList());
|
||||
}
|
||||
|
||||
int chunkSize = Math.max(1, estimateSyncChunkSize);
|
||||
int totalChunks = (quoteList.size() + chunkSize - 1) / chunkSize;
|
||||
List<PlanSyncResponse> mergedSuccessList = new ArrayList<>();
|
||||
int successChunks = 0;
|
||||
int abortedChunks = 0;
|
||||
long startAt = System.currentTimeMillis();
|
||||
|
||||
log.info(
|
||||
"QSP 견적 동기화 분할 송신 시작: totalItems={}, chunkSize={}, totalChunks={}",
|
||||
quoteList.size(),
|
||||
chunkSize,
|
||||
totalChunks);
|
||||
|
||||
for (int i = 0; i < quoteList.size(); i += chunkSize) {
|
||||
long elapsed = System.currentTimeMillis() - startAt;
|
||||
if (elapsed > estimateSyncTotalTimeoutMs) {
|
||||
abortedChunks = totalChunks - (i / chunkSize);
|
||||
log.warn(
|
||||
"QSP 견적 동기화 총 elapsed 상한 초과 — 남은 chunk 중단: elapsedMs={}, limitMs={}, abortedChunks={}",
|
||||
elapsed,
|
||||
estimateSyncTotalTimeoutMs,
|
||||
abortedChunks);
|
||||
break;
|
||||
}
|
||||
|
||||
int end = Math.min(i + chunkSize, quoteList.size());
|
||||
int chunkIndex = i / chunkSize;
|
||||
List<EstimateSendResponse> sub = new ArrayList<>(quoteList.subList(i, end));
|
||||
EstimateSendRequest subRequest = new EstimateSendRequest();
|
||||
subRequest.setQuoteList(sub);
|
||||
|
||||
try {
|
||||
EstimateSyncResponse chunkResponse =
|
||||
interfaceQsp.callApiData(
|
||||
HttpMethod.POST, qspInterfaceUrl, subRequest, EstimateSyncResponse.class);
|
||||
if (chunkResponse != null && chunkResponse.getSuccessList() != null) {
|
||||
mergedSuccessList.addAll(chunkResponse.getSuccessList());
|
||||
}
|
||||
successChunks++;
|
||||
log.info(
|
||||
"QSP 견적 동기화 chunk 성공: chunkIndex={}/{}, range=[{},{}), accumulatedSuccess={}",
|
||||
chunkIndex,
|
||||
totalChunks,
|
||||
i,
|
||||
end,
|
||||
mergedSuccessList.size());
|
||||
} catch (Exception e) {
|
||||
log.error(
|
||||
"QSP 견적 동기화 chunk 실패 — 다음 chunk 계속: chunkIndex={}/{}, range=[{},{})",
|
||||
chunkIndex,
|
||||
totalChunks,
|
||||
i,
|
||||
end,
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
long totalElapsed = System.currentTimeMillis() - startAt;
|
||||
log.info(
|
||||
"QSP 견적 동기화 분할 송신 완료: totalChunks={}, successChunks={}, abortedChunks={}, mergedSuccess={}, totalElapsedMs={}",
|
||||
totalChunks,
|
||||
successChunks,
|
||||
abortedChunks,
|
||||
mergedSuccessList.size(),
|
||||
totalElapsed);
|
||||
|
||||
if (successChunks == 0) {
|
||||
throw new IllegalStateException(
|
||||
"QSP 견적 동기화 모든 chunk 실패: totalChunks="
|
||||
+ totalChunks
|
||||
+ ", quoteSize="
|
||||
+ quoteList.size());
|
||||
}
|
||||
|
||||
return new ListItemReader<>(mergedSuccessList);
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user