Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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 @@ -3,6 +3,8 @@
import java.time.LocalDate;
import java.util.List;
import kr.allcll.backend.admin.preseat.dto.PreSeatResponse;
import kr.allcll.backend.admin.seat.SeatStreamStatus;
import kr.allcll.backend.admin.seat.SeatStreamStatusService;
import kr.allcll.crawler.client.SeatClient;
import kr.allcll.crawler.client.model.SeatResponse;
import kr.allcll.crawler.client.payload.SeatPayload;
Expand All @@ -25,10 +27,11 @@ public class AdminPreSeatService {
private final SeatClient seatClient;
private final Credentials credentials;
private final AllPreSeatBuffer allPreSeatBuffer;
private final SeatStreamStatusService seatStreamStatusService;
private final CrawlerSubjectRepository crawlerSubjectRepository;


public void getAllPreSeat(String userId) {
seatStreamStatusService.updateSeatStreamStatus(SeatStreamStatus.PRESEAT);
Credential credential = credentials.findByUserId(userId);
List<CrawlerSubject> crawlerSubjects = crawlerSubjectRepository.findAllBySemesterAt(CrawlerSemester.now());
for (CrawlerSubject crawlerSubject : crawlerSubjects) {
Expand All @@ -52,6 +55,7 @@ private void sendExternalPreSeatsRequest(CrawlerSubject crawlerSubject, Credenti
} catch (CrawlerAllcllException e) {
log.error(
"[여석] 외부 API 호출에 실패했습니다. PreSeat: " + crawlerSubject.getCuriNo() + "-" + crawlerSubject.getClassName());
seatStreamStatusService.updateSeatStreamStatus(SeatStreamStatus.ERROR);
}
}

Expand Down
8 changes: 4 additions & 4 deletions src/main/java/kr/allcll/backend/admin/seat/AdminSeatApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import jakarta.servlet.http.HttpServletRequest;
import kr.allcll.backend.admin.AdminRequestValidator;
import kr.allcll.backend.admin.seat.dto.SeatStatusResponse;
import kr.allcll.backend.admin.seat.dto.SeatCrawlingStatusResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
Expand Down Expand Up @@ -41,12 +41,12 @@ public ResponseEntity<Void> seasonSeatStart(HttpServletRequest request,
}

@GetMapping("/api/admin/seat/check")
public ResponseEntity<SeatStatusResponse> getSeatStatus(HttpServletRequest request) {
public ResponseEntity<SeatCrawlingStatusResponse> getSeatStatus(HttpServletRequest request) {
if (validator.isRateLimited(request) || validator.isUnauthorized(request)) {
return ResponseEntity.status(401).build();
}
SeatStatusResponse seatStatusResponse = adminSeatService.getSeatCrawlerStatus();
return ResponseEntity.ok(seatStatusResponse);
SeatCrawlingStatusResponse seatCrawlingStatusResponse = adminSeatService.getSeatCrawlingStatus();
return ResponseEntity.ok(seatCrawlingStatusResponse);
}

@PostMapping("/api/admin/seat/cancel")
Expand Down
20 changes: 13 additions & 7 deletions src/main/java/kr/allcll/backend/admin/seat/AdminSeatService.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import java.time.LocalDateTime;
import java.util.concurrent.atomic.AtomicLong;
import kr.allcll.backend.admin.seat.dto.ChangeSubjectsResponse;
import kr.allcll.backend.admin.seat.dto.SeatStatusResponse;
import kr.allcll.backend.admin.seat.dto.SeatCrawlingStatusResponse;
import kr.allcll.crawler.client.SeatClient;
import kr.allcll.crawler.client.model.SeatResponse;
import kr.allcll.crawler.client.payload.SeatPayload;
Expand All @@ -25,42 +25,47 @@
@RequiredArgsConstructor
public class AdminSeatService {

private final AtomicLong lastSuccessCrawlingTime = new AtomicLong(0);
private static final long RECENT_CRAWLING_SUCCESS_THRESHOLD_MS = 3_000;

private final SeatClient seatClient;
private final Credentials credentials;
private final AllSeatBuffer allSeatBuffer;
private final ChangeDetector changeDetector;
private final SjptProperties sjptProperties;
private final TargetSubjectStorage targetSubjectStorage;
private final CrawlerScheduledTaskHandler seatScheduler;
private final SeatPersistenceService seatPersistenceService;
private final SjptProperties sjptProperties;
private final ChangeDetector changeDetector;
private final AllSeatBuffer allSeatBuffer;
private final AtomicLong lastSuccessCrawlingTime = new AtomicLong(0);
private final SeatStreamStatusService seatStreamStatusService;

public void getAllSeatPeriodically(String userId) {
seatStreamStatusService.updateSeatStreamStatus(SeatStreamStatus.LIVE);
Credential credential = credentials.findByUserId(userId);
fetchPinSeat(credential);
fetchGeneralSeat(credential);
}

public void getSeasonSeatPeriodically(String userId) {
seatStreamStatusService.updateSeatStreamStatus(SeatStreamStatus.LIVE);
Credential credential = credentials.findByUserId(userId);
fetchPinSeat(credential);
fetchGeneralSeat(credential);
}

public void cancelSeatScheduling() {
seatScheduler.cancelAll();
seatStreamStatusService.updateSeatStreamStatus(SeatStreamStatus.IDLE);
}

public SeatStatusResponse getSeatCrawlerStatus() {
public SeatCrawlingStatusResponse getSeatCrawlingStatus() {
int seatSchedulerTaskCount = seatScheduler.getTaskCount();
boolean validSeatSchedulerCount = seatSchedulerTaskCount == sjptProperties.getRequestPerSecondCount();
boolean validRecentCrawlingSuccess =
(System.currentTimeMillis() - lastSuccessCrawlingTime.get()) <= RECENT_CRAWLING_SUCCESS_THRESHOLD_MS;

boolean isActive = validSeatSchedulerCount && validRecentCrawlingSuccess;

return SeatStatusResponse.of(isActive);
return SeatCrawlingStatusResponse.of(isActive);
}

private void fetchPinSeat(Credential credential) {
Expand Down Expand Up @@ -125,6 +130,7 @@ private void sendExternalRequestWithOutDetect(CrawlerSubject crawlerSubject, Cre
} catch (CrawlerAllcllException e) {
log.error(
"[여석] 외부 API 호출에 실패했습니다. 과목: " + crawlerSubject.getCuriNo() + "-" + crawlerSubject.getClassName());
seatStreamStatusService.updateSeatStreamStatus(SeatStreamStatus.ERROR);
}
}

Expand Down
18 changes: 18 additions & 0 deletions src/main/java/kr/allcll/backend/admin/seat/SeatStreamStatus.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package kr.allcll.backend.admin.seat;

public enum SeatStreamStatus {
LIVE("과목의 여석을 실시간으로 볼 수 있어요."),
PRESEAT("전체 학년 여석 탭을 이용해 주세요."),
IDLE("과목 실시간 여석 제공 전이에요. 서비스가 시작되면 알림을 드릴게요."),
ERROR("서비스 점검 중이에요. 조금만 기다려주세요.");

private final String message;

SeatStreamStatus(String message) {
this.message = message;
}

public String getMessage() {
return message;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package kr.allcll.backend.admin.seat;

import java.util.Objects;
import kr.allcll.backend.admin.seat.dto.SeatStreamStatusResponse;
import kr.allcll.backend.support.sse.SseEventBuilderFactory;
import kr.allcll.backend.support.sse.SseService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class SeatStreamStatusService {

private final SseService sseService;
public final SeatStreamStatusStore seatStreamStatusStore;

public void updateSeatStreamStatus(SeatStreamStatus newStatus) {
SeatStreamStatus currentStatus = seatStreamStatusStore.getCurrentStatus();
if (Objects.equals(currentStatus, newStatus)) {
return;
}
seatStreamStatusStore.updateCurrentStatus(newStatus);

SeatStreamStatusResponse sseStatusResponse = getSeatStreamStatusResponse(newStatus);
sseService.propagate(SseEventBuilderFactory.EVENT_SEAT_STREAM_STATUS, sseStatusResponse);
}

private SeatStreamStatusResponse getSeatStreamStatusResponse(SeatStreamStatus newStatus) {
return SeatStreamStatusResponse.of(
newStatus.name().toLowerCase(),
newStatus.getMessage()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package kr.allcll.backend.admin.seat;

import org.springframework.stereotype.Component;

@Component
public class SeatStreamStatusStore {

private volatile SeatStreamStatus currentStatus;

public SeatStreamStatusStore() {
this.currentStatus = SeatStreamStatus.IDLE;
}

public SeatStreamStatus getCurrentStatus() {
return currentStatus;
}

public void updateCurrentStatus(SeatStreamStatus newStatus) {
this.currentStatus = newStatus;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package kr.allcll.backend.admin.seat.dto;

public record SeatCrawlingStatusResponse(
boolean isActive
) {

public static SeatCrawlingStatusResponse of(boolean isActive) {
return new SeatCrawlingStatusResponse(isActive);
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package kr.allcll.backend.admin.seat.dto;

public record SeatStreamStatusResponse(
String status,
String message
) {

public static SeatStreamStatusResponse of(String status, String message) {
return new SeatStreamStatusResponse(status, message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import kr.allcll.backend.admin.seat.SeatStreamStatus;
import kr.allcll.backend.admin.seat.dto.SeatStreamStatusResponse;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter.SseEventBuilder;
Expand All @@ -12,6 +14,9 @@ public class SseEventBuilderFactory {

private static final ObjectMapper objectMapper = new ObjectMapper();

public static final String EVENT_CONNECTION = "connection";
public static final String EVENT_SEAT_STREAM_STATUS = "status";

private static final long RECONNECT_TIME_MILLIS = 1000L;

static {
Expand All @@ -32,10 +37,26 @@ public static SseEventBuilder create(String eventName, Object value) {

public static SseEventBuilder createInitialEvent() {
return baseEvent()
.name("connection")
.name(EVENT_CONNECTION)
.data("success", MediaType.TEXT_PLAIN);
}

public static SseEventBuilder createInitialSeatStreamStatusEvent(SeatStreamStatus seatStreamStatus) {
SeatStreamStatusResponse seatStreamStatusResponse = SeatStreamStatusResponse.of(
seatStreamStatus.name().toLowerCase(),
seatStreamStatus.getMessage()
);

try {
String sseEvent = objectMapper.writeValueAsString(seatStreamStatusResponse);
return baseEvent()
.name(EVENT_SEAT_STREAM_STATUS)
.data(sseEvent, MediaType.APPLICATION_JSON);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}

private static SseEventBuilder baseEvent() {
return SseEmitter.event()
.reconnectTime(RECONNECT_TIME_MILLIS);
Expand Down
16 changes: 10 additions & 6 deletions src/main/java/kr/allcll/backend/support/sse/SseService.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

import java.io.IOException;
import java.util.List;
import kr.allcll.backend.support.sse.dto.SseStatusResponse;
import kr.allcll.backend.admin.seat.SeatStreamStatus;
import kr.allcll.backend.admin.seat.SeatStreamStatusStore;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
Expand All @@ -15,12 +16,20 @@
public class SseService {

private final SseEmitterStorage sseEmitterStorage;
private final SeatStreamStatusStore seatStreamStatusStore;

public SseEmitter connect(String token) {
SseEmitter sseEmitter = createSseEmitter();
sseEmitterStorage.add(token, sseEmitter);

SseEventBuilder initialEvent = SseEventBuilderFactory.createInitialEvent();
sendEvent(sseEmitter, initialEvent);

SeatStreamStatus seatStreamStatus = seatStreamStatusStore.getCurrentStatus();
SseEventBuilder initialSeatStreamStatusEvent = SseEventBuilderFactory.createInitialSeatStreamStatusEvent(
seatStreamStatus);
sendEvent(sseEmitter, initialSeatStreamStatusEvent);

return sseEmitter;
}

Expand Down Expand Up @@ -54,9 +63,4 @@ private void sendEvent(SseEmitter sseEmitter, SseEventBuilder eventBuilder) {
public List<String> getConnectedTokens() {
return sseEmitterStorage.getUserTokens().stream().toList();
}

public SseStatusResponse isConnected(String token) {
boolean isConnected = sseEmitterStorage.getEmitter(token).isPresent();
return SseStatusResponse.of(isConnected);
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ void sseConnectionTest() {

// then
SseTestHelper.assertResponseContainsMessage(response, "success");
SseTestHelper.assertResponseContainsMessage(response, "idle");
}

@DisplayName("동시에 여러 클라이언트와 SSE 연결을 유지하고, 모두에게 메시지를 전달한다.")
Expand Down Expand Up @@ -86,6 +87,10 @@ void ssePropagationTest() {
+ "data:success\n"
+ "\n"
+ "retry:1000\n"
+ "event:status\n"
+ "data:{\"status\":\"idle\",\"message\":\"과목 실시간 여석 제공 전이에요. 서비스가 시작되면 알림을 드릴게요.\"}\n"
+ "\n"
+ "retry:1000\n"
+ "event:message\n"
+ "data:\"Hello, SSE!\"\n"
+ "\n";
Expand Down
Loading