Skip to content

#136 배포/종료 중 녹화 중단 시 partial 녹화 파일 정책 - #144

Merged
SeJonJ merged 5 commits into
chatforyou_v2from
bug/136
Jul 12, 2026
Merged

#136 배포/종료 중 녹화 중단 시 partial 녹화 파일 정책#144
SeJonJ merged 5 commits into
chatforyou_v2from
bug/136

Conversation

@SeJonJ

@SeJonJ SeJonJ commented Jul 10, 2026

Copy link
Copy Markdown
Owner
  • partial cleanup 구현
  • TOCTOU 방어
  1. 파일 삭제/로그 기록 전에 현재 redis 마커가 아직 같은 recordingId 인지 확인
  2. 삭제 직전에도 recordingId 가 같을 때만 삭제
  • 시간 순으로 보면 아래 flow 를 따른다
  1. marker 를 생성하는건 재기동 시
  2. 4시간 이후 삭제 대상으로 확인
  3. 4~5 이후 배치 시 파일 삭제 + download_log 기록 + marker 삭제
  4. 6시간 이후 marker TTL 만료(redis)

Summary by Sourcery

TOCTOU-safe Redis 마커 처리와 시스템 자동 삭제 로깅을 포함한 부분 녹화 파일에 대한 스케줄 기반 정리 기능을 추가했습니다.

New Features:

  • 만료된 부분 녹화 파일과 관련 MinIO 오브젝트를 삭제하는 RecordingPartialCleanupService 및 스케줄 배치 잡을 도입했습니다.
  • recordingId 기반으로 녹화 부분 마커를 조회하고 조건부로 삭제할 수 있는 Redis 스캔 API를 추가했습니다.
  • 시스템 주도 삭제 로그 작성을 위한 SYSTEM_AUTO_DELETED 상태와 팩토리를 DownloadLog에 확장 추가했습니다.

Enhancements:

  • 커밋, 장기 실행 에이전트, 동시성/TOCTOU 리뷰 관행에 대한 팀 가이드라인을 문서화하고 강제 적용했습니다.
  • bug/136 브랜치에서 실행될 수 있도록 CI 배포 워크플로를 개선했습니다.

CI:

  • bug/136 브랜치를 포함하도록 GitHub Actions k8s 배포 워크플로를 업데이트했습니다.

Tests:

  • 멱등성, TOCTOU 시나리오, 장애 격리를 포함한 부분 녹화 정리 서비스용 단위/QA 테스트를 추가했습니다.
  • Redis 마커 스캔 및 조건부 삭제 로직에 대한 테스트를 추가했습니다.
  • DownloadLog 시스템 자동 삭제 매핑 및 배치 잡 예외 처리 검증 테스트를 추가했습니다.
Original summary in English

Summary by Sourcery

Add scheduled cleanup for partial recording files with TOCTOU-safe Redis marker handling and system auto-delete logging.

New Features:

  • Introduce RecordingPartialCleanupService and scheduled batch job to delete expired partial recording files and related MinIO objects.
  • Add Redis scan APIs to retrieve and conditionally delete recording partial markers based on recordingId.
  • Extend DownloadLog with a SYSTEM_AUTO_DELETED status and factory for system-driven deletion logs.

Enhancements:

  • Document and enforce team guidelines around commits, long-running agents, and concurrency/TOCTOU review practices.
  • Improve CI deployment workflow to run on the bug/136 branch.

CI:

  • Update GitHub Actions k8s deployment workflow to include the bug/136 branch.

Tests:

  • Add unit and QA tests for partial recording cleanup service, including idempotency, TOCTOU scenarios, and failure isolation.
  • Add tests for Redis marker scanning and conditional deletion logic.
  • Add tests validating DownloadLog system auto-delete mappings and batch job exception handling.

SeJonJ added 3 commits July 6, 2026 13:00
- partial cleanup 구현
- TOCTOU 방어
1. 파일 삭제/로그 기록 전에 현재 redis 마커가 아직 같은 recordingId 인지 확인
2. 삭제 직전에도 recordingId 가 같을 때만 삭제
- 팀 리더 에이전트가 세션 리밋으로 죽었을 때 감지 처리
- 브랜치 분리 작업 중 미승인 push 하지못하도록 명문화
- "idempotency ≠ TOCTOU" 구분 확보
@SeJonJ SeJonJ self-assigned this Jul 10, 2026
@SeJonJ SeJonJ added the bug Something isn't working label Jul 10, 2026
@sourcery-ai

