Merge pull request '[견적배치] estimateSyncJob 만성 QSP timeout — chunk 분할 송신 + 스케줄 슬롯 04:20→05:20 (bomJob 과 스왑)' (#499) from dev into prd-deploy
Reviewed-on: #499
This commit is contained in:
commit
12864acf1a
@ -163,9 +163,11 @@ public class JobLauncherController {
|
|||||||
/**
|
/**
|
||||||
* BOM 아이템 동기화 배치
|
* 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 {
|
public String bomJob() throws JobInstanceAlreadyCompleteException, JobExecutionAlreadyRunningException, JobParametersInvalidException, JobRestartException {
|
||||||
|
|
||||||
return executeScheduledJob("bomJob");
|
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 {
|
public String estimateSyncJob() throws JobInstanceAlreadyCompleteException, JobExecutionAlreadyRunningException, JobParametersInvalidException, JobRestartException {
|
||||||
|
|
||||||
return executeScheduledJob("estimateSyncJob");
|
return executeScheduledJob("estimateSyncJob");
|
||||||
|
|||||||
@ -1,12 +1,15 @@
|
|||||||
package com.interplug.qcast.batch.estimate;
|
package com.interplug.qcast.batch.estimate;
|
||||||
|
|
||||||
import com.interplug.qcast.biz.estimate.EstimateService;
|
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.EstimateSyncResponse;
|
||||||
import com.interplug.qcast.biz.estimate.dto.PlanSyncResponse;
|
import com.interplug.qcast.biz.estimate.dto.PlanSyncResponse;
|
||||||
import com.interplug.qcast.util.InterfaceQsp;
|
import com.interplug.qcast.util.InterfaceQsp;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.batch.core.Job;
|
import org.springframework.batch.core.Job;
|
||||||
import org.springframework.batch.core.JobExecutionListener;
|
import org.springframework.batch.core.JobExecutionListener;
|
||||||
import org.springframework.batch.core.Step;
|
import org.springframework.batch.core.Step;
|
||||||
@ -24,17 +27,24 @@ import org.springframework.http.HttpMethod;
|
|||||||
import org.springframework.transaction.PlatformTransactionManager;
|
import org.springframework.transaction.PlatformTransactionManager;
|
||||||
|
|
||||||
/** Plan 확정 Item 마스터 동기화 배치 */
|
/** Plan 확정 Item 마스터 동기화 배치 */
|
||||||
|
@Slf4j
|
||||||
@Configuration
|
@Configuration
|
||||||
public class EstimateSyncConfiguration implements JobExecutionListener {
|
public class EstimateSyncConfiguration implements JobExecutionListener {
|
||||||
private final EstimateService estimateService;
|
private final EstimateService estimateService;
|
||||||
|
|
||||||
private final InterfaceQsp interfaceQsp;
|
private final InterfaceQsp interfaceQsp;
|
||||||
|
|
||||||
EstimateSyncResponse estimateSyncResponse;
|
|
||||||
|
|
||||||
@Value("${qsp.estimate-sync-batch-url}")
|
@Value("${qsp.estimate-sync-batch-url}")
|
||||||
private String qspInterfaceUrl;
|
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) {
|
public EstimateSyncConfiguration(EstimateService estimateService, InterfaceQsp interfaceQsp) {
|
||||||
this.estimateService = estimateService;
|
this.estimateService = estimateService;
|
||||||
this.interfaceQsp = interfaceQsp;
|
this.interfaceQsp = interfaceQsp;
|
||||||
@ -59,15 +69,94 @@ public class EstimateSyncConfiguration implements JobExecutionListener {
|
|||||||
@Bean
|
@Bean
|
||||||
@StepScope
|
@StepScope
|
||||||
public ListItemReader<PlanSyncResponse> estimateSyncReader() throws Exception {
|
public ListItemReader<PlanSyncResponse> estimateSyncReader() throws Exception {
|
||||||
this.estimateSyncResponse =
|
// [2026-05-13] 만성 timeout 회피: 전체 fail list 를 한 번에 POST 하지 않고 chunk 단위로 분할 송신.
|
||||||
interfaceQsp.callApiData(
|
// 각 chunk 는 try/catch — 일부 chunk 실패해도 다음 chunk 진행.
|
||||||
HttpMethod.POST,
|
// 모든 chunk 실패 시에만 throw 하여 Step FAILED.
|
||||||
qspInterfaceUrl,
|
// 총 elapsed 가 상한 초과 시 남은 chunk 중단 (다음 스케줄 job 침범 방지).
|
||||||
estimateService.selectEstimateSyncFailList(),
|
EstimateSendRequest fullRequest = estimateService.selectEstimateSyncFailList();
|
||||||
EstimateSyncResponse.class);
|
List<EstimateSendResponse> quoteList =
|
||||||
return (estimateSyncResponse != null)
|
(fullRequest != null) ? fullRequest.getQuoteList() : null;
|
||||||
? new ListItemReader<>(estimateSyncResponse.getSuccessList())
|
|
||||||
: new ListItemReader<>(Collections.emptyList());
|
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
|
@Bean
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user