Skip to content

[Feat] 일별 사용량 동기화 배치 구현 (SKIP LOCKED 기반 다중 워커 병렬 처리)#5

Merged
Iwantcod merged 53 commits into
devfrom
feat/daily-persist-batch
May 28, 2026
Merged

[Feat] 일별 사용량 동기화 배치 구현 (SKIP LOCKED 기반 다중 워커 병렬 처리)#5
Iwantcod merged 53 commits into
devfrom
feat/daily-persist-batch

Conversation

@Iwantcod

@Iwantcod Iwantcod commented May 27, 2026

Copy link
Copy Markdown
Owner

개요

  • 매일 03:00 KST, Redis에 누적된 전일 트래픽 사용량(개인/공유/QoS)을 MySQL 사용량 테이블로 동기화하는 일별 사용량 동기화 배치 시스템 전체 구현
  • 순수 MySQL SKIP LOCKED 기반의 다중 워커 병렬 처리 및 단일 트랜잭션 처리 구조 직접 구현으로 고가용성 및 데이터 정합성 확보

관련 BackLog

Resolves: #TBD

PR 유형

  • 새로운 기능 추가
  • 버그 수정
  • CSS 등 사용자 UI 디자인 변경
  • 코드에 영향을 주지 않는 변경사항(오타 수정, 탭 사이즈 변경, 변수명 변경)
  • 코드 리팩토링
  • 주석 추가 및 수정
  • 문서 수정
  • 테스트 추가, 테스트 리팩토링
  • 빌드 부분 혹은 패키지 매니저 수정
  • 파일 혹은 폴더명 수정
  • 파일 혹은 폴더 삭제

💡 주요 변경 사항 및 동작 흐름

본 배치의 핵심 동작은 크게 1) 대상 준비 (Manager), 2) 데이터 동기화 (Worker 병렬 처리) 두 단계로 구분

sequenceDiagram
    autonumber
    participant Scheduler
    participant Manager
    participant Worker
    participant Redis
    participant DB

    Scheduler->>Redis: 03:00 KST 일제히 가동 및 락(Lock) 경쟁
    Redis-->>Manager: 락 획득 (Manager 선출)
    Redis-->>Worker: 락 획득 실패 (Worker로 전환되어 대기)
    
    rect rgb(240, 248, 255)
        note right of Manager: [Target Insert 준비 단계]
        Manager->>DB: 메타데이터 생성 및 유저 타겟 데이터 Chunk Insert
        Manager->>DB: 타겟 준비 완료 후 "RUNNING" 상태 전환
    end

    rect rgb(240, 255, 240)
        note right of Worker: [Usage Sync 병렬 동기화 단계]
        loop 잔여 타겟이 소진될 때까지 여러 워커가 동시 수행
            Worker->>DB: 100건씩 타겟 안전 선점 (SKIP LOCKED)
            Worker->>Redis: 해당 유저의 3종 사용량 스냅샷 조회
            Worker->>DB: 단일 트랜잭션 내에서 [사용량 Insert + 타겟 상태 완료 처리]
        end
    end
    
    Worker->>DB: 잔여 타겟 0건 시 최종 "COMPLETED" 전환
Loading
  • Manager 노드 선출 및 롤백 방어: 매일 03:00 KST, 전 노드 분산 락 시도를 통한 단일 Manager 선출. 중단 시 MAX(line_id) 기반 영리한 재개로 불필요한 전체 스캔 방지
  • 부분 실패 원천 차단: 대상 유저의 Redis 스냅샷 조회, DB 적재, 타겟 진행 상태 완료 전환 행위를 단일 MySQL 트랜잭션으로 단단히 결합하여 데이터 불일치 차단
  • 장애 복구 전용 API 제공: 서버 다운 시 임의 자동 실행 방지. 관리자 통제 하의 /resume(속행) 및 /rerun(실패 건 재처리) API 제공을 통한 유연한 대응 환경 구축

🗄️ DB 관련 변경사항 (중요)

  • 테이블 스키마 리팩토링
    • DAILY_APP_TOTAL_DATA 테이블 내 통합되어 있던 total_usage_data 컬럼을 3개의 구체적 컬럼(individual_usage_data, shared_usage_data, qos_usage_data)으로 분리하여 데이터 명확성 개선
  • 신규 메타데이터 테이블 추가
    • LINE_DAILY_BATCH_JOB: 전체 배치 진행 상태(PENDING/RUNNING/COMPLETED/FAILED) 및 타겟들의 성공/실패/스킵 카운트 집계
    • LINE_DAILY_BATCH_TARGET: 사용자(LINE)별 처리 상태 촘촘히 기록. 결합도 완화를 위해 batch_job_id FK 과감히 제외, usage_date 기반으로 집합 식별
  • 핵심 쿼리 최적화
    • INSERT IGNORE: 기존 데이터의 처리 상태 이력 보호 및 중복 생성 방어 쿼리 활용
    • SELECT ... FOR UPDATE SKIP LOCKED: 다중 워커의 동시 접근 시 Lock 경합 및 교착상태(Deadlock) 방지, 미점유 빈 타겟만의 안정적 선점 제어

⚠️ 중요 사안

  • 엄격한 Insert-only 원칙: DB 동기화 과정에서 중복 키 삽입 시도 발생 시, 덮어쓰기나 갱신(Update) 처리 없이 즉시 실패 시그널로 간주
  • 배치 진입 강제성: LINE_DAILY_TARGET_INSERT_BATCH (대상 준비) 작업의 완전한(100%) 완료 전까지 LINE_DAILY_USAGE_SYNC_BATCH (동기화) 작업 상태 PENDING 강제 유지, Worker 진입 원천 차단
  • 재개 절차 분리 (수동 개입): 서버 일제 다운 등 중단 장애 발생 시 스케줄러 자동 재개 불가. 관리자 현상 파악 후 명시적인 /resume 또는 /rerun API 호출을 통한 이어달리기 강제
  • 스레드 경합 해소 설정: application.yaml 내 TaskScheduler pool.size 2로 확장. 03:00 정각 락 시도 Manager 스레드와 Worker 지연 폴링 스레드 간 자원 병목 현상 방지
  • 모니터링 알람 연동: Micrometer 메트릭(batch_daily_usage_sync_failed_count 등) 추가 구성. 데드라인(06:00 KST) 초과 및 단 1건의 실패 발생 시 AlertManager 기반 Discord 알림 즉각 발송 체계 마련

PR Checklist

  • 커밋 메시지 컨벤션에 맞게 작성했습니다.
  • 변경 사항에 대한 테스트를 했습니다.(버그 수정/기능에 대한 테스트).

Iwantcod added 26 commits May 25, 2026 19:21
- 03:00 manager lock 획득 실패 시 worker 시작 감지로 진입
- scheduler가 계산한 usageDate를 worker 시작 감지에 전달
- 최신 RUNNING batch 대신 usageDate 기준 RUNNING usage sync batch 조회
- 애플리케이션 기동 직후 worker 자동 감지 경로 제거
- TaskScheduler 스레드 경합 해소:
  - `application.yaml`의 `spring.task.scheduling.pool.size`를 2로 확장하여 03:00 KST Manager cron과 Worker 지연 스케줄링 간 병목 방지
- 장애 관측용 Micrometer Metric 노출:
  - `LineDailyUsageSyncBatchMetrics`를 추가해 최신 RUNNING 배치의 실패 타겟 수, 상태 코드, 실행 시간을 Gauge로 노출 (AlertManager 연동용)
- 관리자용 Rerun API 및 실패 타겟 부분 재처리 구현:
  - `AdminTrafficBatchController`에 `FAILED`/`ABANDONED` 배치 재처리를 위한 `/rerun` 엔드포인트 추가
  - `LineDailyUsageSyncRerunService`를 구현하여 과거 이력을 덮어쓰지 않고 새로운 RUNNING 메타데이터 생성
  - `LineDailyBatchTargetMapper`를 통해 기존 성공 데이터(`DONE`/`SKIPPED`)는 보존하고 `FAILED` 타겟만 `RETRYABLE`로 전환하여 안전하게 재개되도록 트랜잭션 분리
@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (2)
  • main
  • develop

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 479c53f8-feeb-49c7-a09c-ed93760c7c2b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

일별 사용량 동기화 배치: 앱 사용량을 individual/shared/qos로 분리해 Redis 스냅샷을 읽고 Manager가 Target을 적재·Worker가 선점해 MySQL로 동기화하며 Resume/Rerun·모니터링·테스트를 추가했습니다.

변경사항

일별 사용량 동기화 배치 (단일 cohort)

