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

[UNI-205] refactor : SSE 연결의 안정성이 불안정할때 대응하기 위한 Stream 방식 API 개발 #230

Merged
merged 6 commits into from
Feb 24, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public final class UniroConst {

public static final int CREATE_ROUTE_LIMIT_COUNT = 2000;


public static final int MAX_CACHE_SIZE = 20;
public static final Integer MAX_GOOGLE_API_BATCH_SIZE = 300;
public static final String SUCCESS_STATUS = "OK";
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import static com.softeer5.uniro_backend.common.constant.UniroConst.MAX_CACHE_SIZE;

@Component
@RequiredArgsConstructor
@Slf4j
Expand Down Expand Up @@ -39,7 +41,7 @@ public boolean hasData(String key){
public void deleteRoutesData(String key, int batchNumber) {
String redisKeyPrefix = key + ":";

for(int i=1; i<=batchNumber; i++){
for(int i=1; i<= Math.max(MAX_CACHE_SIZE,batchNumber); i++){
redisTemplate.delete(redisKeyPrefix + i);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,14 @@ public interface MapApi {
@ApiResponse(responseCode = "200", description = "모든 지도 조회 성공"),
@ApiResponse(responseCode = "400", description = "EXCEPTION(임시)", content = @Content),
})
ResponseEntity<GetAllRoutesResDTO> getAllRoutesAndNodes(@PathVariable("univId") Long univId);
ResponseEntity<AllRoutesInfo> getAllRoutesAndNodes(@PathVariable("univId") Long univId);

@Operation(summary = "모든 지도(노드,루트) 조회 by stream")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "모든 지도 조회 성공"),
@ApiResponse(responseCode = "400", description = "EXCEPTION(임시)", content = @Content),
})
ResponseEntity<AllRoutesInfo> getAllRoutesAndNodesStream(@PathVariable("univId") Long univId);

@Operation(summary = "모든 지도(노드,루트) 조회 by sse")
@ApiResponses(value = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,30 @@ public class MapController implements MapApi {
private final AdminService adminService;

@GetMapping("/{univId}/routes-local")
public ResponseEntity<GetAllRoutesResDTO> getAllRoutesAndNodesByLocalCache(@PathVariable("univId") Long univId){
GetAllRoutesResDTO allRoutes = mapService.getAllRoutesByLocalCache(univId);
public ResponseEntity<AllRoutesInfo> getAllRoutesAndNodesByLocalCache(@PathVariable("univId") Long univId){
AllRoutesInfo allRoutes = mapService.getAllRoutesByLocalCache(univId);
return ResponseEntity.ok().body(allRoutes);
}

@Override
@GetMapping("/{univId}/routes")
public ResponseEntity<GetAllRoutesResDTO> getAllRoutesAndNodes(@PathVariable("univId") Long univId){
GetAllRoutesResDTO allRoutes = mapService.getAllRoutes(univId);
public ResponseEntity<AllRoutesInfo> getAllRoutesAndNodes(@PathVariable("univId") Long univId){
AllRoutesInfo allRoutes = mapService.getAllRoutes(univId);
return ResponseEntity.ok().body(allRoutes);
}

@Override
@GetMapping("/{univId}/routes/stream")
public ResponseEntity<AllRoutesInfo> getAllRoutesAndNodesStream(@PathVariable("univId") Long univId){
AllRoutesInfo allRoutes = mapService.getAllRoutesByStream(univId);
return ResponseEntity.ok().body(allRoutes);
}

@Override
@GetMapping("/{univId}/routes/sse")
public SseEmitter getAllRoutes(@PathVariable("univId") Long univId){
SseEmitter emitter = new SseEmitter(60 * 1000L); // timeout 1분
mapService.getAllRoutesByStream(univId, emitter);
mapService.getAllRoutesBySSE(univId, emitter);
return emitter;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public class MapService {

private final Map<Long, List<LightRoute>> cache = new HashMap<>();

public GetAllRoutesResDTO getAllRoutesByLocalCache(Long univId) {
public AllRoutesInfo getAllRoutesByLocalCache(Long univId) {

if(!cache.containsKey(univId)){
List<Route> routes = routeRepository.findAllRouteByUnivIdWithNodes(univId);
Expand All @@ -78,17 +78,10 @@ public GetAllRoutesResDTO getAllRoutesByLocalCache(Long univId) {
throw new RouteException("Route Not Found", ROUTE_NOT_FOUND);
}

RevInfo revInfo = revInfoRepository.findFirstByUnivIdOrderByRevDesc(univId)
.orElseThrow(() -> new RouteException("Revision not found", RECENT_REVISION_NOT_FOUND));

AllRoutesInfo allRoutesInfo = routeCacheCalculator.assembleRoutes(routes);
GetAllRoutesResDTO response = GetAllRoutesResDTO.of(allRoutesInfo.getNodeInfos(), allRoutesInfo.getCoreRoutes(),
allRoutesInfo.getBuildingRoutes(), revInfo.getRev());

return response;
return routeCacheCalculator.assembleRoutes(routes);
}

public GetAllRoutesResDTO getAllRoutes(Long univId) {
public AllRoutesInfo getAllRoutes(Long univId) {

if(!redisService.hasData(univId.toString())){
List<Route> routes = routeRepository.findAllRouteByUnivIdWithNodes(univId);
Expand All @@ -108,17 +101,96 @@ public GetAllRoutesResDTO getAllRoutes(Long univId) {
throw new RouteException("Route Not Found", ROUTE_NOT_FOUND);
}

RevInfo revInfo = revInfoRepository.findFirstByUnivIdOrderByRevDesc(univId)
.orElseThrow(() -> new RouteException("Revision not found", RECENT_REVISION_NOT_FOUND));
return routeCacheCalculator.assembleRoutes(routes);
}

public AllRoutesInfo getAllRoutesByStream(Long univId) {
List<NodeInfoResDTO> nodeInfos = new ArrayList<>();
List<CoreRouteResDTO> coreRoutes = new ArrayList<>();
List<BuildingRouteResDTO> buildingRoutes = new ArrayList<>();

String redisKeyPrefix = univId + ":";
int batchNumber = 1;

if (!processRedisDataByStream(redisKeyPrefix, batchNumber, nodeInfos, coreRoutes, buildingRoutes)) {
processDatabaseDataByStream(univId, redisKeyPrefix, batchNumber, nodeInfos, coreRoutes, buildingRoutes);
}

return AllRoutesInfo.of(nodeInfos, coreRoutes, buildingRoutes);
}

private boolean processRedisDataByStream(String redisKeyPrefix,
int batchNumber,
List<NodeInfoResDTO> nodeInfos,
List<CoreRouteResDTO> coreRoutes,
List<BuildingRouteResDTO> buildingRoutes){
while (redisService.hasData(redisKeyPrefix + batchNumber)) {
LightRoutes lightRoutes = (LightRoutes) redisService.getData(redisKeyPrefix + batchNumber);
if (lightRoutes == null) {
break;
}

processBatchByStream(lightRoutes.getLightRoutes(), nodeInfos, coreRoutes, buildingRoutes);
batchNumber++;
}

return batchNumber > 1;
}

private void processDatabaseDataByStream(Long univId, String redisKeyPrefix, int batchNumber,
List<NodeInfoResDTO> nodeInfos,
List<CoreRouteResDTO> coreRoutes,
List<BuildingRouteResDTO> buildingRoutes) {
int fetchSize = routeRepository.countByUnivId(univId);
int remain = fetchSize%STREAM_FETCH_SIZE;
fetchSize = fetchSize/STREAM_FETCH_SIZE + (remain > 0 ? 1 : 0);

try (Stream<LightRoute> routeStream = routeRepository.findAllLightRoutesByUnivId(univId)) {
List<LightRoute> batch = new ArrayList<>(STREAM_FETCH_SIZE);
for (LightRoute route : (Iterable<LightRoute>) routeStream::iterator) {
batch.add(route);

if (batch.size() == STREAM_FETCH_SIZE) {
saveAndSendBatchByStream(redisKeyPrefix, batchNumber++, batch, nodeInfos, coreRoutes, buildingRoutes);
}
}

// 남은 배치 처리
if (!batch.isEmpty()) {
saveAndSendBatchByStream(redisKeyPrefix, batchNumber, batch, nodeInfos, coreRoutes, buildingRoutes);
}

redisService.saveDataToString(univId.toString() + ":fetch", String.valueOf(fetchSize));
}

}

AllRoutesInfo allRoutesInfo = routeCacheCalculator.assembleRoutes(routes);
private void saveAndSendBatchByStream(String redisKeyPrefix, int batchNumber, List<LightRoute> batch,
List<NodeInfoResDTO> nodeInfos,
List<CoreRouteResDTO> coreRoutes,
List<BuildingRouteResDTO> buildingRoutes) {
LightRoutes value = new LightRoutes(batch);
redisService.saveData(redisKeyPrefix + batchNumber, value);
processBatchByStream(batch, nodeInfos, coreRoutes, buildingRoutes);
batch.clear();
}

return GetAllRoutesResDTO.of(allRoutesInfo.getNodeInfos(), allRoutesInfo.getCoreRoutes(),
allRoutesInfo.getBuildingRoutes(), revInfo.getRev());
private void processBatchByStream(List<LightRoute> batch,
List<NodeInfoResDTO> nodeInfos,
List<CoreRouteResDTO> coreRoutes,
List<BuildingRouteResDTO> buildingRoutes) {
if (!batch.isEmpty()) {
AllRoutesInfo allRoutesInfo = routeCacheCalculator.assembleRoutes(batch);
nodeInfos.addAll(allRoutesInfo.getNodeInfos());
coreRoutes.addAll(allRoutesInfo.getCoreRoutes());
buildingRoutes.addAll(allRoutesInfo.getBuildingRoutes());
batch.clear();
entityManager.clear();
}
}

@Async
public void getAllRoutesByStream(Long univId, SseEmitter emitter) {
public void getAllRoutesBySSE(Long univId, SseEmitter emitter) {
String redisKeyPrefix = univId + ":";
int batchNumber = 1;

Expand Down