Skip to content

[FEAT#1] 크롤러 기능 구현 - #2

Merged
juhy0987 merged 2 commits into
mainfrom
feature/#1/crawler-core
Feb 20, 2026
Merged

juhy0987 merged 2 commits into
mainfrom
feature/#1/crawler-core

Conversation

@juhy0987

Copy link
Copy Markdown
Member

연관 이슈


구현 내용

  • 정적 페이지 크롤러 구현
  • 동적 페이지 크롤러 구현
    • Chrome headless docker 이용

TODO

  • Classifier 연결
  • DB 연결
  • Kafka 파트 추가
  • 도메인 별 탐색 소스 분리

논의 사항


- core: 크롤러 인터페이스, HTTP 클라이언트, Rate Limiter, Retry, 에러 타입 구현
- goquery: 정적 HTML 크롤러 구현 (Fetch, FetchAndParse)
- chromedp: 동적 크롤러 구현, 로컬/Docker Chrome 자동 분기 지원
- examples: 단계별 크롤링 과정 시연 예제 추가
- Makefile: Docker Chrome 컨테이너 관리 타겟 추가
- test: 전체 패키지 테스트 구현 (커버리지 92.1%)
@juhy0987 juhy0987 self-assigned this Feb 20, 2026
@juhy0987 juhy0987 added the enhancement New feature or request label Feb 20, 2026
@juhy0987 juhy0987 linked an issue Feb 20, 2026 that may be closed by this pull request
2 tasks
@juhy0987
juhy0987 merged commit fef10cc into main Feb 20, 2026
juhy0987 added a commit that referenced this pull request Apr 28, 2026
…(PR #127)

- internal/publisher/publisher.go
  - crawler/stage 필드를 갖는 sub-logger 를 루프 외부에서 1회 생성하여 재사용 —
    per-URL map 할당 회피 (gemini #2 의 첫째 지적)
  - 루프 시작 시 ctx.Err() 검사: 취소된 경우 남은 URL 은 fail-open 으로 통과 +
    WARN 로그 후 즉시 종료 — 셧다운 중 무의미한 cache 호출/WARN 누적 회피
    (gemini #2 의 둘째 지적). 후속 PublishBatch 가 ctx 에러로 자연 실패하므로
    publish 시도까지의 의도는 그대로 보존
- test/internal/publisher/publisher_url_cache_test.go
  - cancelTriggerCache 도입: 첫 Exists 호출 시점에 ctx.cancel() 트리거하여
    후속 iteration 의 ctx 검사 분기 진입을 결정적으로 유도
  - TestPublisher_URLCache_CtxCancelled_AllowsRemaining: 1회 호출 후 cache 호출
    중단되고 남은 2건은 fail-open 으로 모두 batch 에 포함됨을 검증

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
juhy0987 added a commit that referenced this pull request Apr 28, 2026
…존 (PR #128)

- pkg/redis/retry_queue.go
  - EnqueueRetry score: UnixNano → UnixMilli
    근거: Redis ZSET score 는 float64. UnixNano (≈1.78e18) 는 float64 mantissa
    한계 2^53 (≈9e15) 을 초과 → 정밀도 손실. UnixMilli (≈1.78e12) 는 안전 범위.
  - PopDueRetries maxScore 도 UnixMilli 로 통일 (score 단위 일관성)
  - retry 큐의 ms 단위 정렬은 실무 충분 (gemini #2/#3)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
juhy0987 added a commit that referenced this pull request Apr 28, 2026
…t 의도 명시 (PR #128)

- pkg/redis/retry_queue.go
  - ErrRetryEntryGone 제거: 선언만 되어 있고 어디에서도 반환·검사하지 않는 dead code
    (Copilot #2). stale 처리는 PeekDueRetries 가 GET=nil 판정으로 silent skip + ZREM
    자체 정리하므로 외부 시그널 불필요. 관련 주석도 PeekDueRetries 기반으로 정정
  - DeleteRetryEntryForTest 는 "ForTest" suffix 그대로 유지하고 doc 에 "운영 코드 사용
    금지" + 본 프로젝트의 별도 test 디렉토리 컨벤션상 부득이 exported 라는 사정을
    명시 (Copilot #6 — 컨벤션 충돌로 inline 분리 불가, 댓글로 추가 답변)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
juhy0987 added a commit that referenced this pull request Apr 29, 2026
Gemini code review #1, #2, #3 — I/O 가 수반되는 파싱 작업에 ctx 매개변수 누락.
호출자의 cancellation / timeout / trace metadata 전파를 위해 인터페이스 변경.

변경:
- parser.ContentParser.ParsePage(ctx, raw) — ctx 매개변수 추가
- parser.LinkListParser.ParseLinks(ctx, raw) — ctx 매개변수 추가

rule.Parser:
- 호출자 ctx 의 cancel/trace metadata 보존 — context.Background() 대신 ctx 사용
- resolveTimeout (5s) 안전망은 유지하되 호출자 ctx 위에 합성:
  context.WithTimeout(ctx, resolveTimeout) — ctx 의 더 짧은 deadline 이 우선
- 옛 background ctx 사용은 trace ID / logger 필드 유실 → metadata 보존

테스트:
- 모든 ParsePage / ParseLinks 호출에 context.Background() 추가
- context import 추가

전체 race 테스트 23 패키지 통과.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
juhy0987 added a commit that referenced this pull request Apr 29, 2026
Coderabbit code review 7건 통합 처리.

#1 .vscode/settings.json 추적 제외:
- gh CLI 자동 승인이 commit 에 포함됨 (다른 contributor 영향)
- .vscode/settings.json 제거 + .gitignore 의 ".vscode/" 활성화

#2 hasRequiredSelector 헬퍼 (parser.go):
- nil 만 검사하면 zero-value selector (CSS 빈 문자열) 가 ErrParseFailure 로 잘못 분류
- nil + CSS trim 후 빈 문자열 모두 ErrEmptySelector 로 명확 분류
- ParsePage: Title + MainContent 둘 다 필수
- ParseLinks: ItemContainer + ItemLink 둘 다 필수

#4 TargetType 주석 수정:
- "article" | "list" → "page" | "list" (도메인 일반화 commit 후 미반영 부분)

#5, #6 자연키 ↔ lookup 키 정렬 (migration 006):
- 이전 UNIQUE: (source_name, host_pattern, target_type, version)
  → FindActive 가 (host_pattern, target_type) 만 lookup 하면 동일 host/type/version
    의 두 source row 가 활성화될 때 nondeterministic
- 신규 UNIQUE: (host_pattern, target_type, version)
  → resolver lookup 키와 정렬, 의도치 않은 두 row 활성화 schema 단계 차단
- source_name 은 metadata 로 보존 (어느 source 가 등록했는지 추적)

#7 test require.NoError:
- 이전 _, _ := r.Resolve(...) 에러 무시 → call-count assertion 만 검증
- 모든 Resolve 호출에 require.NoError 추가 — 회귀 가시성 강화

#3 (wildcard host_pattern 매칭) 은 별도 후속 — 본 PR 범위 밖, 후속 issue 권장.

전체 race 테스트 23 패키지 통과. 라이브 DB migration 재적용 + UNIQUE 제약 검증 완료.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
juhy0987 added a commit that referenced this pull request May 2, 2026
…hain raw 처리 헬퍼 추출 (PR #227 CodeRabbit Major 3 + Minor 1 + gemini medium 1)

CodeRabbit / gemini 5건 일괄 반영:

1. (Major) semaphore.go panic 정책 위반:
   - Release() → Release() error. ErrReleaseWithoutAcquire sentinel (이슈 #208 / 04-error-handling.md "NEVER panic in production code").
   - chromedp_handler.go 의 defer s.Release() 도 error log 처리 (non-fatal warn).

2. (Major) main.go ENABLED=false dead path:
   - chain_handler 의 republish 분기는 unconditional 인데 chromedp pool 미기동이면 메시지 영구 누적.
   - log.Info → log.Fatal 로 fail-fast — 운영자가 명시적 의사결정 강제.
   - config 코멘트도 정정 — "ENABLED=false 면 republish 분기 fork 도 함께 필요" 명시.

3. (Major) handler.go ErrLazyContentNeedsBrowser 무조건 반환 vs config 코멘트:
   - 위 #2 의 fail-fast 채택으로 자연 해소 — ENABLED=false 시 부팅 차단되어 dead path 진입 자체 불가.

4. (Minor) chromedp_handler.go nil job guard:
   - Handle 진입 시 job==nil 검사 → "chromedp handler received nil job" error.

5. (Medium) chain_handler.go Handle / HandleChromedpOnly 중복:
   - raw 저장 + dup log + publishFetchedRef 흐름을 processFetchedRaw(ctx, job, raw, pool) 헬퍼로 추출.
   - pool 인자 ("default" / "chromedp") 로 운영 가시성 유지.
juhy0987 added a commit that referenced this pull request May 11, 2026
… test / 주석 갱신

Copilot 피드백 4건 반영:

1. wiring.go: first_in_chain → first_in_candidates (Copilot #1)
   - 동적 정책 (cheapest/latency/hybrid) 은 매 호출마다 policy.Select 가 정렬을 변경하므로
     "정책 적용 전 후보 목록의 첫 항목" 의미로 명명 정합화 — 운영 디버깅 시 오해 방지
   - 실제 첫 시도 provider 는 chain Generate 시점 로그에서 확인

2. wiring.go: loadHybridWeights 에러 메시지 형식 통일 (Copilot #2)
   - 기존: "LLM_HYBRID_WEIGHTS expects ..." (대문자 시작)
   - 변경: "invalid LLM_HYBRID_WEIGHTS: ..." (소문자 + invalid 접두)
   - 다른 env 파싱 에러 패턴 (선례 PR #340) 과 일관 — 로그 필터링 / 테스트 안정성 ↑

3. wiring_test.go: clearLLMEnv 에 LLM_POLICY / LLM_HYBRID_WEIGHTS 추가 (Copilot #3)
   - 로컬 운영자 .env 가 set 된 상태에서 hermetic 보장
   - 기존 test 의 first_in_chain → first_in_candidates 리네임 동기화

4. cmd/issuetracker/main.go: stale 주석 갱신 (Copilot #4)
   - 기존: "FixedOrder('gemini') 정책으로 Gemini 단일 provider"
   - 변경: 4가지 정책 (chain/cheapest/latency/hybrid) 선택 가능 + LLM_POLICY 명시
juhy0987 added a commit that referenced this pull request May 11, 2026
…/ port range 메시지

Copilot 4건 일괄 반영:

1. compose project name 고정 (Copilot #1)
   - CHROME_PROJECT_NAME=docker 변수 신설 + CHROME_COMPOSE 매크로
   - 모든 chrome compose 호출 (-p docker -f ... --env-file ...) 일관화
   - working dir 에 무관하게 동일 project 의 replica 인식 — 다른 wd 에서 호출 시
     compose project 변경으로 인한 ps 미감지 + scale 충돌 방지

2. CHROME_WORKER_COUNT 검증 강화 (Copilot #2)
   - 기존: '' / non-digit 거부
   - 변경: + 0 거부 (chrome 비활성은 FETCHER_CHROMEDP_ENABLED=false 권장 안내)
   - .env 추출 시 공백 제거 (tr -d ' ') — '2 # comment' 같은 케이스 방어

3. wc -l 출력 normalize (Copilot #3)
   - 일부 셸 (macOS BSD) 에서 wc -l 선행 공백 padding → [ -lt ] 비교 안정성 우려
   - tr -d ' ' 로 공백 제거하여 순수 정수만 비교

4. chrome-start 메시지에 port range 표시 (Copilot #4)
   - 기존: ws://localhost:9222... 하드코딩
   - 변경: .env 의 FETCHER_CHROMEDP_HOST_PORT_RANGE 추출 후 메시지 동적 포함
     (default 9222-9229) — 운영자가 range 변경해도 정확한 안내
juhy0987 added a commit that referenced this pull request May 11, 2026
Copilot 2건 반영:

1. URL 개수 vs WORKER_COUNT 정합 (Copilot #1):
   - pkg/config.LoadFetcherChromedpPool 가 RemoteURLs 개수 != WorkerCount 시 fail-fast
   - 기존: REMOTE_URLS 만 override → WORKER_COUNT 와 mismatch 시 app boot fail
   - 변경: 발견된 URL 개수 (NF count) 와 WORKER_COUNT 비교 후 분기
     * 일치: REMOTE_URLS 만 override (정상 mode)
     * 불일치: WARNING 로그 + REMOTE_URLS + WORKER_COUNT 둘 다 override (degraded mode)
     * URL 0개: .env fallback (기존 동작 보존)

2. compose 실패와 0 found 구분 (Copilot #2):
   - 기존: 2>/dev/null 로 stderr 버림 → docker daemon down / compose 오류도 'no found' 로 처리
   - 변경: chrome-remote-urls 가 compose ps 의 exit code 보존
     * 0 + 비어있는 output: 정상 (no chrome) — caller 가 fallback
     * 0 + URL 출력: 정상 — caller 가 export
     * non-zero: stderr 보존 + 동일 exit code 로 종료 → run-issuetracker 가 ERROR 로그 + 즉시 exit

추가:
- exec ./scripts/entrypoint.sh 로 shell PID inheritance — signal 전달 깔끔
- 발견된 URL 개수 (found) 를 메시지에 명시 — 운영 가시성

검증:
- chrome-remote-urls 단독 호출: 'ws://localhost:9226,ws://localhost:9227', rc=0
- run-issuetracker: 'Discovered chrome remote URLs (2): ws://localhost:9226,ws://localhost:9227'
  → 정상 boot, 모든 crawler 등록 + scheduler tick 진행 확인
juhy0987 added a commit that referenced this pull request May 11, 2026
…igration

gemini 2건 + Copilot 8건 일괄 반영:

1. AnalyzeSources: 어느 필드가 mismatch 인지 명시 (gemini #1)
   - 기존: 'inconsistent source metadata for source_name=X (host=A vs host=B)'
   - 변경: 'inconsistent Country for source_name=X ...' / Language / SourceType / RequestsPerHour
   - 운영 boot fail 시 즉시 진단 가능

2. AnalyzeSources export 사유 doc 추가 (gemini #2 응답)
   - 프로젝트 규칙 (.claude/rules/05-testing.md) — 모든 테스트는 test/internal 하위 외부 _test
   - same-package _test.go (internal/<pkg>/*_test.go) 패턴은 디렉토리 컨벤션 위반
   - testability 최소 노출로 export — API 안정 약속 아님 명시

3. RegisterAll: AnalyzeSources error wrap (Copilot #1)
   - 'analyze sources: %w' 로 호출 스택 컨텍스트 보존

4. 등록 로그: distinct base_urls 정렬 (Copilot #2)
   - map iteration 비결정성 → sort.Strings(distinct) 로 안정화

5. migrations 025 up/down: <> → IS DISTINCT FROM (Copilot #3-#8, 6건)
   - base_url 컬럼이 NULL 허용 (migration 014) — '<>' 비교는 NULL 시 NULL 반환 → 조건 skip
   - IS DISTINCT FROM 으로 NULL 포함 처리 — NULL row 도 정확히 갱신

테스트 assertion 업데이트 — 'inconsistent source metadata' → 'inconsistent ' 로 부분 매치.
juhy0987 added a commit that referenced this pull request May 11, 2026
Copilot 3건 일괄 반영:

1. worker.go: 'skipping silently' → 'skipping' (Copilot #3)
   - Info 로그를 남기는데 'silently' 표현이 모순 — 운영자 혼동 회피
   - 무로그 의도 아님을 메시지에서 명확화

2. NotFound_CommitsWithoutDLQ: .Once() + AssertNumberOfCalls 추가 (Copilot #1)
   - GetByID 정확히 1회 — ErrNotFound 분기 진입 보증
   - CommitMessages 정확히 1회 — commit 수행 보증
   - AssertExpectations 추가로 mock setup 미사용 검증

3. OtherFetchError_SendsToDLQ: 동일 강화 (Copilot #2)
   - GetByID / DLQ Publish / CommitMessages 각 1회 호출 보증
   - DLQ 분기 진입 회귀 방지력 강화
juhy0987 added a commit that referenced this pull request May 12, 2026
…er.CrawlTopic 통합 + 불필요 counter 제거

gemini + coderabbit + Copilot 리뷰 반영:

1. RetrySchedulerHolder 필드 rename — `S` → `Scheduler`
   - atomic.Pointer 호환을 위해 노출하되 명확한 이름으로 가독성 개선 (gemini #1, #3, #4)
   - publisher/retry.go 정의 + pool.go SetRetryScheduler / resolveRetryScheduler 의 참조 갱신

2. CrawlTopic 단일화 (gemini #2 + coderabbit)
   - publisher 의 `crawlTopic` (unexported) → `CrawlTopic` (exported) 로 노출
   - worker 의 `topicForPriority` 제거 — pool.requeueWithRetry / manager.Publish 가
     publisher.CrawlTopic 직접 호출
   - scheduler/throttle.go 의 중복 `crawlTopic` 제거 — publisher.CrawlTopic 사용
   - Kafka I/O 책임이 publisher 단일 출처라는 메타 #385 원칙과 일관

3. pool_retry_scheduler_test.go 의 불필요 counter 제거 (Copilot)
   - poolRetrySchedulerCounter atomic.Int32 + sync/atomic import 삭제
   - 테스트 검증 로직과 무관한 placeholder 였음

4. fakeRetryQueue 의 `// ScheduledAt 정렬 유지` 주석 복원 (gemini #7)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
juhy0987 added a commit that referenced this pull request May 14, 2026
…ce decorator 로 재구성 (5 critical + 2 medium)

피드백 (CodeRabbit + gemini critical 5건, gemini medium 2건):
- Critical: TimedPool 의 Query / QueryRow / Begin / SendBatch 에서 즉시 defer cancel() 호출이 pgx ctx lifetime 과 충돌. 반환된 pgx.Rows / pgx.Row / pgx.Tx / pgx.BatchResults 는 ctx 가 후속 작업 (rows 읽기 / tx 진행 / batch 처리) 까지 유효해야 하는데 호출 직후 cancel 하면 invalid.
- Medium: 전역 atomic 으로 query timeout 관리 → 다중 DB pool 시나리오 제한
- Medium: withQueryTimeout 의 redundant deadline 체크 (context.WithTimeout 이 이미 부모 deadline 보존)

해결:
- internal/storage/postgres/timed_pool.go 폐기 — Pool-level decoration 의 ctx lifetime 충돌은 본질적으로 회피 불가
- internal/storage/timeout_decorators.go 신설 — 8개 Repository 인터페이스마다 timeout decorator (각 메서드는 inner 의 결과를 받기 전 cancel 호출되지 않음 → pgx rows 완전 소비 후 메서드 종료 시점 cancel 안전)
- timeout 은 각 decorator 의 instance 필드 — 전역 atomic 제거 (gemini medium #1)
- withTimeout helper 는 d>0 일 때만 context.WithTimeout 적용 — redundant deadline 체크 제거 (gemini medium #2)
- NewPool 반환 타입 *pgxpool.Pool 로 복원 (Pool wrap 제거)
- main.go / processor / rule-validator wiring: 각 repository 를 storage.WrapXxxWithTimeout(repo, dbCfg.QueryTimeout) 으로 감쌈

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] 크롤러 기본 구현

1 participant