sourcery-ai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Redis 마커를 스캔하여 오래된 부분 녹화 파일(로컬 및 MinIO)을 삭제하고, 시스템 자동 삭제 내역을 DownloadLog에 기록하며, Redis 마커 연산을 TOCTOU에 안전하도록 강화한 예약 기반 부분 녹화 정리 파이프라인을 구현합니다. 이에 대한 포괄적인 단위/QA 테스트와 일부 내부 팀 프로세스 문서 업데이트도 포함됩니다.

부분 녹화 정리 배치 파이프라인 시퀀스 다이어그램

sequenceDiagram
    participant Scheduler as SpringScheduler
    participant BatchJob as RecordingPartialCleanupBatchJob
    participant Service as RecordingPartialCleanupService
    participant Redis as RedisService
    participant FileSvc as RecordingFileService
    participant LogSvc as DownloadLogService

    Scheduler->>BatchJob: cleanupPartialRecording()
    BatchJob->>Service: cleanupExpiredPartialRecordings()
    Service->>Redis: getAllRecordingPartialMarkers()
    Redis-->>Service: List<RecordingPartialMarker>

    loop for each marker
        Service->>Redis: getRecordingPartialMarker(roomId)
        Redis-->>Service: RecordingPartialMarker
        alt [markedAt age >= cleanupAgeSeconds && recordingId matches]
            Service->>FileSvc: deleteFileDir(minioFilePath) [via deleteMinioObjectIfPresent]
            Service->>Service: deleteLocalFile(marker)
            Service->>LogSvc: saveDownloadLog(DownloadLog.ofSystemAutoDeleted(marker))
            Service->>Redis: deleteRecordingPartialMarkerIfRecordingIdMatches(roomId, recordingId)
            Redis-->>Service: boolean
        else [age not reached or marker changed]
            Service->>Service: skipped++
        end
    end
    Service-->>BatchJob: CleanupResult(deleted, skipped, failed)
    BatchJob-->>Scheduler: (log result)
Loading

TOCTOU 안전 Redis 마커 삭제 시퀀스 다이어그램

sequenceDiagram
    participant Service as RecordingPartialCleanupService
    participant RedisSvc as RedisServiceImpl
    participant Redis as RedisOperations

    Service->>RedisSvc: deleteRecordingPartialMarkerIfRecordingIdMatches(roomId, recordingId)
    RedisSvc->>Redis: watch(key)
    RedisSvc->>Redis: opsForValue().get(key)
    Redis-->>RedisSvc: value
    alt [value is RecordingPartialMarker && recordingId matches]
        RedisSvc->>Redis: multi()
        RedisSvc->>Redis: delete(key)
        Redis-->>RedisSvc: queued
        RedisSvc->>Redis: exec()
        Redis-->>RedisSvc: List<Object> results
        RedisSvc-->>Service: true
    else [value null or recordingId different]
        RedisSvc->>Redis: unwatch()
        RedisSvc-->>Service: false
    end
Loading

파일 수준 변경 사항

Change Details Files
RecordingPartialCleanupService 및 예약 배치 작업을 추가하여 일정 기간이 지난 부분 녹화 파일을 삭제하고 시스템 자동 삭제를 로깅합니다.
  • CleanupResult 레코드와 RecordingPartialCleanupService를 도입하여 마커 스캔, 기간(나이) 필터링, 파일/MinIO 삭제, 로깅, 마커 삭제를 오케스트레이션하며, 마커별 에러 격리와 TOCTOU 체크를 수행합니다.
  • 매시간 @scheduled 로 실행되는 RecordingPartialCleanupBatchJob을 추가하고, 선택적으로 ShedLock을 연동하며, 서비스 예외를 삼키되 집계된 정리 결과 카운트를 로깅합니다.
  • 누락된 파일, file:// URI 처리, 부분 실패, TOCTOU 레이스, 다중 실행 시 멱등성 등 정리 플로우에 대한 멱등성 및 QA 중심 테스트를 구현합니다.
springboot-backend/src/main/java/webChat/service/recording/RecordingPartialCleanupService.java
springboot-backend/src/main/java/webChat/service/recording/CleanupResult.java
springboot-backend/src/main/java/webChat/batch/RecordingPartialCleanupBatchJob.java
springboot-backend/src/test/java/webChat/service/recording/RecordingPartialCleanupServiceTest.java
springboot-backend/src/test/java/webChat/service/recording/RecordingPartialCleanupServiceQaTest.java
springboot-backend/src/test/java/webChat/service/recording/RecordingPartialCleanupIdempotencyTest.java
springboot-backend/src/test/java/webChat/batch/RecordingPartialCleanupBatchJobTest.java
RedisService를 확장하여 TOCTOU에 안전한 부분 마커 삭제와 정리 배치를 위한 SCAN 기반 조회를 지원합니다.
  • Redis WATCH/MULTI/EXEC를 사용하는 deleteRecordingPartialMarkerIfRecordingIdMatches를 추가하여 recordingId가 일치할 때에만 조건부로 마커를 삭제하고, 블라인드 삭제를 방지합니다.
  • 슬레이브 커넥션에서 SCAN을 사용하고 일관성을 위해 마스터에서 값을 읽어오는 getAllRecordingPartialMarkers를 구현하며, null 또는 마커가 아닌 값에 대한 방어적 처리를 추가합니다.
  • 조건부 삭제 동작과 SCAN 기반 조회에 대한 단위 테스트를 추가하고, 빈 스캔 및 패턴 검증 시나리오를 포함합니다.
