Skip to content

Commit

Permalink
[UNI-205] refactor : SSE 연결의 안정성이 불안정할때 대응하기 위한 Stream 방식 API 개발 (#230)
Browse files Browse the repository at this point in the history
* [UNI-205] feat : Stream + 캐시 로직 작성

* [UNI-205] GetAllRoutesResInfo -> AllRoutesInfo로 변경

* 테스트용 CD

* 테스트용 CD 삭제

* [UNI-205] feat : 캐시가 지워지지 않던 문제 해결

* cd 삭제
  • Loading branch information
thdgustjd1 authored Feb 24, 2025
1 parent d32f963 commit 19ad5a8
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 24 deletions.
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

0 comments on commit 19ad5a8

Please sign in to comment.