Layer / File(s) Summary
정책·문서 및 렌더링 규칙
AGENTS.md, GEMINI.md, docs/일별 동기화 배치 요약.md, docs/일별 사용량 동기화 배치 가이드.md
테스트 설계 원칙, AI 주석 규칙, Strict Read-Only 예외, Mermaid 렌더링 지침 및 배치 운영 절차(Manager/Worker/Resume/Rerun)를 문서화했습니다.
앱 사용량 모델 분할·CSV 스크립트
src/main/java/com/pooli/data/domain/entity/DailyAppTotalData.java, scripts/test-data/generate_test_data_csv.py, src/main/resources/db/migration/V2605250300__split_daily_app_total_usage_data.sql
기존 totalUsageDataindividual/shared/qos로 분할하고 CSV 생성·검증 로직과 마이그레이션을 갱신했습니다.
Redis Lua 확장 & 키 팩토리
src/main/resources/lua/traffic/deduct_unified.lua, src/main/java/com/pooli/traffic/service/runtime/TrafficRedisKeyFactory.java
앱별 source 필드 분리, daily/monthly shared를 해시로 누적, family_id 인자·검증 추가 및 daily shared key/manager lock key 생성 메서드 추가.
배치 도메인·매퍼·마이그레이션
src/main/java/com/pooli/traffic/domain/batch/*, src/main/resources/mapper/traffic/*, src/main/resources/db/migration/V2605250400__*.sql, V2605250410__*.sql
LineDailyBatchJob/Target/Status 모델과 MyBatis 매퍼(삽입·상태전이·CAS), 관련 테이블 마이그레이션을 추가했습니다.
Manager 스케줄러 및 매니저 서비스
src/main/java/com/pooli/traffic/service/batch/LineDailyBatchManagerScheduler.java, LineDailyBatchManagerService.java
03:00 KST 분산 락으로 단일 Manager 선출, target insert 청크 적재 및 usage sync 배치 시작/재개 로직을 구현했습니다.
Worker 스케줄러·클레임·워커 서비스
src/main/java/com/pooli/traffic/service/batch/LineDailyBatchWorkerScheduler.java, LineDailyBatchTargetClaimService.java, LineDailyUsageSyncWorkerService.java
FOR UPDATE SKIP LOCKED 기반 선점, PROCESSING 전환, 워커 루프(선점→Redis 스냅샷 읽기→영속화→종료/재시도 분기)를 구현했습니다.
Redis 스냅샷 리더 및 스냅샷 모델
src/main/java/com/pooli/traffic/service/batch/LineDailyUsageRedisReader.java, LineDailyUsageReadResult.java, DailyAppUsage.java, DailySharedUsage.java
Redis에서 total/app(shared/individual/qos)/shared hash를 읽어 스냅샷 객체로 변환하고 계약 위반 시 예외 처리합니다.
영속화·상태 전이 서비스
src/main/java/com/pooli/traffic/service/batch/LineDailyUsageSyncPersistenceService.java
스냅샷을 MySQL로 반영(총합·앱별 다중 insert·family shared insert), target terminal 전환 및 processed count 원자적 갱신, 실패 분류( retryable/non-retryable )를 구현했습니다.
Admin API / Resume & Rerun
src/main/java/com/pooli/traffic/controller/AdminTrafficBatchController.java, LineDailyUsageSyncResumeService.java, LineDailyUsageSyncRerunService.java
관리자 POST /daily-usage-sync/{usageDate}/resume/rerun 엔드포인트와 서비스 구현, 트랜잭션 커밋 후 워커 시작 예약을 제공.
차감 오케스트레이터 변경
src/main/java/com/pooli/traffic/service/decision/TrafficDeductLuaExecutor.java, TrafficDeductOrchestratorService.java
Lua 호출 키/인자에 dailySharedUsageKey와 familyId를 추가하고 Lua 결과 기반 finishedAt 결정 로직 순서를 조정했습니다.
모니터링·스케줄 설정
src/main/java/com/pooli/monitoring/metrics/LineDailyUsageSyncBatchMetrics.java, src/main/resources/application.yaml, README.md
Micrometer 게이지 3종 추가, 스케줄링 풀 크기(spring.task.scheduling.pool.size) 설정과 README 예시 환경변수 추가.
테스트 확장(계약/유닛/인수)
src/test/java/.../mapper/*, service/batch/*, acceptance/*, TrafficLuaPolicyContractTest
MyBatis SQL 계약 테스트, 매니저/워커/클레임/영속화/리더 유닛 테스트 및 Redis 데이터 구조 변경에 맞춘 인수 테스트를 추가/수정했습니다.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Iwantcod/pooli-be#4: deduct_unified.lua 및 Lua 호출 흐름 변경과 관련된 PR로, Lua/키 계약·오케스트레이션 경로가 겹칩니다.

Suggested labels

test, lua-script, db-migration

Sequence Diagram (high-level manager→worker flow)

sequenceDiagram
  participant Scheduler
  participant Redis
  participant Manager
  participant DB
  participant Worker
  Scheduler->>Redis: try acquire manager lock (SET NX PX)
  alt lock acquired
    Scheduler->>Manager: run(usageDate, managerId)
    Manager->>DB: insertPendingBatch / insertTargets(INSERT IGNORE chunks)
    Manager->>DB: completeTargetInsert -> startUsageSyncWithTargetCount
    Manager->>Redis: release lock (lua)
    Manager->>Worker: trigger worker scheduler (afterCommit)
  else lock not acquired
    Scheduler->>Worker: start worker scheduler (polling)
  end
  Worker->>DB: selectClaimableTargets FOR UPDATE SKIP LOCKED
  Worker->>Redis: read daily total/app/shared keys
  Worker->>DB: insertDailyTotal/app/shared, mark target DONE/FAILED/RETRYABLE
  DB->>Worker: update processed count (CAS)
Loading

건설적 권고(요약):

  • Redis 문자열→해시 전환에 대해 운영 환경의 폴백(strategy) 필요: 읽기 메서드에 문자열 폴백 또는 데이터 마이그레이션 스텝을 추가하세요.
  • 설정 외부화: lease timeout, chunk sizes, worker claim 등 하드코딩된 상수를 application.yaml로 옮겨 운영 환경별 튜닝을 허용하세요.
  • Lua 버전 관리·호환성 테스트: Lua 인자·KEYS 변경이 서비스 경로 전반에 영향 주므로 버전태깅과 Canary/백워드 호환성 테스트를 마련하세요.
  • DataServiceImpl 보정 로직은 복잡하므로 핵심 정규화 규칙을 작은 유닛으로 분리해 단위 테스트를 늘리고 문서화하세요.
  • 에러 메시지 저장/트렁케이션 정책을 명확히 하고, last_error_message 컬럼 사이즈·인덱스 영향 검토를 권장합니다.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/daily-persist-batch
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch feat/daily-persist-batch

@Iwantcod

Copy link
Copy Markdown
Owner Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 31

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/main/java/com/pooli/data/domain/entity/DailyAppTotalData.java (1)

12-25: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

클래스 책임 Javadoc 보강이 필요합니다.

Line 12의 클래스가 변경되었는데 클래스 책임/역할 설명 Javadoc이 없습니다. 이번 필드 분리 의도(개인/공유/QoS 사용량 보관)를 클래스 수준에서 명시해 주세요.

As per coding guidelines **/*.{java,kt}: "When creating or modifying a class, write or update Javadoc that explains the class responsibility and role."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/pooli/data/domain/entity/DailyAppTotalData.java` around
lines 12 - 25, Add a Javadoc comment to the DailyAppTotalData class documenting
its responsibility and role: state that this DTO/entity stores per-day
aggregated app usage totals and explicitly explain the intent of the separated
fields individualUsageData, sharedUsageData, and qosUsageData (i.e., personal
usage, shared/pooled usage, and QoS-attributed usage) and note any invariants or
usage expectations (e.g., which fields can be null or summed). Place the Javadoc
immediately above the class declaration for DailyAppTotalData so it accompanies
the class and clarifies its purpose for future maintainers.
src/main/java/com/pooli/traffic/service/runtime/TrafficRemainingBalanceCacheService.java (1)

78-98: 🧹 Nitpick | 🔵 Trivial | ⚖️ Poor tradeoff

incrementAmountIfPresent의 Redis 호출 2회 구조 검토

현재 readAmount()로 존재 여부를 확인한 후 increment()를 호출하므로 Redis 왕복이 2회 발생합니다. 동시성 환경에서 두 호출 사이에 다른 스레드가 키를 삭제하면 의도치 않게 새 키가 생성될 수 있습니다.

Lua 스크립트로 "존재하면 increment" 로직을 원자적으로 처리하거나, HINCRBY의 반환값과 HEXISTS 결과를 파이프라인으로 묶는 방식을 고려할 수 있습니다. 다만 현재 사용 패턴에서 실제 문제가 되는지는 호출 빈도와 키 생명주기에 따라 다릅니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/pooli/traffic/service/runtime/TrafficRemainingBalanceCacheService.java`
around lines 78 - 98, incrementAmountIfPresent currently does a readAmount(...)
then opsForHash().increment(...), causing two Redis round-trips and risking a
race that can create a new hash between calls; fix by replacing the two-step
logic with a single atomic Redis call (use cacheStringRedisTemplate.execute with
a RedisCallback or EVAL a Lua script) that: 1) checks that the hash and its
"amount" field exist, 2) reads the current value to enforce the unlimited
sentinel (-1) no-decrement rule, and 3) performs HINCRBY only when allowed,
returning a boolean success; update incrementAmountIfPresent to call this single
atomic operation and remove the separate readAmount call to avoid creating a key
inadvertently.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/일별` 동기화 배치 요약.md:
- Line 38: Multiple Markdown headers (e.g., the header "## 1. 동기화 배치 절차 구성 요소"
and other headers at the referenced locations) violate markdownlint MD022 by
missing the required blank line before or after; fix each header by ensuring
there is exactly one blank line both immediately above and immediately below
each header line (or at least one blank line where appropriate) so headers are
separated from surrounding content, then re-run markdownlint to confirm warnings
at the listed header locations (45, 85-86, 126, 165-166, 205, 249, 254, 262,
267) are resolved.
- Around line 5-36: Add the standard Mermaid init directive line %%{init:
{"theme": "default", "themeVariables": {"background": "`#ffffff`"}}}%% at the very
top of every Mermaid code block (the blocks that start with ```mermaid and
contain sequenceDiagram/other diagrams) so they render with a consistent white
background; update the existing sequenceDiagram block (the one with participants
Scheduler/Redis/Manager/Worker/DB) and repeat the same change for the other
Mermaid blocks referenced in the comment (the blocks currently at the later
sections of the document) to ensure consistent rendering across all diagrams.

In `@docs/일별` 사용량 동기화 배치 가이드.md:
- Around line 41-46: Several fenced code blocks lack a language tag and
empty-line padding which triggers MD040/MD031 lint warnings; update each
triple-backtick block (e.g., the block containing "Spring 앱 (Micrometer Gauge) →
Prometheus (metric scraping) → AlertManager (Docker 컨테이너) → Discord 웹훅" and the
other fenced sections referenced) to include an explicit language token such as
text, bash, or sql as appropriate, and ensure there is a blank line before and
after each fence (add a blank line above the opening ``` and below the closing
```), so all fenced blocks are consistently annotated and have surrounding
whitespace to satisfy the linter.

In `@GEMINI.md`:
- Around line 218-223: The nested fenced block that starts with the outer
````markdown```` wrapper and contains the inner ```mermaid ... ``` block is
missing blank lines before and after, causing MD031; update the document so
there is an empty line immediately before the outer fenced block and an empty
line immediately after it (i.e., add a blank line above the ````markdown````
opening fence and below the closing fence) to satisfy the lint rule.

In `@scripts/test-data/generate_test_data_csv.py`:
- Around line 1397-1402: The zip over chosen_apps, individual_app_usage_parts,
shared_app_usage_parts, and qos_app_usage_parts should be made strict to enforce
equal lengths; update the zip call in the loop that iterates "for app_id,
individual_usage, shared_app_usage, qos_usage in zip(...)" to pass strict=True
(i.e., zip(chosen_apps, individual_app_usage_parts, shared_app_usage_parts,
qos_app_usage_parts, strict=True)) so mismatched list lengths raise immediately
instead of silently truncating.

In `@src/main/java/com/pooli/data/service/impl/DataServiceImpl.java`:
- Around line 211-214: In DataServiceImpl.java update the recalculation guards
to skip sentinel remaining=-1 cases by requiring remainingAmount >= 0 before
doing total - remaining; specifically, change the conditional that uses
personalTotalAmount and personalRemainingAmount (where personalUsedAmount is
computed) to include personalRemainingAmount >= 0, and do the same for the
analogous businessTotalAmount/businessRemainingAmount block so you never compute
total - remaining when remaining is the -1 sentinel.

In
`@src/main/java/com/pooli/monitoring/metrics/LineDailyUsageSyncBatchMetrics.java`:
- Around line 82-86: The current branch in LineDailyUsageSyncBatchMetrics that
checks batchJob.getStatus() != LineDailyBatchStatus.RUNNING incorrectly zeroes
failedCount and runDurationSeconds; stop resetting failedCount there so failure
metrics are preserved across terminal states and only reset runDurationSeconds
to 0 for non-RUNNING states. Update the block in the method containing
failedCount and runDurationSeconds (refer to failedCount.set(...),
runDurationSeconds.set(...), and the status check using
LineDailyBatchStatus.RUNNING) to only set runDurationSeconds.set(0L) when not
RUNNING and leave failedCount unchanged.

In
`@src/main/java/com/pooli/traffic/domain/batch/LineDailyBatchJobCreateResult.java`:
- Around line 7-10: Add Javadoc comments to the record
LineDailyBatchJobCreateResult and document each record component: explain that
the boolean created indicates whether a new LineDailyBatchJob was created (true)
or an existing one was reused (false), and that the batchJob component holds the
corresponding LineDailyBatchJob instance returned (the newly created job when
created==true or the existing job when created==false); also state the
nullability contract for batchJob (if it can be null or never null) so callers
understand the exact behavior per the project DTO/commenting guideline.

In
`@src/main/java/com/pooli/traffic/domain/dto/response/LineDailyUsageSyncResumeResDto.java`:
- Around line 20-23: Add short, field-level JavaDoc or line comments immediately
above each DTO field in LineDailyUsageSyncResumeResDto (batchJobId, usageDate,
status, resumeAccepted) that explain the domain meaning and response contract
for that field; for example, describe what batchJobId identifies, the
semantics/format of usageDate, the possible states represented by
LineDailyBatchStatus, and what true/false for resumeAccepted denotes. Keep each
comment concise (one sentence) and use the same comment style as other DTOs to
satisfy the /* .java */ guideline for DTOs/enums/records.

In `@src/main/java/com/pooli/traffic/mapper/DailyAppUsageBatchInsertRow.java`:
- Around line 6-11: Add Javadoc comments for the record
DailyAppUsageBatchInsertRow and document each record component: applicationId,
individualUsageData, sharedUsageData, and qosUsageData. For each component
(applicationId, individualUsageData, sharedUsageData, qosUsageData) add a short
description explaining its meaning, units (e.g., bytes, seconds), and any
important constraints or interpretation (nullable/optional, aggregation
semantics). Ensure the comments follow JavaDoc style above the record
declaration so the DTO, enum, and record documentation guideline is satisfied.

In `@src/main/java/com/pooli/traffic/mapper/LineDailyBatchJobMapper.java`:
- Around line 13-16: The mapper currently relies on "select then insert" in
LineDailyBatchJobMapper which exposes a TOCTOU race causing duplicate
batch_name+usage_date rows; add DB-level idempotency and an atomic upsert in the
mapper (e.g., add a new method like upsertLineDailyBatchJob or
insertIfNotExists) that uses a unique constraint on (batch_name, usage_date)
plus a single-statement upsert (INSERT ... ON CONFLICT DO NOTHING/UPDATE or
MERGE depending on your DB) and return the existing/created record id; update
callers to use this new upsert method instead of separate select+insert so
creation is atomic and safe under concurrent manager retries.

In `@src/main/java/com/pooli/traffic/service/batch/DailyAppUsage.java`:
- Around line 3-11: Add Javadoc to the DailyAppUsage record that documents each
record component: annotate the record with a Javadoc summary and include `@param`
tags describing applicationId (the app identifier), individualUsageData
(per-user usage bytes/count for that app), sharedUsageData (shared/group usage
bytes/count attributed to that app), and qosUsageData (QoS-related usage
bytes/count for that app); update the JavaDoc above the record declaration for
DailyAppUsage so each component’s meaning is explicitly documented per the
DTO/record guideline.

In `@src/main/java/com/pooli/traffic/service/batch/DailySharedUsage.java`:
- Around line 6-9: Add component-level Javadoc comments to the record
DailySharedUsage describing each record component: document what familyId
represents (e.g., the unique identifier of the family/group) and what
usageAmount represents (e.g., the aggregated/shared usage amount in the chosen
unit), using brief, clear descriptions for the components so the record serves
as a self-documented DTO per coding guidelines.

In `@src/main/java/com/pooli/traffic/service/batch/LineDailyBatchJobService.java`:
- Around line 31-54: The current check-then-insert in
createPendingForAutomaticRunIfAbsent (using
lineDailyBatchJobMapper.selectLatestByBatchNameAndUsageDate then
lineDailyBatchJobMapper.insert) can race and create duplicates for (batch_name,
usage_date); add a DB-level uniqueness constraint on LINE_DAILY_BATCH_JOB for
(batch_name, usage_date) and change the persistence logic in
LineDailyBatchJobMapper.xml to an atomic upsert/INSERT IGNORE/ON DUPLICATE KEY
UPDATE (or catch duplicate-key SQL exception) so concurrent callers either reuse
the existing row; if you catch a duplicate-key error, re-query
selectLatestByBatchNameAndUsageDate and return that existing LineDailyBatchJob
instead of failing.

In
`@src/main/java/com/pooli/traffic/service/batch/LineDailyBatchManagerScheduler.java`:
- Around line 49-74: The current runForUsageDate method can skip starting
workers if lineDailyBatchManagerService.run(usageDate, managerInstanceId)
throws; wrap the manager invocation in try/catch and ensure workerStartAllowed
is determined even on exception by invoking a safe fallback check (e.g., call a
new/existing idempotent method on LineDailyBatchManagerService such as
isWorkerStartRequired(usageDate) or fetch the batch state) and then call
lineDailyBatchWorkerScheduler.startForUsageDate(usageDate) if that check
indicates work exists; keep the finally block that calls
releaseManagerLock(lockKey, managerInstanceId) unchanged and ensure exceptions
are logged/rethrown only after the fallback decision so the lock release and
worker start decision are always handled.

In
`@src/main/java/com/pooli/traffic/service/batch/LineDailyBatchTargetClaimService.java`:
- Around line 30-35: The claim method is missing input validation: at the start
of LineDailyBatchTargetClaimService.claim(usageDate, workerId, limit) add checks
to reject null/empty workerId and non-positive limit (e.g., throw
IllegalArgumentException or a domain-specific exception) before calling
lineDailyBatchTargetMapper.selectClaimableTargetsForUpdate; ensure the
validation runs prior to using PROCESSING_LEASE_TIMEOUT_SECONDS and limit so no
invalid values are passed into the DB claim query and include clear error
messages referencing workerId and limit.

In
`@src/main/java/com/pooli/traffic/service/batch/LineDailyBatchWorkerScheduler.java`:
- Around line 60-80: The runStartCheckCycle method can exit without scheduling a
next cycle if an exception is thrown; wrap the entire body of runStartCheckCycle
in a try/catch that catches Throwable, logs the error (include context like
usageDate), and always calls scheduleNextCheck(usageDate, <RETRY_DELAY_MS> or
START_CHECK_DELAY_MS + nextJitterMs()) in the catch/finally path to guarantee
the loop continues; ensure exceptions from
lineDailyBatchJobService.findRunningUsageSyncBatch(...) and
lineDailyUsageSyncWorkerService.run(...) are handled and do not prevent
scheduling the retry.

In `@src/main/java/com/pooli/traffic/service/batch/LineDailyUsageReadResult.java`:
- Around line 17-18: The hasAnyUsage() method in the LineDailyUsageReadResult
record can NPE when appUsages is null; either normalize appUsages to an empty
list in the record compact constructor (e.g., set appUsages = List.of() when
null) or make hasAnyUsage() null-safe by changing its check to use (appUsages !=
null && !appUsages.isEmpty()). Update the record compact constructor or the
hasAnyUsage() method accordingly so appUsages is never dereferenced when null.
- Around line 11-15: Add Javadoc or inline comments for the record
LineDailyUsageReadResult explaining the semantic meaning of each record
component: describe what totalUsageData represents (e.g., aggregated total usage
value and its unit/source), what appUsages contains (e.g., list of DailyAppUsage
entries and their aggregation scope/source), and what sharedUsage represents
(e.g., DailySharedUsage for cross-app/shared resources and its aggregation
scope); place the comments directly above the record declaration and annotate
each component so downstream batch logic can unambiguously interpret the data.

In
`@src/main/java/com/pooli/traffic/service/batch/LineDailyUsageSyncPersistenceService.java`:
- Around line 26-27: Add brief JavaDoc or inline comments above the constants
MAX_TARGET_RETRY_COUNT and LAST_ERROR_MESSAGE_MAX_LENGTH in
LineDailyUsageSyncPersistenceService explaining what each constant controls
(e.g., MAX_TARGET_RETRY_COUNT = max retry attempts for target persistence
operations, LAST_ERROR_MESSAGE_MAX_LENGTH = truncation limit for stored error
messages), why the chosen values (10 and 512) were selected (e.g., tradeoff
between retry aggressiveness and system load; reason for 512 bytes/characters as
reasonable log size), and any implications of changing them (what to watch for
if increased/decreased). Ensure the comments reference the exact symbols
MAX_TARGET_RETRY_COUNT and LAST_ERROR_MESSAGE_MAX_LENGTH so future readers can
quickly understand purpose and tuning considerations.

In
`@src/main/java/com/pooli/traffic/service/batch/LineDailyUsageSyncWorkerService.java`:
- Around line 192-230: The cause-chain traversal in
isRetryableDbException(DataAccessException) can loop if an exception's
getCause() returns itself; modify that method to guard against cycles by
tracking visited Throwables (e.g., a Set<Throwable> seen) or enforcing a max
depth and bail out when a repeat or depth limit is reached, while preserving the
existing instanceof checks (QueryTimeoutException, CannotAcquireLockException,
DeadlockLoserDataAccessException, PessimisticLockingFailureException,
ConcurrencyFailureException); keep the helper
isRetryableDbException(RuntimeException) delegating to the DataAccessException
overload unchanged.

In `@src/main/resources/application.yaml`:
- Around line 36-39: 현재 application.yaml에 하드코딩된 task.scheduling.pool.size 값을
환경변수로 외부화하세요: task.scheduling.pool.size 항목을 ${TASK_SCHED_POOL_SIZE:2} 형태의
플레이스홀더로 바꿔 기본값 2를 유지하고 외부에서 TASK_SCHED_POOL_SIZE로 재정의할 수 있게 하며, 관련 문서(환경변수 이름 및
기대 타입: integer)를 업데이트하고 배포 환경에서 해당 환경변수로 값을 주입해 변경 없이 풀 크기를 조정할 수 있도록 합니다.

In
`@src/main/resources/db/migration/V2605250300__split_daily_app_total_usage_data.sql`:
- Around line 1-5: The migration currently alters DAILY_APP_TOTAL_DATA by adding
individual_usage_data, shared_usage_data, qos_usage_data and dropping
total_usage_data in one step, which breaks zero-downtime deploys; split this
into two migrations: first migration should only ADD the three columns (BIGINT
NOT NULL DEFAULT 0) and leave total_usage_data intact, rollout and perform
application switch/backfill to start using
individual_usage_data/shared_usage_data/qos_usage_data, then create a follow-up
migration that only DROP COLUMN total_usage_data once all instances no longer
read it. Ensure you reference DAILY_APP_TOTAL_DATA and the columns
individual_usage_data, shared_usage_data, qos_usage_data, total_usage_data when
implementing the split.

In
`@src/main/resources/db/migration/V2605250400__create_line_daily_batch_job.sql`:
- Around line 2-23: The table LINE_DAILY_BATCH_JOB currently only has a
non-unique index idx_line_daily_batch_job_lookup on (batch_name, usage_date)
which allows concurrent inserts of the same batch_name+usage_date; add a
DB-level uniqueness constraint on (batch_name, usage_date) to enforce integrity
(e.g. create a UNIQUE INDEX/CONSTRAINT on those columns or change
idx_line_daily_batch_job_lookup to be UNIQUE). Before creating the constraint,
include a safe deduplication step in the migration to detect and resolve any
existing duplicate rows (merge/delete duplicates or abort with an explicit
error), and create the unique index/constraint in a way that won’t break
existing data (handle locking/long-running operations per your DB best
practices). Ensure references in code that expect non-uniqueness handle the new
constraint if needed.

In
`@src/main/resources/db/migration/V2605260100__drop_line_daily_batch_job_updated_at.sql`:
- Around line 1-2: The migration currently drops LINE_DAILY_BATCH_JOB.updated_at
immediately which is unsafe for zero-downtime deploys; revert this DROP COLUMN
from V2605260100__drop_line_daily_batch_job_updated_at.sql (remove the ALTER ...
DROP COLUMN statement) and instead split into two steps: (1) remove all
application references to updated_at in your app changes and deploy, and (2) add
a separate follow-up migration (e.g.,
V<later>__drop_line_daily_batch_job_updated_at.sql) that performs ALTER TABLE
LINE_DAILY_BATCH_JOB DROP COLUMN updated_at only after the code no longer
references the column; ensure the migration files and commit message reference
the table LINE_DAILY_BATCH_JOB and column updated_at so reviewers can track the
two-step removal.

In `@src/main/resources/lua/traffic/deduct_unified.lua`:
- Around line 145-156: The unconditional checks for daily_shared_usage_key and
family_id cause non-shared requests to fail; instead, keep existing general
validations for target_data and app_id but move or narrow the validation of
daily_shared_usage_key and family_id into the shared-deduction branch (the code
path where shared_deducted > 0) so they are only required when shared usage will
be written/read; update logic around the shared_deducted calculation to assert
daily_shared_usage_key is present and family_id > 0 before any shared-related
Redis operations.

In `@src/main/resources/mapper/traffic/LineDailyBatchTargetMapper.xml`:
- Around line 105-133: The query selectClaimableTargetsForUpdate can benefit
from the existing composite index KEY idx_line_daily_batch_target_claim
(usage_date, status, status_updated_at); to avoid extra sorting/lookup caused by
ORDER BY status_updated_at ASC, id ASC you should extend that index to include
id (e.g. (usage_date, status, status_updated_at, id)) in the migration
V2605250410__create_line_daily_batch_target.sql or add a new migration that adds
the fourth column, and validate under load that the index expansion improves
throughput before rolling it out.

In `@src/main/resources/mapper/traffic/TrafficDailyUsageBatchMapper.xml`:
- Around line 32-55: 현재 insertDailyAppUsages 매퍼는 빈 appUsages 컬렉션이 들어오면 SQL 문법
오류가 날 가능성이 있으므로, insertDailyAppUsages 엘리먼트 내부에서 appUsages가 null이 아니고 size>0일 때만
VALUES/foreach 블록을 렌더하도록 가드(<if test="appUsages != null and appUsages.size() >
0">)를 추가하세요; 호출부인 LineDailyUsageSyncPersistenceService#insertUsages가 현재 방어하고 있지만
향후 호출 경로를 대비해 매퍼 수준에서 appUsages를 체크하여 안전하게 방어하십시오.

In `@src/test/java/com/pooli/traffic/config/TrafficSchedulingConfigTest.java`:
- Around line 18-24: The test taskSchedulerPoolSizeIsTwo in
TrafficSchedulingConfigTest currently asserts YAML text contains a specific
indentation-sensitive fragment; instead load the YAML string returned by
read(APPLICATION_YAML) into a PropertySource (e.g., using
YamlPropertySourceLoader or Spring's Binder) and read the property key
"task.scheduling.pool.size" as an int, then assertEquals(2). Replace the fragile
assertTrue(yaml.contains(...)) with code that parses the YAML and asserts the
numeric value for task.scheduling.pool.size is 2.

In
`@src/test/java/com/pooli/traffic/mapper/LineDailyBatchTargetMapperSqlContractTest.java`:
- Around line 95-226: Several tests extract SQL using
substring(sql.indexOf("<select id=\"...\"")) or substring(sql.indexOf("<update
id=\"...\"")) without finding the matching closing tag, allowing overbroad
snippets and false positives; update each extraction (for ids:
selectClaimableTargetsForUpdate, markTargetsProcessing,
markTargetTerminalIfProcessing, markTargetRetryableIfProcessing,
markTargetFailedIfProcessing, markFailedTargetsRetryableByUsageDate and any
other tests using substring(indexOf(...))) to compute a start = sql.indexOf(tag)
and an explicit end = sql.indexOf(closingTag, start) (use "</select>" or
"</update>" as appropriate) and then use substring(start, end +
closingTag.length()) (with a guard for end == -1 to fail the test meaningfully)
so each assertion checks only the exact element body.

In
`@src/test/java/com/pooli/traffic/service/batch/LineDailyBatchJobServiceTest.java`:
- Around line 281-292: Add a complementary unit test in
LineDailyBatchJobServiceTest that covers the CAS failure path: create a
LineDailyBatchJob via batchJob(LineDailyBatchStatus.RUNNING), mock
lineDailyBatchJobMapper.completeRunningUsageSyncBatchIfCountsMatch(running.getId())
to return 0, call
lineDailyBatchJobService.completeRunningUsageSyncBatchIfCountsMatch(running),
assert that the result is false, and verify the mapper method was invoked; this
mirrors the existing successful test but asserts false for the 0-affected-rows
case to ensure the failure path is covered.

---

Outside diff comments:
In `@src/main/java/com/pooli/data/domain/entity/DailyAppTotalData.java`:
- Around line 12-25: Add a Javadoc comment to the DailyAppTotalData class
documenting its responsibility and role: state that this DTO/entity stores
per-day aggregated app usage totals and explicitly explain the intent of the
separated fields individualUsageData, sharedUsageData, and qosUsageData (i.e.,
personal usage, shared/pooled usage, and QoS-attributed usage) and note any
invariants or usage expectations (e.g., which fields can be null or summed).
Place the Javadoc immediately above the class declaration for DailyAppTotalData
so it accompanies the class and clarifies its purpose for future maintainers.

In
`@src/main/java/com/pooli/traffic/service/runtime/TrafficRemainingBalanceCacheService.java`:
- Around line 78-98: incrementAmountIfPresent currently does a readAmount(...)
then opsForHash().increment(...), causing two Redis round-trips and risking a
race that can create a new hash between calls; fix by replacing the two-step
logic with a single atomic Redis call (use cacheStringRedisTemplate.execute with
a RedisCallback or EVAL a Lua script) that: 1) checks that the hash and its
"amount" field exist, 2) reads the current value to enforce the unlimited
sentinel (-1) no-decrement rule, and 3) performs HINCRBY only when allowed,
returning a boolean success; update incrementAmountIfPresent to call this single
atomic operation and remove the separate readAmount call to avoid creating a key
inadvertently.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 86877dc1-c285-4786-a44d-9e7f5a6dc559

📥 Commits

Reviewing files that changed from the base of the PR and between c11ed75 and cbeacb8.

📒 Files selected for processing (77)
  • AGENTS.md
  • GEMINI.md
  • docs/일별 동기화 배치 요약.md
  • docs/일별 사용량 동기화 배치 가이드.md
  • scripts/test-data/generate_test_data_csv.py
  • src/main/java/com/pooli/common/config/AppClockConfig.java
  • src/main/java/com/pooli/data/domain/entity/DailyAppTotalData.java
  • src/main/java/com/pooli/data/service/DataService.java
  • src/main/java/com/pooli/data/service/impl/DataServiceImpl.java
  • src/main/java/com/pooli/family/service/FamilySharedPoolsService.java
  • src/main/java/com/pooli/monitoring/metrics/LineDailyUsageSyncBatchMetrics.java
  • src/main/java/com/pooli/traffic/controller/AdminTrafficBatchController.java
  • src/main/java/com/pooli/traffic/domain/batch/BatchName.java
  • src/main/java/com/pooli/traffic/domain/batch/LineDailyBatchJob.java
  • src/main/java/com/pooli/traffic/domain/batch/LineDailyBatchJobCreateResult.java
  • src/main/java/com/pooli/traffic/domain/batch/LineDailyBatchStatus.java
  • src/main/java/com/pooli/traffic/domain/batch/LineDailyBatchTarget.java
  • src/main/java/com/pooli/traffic/domain/batch/LineDailyBatchTargetStatus.java
  • src/main/java/com/pooli/traffic/domain/dto/response/LineDailyUsageSyncRerunResDto.java
  • src/main/java/com/pooli/traffic/domain/dto/response/LineDailyUsageSyncResumeResDto.java
  • src/main/java/com/pooli/traffic/mapper/DailyAppUsageBatchInsertRow.java
  • src/main/java/com/pooli/traffic/mapper/LineDailyBatchJobMapper.java
  • src/main/java/com/pooli/traffic/mapper/LineDailyBatchTargetMapper.java
  • src/main/java/com/pooli/traffic/mapper/TrafficDailyUsageBatchMapper.java
  • src/main/java/com/pooli/traffic/service/batch/DailyAppUsage.java
  • src/main/java/com/pooli/traffic/service/batch/DailyAppUsageAccumulator.java
  • src/main/java/com/pooli/traffic/service/batch/DailySharedUsage.java
  • src/main/java/com/pooli/traffic/service/batch/LineDailyBatchJobService.java
  • src/main/java/com/pooli/traffic/service/batch/LineDailyBatchManagerScheduler.java
  • src/main/java/com/pooli/traffic/service/batch/LineDailyBatchManagerService.java
  • src/main/java/com/pooli/traffic/service/batch/LineDailyBatchTargetClaimService.java
  • src/main/java/com/pooli/traffic/service/batch/LineDailyBatchWorkerScheduler.java
  • src/main/java/com/pooli/traffic/service/batch/LineDailyUsageReadResult.java
  • src/main/java/com/pooli/traffic/service/batch/LineDailyUsageRedisReader.java
  • src/main/java/com/pooli/traffic/service/batch/LineDailyUsageSyncPersistenceService.java
  • src/main/java/com/pooli/traffic/service/batch/LineDailyUsageSyncRerunService.java
  • src/main/java/com/pooli/traffic/service/batch/LineDailyUsageSyncResumeService.java
  • src/main/java/com/pooli/traffic/service/batch/LineDailyUsageSyncWorkerRunResult.java
  • src/main/java/com/pooli/traffic/service/batch/LineDailyUsageSyncWorkerService.java
  • src/main/java/com/pooli/traffic/service/decision/TrafficDeductLuaExecutor.java
  • src/main/java/com/pooli/traffic/service/runtime/TrafficRedisKeyFactory.java
  • src/main/java/com/pooli/traffic/service/runtime/TrafficRemainingBalanceCacheService.java
  • src/main/resources/application.yaml
  • src/main/resources/db/migration/V2605250300__split_daily_app_total_usage_data.sql
  • src/main/resources/db/migration/V2605250400__create_line_daily_batch_job.sql
  • src/main/resources/db/migration/V2605250410__create_line_daily_batch_target.sql
  • src/main/resources/db/migration/V2605260100__drop_line_daily_batch_job_updated_at.sql
  • src/main/resources/lua/traffic/deduct_unified.lua
  • src/main/resources/mapper/data/DataMapper.xml
  • src/main/resources/mapper/traffic/LineDailyBatchJobMapper.xml
  • src/main/resources/mapper/traffic/LineDailyBatchTargetMapper.xml
  • src/main/resources/mapper/traffic/TrafficDailyUsageBatchMapper.xml
  • src/test/java/com/pooli/data/service/impl/DataServiceImplTest.java
  • src/test/java/com/pooli/family/service/FamilySharedPoolsServiceTest.java
  • src/test/java/com/pooli/monitoring/metrics/LineDailyUsageSyncBatchMetricsTest.java
  • src/test/java/com/pooli/traffic/acceptance/TrafficAcceptanceTestSupport.java
  • src/test/java/com/pooli/traffic/acceptance/TrafficDataPolicyAcceptanceTest.java
  • src/test/java/com/pooli/traffic/acceptance/TrafficDataUsageAcceptanceTest.java
  • src/test/java/com/pooli/traffic/config/TrafficSchedulingConfigTest.java
  • src/test/java/com/pooli/traffic/controller/AdminTrafficBatchControllerTest.java
  • src/test/java/com/pooli/traffic/mapper/LineDailyBatchJobMapperSqlContractTest.java
  • src/test/java/com/pooli/traffic/mapper/LineDailyBatchTargetMapperSqlContractTest.java
  • src/test/java/com/pooli/traffic/mapper/TrafficDailyUsageBatchMapperSqlContractTest.java
  • src/test/java/com/pooli/traffic/service/batch/LineDailyBatchJobServiceTest.java
  • src/test/java/com/pooli/traffic/service/batch/LineDailyBatchManagerSchedulerTest.java
  • src/test/java/com/pooli/traffic/service/batch/LineDailyBatchManagerServiceTest.java
  • src/test/java/com/pooli/traffic/service/batch/LineDailyBatchTargetClaimServiceTest.java
  • src/test/java/com/pooli/traffic/service/batch/LineDailyBatchWorkerSchedulerTest.java
  • src/test/java/com/pooli/traffic/service/batch/LineDailyUsageRedisReaderTest.java
  • src/test/java/com/pooli/traffic/service/batch/LineDailyUsageSyncPersistenceServiceTest.java
  • src/test/java/com/pooli/traffic/service/batch/LineDailyUsageSyncRerunServiceTest.java
  • src/test/java/com/pooli/traffic/service/batch/LineDailyUsageSyncResumeServiceTest.java
  • src/test/java/com/pooli/traffic/service/batch/LineDailyUsageSyncWorkerServiceTest.java
  • src/test/java/com/pooli/traffic/service/decision/TrafficDeductLuaExecutorTest.java
  • src/test/java/com/pooli/traffic/service/runtime/TrafficLuaPolicyContractTest.java
  • src/test/java/com/pooli/traffic/service/runtime/TrafficRedisKeyFactoryTest.java
  • src/test/java/com/pooli/traffic/service/runtime/TrafficRemainingBalanceCacheServiceTest.java

Comment on lines +5 to +36
```mermaid
sequenceDiagram
autonumber
participant Scheduler as 모든 서버
participant Redis as Redis
participant Manager as Manager (1대)
participant Worker as Worker (나머지 + Manager)
participant DB as MySQL

Scheduler->>Redis: 03:00 KST Manager 락 획득 시도
Redis-->>Manager: 락 획득 성공 (Manager 선출)
Redis-->>Worker: 락 획득 실패 (Worker 대기)

rect rgb(240, 248, 255)
note right of Manager: 1. Target Insert 절차 (대상 준비)
Manager->>DB: 메타데이터 생성 및 Target Row 일괄 적재
Manager->>DB: 동기화 배치 상태 "RUNNING" 전환
end

Manager-->>Worker: Manager도 세팅 완료 후 Worker로 합류

rect rgb(240, 255, 240)
note right of Worker: 2. Usage Sync 절차 (다중 워커 병렬 동기화)
loop 잔여 타겟 소진 시까지 반복
Worker->>DB: 100건 할당/선점 (SKIP LOCKED)
Worker->>Redis: 대상 유저의 사용량 스냅샷 조회
Worker->>DB: [단일 트랜잭션] 사용량 적재 & 상태 완료 갱신
end
end

Worker->>DB: 처리할 타겟이 없으면 최종 "COMPLETED" 갱신
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Mermaid 블록에 공통 init directive를 추가해 렌더링 일관성을 맞춰주세요.

여러 Mermaid 다이어그램에 %%{init: {"theme": "default", "themeVariables": {"background": "#ffffff"}}}%%가 빠져 있어 환경별 배경/테마가 달라질 수 있습니다.

Based on learnings "All Mermaid diagrams must include the init directive %%{init: {"theme": "default", "themeVariables": {"background": "#ffffff"}}}%% at the top to render with a white background".

Also applies to: 53-83, 91-124, 131-163, 173-203, 215-247

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/일별` 동기화 배치 요약.md around lines 5 - 36, Add the standard Mermaid init
directive line %%{init: {"theme": "default", "themeVariables": {"background":
"`#ffffff`"}}}%% at the very top of every Mermaid code block (the blocks that
start with ```mermaid and contain sequenceDiagram/other diagrams) so they render
with a consistent white background; update the existing sequenceDiagram block
(the one with participants Scheduler/Redis/Manager/Worker/DB) and repeat the
same change for the other Mermaid blocks referenced in the comment (the blocks
currently at the later sections of the document) to ensure consistent rendering
across all diagrams.

Worker->>DB: 처리할 타겟이 없으면 최종 "COMPLETED" 갱신
```

## 1. 동기화 배치 절차 구성 요소

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

헤더 전후 공백줄(MD022) 경고를 정리해 주세요.

현재 다수 헤더에서 빈 줄 규칙이 깨져 markdownlint 경고가 발생합니다. 문서 린트가 파이프라인에 포함되어 있으면 병합 저해 요인이 됩니다.

Also applies to: 45-45, 85-86, 126-126, 165-166, 205-205, 249-249, 254-254, 262-262, 267-267

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 38-38: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/일별` 동기화 배치 요약.md at line 38, Multiple Markdown headers (e.g., the header
"## 1. 동기화 배치 절차 구성 요소" and other headers at the referenced locations) violate
markdownlint MD022 by missing the required blank line before or after; fix each
header by ensuring there is exactly one blank line both immediately above and
immediately below each header line (or at least one blank line where
appropriate) so headers are separated from surrounding content, then re-run
markdownlint to confirm warnings at the listed header locations (45, 85-86, 126,
165-166, 205, 249, 254, 262, 267) are resolved.

Comment on lines +41 to +46
```
Spring 앱 (Micrometer Gauge)
→ Prometheus (metric scraping)
→ AlertManager (Docker 컨테이너)
→ Discord 웹훅
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

코드 블록 언어 지정/공백 규칙을 일관되게 맞춰 주세요.

여러 fenced block에서 언어 미지정(MD040)과 fence 주변 공백(MD031) 경고가 확인됩니다. text, bash, sql 등 의도를 나타내는 language를 명시하고, Line 139~Line 141처럼 fence 전후 빈 줄을 추가해 문서 lint 안정성을 확보해 주세요.

Also applies to: 76-78, 86-88, 114-133, 139-141, 184-187

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 41-41: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/일별` 사용량 동기화 배치 가이드.md around lines 41 - 46, Several fenced code blocks
lack a language tag and empty-line padding which triggers MD040/MD031 lint
warnings; update each triple-backtick block (e.g., the block containing "Spring
앱 (Micrometer Gauge) → Prometheus (metric scraping) → AlertManager (Docker 컨테이너)
→ Discord 웹훅" and the other fenced sections referenced) to include an explicit
language token such as text, bash, or sql as appropriate, and ensure there is a
blank line before and after each fence (add a blank line above the opening ```
and below the closing ```), so all fenced blocks are consistently annotated and
have surrounding whitespace to satisfy the linter.

Comment thread GEMINI.md
Comment on lines +218 to +223
````markdown
```mermaid
%%{init: {"theme": "default", "themeVariables": {"background": "#ffffff"}}}%%
... diagram code ...
```

</div>
```

**Critical line-break rules** (to prevent markdown parsing errors):
- There MUST be at least **one empty line** immediately after the opening `<div>` tag.
- There MUST be at least **one empty line** immediately before the closing `</div>` tag.
- Violating this rule will cause the Mermaid block to render as plain text.
````

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

펜스 코드 블록 주변 공백 규칙을 맞춰 주세요.

Line 218~Line 223의 중첩 fenced block은 앞뒤 빈 줄이 없어 MD031 경고가 발생합니다. 문서 lint 파이프라인 안정성을 위해 fenced block 전후에 빈 줄을 추가해 주세요.

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 218-218: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@GEMINI.md` around lines 218 - 223, The nested fenced block that starts with
the outer ````markdown```` wrapper and contains the inner ```mermaid ... ```
block is missing blank lines before and after, causing MD031; update the
document so there is an empty line immediately before the outer fenced block and
an empty line immediately after it (i.e., add a blank line above the
````markdown```` opening fence and below the closing fence) to satisfy the lint
rule.

Comment on lines +1397 to +1402
for app_id, individual_usage, shared_app_usage, qos_usage in zip(
chosen_apps,
individual_app_usage_parts,
shared_app_usage_parts,
qos_app_usage_parts,
):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 1) Ruff 설정에서 B905 적용 여부 확인
rg -n "B905|\\[tool\\.ruff|select\\s*=|ignore\\s*=" .

# 2) 대상 zip 사용 위치 확인
rg -n -C2 "zip\\(" scripts/test-data/generate_test_data_csv.py

Repository: Iwantcod/pooli-be

Length of output: 554


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) ruff.toml에서 B905가 select/ignore에 어떻게 포함되는지 확인
python3 - <<'PY'
import pathlib, re
p = pathlib.Path("ruff.toml")
txt = p.read_text(encoding="utf-8")
for m in re.finditer(r'^\s*select\s*=\s*\[', txt, re.M):
    start = max(0, txt.rfind("\n", 0, m.start()))
print(txt.splitlines()[0:120])
PY

rg -n "B905" ruff.toml || true
rg -n "select\s*=" ruff.toml || true
rg -n "ignore\s*=" ruff.toml || true

# 2) 문제의 zip 직전/직후에서 변수들이 어떻게 만들어지는지 확인
FILE="scripts/test-data/generate_test_data_csv.py"
sed -n '1370,1435p' "$FILE" | nl -ba -w2 -s':'

Repository: Iwantcod/pooli-be

Length of output: 3168


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="scripts/test-data/generate_test_data_csv.py"

# 1) 해당 zip 입력들이 어디서 만들어지는지(길이 보장/가정이 있는지) 추적
for v in individual_app_usage_parts shared_app_usage_parts qos_app_usage_parts chosen_apps; do
  echo "---- $v (assignments) ----"
  rg -n "^[[:space:]]*$v[[:space:]]*=" "$FILE" || true
  echo "---- $v (usage near zip) ----"
  rg -n "$v" "$FILE" | head -n 40
done

# 2) zip 주변 더 넓게 문맥 확인(라인 번호는 awk로 표시)
python3 - <<'PY'
from pathlib import Path
p=Path("scripts/test-data/generate_test_data_csv.py")
lines=p.read_text(encoding="utf-8").splitlines()
start=1375
end=1425
for i in range(start, end+1):
    if i-1 < 0 or i-1 >= len(lines): continue
    print(f"{i:4d}: {lines[i-1]}")
PY

Repository: Iwantcod/pooli-be

Length of output: 3647


🌐 Web query:

Ruff B905 zip() strict keyword explanation and rationale

💡 Result:

Ruff rule B905 enforces the use of the strict keyword argument in Python's built-in zip function [1]. Rationale and Explanation The B905 rule is derived from flake8-bugbear and addresses a common source of silent logic errors in Python [1]. By default, the built-in zip function truncates its output to the length of the shortest input iterable [1][2]. If iterables are expected to be the same length, this behavior can mask bugs where data is missing or mismatched, often resulting in silent failures that are difficult to diagnose [3][4]. To mitigate this, Python 3.10 introduced the strict=True keyword argument (via PEP 618), which causes zip to raise a ValueError if it encounters iterables of different lengths [3][2]. The B905 rule mandates that developers explicitly define the strictness of their zip operations [1]. - If your iterables are intended to be of equal length, you should use strict=True [1][2]. - If you intend to work with iterables of different lengths, you should use strict=False to make that design choice explicit [1]. Implementation Details The rule B905 applies when zip is called with two or more iterables or any starred argument [1]. Because this rule relies on features introduced in Python 3.10, Ruff typically gates it based on the project's configured target Python version [5][6]. If you are targeting older versions of Python that do not support strict, you may need to adjust your Ruff configuration (e.g., target-version or requires-python) to prevent the rule from being incorrectly enforced [5][6]. Note that zip is lazy, meaning the length-checking (and any resulting ValueError) occurs only when the zip object is actually iterated over, not when it is initially created [2][7].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Ruff가 B905를 실제로 적용하는지(타겟 파이썬 버전/규칙 범위) 확인
echo "---- ruff.toml (relevant) ----"
rg -n "target-version|requires-python|python-version|line-length|lint\.select|lint\.ignore|select|ignore" ruff.toml

# 2) split_integer_total가 app_count 길이를 보장하는지 확인
FILE="scripts/test-data/generate_test_data_csv.py"
rg -n "def split_integer_total\\b" "$FILE"
python3 - <<'PY'
from pathlib import Path
p=Path("scripts/test-data/generate_test_data_csv.py")
lines=p.read_text(encoding="utf-8").splitlines()
# def split_integer_total 있는 줄 찾기
defline=None
for i,l in enumerate(lines, start=1):
    if l.startswith("def split_integer_total"):
        defline=i; break
if not defline:
    print("split_integer_total not found"); raise SystemExit(0)
start=defline-5; end=defline+80
for i in range(start, end+1):
    if 1<=i<=len(lines):
        print(f"{i:4d}: {lines[i-1]}")
PY

# 3) split_integer_total 호출부 인자들이 의도대로 길이 일치하는지(반환값 형태) 보강 확인
rg -n "split_integer_total\\(" "$FILE"

Repository: Iwantcod/pooli-be

Length of output: 3969


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# ruff config에서 target-version/파이썬 버전 지정 여부 확인
rg -n "target-version|requires-python|python-version" -S ruff.toml pyproject.toml setup.cfg . || true

# app_ids 정의 및 길이 제약 확인
FILE="scripts/test-data/generate_test_data_csv.py"
rg -n "app_ids\s*=|rng\.sample\(app_ids" "$FILE"
python3 - <<'PY'
from pathlib import Path
p=Path("scripts/test-data/generate_test_data_csv.py")
lines=p.read_text(encoding="utf-8").splitlines()
# zip 근처에서 app_ids가 어디서 오나 역추적용으로 앞부분(대략 1350~1398)만 출력
for i in range(1320, 1405):
    if 1<=i<=len(lines):
        print(f"{i:4d}: {lines[i-1]}")
PY

# split_integer_total 호출에서 total/shared 값 음수 가능성만 간단 체크(관련 정의 근처만 출력)
python3 - <<'PY'
from pathlib import Path
p=Path("scripts/test-data/generate_test_data_csv.py")
lines=p.read_text(encoding="utf-8").splitlines()
for i in range(1280, 1385):
    if 1<=i<=len(lines):
        if any(k in lines[i-1] for k in ["total_usage", "shared_usage", "shared_usage", "total_usage"]):
            print(f"{i:4d}: {lines[i-1]}")
PY

Repository: Iwantcod/pooli-be

Length of output: 4864


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Python 타겟/요구 버전 관련 설정 전역 탐색
rg -n --hidden --no-ignore-vcs "target-version|requires-python|python-version|python_version" . || true

# 혹시 pyproject에만 있을 수도 있어 파일 존재 여부도 확인
ls -la | head
ls -la pyproject.toml 2>/dev/null || true
ls -la ruff.toml 2>/dev/null || true
ls -la .python-version 2>/dev/null || true

Repository: Iwantcod/pooli-be

Length of output: 675


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "ruff.toml lines:"
wc -l ruff.toml

echo "---- ruff.toml (head) ----"
nl -ba ruff.toml | head -n 120

echo "---- ruff.toml (tail) ----"
nl -ba ruff.toml | tail -n 120

Repository: Iwantcod/pooli-be

Length of output: 179


🏁 Script executed:

#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
p=Path("ruff.toml")
lines=p.read_text(encoding="utf-8").splitlines()
for i,l in enumerate(lines, start=1):
    print(f"{i:3d}: {l}")
PY

Repository: Iwantcod/pooli-be

Length of output: 2905


🏁 Script executed:

#!/bin/bash
set -euo pipefail
ruff check scripts/test-data/generate_test_data_csv.py

Repository: Iwantcod/pooli-be

Length of output: 39007


zip()strict=를 명시해 의도(길이 동일)를 코드에 고정해 주세요.
Ruff B905가 실제로 해당 zip()에 대해 발생합니다. 현재는 app_count를 기준으로 리스트 길이를 맞추고 있어 바로 데이터 누락이 생길 가능성은 낮지만, strict=True를 명시하면 이후 refactor로 길이 불일치가 들어올 때 조용히 잘려서 DAILY_APP_TOTAL_DATA의 행 수 검증(C03)을 깨는 상황을 막을 수 있습니다.

  • 위치: scripts/test-data/generate_test_data_csv.py (1397-1402)
  • 변경: zip(..., strict=True)로 수정
🧰 Tools
🪛 Ruff (0.15.14)

[warning] 1397-1402: zip() without an explicit strict= parameter

Add explicit value for parameter strict=

(B905)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/test-data/generate_test_data_csv.py` around lines 1397 - 1402, The
zip over chosen_apps, individual_app_usage_parts, shared_app_usage_parts, and
qos_app_usage_parts should be made strict to enforce equal lengths; update the
zip call in the loop that iterates "for app_id, individual_usage,
shared_app_usage, qos_usage in zip(...)" to pass strict=True (i.e.,
zip(chosen_apps, individual_app_usage_parts, shared_app_usage_parts,
qos_app_usage_parts, strict=True)) so mismatched list lengths raise immediately
instead of silently truncating.