springboot-backend/src/main/java/webChat/service/redis/RedisService.java
springboot-backend/src/main/java/webChat/service/redis/impl/RedisServiceImpl.java
springboot-backend/src/test/java/webChat/service/redis/impl/RedisServiceImplRecordingPartialMarkerTest.java
springboot-backend/src/test/java/webChat/service/redis/impl/RedisServiceImplRecordingPartialMarkerScanTest.java
DownloadLog를 확장하여 부분 녹화에 대한 시스템 주도 삭제를 표현하고 테스트합니다.
  • 시스템 정리 이벤트를 구분하기 위해 DownloadStatus enum에 SYSTEM_AUTO_DELETED 값을 추가합니다.
  • RecordingPartialMarker 필드를 매핑하여 userIdx/ip/userAgent는 null, recordingId는 targetId로 설정된 로그 엔트리를 생성하는 DownloadLog.ofSystemAutoDeleted 팩토리 메서드를 도입합니다.
  • 시스템 자동 삭제 로그에 대해 필드 매핑과 status/targetType의 정확성을 검증하는 DownloadLogTest를 추가합니다.
springboot-backend/src/main/java/webChat/entity/DownloadLog.java
springboot-backend/src/test/java/webChat/entity/DownloadLogTest.java
새 bug/136 워크플로를 지원하고 동시성 리뷰 관행을 강조하기 위해 CI/CD 및 내부 agent/skill 문서를 업데이트합니다.
  • 기존 GitHub Actions K8S 배포 파이프라인을 bug/136 브랜치에서도 트리거할 수 있도록 허용합니다.
  • chatforyou lead/skills 문서에서 커밋/푸시 금지 및 브랜치 처리 규칙을 정교화하고, 생존 모니터링 및 단계별 체크포인팅을 포함합니다.
  • QA 및 외부 전문가가 멱등성과 TOCTOU 관점의 동시성 민감 코드에 대해 별도로 검증하도록 명시적인 가이던스를 추가하고, 본 버그를 예시로 참조합니다.
.github/workflows/GitAction-k8s-deploy.yml
.claude/skills/chatforyou-dev-team.md
.codex/skills/chatforyou-dev-team/SKILL.md
.claude/agents/chatforyou-qa-expert.md
.codex/agents/chatforyyou-qa-expert.md
.codex/agents/chatforyou-external-expert.md
.claude/agents/chatforyou-external-expert.md
.claude/agents/chatforyou-lead.md
.codex/agents/chatforyou-lead.md
bug 136 부분 녹화 정리에 대한 기본 계획을 문서화합니다.
  • 목표, 제약 조건, Redis TTL과 정리 기준 시간(cleanup age) 간의 결정, DownloadLog 상태 이름, 파일 소유권, bug 136 사이클을 위한 워크플로 게이트 등을 설명하는 상세 계획 문서를 추가합니다.
plan_docs/00-base_plan/2026/07/bug_136_recording_partial_cleanup_plan.md

관련 가능 이슈


Tips and commands

Sourcery와 상호작용

  • 새 리뷰 트리거: Pull request에 @sourcery-ai review 댓글을 남깁니다.
  • 논의 계속하기: Sourcery의 리뷰 댓글에 직접 답글을 달아 논의를 이어갑니다.
  • 리뷰 댓글에서 GitHub 이슈 생성: 리뷰 댓글에 답글로 요청하여, 해당 리뷰 댓글로부터 이슈를 생성하도록 Sourcery에 요청할 수 있습니다. 또한 리뷰 댓글에 @sourcery-ai issue라고 답글을 달면, 그 댓글로부터 이슈가 생성됩니다.
  • Pull request 제목 생성: Pull request 제목 어디에나 @sourcery-ai를 입력하면 언제든지 제목을 생성할 수 있습니다. 또한 Pull request에 @sourcery-ai title 댓글을 달아 제목을 (재)생성할 수 있습니다.
  • Pull request 요약 생성: Pull request 본문 어디에나 @sourcery-ai summary를 입력하면 원하는 위치에 PR 요약을 생성할 수 있습니다. 또한 Pull request에 @sourcery-ai summary 댓글을 달아 요약을 (재)생성할 수 있습니다.
  • Reviewer's guide 생성: Pull request에 @sourcery-ai guide 댓글을 달아 리뷰어 가이드를 언제든지 (재)생성할 수 있습니다.
  • 모든 Sourcery 댓글 해제: Pull request에 @sourcery-ai resolve 댓글을 달면 모든 Sourcery 댓글을 resolved 상태로 만듭니다. 이미 모든 댓글을 처리했고 더 이상 보지 않으려 할 때 유용합니다.
  • 모든 Sourcery 리뷰 해제: Pull request에 @sourcery-ai dismiss 댓글을 달면 기존의 모든 Sourcery 리뷰를 해제합니다. 새 리뷰를 처음부터 다시 받고 싶을 때 특히 유용합니다. 이후 새 리뷰를 트리거하려면 @sourcery-ai review 댓글을 다는 것을 잊지 마세요!

