Skip to content

[FIX#347] fetcher registry — BaseURL strict check 제거 + 호스트별 base_url 허용 - #350

Merged
juhy0987 merged 3 commits into
mainfrom
fix/#347/registry-baseurl-strict-check
May 11, 2026
Merged

juhy0987 merged 3 commits into
mainfrom
fix/#347/registry-baseurl-strict-check

Conversation

@juhy0987

Copy link
Copy Markdown
Member

연관 이슈

Closes #347

배경 — 라이브 #11 boot fail

`make build && ./bin/issuetracker` 라이브 기동 시 fatal:
```
{"level":"fatal","error":"inconsistent source metadata for source_name=\"dcinside\" (host=\"gall.dcinside.com\" vs host=\"gallery.dcinside.com\")",...}
```

dcinside / reddit / slashdot 의 host 별 다른 base_url (PR #334/#335 의도) 이 `RegisterAll` 의 strict check 에 거부됨.

심층 분석 결과 (issue #347 본문에 기록)

BaseURL 의 실제 사용처

경로 사용 위치 영향
GoQuery/Generic HealthCheck crawler.go 3 line HTTP GET 가용성 확인
raw_contents persistence DB 컬럼 메타데이터만
나머지 모든 곳 사용 안 함

rate_limiter 의 RPH lookup

  • `IPRateLimiterRegistry.Wait(rawURL)` 가 host 별 `SourceConfigResolver.Resolve(host_pattern)` 호출
  • BaseURL 무관 — host_pattern 으로 `fetcher_rules.requests_per_hour` exact match
  • rate_limiter 는 source_name 단일 BaseURL 가정과 무관

bySource collapse 의 실상

  • 동일 source_name 의 모든 row 를 하나의 canonical entry 로 축소 (`isCanonicalHost` 우선)
  • 비-canonical row 의 BaseURL 은 이미 폐기되고 있었음
  • → strict check 가 다른 BaseURL 을 거부한 것은 저장도 안 되는 값에 대한 보호 — 거짓 양성

결론

  • BaseURL 의 strict check 는 보호 가치 0, 거짓 양성만 발생
  • Country / Language / SourceType / RequestsPerHour 는 보호 가치 유지 (RPH 는 같은 IP 공유 시 race 방어)

구현

1. AnalyzeSources 함수 분리 (testability)

`RegisterAll` 안에 inline 되어 있던 collapse 로직을 패키지 레벨 함수로 추출:
```go
func AnalyzeSources(rules []*storage.FetcherRuleRecord) (
bySource map[string]SourceEntry,
hostsBySource map[string][]string,
baseURLsBySource map[string]map[string]struct{},
err error,
)
```
`SourceEntry { Rec, HasExact }` 도 export — 단위 테스트 친화.

2. strict check 에서 BaseURL 제외

```go
if prev.Rec.Country != r.Country ||
prev.Rec.Language != r.Language ||
// BaseURL 제거 — HealthCheck 만 사용, 호스트별 차이 허용
prev.Rec.SourceType != r.SourceType ||
prev.Rec.RequestsPerHour != r.RequestsPerHour {
return ..., fmt.Errorf("inconsistent source metadata ...")
}
```

3. 등록 로그 강화

같은 source_name 의 host 들이 다른 base_url 을 가지면 운영 가시성 위해 명시:
```
crawler registered from db (multiple base_urls — canonical used for HealthCheck only)
source=dcinside hosts=[gall.dcinside.com gallery.dcinside.com]
base_urls=[https://gall.dcinside.com https://gallery.dcinside.com]
canonical_base_url=https://gall.dcinside.com
```

4. Migration 025 — 라이브 우회 정리

라이브 #11 의 manual SQL (운영자가 base_url 통일) 을 host-specific 값으로 정합 복원:

기능 영향 0 (HealthCheck canonical 만 사용), DB 의 의도된 host-specific 값 정합 보존.

테스트 (9건)

`test/internal/processor/fetcher/domain/general/sources/analyze_test.go`:

  • MultiHostDifferentBaseURLs_Pass — 이슈 [FIX] fetcher registry — 동일 source_name 의 host 별 base_url 차이 거부로 boot fail #347 핵심 회귀 (dcinside dual-host)
  • CanonicalSelection_PrefersBaseURLHostMatch — canonical 선택 보존
  • RPHMismatch_Rejected — RPH 불일치는 여전히 reject (race 방어)
  • CountryMismatch / LanguageMismatch / SourceTypeMismatch_Rejected — 의미론적 metadata 불일치 reject
  • EmptySourceNameSkipped — legacy row skip
  • BaseURLsSetCapturesAllVariants — 다양성 캡쳐
  • SingleHostUniformBaseURLsSet — 단일 host set size=1

CI / 머지 게이트 점검

  • `go build ./...` 통과
  • `go test -race -count=1 ./test/...` 전부 ok
  • `go vet ./...` 깨끗
  • gofmt 정리
  • 로컬 DB 에 migration 025 적용 검증 — 3 source 의 host-specific base_url 복원 확인

변경 영향 범위 + 위험도

  • 영향: BaseURL 불일치로 인한 boot fail 제거. dcinside/reddit/slashdot 의 다중 host seed 가 정상 등록됨.
  • 위험도: 낮음
    • canonical 선택 로직 + HealthCheck 동작 보존
    • rate_limiter / handler routing 영향 없음 (BaseURL 사용 안 함)
    • RPH strict check 유지 — race 방어 보존

롤백 계획

  1. 코드 롤백: `git revert` — registry.go + AnalyzeSources export 되돌리기
  2. Migration 025 down: 라이브 [FIX] content repository의 URL 키에 대한 충돌 및 FK 위배 정책 수정 #11 의 manual 우회 상태로 환원
  3. 부분 롤백: registry.go 에 BaseURL strict check 만 복원 가능 (다른 변경은 무관)

🤖 Generated with Claude Code

juhy0987 added 2 commits May 11, 2026 21:29
… (이슈 #347)

배경:
- 라이브 boot fail: 'inconsistent source metadata for source_name=\"dcinside\"
  (host=\"gall.dcinside.com\" vs host=\"gallery.dcinside.com\")'
- dcinside / reddit / slashdot 의 host 별 다른 base_url (PR #334/#335 의도) 이
  strict check 에 거부됨

심층 분석 결과:
- BaseURL 의 사용처는 GoQuery/Generic HealthCheck (3 line) + raw_contents persistence 만
- rate_limiter 의 RPH lookup 은 SourceConfigResolver.Resolve(host_pattern) 으로 host 단위 —
  source_name 단일 BaseURL 가정과 무관
- bySource collapse 는 canonical (base_url hostname == host_pattern) 하나만 사용 —
  비-canonical 의 BaseURL 은 이미 무시되고 있었음
- → BaseURL strict check 는 거짓 양성만 야기, 보호 가치 없음

수정:
1. AnalyzeSources 함수 분리 (registry.go 의 collapse 로직 단위 테스트 친화로 추출)
   - 시그니처: (rules) -> (bySource, hostsBySource, baseURLsBySource, err)
   - SourceEntry 타입도 함께 export — Rec / HasExact
2. strict check 에서 BaseURL 제외:
   - 유지: Country / Language / SourceType / RequestsPerHour (의미론적 메타데이터)
   - 제거: BaseURL (HealthCheck 만 사용, 호스트별 차이 허용)
3. baseURLsBySource 캡쳐 + 등록 로그에 base_urls / canonical_base_url 추가:
   - 같은 source 의 host 들이 다른 base_url 을 가지면 운영 가시성 위해 명시
   - canonical 만 HealthCheck 사용한다는 사실 로그에서 명확화

테스트 9건 (test/internal/processor/fetcher/domain/general/sources/analyze_test.go):
- MultiHostDifferentBaseURLs_Pass — 이슈 #347 핵심 회귀 (dcinside dual-host)
- CanonicalSelection_PrefersBaseURLHostMatch — canonical 선택 보존
- RPHMismatch / CountryMismatch / LanguageMismatch / SourceTypeMismatch — 각 reject
- EmptySourceNameSkipped — legacy row skip
- BaseURLsSetCapturesAllVariants — 다양성 캡쳐
- SingleHostUniformBaseURLsSet — 단일 host set size=1
라이브 #11 (PR #346 머지 직후) 의 manual SQL 우회 (운영자가 dcinside/reddit/slashdot 의
base_url 을 source_name 별 단일 값으로 통일) 를 #347 의 strict check 완화 코드 변경 후의
정합 상태로 복원.

변경:
- dcinside / gallery.dcinside.com → https://gallery.dcinside.com
- reddit   / www.reddit.comhttps://www.reddit.com
- slashdot / news.slashdot.org    → https://news.slashdot.org

각 UPDATE 는 host_pattern + source_name 으로 정확히 식별. 멱등 — 이미 값이면 noop.

기능 영향: HealthCheck 가 canonical 만 사용하므로 비-canonical 의 base_url 차이는
runtime 영향 0. DB 의 의도된 host-specific 값 정합 보존 의도.
Copilot AI review requested due to automatic review settings May 11, 2026 12:30
@coderabbitai

coderabbitai Bot commented May 11, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@juhy0987 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 1 minute and 54 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ce38d65b-3a84-4d87-932d-946e7c0f9756

📥 Commits

Reviewing files that changed from the base of the PR and between 515e536 and 4f19b01.

📒 Files selected for processing (4)
  • internal/processor/fetcher/domain/general/sources/registry.go
  • migrations/down/025_fetcher_rules_per_host_base_url.sql
  • migrations/up/025_fetcher_rules_per_host_base_url.sql
  • test/internal/processor/fetcher/domain/general/sources/analyze_test.go
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/#347/registry-baseurl-strict-check

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@juhy0987 juhy0987 added the bug Something isn't working label May 11, 2026

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request refactors the source registration logic by extracting analysis into a new AnalyzeSources function. This change allows sources to have multiple base URLs, resolving Issue #347, while still enforcing consistency for other metadata like Country and Language. It also includes SQL migrations to restore host-specific base URLs and comprehensive tests for the new analysis logic. Review feedback suggests enhancing error messages to pinpoint specific metadata mismatches and unexporting internal types and functions to keep the public API clean.

Comment thread internal/processor/fetcher/domain/general/sources/registry.go Outdated
Comment thread internal/processor/fetcher/domain/general/sources/registry.go

Copilot AI 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.

Pull request overview

이 PR은 fetcher registry의 source 분석/등록 로직에서 동일 source_name 내 host별 base_url 차이를 허용하도록 strict check를 완화해, dcinside/reddit/slashdot 같은 dual-host seed에서 발생하던 boot fail을 제거합니다. 또한 분석 로직을 함수로 분리해 테스트 가능하게 만들고, 운영 가시성을 위한 로그 및 데이터 정합 복원 마이그레이션을 추가합니다.

Changes:

  • RegisterAll의 source collapse 로직을 AnalyzeSources로 분리하고, strict consistency check에서 BaseURL 비교를 제외
  • 동일 source_name에 base_url 변형이 여러 개면 이를 운영 로그로 명시
  • host별 base_url 정합을 복원하는 migration 025(up/down)AnalyzeSources 단위 테스트 추가

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 8 comments.

File Description
internal/processor/fetcher/domain/general/sources/registry.go AnalyzeSources로 분석 로직 분리, BaseURL strict check 제거, multiple base_urls 로그 강화
test/internal/processor/fetcher/domain/general/sources/analyze_test.go host별 base_url 차이 허용/거부 조건 및 canonical 선택 회귀 테스트 추가
migrations/up/025_fetcher_rules_per_host_base_url.sql 운영 우회로 통일됐던 base_url을 host별 의도 값으로 복원
migrations/down/025_fetcher_rules_per_host_base_url.sql 롤백 시 운영 우회 상태(통일 값)로 되돌리는 DOWN 추가

Comment thread internal/processor/fetcher/domain/general/sources/registry.go Outdated
Comment thread internal/processor/fetcher/domain/general/sources/registry.go
Comment thread migrations/up/025_fetcher_rules_per_host_base_url.sql Outdated
Comment thread migrations/down/025_fetcher_rules_per_host_base_url.sql Outdated
Comment thread migrations/up/025_fetcher_rules_per_host_base_url.sql Outdated
Comment thread migrations/up/025_fetcher_rules_per_host_base_url.sql Outdated
Comment thread migrations/down/025_fetcher_rules_per_host_base_url.sql Outdated
Comment thread migrations/down/025_fetcher_rules_per_host_base_url.sql Outdated
…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 juhy0987 self-assigned this May 11, 2026
@juhy0987
juhy0987 merged commit b3215c8 into main May 11, 2026
8 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.

[FIX] fetcher registry — 동일 source_name 의 host 별 base_url 차이 거부로 boot fail

2 participants