Compare commits

...

2 Commits

Author SHA1 Message Date
joungmin
d3cd1b5d5f feat(daemon): instance-level enable flag (dev/prod 중복 폴링 방지)
dev와 prod가 같은 Oracle ATP 인스턴스(_low vs _medium tier만 다름)를 공유하는
환경에서 dev/prod 양쪽 DaemonScheduler가 같은 daemon_config row를 폴링하면
같은 시점에 동일 채널 스캔이 발생 → YouTube 봇 감지 위험 증가.

수정:
- application.yml: app.daemon.enabled (env DAEMON_ENABLED, 기본 true)
- DaemonScheduler.run() 첫 줄에서 인스턴스 플래그 검사 후 차단
- dev/backend/.env에 DAEMON_ENABLED=false 설정 (이 커밋엔 미포함, 로컬만)

운영(OKE)은 env 미설정 → 기본 true로 정상 동작.
dev(PM2)는 .env로 false → 스케줄러 자체가 동작 안 함.

Refs: #275 #321
2026-06-15 12:50:41 +09:00
joungmin
51dcacc728 fix(scan): #291 YouTubeService.fetchChannelVideos publishedAfter 조기 종료 버그
업로드 재생목록(uploads playlist) 스캔에서 publishedAfter 이전 영상을 만나
break해도, do-while 조건이 응답의 nextPageToken을 보고 paging을 지속하던 결함.

수정:
- stopPaging boolean 플래그 추가
- inner-loop 조기 break에서 stopPaging = true
- outer paging 갱신 시 stopPaging 검사 우선

영향:
- 백필 효율 향상 (불필요한 API quota 소모 방지)
- 봇 감지 회피 (과한 페이징 요청 안 함)
- daemon 자동 모드의 안정적 동작 기반

Refs: #291 #321
2026-06-15 12:41:35 +09:00
3 changed files with 18 additions and 2 deletions

View File

@@ -3,6 +3,7 @@ package com.tasteby.service;
import com.tasteby.domain.DaemonConfig; import com.tasteby.domain.DaemonConfig;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -22,6 +23,9 @@ public class DaemonScheduler {
private final PipelineService pipelineService; private final PipelineService pipelineService;
private final CacheService cacheService; private final CacheService cacheService;
@Value("${app.daemon.enabled:true}")
private boolean instanceEnabled;
public DaemonScheduler(DaemonConfigService daemonConfigService, public DaemonScheduler(DaemonConfigService daemonConfigService,
YouTubeService youTubeService, YouTubeService youTubeService,
PipelineService pipelineService, PipelineService pipelineService,
@@ -34,6 +38,10 @@ public class DaemonScheduler {
@Scheduled(fixedDelay = 30_000) // Check every 30 seconds @Scheduled(fixedDelay = 30_000) // Check every 30 seconds
public void run() { public void run() {
// 인스턴스 차원 차단(dev/prod 동일 DB 공유 환경에서 dev 쪽 동시 폴링 방지).
// dev .env: DAEMON_ENABLED=false → 이 인스턴스는 스케줄러 동작 안 함.
// prod: 미설정 → 기본 true.
if (!instanceEnabled) return;
try { try {
var config = getConfig(); var config = getConfig();
if (config == null) return; if (config == null) return;

View File

@@ -59,6 +59,7 @@ public class YouTubeService {
String uploadsPlaylistId = "UU" + channelId.substring(2); String uploadsPlaylistId = "UU" + channelId.substring(2);
List<Map<String, Object>> allVideos = new ArrayList<>(); List<Map<String, Object>> allVideos = new ArrayList<>();
String nextPage = null; String nextPage = null;
boolean stopPaging = false;
try { try {
do { do {
@@ -88,7 +89,7 @@ public class YouTubeService {
// publishedAfter 필터: 이미 스캔한 영상 이후만 // publishedAfter 필터: 이미 스캔한 영상 이후만
if (publishedAfter != null && publishedAt.compareTo(publishedAfter) <= 0) { if (publishedAfter != null && publishedAt.compareTo(publishedAfter) <= 0) {
// 업로드 재생목록은 최신순이므로 이전 날짜 만나면 중단 // 업로드 재생목록은 최신순이므로 이전 날짜 만나면 중단
nextPage = null; stopPaging = true;
break; break;
} }
@@ -105,7 +106,9 @@ public class YouTubeService {
} }
allVideos.addAll(pageVideos); allVideos.addAll(pageVideos);
if (nextPage != null || data.has("nextPageToken")) { if (stopPaging) {
nextPage = null;
} else {
nextPage = data.has("nextPageToken") ? data.path("nextPageToken").asText() : null; nextPage = data.has("nextPageToken") ? data.path("nextPageToken").asText() : null;
} }
} while (nextPage != null); } while (nextPage != null);

View File

@@ -59,6 +59,11 @@ app:
cache: cache:
ttl-seconds: 600 ttl-seconds: 600
daemon:
# 인스턴스 차원 스케줄러 활성화. dev/prod가 같은 DB를 공유하므로
# dev .env에 DAEMON_ENABLED=false를 설정해 dev 폴링을 끄고 prod만 동작시킨다.
enabled: ${DAEMON_ENABLED:true}
mybatis: mybatis:
mapper-locations: classpath:mybatis/mapper/*.xml mapper-locations: classpath:mybatis/mapper/*.xml
config-location: classpath:mybatis/mybatis-config.xml config-location: classpath:mybatis/mybatis-config.xml