내 경험 맞춤 설정

dashboard에 접속하여:

  • Sourcery가 생성하는 Pull request 요약, reviewer's guide 등 리뷰 기능을 활성화/비활성화할 수 있습니다.
  • 리뷰 언어를 변경할 수 있습니다.
  • 커스텀 리뷰 지침을 추가/삭제/수정할 수 있습니다.
  • 기타 리뷰 설정을 조정할 수 있습니다.

도움 받기

Original review guide in English

Reviewer's Guide

Implements a scheduled partial recording cleanup pipeline that scans Redis markers, deletes stale partial recording files (local and MinIO), logs system auto-deletion in DownloadLog, and hardens Redis marker operations against TOCTOU, with comprehensive unit/QA tests and some internal team-process documentation updates.

Sequence diagram for partial recording cleanup batch pipeline

sequenceDiagram
    participant Scheduler as SpringScheduler
    participant BatchJob as RecordingPartialCleanupBatchJob
    participant Service as RecordingPartialCleanupService
    participant Redis as RedisService
    participant FileSvc as RecordingFileService
    participant LogSvc as DownloadLogService

    Scheduler->>BatchJob: cleanupPartialRecording()
    BatchJob->>Service: cleanupExpiredPartialRecordings()
    Service->>Redis: getAllRecordingPartialMarkers()
    Redis-->>Service: List<RecordingPartialMarker>

    loop for each marker
        Service->>Redis: getRecordingPartialMarker(roomId)
        Redis-->>Service: RecordingPartialMarker
        alt [markedAt age >= cleanupAgeSeconds && recordingId matches]
            Service->>FileSvc: deleteFileDir(minioFilePath) [via deleteMinioObjectIfPresent]
            Service->>Service: deleteLocalFile(marker)
            Service->>LogSvc: saveDownloadLog(DownloadLog.ofSystemAutoDeleted(marker))
            Service->>Redis: deleteRecordingPartialMarkerIfRecordingIdMatches(roomId, recordingId)
            Redis-->>Service: boolean
        else [age not reached or marker changed]
            Service->>Service: skipped++
        end
    end
    Service-->>BatchJob: CleanupResult(deleted, skipped, failed)
    BatchJob-->>Scheduler: (log result)
Loading

Sequence diagram for TOCTOU safe Redis marker deletion

sequenceDiagram
    participant Service as RecordingPartialCleanupService
    participant RedisSvc as RedisServiceImpl
    participant Redis as RedisOperations

    Service->>RedisSvc: deleteRecordingPartialMarkerIfRecordingIdMatches(roomId, recordingId)
    RedisSvc->>Redis: watch(key)
    RedisSvc->>Redis: opsForValue().get(key)
    Redis-->>RedisSvc: value
    alt [value is RecordingPartialMarker && recordingId matches]
        RedisSvc->>Redis: multi()
        RedisSvc->>Redis: delete(key)
        Redis-->>RedisSvc: queued
        RedisSvc->>Redis: exec()
        Redis-->>RedisSvc: List<Object> results
        RedisSvc-->>Service: true
    else [value null or recordingId different]
        RedisSvc->>Redis: unwatch()
        RedisSvc-->>Service: false
    end
Loading

File-Level Changes

Change Details Files
Add RecordingPartialCleanupService and scheduled batch job to delete aged partial recording files and log system auto-deletion.
  • Introduce CleanupResult record and RecordingPartialCleanupService to orchestrate marker scanning, age filtering, file/MinIO deletion, logging, and marker deletion with per-marker error isolation and TOCTOU checks.
  • Add RecordingPartialCleanupBatchJob with hourly @scheduled execution and optional ShedLock integration, swallowing service exceptions while logging aggregated cleanup counts.
  • Implement idempotency and QA-focused tests for the cleanup flow, including scenarios for missing files, file:// URI handling, partial failures, TOCTOU races, and multi-run idempotency.
springboot-backend/src/main/java/webChat/service/recording/RecordingPartialCleanupService.java
springboot-backend/src/main/java/webChat/service/recording/CleanupResult.java
springboot-backend/src/main/java/webChat/batch/RecordingPartialCleanupBatchJob.java
springboot-backend/src/test/java/webChat/service/recording/RecordingPartialCleanupServiceTest.java
springboot-backend/src/test/java/webChat/service/recording/RecordingPartialCleanupServiceQaTest.java
springboot-backend/src/test/java/webChat/service/recording/RecordingPartialCleanupIdempotencyTest.java
springboot-backend/src/test/java/webChat/batch/RecordingPartialCleanupBatchJobTest.java
Extend RedisService to support TOCTOU-safe partial marker deletion and SCAN-based retrieval for the cleanup batch.
  • Add deleteRecordingPartialMarkerIfRecordingIdMatches using Redis WATCH/MULTI/EXEC to conditionally delete markers only when recordingId matches, preventing blind deletes.
  • Implement getAllRecordingPartialMarkers using SCAN on the slave connection and value reads from master for consistency, with defensive handling of null or non-marker values.
  • Add unit tests for conditional deletion behavior and SCAN-based retrieval, including empty-scan and pattern verification scenarios.
springboot-backend/src/main/java/webChat/service/redis/RedisService.java
springboot-backend/src/main/java/webChat/service/redis/impl/RedisServiceImpl.java
springboot-backend/src/test/java/webChat/service/redis/impl/RedisServiceImplRecordingPartialMarkerTest.java
springboot-backend/src/test/java/webChat/service/redis/impl/RedisServiceImplRecordingPartialMarkerScanTest.java
Augment DownloadLog to represent and test system-driven deletions of partial recordings.
  • Add SYSTEM_AUTO_DELETED to DownloadStatus enum to distinguish system cleanup events.
  • Introduce DownloadLog.ofSystemAutoDeleted factory mapping RecordingPartialMarker fields to a log entry with null userIdx/ip/userAgent and recordingId as targetId.
  • Add DownloadLogTest to verify field mappings and status/targetType correctness for system auto-deletion logs.
springboot-backend/src/main/java/webChat/entity/DownloadLog.java
springboot-backend/src/test/java/webChat/entity/DownloadLogTest.java
Update CI/CD and internal agent/skill documentation to support the new bug/136 workflow and emphasize concurrency review practices.
  • Allow the bug/136 branch to trigger the existing GitHub Actions K8S deploy pipeline.
  • Refine team rules around commit/push prohibition and branch handling in chatforyou lead/skills docs, including survival monitoring and phase checkpointing.
  • Add explicit guidance for QA and external experts to separately validate idempotency and TOCTOU aspects of concurrency-sensitive code, referencing this bug as an example.
.github/workflows/GitAction-k8s-deploy.yml
.claude/skills/chatforyou-dev-team.md
.codex/skills/chatforyou-dev-team/SKILL.md
.claude/agents/chatforyou-qa-expert.md
.codex/agents/chatforyou-qa-expert.md
.codex/agents/chatforyou-external-expert.md
.claude/agents/chatforyou-external-expert.md
.claude/agents/chatforyou-lead.md
.codex/agents/chatforyou-lead.md
Document the base plan for bug 136 partial recording cleanup.
  • Add a detailed plan document describing goals, constraints, Redis TTL vs cleanup age decisions, DownloadLog status naming, file ownership, and workflow gates for the bug 136 cycle.
plan_docs/00-base_plan/2026/07/bug_136_recording_partial_cleanup_plan.md

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - 몇 가지 이슈 3개를 발견했고, 전반적인 피드백을 몇 가지 남겼습니다.

  • RedisServiceImpl.getAllRecordingPartialMarkers에서 slaveTemplate.getConnectionFactory().getConnection()으로 얻은 RedisConnection이 명시적으로 닫히지 않고 있습니다. 커넥션 누수를 방지하기 위해 slaveTemplate.executeRedisCallback을 사용하거나, 커넥션 자체에 대해 try-with-resources를 사용하는 방식을 고려해 주세요.
  • 현재 정리(cleanup) 정책(크론 표현식과 cleanupAgeSeconds 기본값 14400)이 하드코딩되어 있습니다. 향후 정책 변경 시 코드 수정 없이 대응할 수 있도록, 스케줄과 기간 임계값 둘 다를 설정 프로퍼티로 외부화하는 편이 더 유연할 것 같습니다.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `RedisServiceImpl.getAllRecordingPartialMarkers`, the `RedisConnection` obtained from `slaveTemplate.getConnectionFactory().getConnection()` is never explicitly closed; consider using `slaveTemplate.execute` with a `RedisCallback` or a try-with-resources on the connection itself to avoid leaking connections.
- The cleanup policy (cron expression and `cleanupAgeSeconds` default of 14400) is currently hardcoded; it would be more flexible to externalize both the schedule and age threshold into configuration properties so future policy changes don’t require code changes.

## Individual Comments

### Comment 1
<location path="springboot-backend/src/main/java/webChat/service/redis/impl/RedisServiceImpl.java" line_range="563-572" />
<code_context>
     }

+    @Override
+    public boolean deleteRecordingPartialMarkerIfRecordingIdMatches(String roomId, String recordingId) {
+        String key = RECORDING_PARTIAL_PREFIX.getPrefix() + roomId;
+        Boolean deleted = masterTemplate.execute(new SessionCallback<Boolean>() {
+            @Override
+            public Boolean execute(@NotNull RedisOperations operations) {
+                operations.watch(key);
+                Object value = operations.opsForValue().get(key);
+                if (!(value instanceof RecordingPartialMarker marker)
+                        || !Objects.equals(marker.getRecordingId(), recordingId)) {
+                    operations.unwatch();
+                    return false;
+                }
+
+                operations.multi();
+                operations.delete(key);
+                List<Object> results = operations.exec();
+                return results != null && !results.isEmpty() && Boolean.TRUE.equals(results.get(0));
+            }
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Clarify transactional delete result handling to avoid silent failures on unexpected exec results.

Inside the transaction you assume `operations.delete(key)` will yield a single `Boolean` at `results.get(0)`. Depending on the `RedisTemplate` / driver, `exec()` may return different types (e.g., status strings, `Long`, or `null`) for delete operations. If that changes, this method may silently return `false` (or `true`) even when the actual delete outcome differs. Please validate the `results` size and element type (with an `instanceof` check) and either log or fail fast when the shape is unexpected to avoid silent misreports.

Suggested implementation:

```java
                operations.multi();
                operations.delete(key);
                List<Object> results = operations.exec();

                if (results == null || results.isEmpty()) {
                    // No results returned from exec, treat as failure
                    // and avoid silently reporting success
                    if (log.isWarnEnabled()) {
                        log.warn("Unexpected empty/null Redis transaction result when deleting recording partial marker for key {}", key);
                    }
                    return false;
                }

                if (results.size() != 1) {
                    // We only expect a single operation in this transaction
                    if (log.isWarnEnabled()) {
                        log.warn("Unexpected Redis transaction result size {} when deleting recording partial marker for key {}. Results: {}",
                                results.size(), key, results);
                    }
                    return false;
                }

                Object deleteResult = results.get(0);

                if (deleteResult instanceof Boolean booleanResult) {
                    return booleanResult;
                }

                if (deleteResult instanceof Long longResult) {
                    // Redis DEL commonly returns the number of keys removed
                    return longResult > 0;
                }

                if (log.isWarnEnabled()) {
                    log.warn("Unexpected Redis DEL result type {} (value={}) when deleting recording partial marker for key {}",
                            deleteResult != null ? deleteResult.getClass().getName() : "null", deleteResult, key);
                }

                return false;

```

1. This change assumes that a logger named `log` is available in `RedisServiceImpl` (for example, `private static final Logger log = LoggerFactory.getLogger(RedisServiceImpl.class);`). If it is not already present, add such a logger following the existing logging conventions in this codebase.
2. If your project uses a different logging abstraction or message format, adjust the `log.warn` calls to match the existing style.
</issue_to_address>

### Comment 2
<location path="springboot-backend/src/test/java/webChat/service/recording/RecordingPartialCleanupServiceTest.java" line_range="67-76" />
<code_context>
+        return new CleanupResult(2, 1, 0);
+    }
+
+    @Test
+    @DisplayName("cleanupPartialRecording_서비스정상반환_예외없이카운트로깅하고종료")
+    void cleanupPartialRecording_serviceSucceeds_completesWithoutException() {
</code_context>
<issue_to_address>
**suggestion (testing):** cleanupExpiredPartialRecordings에서 마커 삭제 조건 실패(deleteRecordingPartialMarkerIfRecordingIdMatches=false) 경로를 검증하는 테스트가 누락된 것 같습니다

현재 구현에서는 `deleteRecordingPartialMarkerIfRecordingIdMatches``false`를 반환하면 해당 건을 `deleted`가 아닌 `skipped`로 집계하고 warn 로그를 남기지만, 이 경로를 검증하는 테스트가 없습니다.

예시로는:
- expired 마커 1건에 대해 `deleteRecordingPartialMarkerIfRecordingIdMatches("room-1", "rec-1")``false`를 반환하도록 설정
- `cleanupExpiredPartialRecordings()` 실행 후 `deleted=0`, `skipped=1`, `failed=0`을 기대
정도가 있을 것 같습니다.

이 케이스를 추가하면 "파일은 삭제되었지만 경쟁 조건 등으로 마커 삭제 조건이 불일치한 경우"에도 집계/로깅 동작이 의도대로 유지되는지 회귀 테스트가 가능해질 것 같습니다.
</issue_to_address>

### Comment 3
<location path="plan_docs/00-base_plan/2026/07/bug_136_recording_partial_cleanup_plan.md" line_range="65-74" />
<code_context>
+- **(OPEN-2 확정)**: `DownloadStatus` 신규 값 = **`SYSTEM_AUTO_DELETED`** (유저 직접 지정).
</code_context>
<issue_to_address>
**issue (bug_risk):** `DownloadStatus` 값 표기 불일치 (`SYSTEM_AUTO_DELETED` vs `SYSTEM_DELETED`)

여기서는 신규 값이 `SYSTEM_AUTO_DELETED`로 적혀 있는데, 아래 예시에서는 `SYSTEM_DELETED`를 사용하고 있습니다. 실제 enum 상수명을 무엇으로 쓸지 결정한 뒤, 문서/코드 전체에서 동일한 이름으로 맞춰 주세요.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
제가 더 도움이 될 수 있도록 도와주세요! 각 코멘트에 대해 👍 또는 👎를 클릭해 주시면, 그 피드백을 바탕으로 리뷰 품질을 계속 개선해 나가겠습니다.
Original comment in English

Hey - I've found 3 issues, and left some high level feedback:

  • In RedisServiceImpl.getAllRecordingPartialMarkers, the RedisConnection obtained from slaveTemplate.getConnectionFactory().getConnection() is never explicitly closed; consider using slaveTemplate.execute with a RedisCallback or a try-with-resources on the connection itself to avoid leaking connections.
  • The cleanup policy (cron expression and cleanupAgeSeconds default of 14400) is currently hardcoded; it would be more flexible to externalize both the schedule and age threshold into configuration properties so future policy changes don’t require code changes.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `RedisServiceImpl.getAllRecordingPartialMarkers`, the `RedisConnection` obtained from `slaveTemplate.getConnectionFactory().getConnection()` is never explicitly closed; consider using `slaveTemplate.execute` with a `RedisCallback` or a try-with-resources on the connection itself to avoid leaking connections.
- The cleanup policy (cron expression and `cleanupAgeSeconds` default of 14400) is currently hardcoded; it would be more flexible to externalize both the schedule and age threshold into configuration properties so future policy changes don’t require code changes.

## Individual Comments

### Comment 1
<location path="springboot-backend/src/main/java/webChat/service/redis/impl/RedisServiceImpl.java" line_range="563-572" />
<code_context>
     }

+    @Override
+    public boolean deleteRecordingPartialMarkerIfRecordingIdMatches(String roomId, String recordingId) {
+        String key = RECORDING_PARTIAL_PREFIX.getPrefix() + roomId;
+        Boolean deleted = masterTemplate.execute(new SessionCallback<Boolean>() {
+            @Override
+            public Boolean execute(@NotNull RedisOperations operations) {
+                operations.watch(key);
+                Object value = operations.opsForValue().get(key);
+                if (!(value instanceof RecordingPartialMarker marker)
+                        || !Objects.equals(marker.getRecordingId(), recordingId)) {
+                    operations.unwatch();
+                    return false;
+                }
+
+                operations.multi();
+                operations.delete(key);
+                List<Object> results = operations.exec();
+                return results != null && !results.isEmpty() && Boolean.TRUE.equals(results.get(0));
+            }
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Clarify transactional delete result handling to avoid silent failures on unexpected exec results.

Inside the transaction you assume `operations.delete(key)` will yield a single `Boolean` at `results.get(0)`. Depending on the `RedisTemplate` / driver, `exec()` may return different types (e.g., status strings, `Long`, or `null`) for delete operations. If that changes, this method may silently return `false` (or `true`) even when the actual delete outcome differs. Please validate the `results` size and element type (with an `instanceof` check) and either log or fail fast when the shape is unexpected to avoid silent misreports.

Suggested implementation:

```java
                operations.multi();
                operations.delete(key);
                List<Object> results = operations.exec();

                if (results == null || results.isEmpty()) {
                    // No results returned from exec, treat as failure
                    // and avoid silently reporting success
                    if (log.isWarnEnabled()) {
                        log.warn("Unexpected empty/null Redis transaction result when deleting recording partial marker for key {}", key);
                    }
                    return false;
                }

                if (results.size() != 1) {
                    // We only expect a single operation in this transaction
                    if (log.isWarnEnabled()) {
                        log.warn("Unexpected Redis transaction result size {} when deleting recording partial marker for key {}. Results: {}",
                                results.size(), key, results);
                    }
                    return false;
                }

                Object deleteResult = results.get(0);

                if (deleteResult instanceof Boolean booleanResult) {
                    return booleanResult;
                }

                if (deleteResult instanceof Long longResult) {
                    // Redis DEL commonly returns the number of keys removed
                    return longResult > 0;
                }

                if (log.isWarnEnabled()) {
                    log.warn("Unexpected Redis DEL result type {} (value={}) when deleting recording partial marker for key {}",
                            deleteResult != null ? deleteResult.getClass().getName() : "null", deleteResult, key);
                }

                return false;

```

1. This change assumes that a logger named `log` is available in `RedisServiceImpl` (for example, `private static final Logger log = LoggerFactory.getLogger(RedisServiceImpl.class);`). If it is not already present, add such a logger following the existing logging conventions in this codebase.
2. If your project uses a different logging abstraction or message format, adjust the `log.warn` calls to match the existing style.
</issue_to_address>

### Comment 2
<location path="springboot-backend/src/test/java/webChat/service/recording/RecordingPartialCleanupServiceTest.java" line_range="67-76" />
<code_context>
+        return new CleanupResult(2, 1, 0);
+    }
+
+    @Test
+    @DisplayName("cleanupPartialRecording_서비스정상반환_예외없이카운트로깅하고종료")
+    void cleanupPartialRecording_serviceSucceeds_completesWithoutException() {
</code_context>
<issue_to_address>
**suggestion (testing):** cleanupExpiredPartialRecordings에서 마커 삭제 조건 실패(deleteRecordingPartialMarkerIfRecordingIdMatches=false) 경로를 검증하는 테스트가 누락된 것 같습니다

현재 구현에서는 `deleteRecordingPartialMarkerIfRecordingIdMatches``false`를 반환하면 해당 건을 `deleted`가 아닌 `skipped`로 집계하고 warn 로그를 남기지만, 이 경로를 검증하는 테스트가 없습니다.

예시로는:
- expired 마커 1건에 대해 `deleteRecordingPartialMarkerIfRecordingIdMatches("room-1", "rec-1")``false`를 반환하도록 설정
- `cleanupExpiredPartialRecordings()` 실행 후 `deleted=0`, `skipped=1`, `failed=0`을 기대
정도가 있을 것 같습니다.

이 케이스를 추가하면 "파일은 삭제되었지만 경쟁 조건 등으로 마커 삭제 조건이 불일치한 경우"에도 집계/로깅 동작이 의도대로 유지되는지 회귀 테스트가 가능해질 것 같습니다.
</issue_to_address>

### Comment 3
<location path="plan_docs/00-base_plan/2026/07/bug_136_recording_partial_cleanup_plan.md" line_range="65-74" />
<code_context>
+- **(OPEN-2 확정)**: `DownloadStatus` 신규 값 = **`SYSTEM_AUTO_DELETED`** (유저 직접 지정).
</code_context>
<issue_to_address>
**issue (bug_risk):** `DownloadStatus` 값 표기 불일치 (`SYSTEM_AUTO_DELETED` vs `SYSTEM_DELETED`)

여기서는 신규 값이 `SYSTEM_AUTO_DELETED`로 적혀 있는데, 아래 예시에서는 `SYSTEM_DELETED`를 사용하고 있습니다. 실제 enum 상수명을 무엇으로 쓸지 결정한 뒤, 문서/코드 전체에서 동일한 이름으로 맞춰 주세요.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

…al 녹화 정리 배치 버그수정

- sourcery-ai 리뷰에 따른 코드 수정
- 방 삭제 배치로 인해 roomId 와 관련된 모든 redis key 가 삭제되어 partial 녹화 정리 배치가 정상 실행되지 않는 버그 수정
@SeJonJ
SeJonJ temporarily deployed to chatforyou-back-env July 11, 2026 16:30 — with GitHub Actions Inactive
@SeJonJ
SeJonJ temporarily deployed to chatforyou-back-env July 12, 2026 05:44 — with GitHub Actions Inactive
@SeJonJ

SeJonJ commented Jul 12, 2026

Copy link
Copy Markdown
Owner Author

26.07.12 녹화 파일 삭제확인 완료

@SeJonJ
SeJonJ merged commit 043eb85 into chatforyou_v2 Jul 12, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant