Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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 @@ -48,10 +48,4 @@ public void validateNearbySubwaysExist(List<Subway> nearBySubways) {
throw new EventException(EventErrorType.NO_INTERMEDIATE_SUBWAYS_FOUND);
}
}

public void validateEventCacheExist(Cache.ValueWrapper eventCache) {
if (eventCache == null) {
throw new EventException(EventErrorType.CACHE_NOT_FOUND);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import com.meetup.server.event.dto.response.route.MeetingPointRouteGroup;
import com.meetup.server.event.dto.response.route.RouteResponse;
import com.meetup.server.event.exception.EventErrorType;
import com.meetup.server.event.exception.EventException;
import com.meetup.server.parkinglot.implement.ParkingLotFinder;
import com.meetup.server.parkinglot.infrastructure.jpa.projection.ClosestParkingLot;
import com.meetup.server.startpoint.domain.StartPoint;
Expand All @@ -14,57 +16,65 @@
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;

@Slf4j
@Component
@RequiredArgsConstructor
public class RouteAssembler {

private static final int THREAD_POOL_SIZE = 2;
private static final int MAX_ATTEMPTS = 2;
private static final int RETRY_DELAY_MS = 100;

private final ExecutorService routeExecutor;
private final ParkingLotFinder parkingLotFinder;
private final RouteFetcher routeFetcher;

public CompletableFuture<MeetingPointRouteGroup> assemble(List<StartPoint> startPoints, Subway subway) {
try (ExecutorService executor = Executors.newFixedThreadPool(THREAD_POOL_SIZE)) {
List<CompletableFuture<RouteResponse>> routeFutures = startPoints.stream()
.map(startPoint -> CompletableFuture.supplyAsync(() -> routeFetcher.fetch(startPoint, subway), executor))
.toList();
List<CompletableFuture<RouteResponse>> routeFutures = startPoints.stream()
.map(startPoint -> CompletableFuture.supplyAsync(() -> fetchWithRetry(startPoint, subway), routeExecutor))
.toList();

CompletableFuture<List<RouteResponse>> allRoutesFuture = CompletableFuture.allOf(routeFutures.toArray(new CompletableFuture[0]))
.thenApply(v -> routeFutures.stream()
.map(routeFuture -> {
try {
return routeFuture.get();
} catch (Exception e) {
log.warn("[RouteAssembler] Failed fetching route", e);
return null;
}
})
.filter(Objects::nonNull)
.collect(Collectors.toList()));
CompletableFuture<List<RouteResponse>> allRoutesFuture = CompletableFuture.allOf(routeFutures.toArray(new CompletableFuture[0]))
.thenApply(v -> routeFutures.stream()
.map(CompletableFuture::join)
.filter(Objects::nonNull)
.collect(Collectors.toList()));

CompletableFuture<ClosestParkingLot> closestParkingLotFuture =
CompletableFuture.supplyAsync(() -> parkingLotFinder.findClosestParkingLot(subway.getPoint()), executor);
CompletableFuture<ClosestParkingLot> closestParkingLotFuture =
CompletableFuture.supplyAsync(() -> parkingLotFinder.findClosestParkingLot(subway.getPoint()), routeExecutor);

CompletableFuture<Void> combinedFuture = CompletableFuture.allOf(allRoutesFuture, closestParkingLotFuture);
return combinedFuture.thenApply(v -> {
try {
List<RouteResponse> routes = allRoutesFuture.get();
ClosestParkingLot closestParkingLot = closestParkingLotFuture.get();
return allRoutesFuture.thenCombine(closestParkingLotFuture, (routes, closestParkingLot) -> {
if (routes.isEmpty()) {
log.warn("[RouteAssembler] No routes found for subway: {}", subway.getName());
return null;
}
return MeetingPointRouteGroup.of(routes, subway, closestParkingLot);
});
}

private RouteResponse fetchWithRetry(StartPoint startPoint, Subway subway) {
for (int attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
RouteResponse route = routeFetcher.fetch(startPoint, subway);
if (isValid(route)) return route;

if (routes == null || routes.isEmpty()) {
log.warn("[RouteAssembler] No routes found for subway: {}", subway);
return null;
}
return MeetingPointRouteGroup.of(routes, subway, closestParkingLot);
} catch (Exception e) {
log.warn("[RouteAssembler] Failed assembling MeetingPointRouteGroup", e);
return null;
if (attempt < MAX_ATTEMPTS - 1) {
log.warn("[RouteAssembler] Route fetch failed for {}. Retrying... (Attempt {}/{})",
startPoint.getName(), attempt + 1, MAX_ATTEMPTS);
try {
Thread.sleep(RETRY_DELAY_MS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new EventException(EventErrorType.ROUTE_FETCH_FAILED);
}
});
}
}
return null;
}

private boolean isValid(RouteResponse route) {
if (route == null) return false;
return (route.getIsTransit() && route.getTransitRoute() != null) ||
(!route.getIsTransit() && route.getDrivingRoute() != null);
}
}
18 changes: 18 additions & 0 deletions src/main/java/com/meetup/server/global/config/ThreadConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.meetup.server.global.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;

@Configuration
public class ThreadConfig {

@Bean
public ExecutorService routeExecutor() {
ThreadFactory threadFactory = Thread.ofVirtual().name("route-thread-", 0).factory();
return Executors.newThreadPerTaskExecutor(threadFactory);
}
}