Comment thread src/main/resources/mapper/traffic/LineDailyBatchTargetMapper.xml
Comment thread src/main/resources/mapper/traffic/TrafficDailyUsageBatchMapper.xml
Comment thread src/test/java/com/pooli/traffic/config/TrafficSchedulingConfigTest.java Outdated
@Iwantcod

Copy link
Copy Markdown
Owner Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/test/java/com/pooli/monitoring/metrics/LineDailyUsageSyncBatchMetricsTest.java (1)

29-73: 🧹 Nitpick | 🔵 Trivial | 🏗️ Heavy lift

테스트 간 중복 코드 제거 및 경계값 테스트 보강 권장

현재 두 테스트 메서드에서 SimpleMeterRegistry, LineDailyBatchJobService mock, Clock 설정이 중복됩니다. 또한 2.0, 3.0, 1800.0 같은 매직 넘버가 하드코딩되어 있어 테스트 의도 파악이 어렵습니다.

개선 제안:

  1. @BeforeEach에서 공통 fixture 설정
  2. 매직 넘버를 상수로 추출 (예: EXPECTED_FAILED_COUNT, RUNNING_STATUS_CODE, EXPECTED_DURATION_SECONDS)
  3. 경계값 테스트 추가:
    • findLatestUsageSyncBatch()null 반환 시
    • failedCount0일 때
    • runStartedAtnull일 때 (non-RUNNING 상태)
♻️ 테스트 구조 개선 예시
class LineDailyUsageSyncBatchMetricsTest {

    private static final ZoneId KST = ZoneId.of("Asia/Seoul");
    private static final LocalDate USAGE_DATE = LocalDate.of(2026, 5, 25);
    private static final long EXPECTED_FAILED_COUNT = 2L;
    private static final double RUNNING_STATUS_CODE = 2.0;
    private static final double COMPLETED_STATUS_CODE = 3.0;
    private static final double EXPECTED_DURATION_SECONDS = 1800.0;

    private SimpleMeterRegistry meterRegistry;
    private LineDailyBatchJobService batchJobService;
    private Clock clock;

    `@BeforeEach`
    void setUp() {
        meterRegistry = new SimpleMeterRegistry();
        batchJobService = org.mockito.Mockito.mock(LineDailyBatchJobService.class);
        clock = Clock.fixed(Instant.parse("2026-05-25T21:30:00Z"), KST);
    }

    `@Test`
    `@DisplayName`("RUNNING 상태 배치의 failed_count와 경과 시간을 gauge로 노출한다")
    void exposesRunningBatchMetrics() {
        // given
        when(batchJobService.findLatestUsageSyncBatch()).thenReturn(
            createBatchJob(LineDailyBatchStatus.RUNNING, EXPECTED_FAILED_COUNT, 
                LocalDateTime.of(2026, 5, 26, 6, 0)));

        // when
        LineDailyUsageSyncBatchMetrics metrics = 
            new LineDailyUsageSyncBatchMetrics(meterRegistry, batchJobService, clock);
        metrics.init();

        // then
        assertThat(meterRegistry.get("batch_daily_usage_sync_failed_count").gauge().value())
            .isEqualTo((double) EXPECTED_FAILED_COUNT);
        assertThat(meterRegistry.get("batch_daily_usage_sync_status").gauge().value())
            .isEqualTo(RUNNING_STATUS_CODE);
        assertThat(meterRegistry.get("batch_daily_usage_sync_run_duration_seconds").gauge().value())
            .isEqualTo(EXPECTED_DURATION_SECONDS);
    }

    private LineDailyBatchJob createBatchJob(LineDailyBatchStatus status, long failedCount, 
                                              LocalDateTime runStartedAt) {
        return LineDailyBatchJob.builder()
            .id(10L)
            .batchName(BatchName.LINE_DAILY_USAGE_SYNC_BATCH)
            .usageDate(USAGE_DATE)
            .status(status)
            .runStartedAt(runStartedAt)
            .failedCount(failedCount)
            .build();
    }
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/test/java/com/pooli/monitoring/metrics/LineDailyUsageSyncBatchMetricsTest.java`
around lines 29 - 73, The two tests in LineDailyUsageSyncBatchMetricsTest
duplicate fixture setup and hardcode magic numbers; move common setup
(SimpleMeterRegistry, mocked LineDailyBatchJobService, Clock) into a `@BeforeEach`
and extract magic numbers into constants (e.g. EXPECTED_FAILED_COUNT,
RUNNING_STATUS_CODE, COMPLETED_STATUS_CODE, EXPECTED_DURATION_SECONDS), add a
private helper createBatchJob(...) to build LineDailyBatchJob instances, and
update exposesRunningBatchMetrics and
preservesFailedCountAndResetsRunDurationWhenLatestBatchIsNotRunning to use those
constants and the helper; also add boundary tests covering when
batchJobService.findLatestUsageSyncBatch() returns null, when failedCount == 0,
and when runStartedAt == null to assert expected gauges from
LineDailyUsageSyncBatchMetrics.
src/main/java/com/pooli/traffic/service/batch/LineDailyBatchWorkerScheduler.java (1)

41-43: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

동일 usageDate 중복 스케줄 루프를 차단해 주세요.

Line 41에서 startForUsageDate가 호출될 때마다 새 루프가 생성되어, 같은 usageDate에 대해 runStartCheckCycle이 병렬로 중복 실행될 수 있습니다. 트래픽 도메인 핵심 경로라 DB claim 폴링/worker 실행이 불필요하게 증폭될 수 있습니다.

개선 예시 (usageDate 단위 단일 루프 보장)
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
...
 public class LineDailyBatchWorkerScheduler {
+    private final ConcurrentMap<LocalDate, Boolean> scheduledUsageDates = new ConcurrentHashMap<>();
...
     public void startForUsageDate(LocalDate usageDate) {
-        scheduleNextCheck(usageDate, 0L);
+        if (scheduledUsageDates.putIfAbsent(usageDate, Boolean.TRUE) == null) {
+            scheduleNextCheck(usageDate, 0L);
+        }
     }
...
     void runStartCheckCycle(LocalDate usageDate) {
         try {
             if (stopped) {
+                scheduledUsageDates.remove(usageDate);
                 return;
             }
...
             if (runningBatch == null) {
                 scheduleNextCheck(usageDate, START_CHECK_DELAY_MS + nextJitterMs());
                 return;
             }
...
         } catch (Throwable t) {
             log.error("Failed to execute worker cycle for usageDate: {}. Retrying after delay.", usageDate, t);
             scheduleNextCheck(usageDate, START_CHECK_DELAY_MS + nextJitterMs());
         }
     }

As per coding guidelines src/main/java/com/pooli/traffic/**/*.java: "동시성(concurrency) 이슈가 발생할 수 있는 구간"을 추가 확인해야 합니다.

Also applies to: 60-85

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/pooli/traffic/service/batch/LineDailyBatchWorkerScheduler.java`
around lines 41 - 43, Avoid creating duplicate scheduling loops for the same
usageDate: add a thread-safe registry (e.g., a ConcurrentHashMap or
ConcurrentSkipListSet) of active usageDates in LineDailyBatchWorkerScheduler and
check it in startForUsageDate before calling scheduleNextCheck; if the date is
already present, return early. When the runStartCheckCycle completes or the loop
is cancelled/terminated, remove the usageDate from the registry to allow future
restarts. Ensure all accesses to the registry are thread-safe and reference the
existing methods scheduleNextCheck(...) and runStartCheckCycle(...) so the guard
integrates with current lifecycle and cleanup logic.
♻️ Duplicate comments (1)
docs/일별 사용량 동기화 배치 가이드.md (1)

41-46: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fenced code block 언어 태그를 명시해 문서 린트를 안정화해 주세요.

Line 41, 76, 86, 114, 139, 184 구간의 코드 블록에 text/sql/bash 같은 언어 태그가 없어 MD040 경고가 재발할 수 있습니다. 블록 전후 빈 줄도 함께 맞춰 주세요.

Also applies to: 76-78, 86-88, 114-133, 139-141, 184-187

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/일별` 사용량 동기화 배치 가이드.md around lines 41 - 46, Update the fenced code
blocks in the document so each triple-backtick block includes an appropriate
language tag (e.g., text, sql, bash) and ensure there's a blank line before and
after each fenced block to satisfy MD040; specifically update the blocks that
show the flow "Spring 앱 (Micrometer Gauge) → Prometheus (metric scraping) →
AlertManager (Docker 컨테이너) → Discord 웹훅" and the other fenced blocks containing
SQL snippets and bash commands so they have correct language identifiers and
surrounding blank lines.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/main/java/com/pooli/traffic/service/batch/LineDailyUsageReadResult.java`:
- Around line 25-27: LineDailyUsageReadResult의 compact 생성자에서 appUsages를 그대로 할당하면
외부에서 리스트를 변경해 불변성이 깨지므로, appUsages 필드에 방어적 복사를 적용하세요: compact 생성자(구현부)에서
appUsages가 null이면 List.of()로 대체한 뒤 List.copyOf(...)로 복사하여 할당하도록 변경하여 호출자 변경으로부터
안전하게 만드세요; 관련 심볼: record LineDailyUsageReadResult, 파라미터/필드 appUsages, compact
constructor 내 할당 로직.

In `@src/test/java/com/pooli/traffic/config/TrafficSchedulingConfigTest.java`:
- Around line 23-33: Replace the FileSystemResource-based loading with a
ClassPathResource to avoid working-directory issues: in the test where
YamlPropertySourceLoader loader is used and you call loader.load("application",
new FileSystemResource(APPLICATION_YAML)), change to loader.load("application",
new ClassPathResource(APPLICATION_YAML)) so the YAML is resolved from the
classpath; also change the exception handling in the try/catch that catches
IOException (around YamlPropertySourceLoader loader / StandardEnvironment env /
propertySources.forEach(...)) to rethrow an AssertionError (or fail the test)
with the caught exception as the cause instead of wrapping it in
UncheckedIOException so test failures are reported as test assertion failures.

In
`@src/test/java/com/pooli/traffic/mapper/LineDailyBatchTargetMapperSqlContractTest.java`:
- Around line 79-82: In LineDailyBatchTargetMapperSqlContractTest, the SQL block
extraction logic uses sql.indexOf(...) to compute start (e.g., for "<select
id=\"countNonTerminalByUsageDate\"") but lacks a start == -1 guard causing
StringIndexOutOfBoundsException; immediately after computing start add a check
like if (start == -1) fail("start tag not found: <select
id=\"countNonTerminalByUsageDate\"") (use a message that includes the searched
token) and refactor the repeated extraction logic into a small helper method
(e.g., extractBlock(String sql, String startToken, String endToken, String
failMsg)) and update all usages in LineDailyBatchTargetMapperSqlContractTest to
call that helper instead of duplicating indexOf/substring logic.

---

Outside diff comments:
In
`@src/main/java/com/pooli/traffic/service/batch/LineDailyBatchWorkerScheduler.java`:
- Around line 41-43: Avoid creating duplicate scheduling loops for the same
usageDate: add a thread-safe registry (e.g., a ConcurrentHashMap or
ConcurrentSkipListSet) of active usageDates in LineDailyBatchWorkerScheduler and
check it in startForUsageDate before calling scheduleNextCheck; if the date is
already present, return early. When the runStartCheckCycle completes or the loop
is cancelled/terminated, remove the usageDate from the registry to allow future
restarts. Ensure all accesses to the registry are thread-safe and reference the
existing methods scheduleNextCheck(...) and runStartCheckCycle(...) so the guard
integrates with current lifecycle and cleanup logic.

In
`@src/test/java/com/pooli/monitoring/metrics/LineDailyUsageSyncBatchMetricsTest.java`:
- Around line 29-73: The two tests in LineDailyUsageSyncBatchMetricsTest
duplicate fixture setup and hardcode magic numbers; move common setup
(SimpleMeterRegistry, mocked LineDailyBatchJobService, Clock) into a `@BeforeEach`
and extract magic numbers into constants (e.g. EXPECTED_FAILED_COUNT,
RUNNING_STATUS_CODE, COMPLETED_STATUS_CODE, EXPECTED_DURATION_SECONDS), add a
private helper createBatchJob(...) to build LineDailyBatchJob instances, and
update exposesRunningBatchMetrics and
preservesFailedCountAndResetsRunDurationWhenLatestBatchIsNotRunning to use those
constants and the helper; also add boundary tests covering when
batchJobService.findLatestUsageSyncBatch() returns null, when failedCount == 0,
and when runStartedAt == null to assert expected gauges from
LineDailyUsageSyncBatchMetrics.

---

Duplicate comments:
In `@docs/일별` 사용량 동기화 배치 가이드.md:
- Around line 41-46: Update the fenced code blocks in the document so each
triple-backtick block includes an appropriate language tag (e.g., text, sql,
bash) and ensure there's a blank line before and after each fenced block to
satisfy MD040; specifically update the blocks that show the flow "Spring 앱
(Micrometer Gauge) → Prometheus (metric scraping) → AlertManager (Docker 컨테이너) →
Discord 웹훅" and the other fenced blocks containing SQL snippets and bash
commands so they have correct language identifiers and surrounding blank lines.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b6186259-d30d-47ac-9089-d5b4a943cd8e

📥 Commits

Reviewing files that changed from the base of the PR and between cbeacb8 and da75151.

📒 Files selected for processing (22)
  • README.md
  • docs/일별 사용량 동기화 배치 가이드.md
  • src/main/java/com/pooli/data/service/impl/DataServiceImpl.java
  • src/main/java/com/pooli/monitoring/metrics/LineDailyUsageSyncBatchMetrics.java
  • src/main/java/com/pooli/traffic/domain/batch/LineDailyBatchJobCreateResult.java
  • src/main/java/com/pooli/traffic/domain/dto/response/LineDailyUsageSyncResumeResDto.java
  • src/main/java/com/pooli/traffic/mapper/DailyAppUsageBatchInsertRow.java
  • src/main/java/com/pooli/traffic/service/batch/DailyAppUsage.java
  • src/main/java/com/pooli/traffic/service/batch/LineDailyBatchManagerService.java
  • src/main/java/com/pooli/traffic/service/batch/LineDailyBatchTargetClaimService.java
  • src/main/java/com/pooli/traffic/service/batch/LineDailyBatchWorkerScheduler.java
  • src/main/java/com/pooli/traffic/service/batch/LineDailyUsageReadResult.java
  • src/main/java/com/pooli/traffic/service/batch/LineDailyUsageSyncPersistenceService.java
  • src/main/java/com/pooli/traffic/service/batch/LineDailyUsageSyncWorkerService.java
  • src/main/java/com/pooli/traffic/service/decision/TrafficDeductOrchestratorService.java
  • src/main/resources/application.yaml
  • src/test/java/com/pooli/monitoring/metrics/LineDailyUsageSyncBatchMetricsTest.java
  • src/test/java/com/pooli/traffic/config/TrafficSchedulingConfigTest.java
  • src/test/java/com/pooli/traffic/mapper/LineDailyBatchTargetMapperSqlContractTest.java
  • src/test/java/com/pooli/traffic/service/batch/LineDailyBatchManagerServiceTest.java
  • src/test/java/com/pooli/traffic/service/batch/LineDailyBatchTargetClaimServiceTest.java
  • src/test/java/com/pooli/traffic/service/batch/LineDailyUsageSyncWorkerServiceTest.java

Comment on lines +25 to +27
LineDailyUsageReadResult {
appUsages = (appUsages != null) ? appUsages : List.of();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

compact constructor에서 방어적 복사 누락

appUsages를 그대로 할당하면 호출자가 외부에서 리스트를 변경할 수 있어 불변성이 깨질 수 있습니다. record는 불변 데이터 컨테이너로 설계되므로, 가변 컬렉션을 받을 때는 방어적 복사를 권장합니다.

♻️ 불변성 보장을 위한 방어적 복사 제안
     LineDailyUsageReadResult {
-        appUsages = (appUsages != null) ? appUsages : List.of();
+        appUsages = (appUsages != null) ? List.copyOf(appUsages) : List.of();
     }

List.copyOf()는 얕은 복사본을 만들어 외부 수정으로부터 안전하게 보호하며, 이미 불변 리스트라면 그대로 반환해 오버헤드가 최소화됩니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/pooli/traffic/service/batch/LineDailyUsageReadResult.java`
around lines 25 - 27, LineDailyUsageReadResult의 compact 생성자에서 appUsages를 그대로
할당하면 외부에서 리스트를 변경해 불변성이 깨지므로, appUsages 필드에 방어적 복사를 적용하세요: compact 생성자(구현부)에서
appUsages가 null이면 List.of()로 대체한 뒤 List.copyOf(...)로 복사하여 할당하도록 변경하여 호출자 변경으로부터
안전하게 만드세요; 관련 심볼: record LineDailyUsageReadResult, 파라미터/필드 appUsages, compact
constructor 내 할당 로직.

Comment on lines +23 to +33
YamlPropertySourceLoader loader = new YamlPropertySourceLoader();
try {
List<PropertySource<?>> propertySources = loader.load("application", new FileSystemResource(APPLICATION_YAML));
StandardEnvironment env = new StandardEnvironment();
propertySources.forEach(env.getPropertySources()::addFirst);

int poolSize = env.getProperty("spring.task.scheduling.pool.size", Integer.class, -1);
assertEquals(2, poolSize);
} catch (IOException e) {
throw new UncheckedIOException(e);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

FileSystemResource 대신 ClassPathResource 사용 권장

현재 FileSystemResource("src/main/resources/application.yaml")는 상대 경로에 의존하므로, 테스트 실행 디렉토리에 따라 파일을 찾지 못할 수 있습니다. IDE와 Gradle에서 working directory가 다를 수 있어 안정성이 떨어집니다.

♻️ ClassPathResource로 개선
 class TrafficSchedulingConfigTest {
 
-    private static final String APPLICATION_YAML = "src/main/resources/application.yaml";
-
     `@Test`
     `@DisplayName`("Spring TaskScheduler pool size는 2로 설정한다")
     void taskSchedulerPoolSizeIsTwo() {
         YamlPropertySourceLoader loader = new YamlPropertySourceLoader();
         try {
-            List<PropertySource<?>> propertySources = loader.load("application", new FileSystemResource(APPLICATION_YAML));
+            List<PropertySource<?>> propertySources = loader.load("application", 
+                new ClassPathResource("application.yaml"));
             StandardEnvironment env = new StandardEnvironment();
             propertySources.forEach(env.getPropertySources()::addFirst);
 
             int poolSize = env.getProperty("spring.task.scheduling.pool.size", Integer.class, -1);
             assertEquals(2, poolSize);
-        } catch (IOException e) {
-            throw new UncheckedIOException(e);
+        } catch (IOException e) {
+            throw new AssertionError("Failed to load application.yaml", e);
         }
     }
 }

ClassPathResource는 클래스패스 기준이므로 실행 위치와 무관하게 안정적이며, AssertionError로 래핑하면 테스트 실패 의도가 더 명확해집니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/test/java/com/pooli/traffic/config/TrafficSchedulingConfigTest.java`
around lines 23 - 33, Replace the FileSystemResource-based loading with a
ClassPathResource to avoid working-directory issues: in the test where
YamlPropertySourceLoader loader is used and you call loader.load("application",
new FileSystemResource(APPLICATION_YAML)), change to loader.load("application",
new ClassPathResource(APPLICATION_YAML)) so the YAML is resolved from the
classpath; also change the exception handling in the try/catch that catches
IOException (around YamlPropertySourceLoader loader / StandardEnvironment env /
propertySources.forEach(...)) to rethrow an AssertionError (or fail the test)
with the caught exception as the cause instead of wrapping it in
UncheckedIOException so test failures are reported as test assertion failures.

Comment on lines +79 to +82
int start = sql.indexOf("<select id=\"countNonTerminalByUsageDate\"");
int end = sql.indexOf("</select>", start);
if (end == -1) fail("</select> not found");
String selectSql = sql.substring(start, end + "</select>".length());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/test/java/com/pooli/traffic/mapper/LineDailyBatchTargetMapperSqlContractTest.java"
echo "== File =="
wc -l "$FILE"
echo
echo "== Lines 60-120 =="
nl -ba "$FILE" | sed -n '60,120p'
echo
echo "== Lines 120-220 =="
nl -ba "$FILE" | sed -n '120,220p'
echo
echo "== Lines 220-280 =="
nl -ba "$FILE" | sed -n '220,280p'

Repository: Iwantcod/pooli-be

Length of output: 243


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/test/java/com/pooli/traffic/mapper/LineDailyBatchTargetMapperSqlContractTest.java"
echo "== File =="
wc -l "$FILE"
echo
echo "== Lines 60-120 =="
nl -ba "$FILE" | sed -n '60,120p'
echo
echo "== Lines 120-220 =="
nl -ba "$FILE" | sed -n '120,220p'
echo
echo "== Lines 220-280 =="
nl -ba "$FILE" | sed -n '220,280p'

Repository: Iwantcod/pooli-be

Length of output: 243


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/test/java/com/pooli/traffic/mapper/LineDailyBatchTargetMapperSqlContractTest.java"
echo "== File =="
wc -l "$FILE"
echo
echo "== Lines 60-120 =="
nl -ba "$FILE" | sed -n '60,120p'
echo
echo "== Lines 120-220 =="
nl -ba "$FILE" | sed -n '120,220p'
echo
echo "== Lines 220-280 =="
nl -ba "$FILE" | sed -n '220,280p'

Repository: Iwantcod/pooli-be

Length of output: 243


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/test/java/com/pooli/traffic/mapper/LineDailyBatchTargetMapperSqlContractTest.java"
echo "== File =="
wc -l "$FILE"
echo
echo "== Lines 60-120 =="
nl -ba "$FILE" | sed -n '60,120p'
echo
echo "== Lines 120-220 =="
nl -ba "$FILE" | sed -n '120,220p'
echo
echo "== Lines 220-280 =="
nl -ba "$FILE" | sed -n '220,280p'

Repository: Iwantcod/pooli-be

Length of output: 243


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/test/java/com/pooli/traffic/mapper/LineDailyBatchTargetMapperSqlContractTest.java"
echo "== File =="
wc -l "$FILE"
echo

echo "== Lines 60-120 =="
cat -n "$FILE" | sed -n '60,120p'
echo

echo "== Lines 120-220 =="
cat -n "$FILE" | sed -n '120,220p'
echo

echo "== Lines 220-268 =="
cat -n "$FILE" | sed -n '220,268p'

Repository: Iwantcod/pooli-be

Length of output: 11488


SQL 블록 추출 로직에 start == -1 실패 가드 추가 필요
현재 여러 테스트에서 sql.indexOf("<select|<insert|<update> id=...")start 값에 대해 start == -1 체크가 없어, 매칭이 없을 때 substring(start, ...)로 인해 StringIndexOutOfBoundsException이 나며 어느 계약(블록)이 깨졌는지 진단이 흐려집니다(예: 79-82, 96-99, 134-137, 150-153, 169-172, 186-189, 201-204, 220-223, 237-240).

  • start 계산 직후 if (start == -1) fail("start tag not found: ...");을 추가하세요.
  • 반복되는 추출 코드는 작은 헬퍼(예: extractBlock(sql, startToken, endToken, failMsg))로 묶어 유지보수 비용을 줄이세요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/test/java/com/pooli/traffic/mapper/LineDailyBatchTargetMapperSqlContractTest.java`
around lines 79 - 82, In LineDailyBatchTargetMapperSqlContractTest, the SQL
block extraction logic uses sql.indexOf(...) to compute start (e.g., for
"<select id=\"countNonTerminalByUsageDate\"") but lacks a start == -1 guard
causing StringIndexOutOfBoundsException; immediately after computing start add a
check like if (start == -1) fail("start tag not found: <select
id=\"countNonTerminalByUsageDate\"") (use a message that includes the searched
token) and refactor the repeated extraction logic into a small helper method
(e.g., extractBlock(String sql, String startToken, String endToken, String
failMsg)) and update all usages in LineDailyBatchTargetMapperSqlContractTest to
call that helper instead of duplicating indexOf/substring logic.

@Iwantcod Iwantcod merged commit 3e0028c into dev May 28, 2026
2 checks passed
@Iwantcod Iwantcod deleted the feat/daily-persist-batch branch May 28, 2026 10:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant