# labrador-scrapers 라이브러리 크롤러 누락방지 수정 플랜 (DRAFT)
AI Summary
Purpose:
- labrador-scrapers 모노레포의 라이브러리 크롤러(npm/pypi/conan/vcpkg/hunter)에서
① 누락 아닌 error→warning ② insert 행별 폴백 ③ 증분/resync 분류 ④ 기타 누락방지를 적용하기 위한 조사 결과 + 수정 플랜. 사용자와 함께 다듬는 중인 DRAFT.
Key points:
- 데이터 포맷 절대 불변: 모든 제안은 control-flow / log-level / scheduling /
skip-logic / WHERE-filter / PROCESSED 정수값(기존 tinyint 컬럼 재사용)만. 컬럼/JSON/ 인코딩/row-shape/PK/SQLModel model-field 변경 0.
- 이미 된 것이 많음(재작업 금지): SQLModel 계열 insert 폴백=프레임워크 RecordInserter,
npm insert 폴백+증분 skip, pypi 증분 gating.
- 핵심 미해결 = "feed-ahead-of-meta" 영구누락: 피드/시퀀스가 메타보다 먼저 패키지를
announce → 메타 404(전파 지연) → 현재 영구 종료(npm processed=5, pypi 4~7 stuck). → golang식 PROCESSED 상태머신(재시도 ladder + grace window)으로 해결.
Relevant when:
- 위 5개 크롤러의 누락방지/로그/증분 작업을 진행할 때.
Do not read full document unless:
- 구현 착수, 또는 설계 결정(ladder 깊이/백오프, transient vs 404 분리 등) 논의.
Linked documents:
ai/repo-notes/labrador-scrapers.md(컴포넌트 인벤토리)ai/repo-notes/crawler-lib-golang.md(PROCESSED 상태머신 레퍼런스: ladder/reclaim/RESYNC)ai/repo-notes/crawler-lib-ruby.md부재 /crawler-lib-php.md·crawler-lib-java.md
(grace-window 404=retryable 선례)
ai/worklog/2026/2026-W26.md
검증 결과 (2026-06-23, ladder 설계 전제 — 전부 PASS)
- LAST_UPDATED 자동갱신:
TB_COMP_LIB_JAVASCRIPT_LIST·TB_COMP_LIB_PYTHON_LIST둘 다
LAST_UPDATED timestamp + EXTRA = "on update CURRENT_TIMESTAMP" → 어떤 UPDATE든 자동 갱신. npm _updatePackageStatus(VO upsert, LAST_UPDATED 미지정)·pypi _update_package_status (SET PROCESSED=:status만) 모두 DB 자동갱신 의존 → PROCESSED만 바꾸면 백오프 게이트 (LAST_UPDATED < NOW()-INTERVAL)가 공짜로 동작. (열린질문 #7 해소.)
- PROCESSED 타입: 둘 다
tinyint NOT NULL DEFAULT 0(signed −128~127) → **20, 30~34 값 추가 OK,
스키마 변경 0.**
- claim WHERE: npm
processed IN (0,4), pypiPROCESSED = 0→ ladder는(0,30-33)+백오프로 확장. - 누락 규모 실측: npm PROCESSED=5 162,860 / pypi PROCESSED=5 120,893 = 현재 영구종료
backlog. (npm 3=버전없음 1.28M, 110=라이선스폭초과 2; pypi 0=43.)
- 결정 확정(사용자): A안(ladder 후 34-terminal) + pypi 동시 적용.
기존 backlog 복구 (ladder는 미래 방지 / 과거 16.3만+12.1만은 별도)
- ladder는 앞으로 박히는 걸 막음. 이미 PROCESSED=5인 npm 16.3만 + pypi 12.1만은
1회 resync로 재검증: npm setProcessedValues([5]) resync / pypi pypi_crawling_resync (resync_status=5). 전파지연 해소된 건 수집(1), 진짜 gone은 다시 5(또는 ladder 도입 후 34). → 코드 변경 없이 기존 resync 모드로 가능(포맷 불변). ladder 배포 후 1회 실행 권장.
스케줄 구성 & 누락 함의 (검증 2026-06-23 — 결정적)
스케줄은 labrador-scrapers가 아니라 ~/labrador/platform/labrador-data-platform Airflow DAG에 있음 (dags/<crawler>_crawler.py). 크롤러별 모드/주기:
| 크롤러 | 주기 | 모드 | resync/풀 보완 |
|---|---|---|---|
| npm | 5분(*/5) | incremental(npm_package_update+npm_crawling) | 없음(PROCESSED=5 resync DAG 없음) |
| pypi | 30분(*/30) | incremental(pypi_crawling_incremental, PROCESSED=0) | 수동만(pypi_crawling_resync schedule=None) |
| vcpkg | 매일 13:00 | raw=풀, version=증분(버그), product=풀 | version 보완 없음(수동 풀만) |
| conan | 매일 13:00 | raw/version/product incremental(version=RECORD_UPDATED per-product 게이트=정확) | — |
| hunter | 매일 13:00 | 대부분 풀(--no-incremental, 소형), archive만 증분 | 풀이 보완 |
함의(이 플랜의 가치 확정):
- npm/pypi: 증분만 스케줄, PROCESSED=5(메타404 종료) 재검증 미스케줄 → 404→5 누락 실제 누적
(npm 162,879 / pypi 120,892, 최근30일 재검증 1.7%/0.3%뿐). → feed-ahead-of-meta ladder(P0)가 활성 누락을 막음(실가치 큼). 기존 backlog는 1회 resync(pypi DAG는 있으나 수동; npm은 resync DAG 자체 없음 → 추가 필요).
- vcpkg: version이 버그난 증분으로 매일 돌아 신규버전이 수동 풀 전까지 누락 → 증분 수정(
39a8d35)이 실가치
(매일 증분이 신규를 잡게 됨). (앞서 "풀이 드레이닝"으로 본 건 수동/일시 풀이었던 것.)
- conan/hunter: per-product 게이트/풀이라 누락 영향 작음.
- 후속(스케줄측, labrador-data-platform 수정): npm/pypi에 PROCESSED=5(또는 ladder 도입 후 미수렴분) 정기 resync DAG 추가 권장.
배경 / 범위
- 대상 5종, 2계열:
- 레거시 ai/labradorlabs/ 포팅: npm_crawler(js/NPM), pypi_crawler(python/PYPI). LIST work-list(PROCESSED) + info_data + baseCralwer(setInsert) = crawler-lib- 계보. - labrador_sqlmodel BaseScraper 3단: conan_crawler, vcpkg_crawler, lib_hunter_crawler. RAW→VERSION→PRODUCT, 프레임워크 ~/labrador/platform/labrador-sqlmodel.
- 전제: 데이터 포맷 불변.
이미 되어 있는 것 (재작업 금지)
- SQLModel 계열 insert 행별 폴백: 프레임워크
core/queueing.pyRecordInserter가
bulk UPSERT(ON DUP)→실패 시 이분할(bisect)→단일행 격리→불량행만 drop. ruby _insertChunked 보다 정교. Axis 2 완료(conan/vcpkg/hunter).
- npm:
_executeUpsert= ruby_insertChunked식 행별 폴백 이미 있음 + 증분 skip
(_crawlPackage에서 api−db 버전 diff, resync는 전체 재수집). Axis 2·3 완료.
- pypi: 증분 gating 이미 정확(mode-switch + 버전 단위 skip-existing).
pypi_crawling(=resync_all, 이름 함정) / pypi_crawling_incremental(processed=0) / pypi_crawling_resync. Axis 3 완료.
- conan VERSION(
RECORD_UPDATEDself-gate), vcpkg/hunter ARCHIVE·VERSION 증분 모드 존재.
★ 핵심 설계: "feed-ahead-of-meta" 영구누락 → golang식 PROCESSED 상태머신
문제 (사용자 발견, npm)
- npm
_changes피드가 패키지를 시퀀스에 등록(LIST processed=0)했는데, **메타 문서가 아직
레지스트리에 전파 안 됨** → registry에서 404.
- 현재(
npmCrawler.py:777-804): meta 없음 →last_updated기준 **5분 이내면 processed=4(재시도),
초과면 processed=5(영구 종료). 게다가 processed=4도 다음 run엔 이미 5분 초과 → 또 404면 5. → 5분 안에 메타가 안 올라오면 영구 누락.** (전파 지연은 수분~수시간일 수 있음.)
- 원인의 본질: 404를 "없음(gone)"으로 즉시 단정. 실제로는 대부분 "아직 없음(not-yet)".
일반화 (pypi 및 유사)
- pypi: changes/RSS 피드가 패키지 announce → PyPI JSON API 404(전파 지연) → 현재
PROCESSED=4/5/6/7로 빠지면 증분(processed=0만)에서 영영 재수집 안 됨(stuck). 같은 부류.
- 선례: crawler-lib-php/java는 "POM 404가 grace window(7d) 내 신규면 retryable", golang은
full ladder. *즉 crawler-lib- 계열엔 이미 검증된 패턴**; 스크래퍼 계열(npm/pypi)에 없음.
설계 원칙
피드/시퀀스는 메타보다 먼저 존재를 알린다(eventual consistency). 따라서 큐에 든 항목의 404는 기본적으로 "아직 없음"이지 "없음"이 아니다. 전파 지연을 넘길 만큼의 시간/재시도 예산 동안 재시도해야 하며, 그 예산을 넘겨 404가 지속될 때만 gone으로 단정한다.
PROCESSED 상태머신 (기존 tinyint 컬럼 재사용 — 포맷 불변, golang 방식)
0pending (피드 신규 / 재큐)20processing (워커 claim, 인플라이트) — 크래시 reclaim용1complete /3empty(버전 없음, 정상)30/31/32/33재시도 ladder(시도 1~4). transient·404-not-yet 공통. 각 레벨에 최소 백오프
(LAST_UPDATED 나이로 게이트). claim WHERE = PROCESSED IN (0,30,31,32,33) + (PROCESSED=0 OR LAST_UPDATED < NOW()-INTERVAL <레벨 백오프>). - 예: 30→~15분 후, 31→~1h, 32→~6h, 33→~24h 후 재claim.
34exhausted/gone: ladder 전체(~1일+) 내내 404 지속 → 진짜 unpublish/삭제로 단정.
증분 재수집 X, resync 재수집 O, 감사 가능. (= 현 5의 자리, 단 "충분히 재시도 후"에만.)
- (옵션) transient(429/5xx/timeout/conn) vs 404 분리: transient는 ladder를 돌되 exhaustion
카운트에 넣지 않음(또는 더 높은 cap) — 명백히 gone 아님. 404만 exhaustion으로.
워커 흐름
- claim(0,30-33 + 백오프 게이트) → 20(processing) 마킹.
- meta fetch:
- 성공 → 수집 → 1 - 버전 없음 → 3 - 404 → 현 ladder 레벨 k<max면 k+1(다음 백오프), k==max면 34(gone) - transient → retryable 유지(30 또는 짧은 백오프), 절대 34 아님
- reclaim: 20이 X분 초과 → retryable 복원(크래시 안전, golang
_reclaimStuckProcessingEvents).
왜 이게 푸나
시퀀스에 등록됐지만 메타가 전파 지연으로 404인 패키지 → ~1일에 걸쳐 4회 재시도 → 전파 지연 (초~시간)은 그 안에 들어오므로 메타가 뜨는 즉시 수집(1). 진짜 미전파/삭제만 하루+ 지속 → 34(gone)로 명시(조용한 누락 아님). 포맷 불변(PROCESSED 정수값 추가 + 기존 LAST_UPDATED 게이트).
적용 범위
- npm: 5분 binary window 제거 → 위 ladder. (1순위)
- pypi: 동일 ladder + 증분 타겟에 재시도 상태 포함(현재 4~7 stuck 해소). (1순위)
- conan/vcpkg/hunter: git-clone 기반이라 per-item API 404 부류는 약함. 단 archive URL 404는
enrichment(코어행 아님)라 후순위. "feed-ahead-of-meta"는 주로 npm/pypi.
수정 항목 (우선순위)
P0 — 진짜 영구누락 차단
- npm [구현완료
feature/scraper-lib-anti-omission, 미커밋·미배포]: feed-ahead-of-meta ladder.
getJsonRequestsStatus(404/transient/ok 구분, 기존 getJsonRequests 유지) + ladder 전이 (_nextLadderOn404/_holdLadderOnTransient) + claim WHERE (0,4,30@15m,31@1h,32@6h,33@24h), 34 제외. SINGLE-process(파티션 CRC32 샤딩=disjoint)라 20/reclaim 생략(crash 시 0/30-33로 자연 재수집, 20 추가는 stuck-at-20 신설). py_compile OK, 포맷 불변. 변경=npmCrawler.py + sw/getData.py.
- pypi [구현완료, 미커밋·미배포]: ①
_upsert_batchraise 제거+행별 폴백(불량행만 skip).
② ladder(_classify_fetch_outcome+_next_ladder_status, http_client get_json_with_detail 활용) + claim WHERE 동일 확장. SINGLE-process 동일 판정(20/reclaim 생략). py_compile OK, 포맷 불변. 변경=pypiCrawler.py. 주의: _upsert_batch 비-raise화는 모든 호출부(version/product/license/ package_list)에 적용(의도된 anti-omission).
- conan·vcpkg·hunter [구현완료
e933a99, 미커밋푸시]: git clone/pull 실패 묵살 → stale/빈
트리로 "0건 수집을 성공"으로. → 클론 실패(사용가능 트리 없음)=LabradorScraperCriticalError(abort) + ports/recipes/cmake-projects 비면 critical 가드, pull 실패(기존 트리)=warning+stale 진행.
- hunter [구현완료
e933a99]: sort-gen이 한 버전 문자열 실패 시CriticalError→전 product SORT_ORDER
누락 → per-product catch+warning+return [](스킵), JVM/jar-load 실패만 critical 유지.
- hunter [확인완료
e933a99, 코드변경 없음]: RAW⋈ARCHIVE INNER join — archivetransform_record가
enrichment 실패해도 항상 SHA1 row emit함 확인 → 코드변경 불필요. archive→version 스케줄 순서는 운영.
- vcpkg [구현완료
e933a99]: PRODUCTeval(license)→json.loads+(JSONDecodeError,TypeError)폴백.
감사완료: TB_COMP_LIB_VERSION_VCPKG.LICENSE 10,097건 전부 JSON 배열(non-JSON 0) → 동등·byte-identical.
P1 — 로그레벨(Axis 1, 누락 아닌 error→warning, severity만) [구현완료 7903445, 미커밋푸시]
- 순수
.error(→.warning(61/61, control-flow/포맷 변경 0. 프레임워크labrador-sqlmodel미수정
(queueing.py 실제 drop 지점=진짜 유실이라 error 유지 — vcpkg 에이전트 제안은 기각).
- npm 16강등/6유지, pypi 16사이트(25줄)강등/7유지, hunter 10강등, conan/vcpkg db.py license-load
+ licenseeUtil(AI/subprocess) enrichment 강등.
- 유지(진짜 유실): 프레임워크
queueing.py실제 drop(depth-10 cap, depth-1 problematic),
npm spdx-empty/product-build/crawl-catch-all/_executeUpsert 행skip/DB open, pypi _upsert_batch 행skip/_insert_ignore/_start_crawling raise/DB open/main critical, setup_iterator critical.
P2 — 효율/증분(누락 아님, 선택)
- vcpkg VERSION 정상모드가 매 run 전체 git 히스토리 재walk(매우 비쌈) → 라우틴
--incremental. - conan PRODUCT gating 없음 → 선택적
RECORD_UPDATEDgate. - hunter ARCHIVE full 재다운로드 비쌈 → incremental 스케줄.
- 프레임워크 R2a: transient DB 에러가 bisection 타고 depth-10 drop →
OperationalError(retry)
vs IntegrityError(bisect) 구분.
결정/검증 상태
- [확정] A안 + pypi 동시 (사용자, 2026-06-23).
- [확정] ladder 파라미터(기본값, 미세조정 가능): 4단계 백오프 30→~15분, 31→~1h, 32→~6h,
33→~24h, 이후 34(gone). 총 grace ≈ 1일+4회 재시도. transient(429/5xx/timeout)는 exhaustion 카운트 제외(34로 안 감), 404만 카운트. processing(20)+reclaim 추가(크래시 안전).
- [검증완료] LAST_UPDATED ON UPDATE (두 테이블, 두 코드경로 모두 자동갱신 의존) — 게이트 OK.
- [검증완료] PROCESSED tinyint 20/30-34 수용.
남은 열린 질문 (다른 축/크롤러)
- vcpkg 증분 정합성 [검증완료 2026-06-23: 증분 필터 버그 실재, 단 풀run이 완화 — "영구누락" 아님]:
- 증분 필터 버그(실재): VCPKGVersionIncrementalCrawler가 WHERE RAW.RECORD_UPDATED > query_max(전역 VERSION.RECORD_UPDATED). VERSION max는 매 run 전진 → 그보다 오래된 RAW 변경은 증분에서 영구 제외. (RECORD_UPDATED bump 메커니즘 자체는 정상.) - 정정(원인 점검 2026-06-23): 처음 "누락 43/2,989"로 봤으나 재확인 결과 영구누락이 아니라 풀 run이 드레이닝 중인 lag였음. 점검 중 누락 43→1, 직전 2h 내 version 행 82건 생성(라이브 풀 run 가동). aliyun 3.11.2 수집 확인(byte-exact, RAW=VERSION=3.11.2). 즉 git-walk는 정상(신규버전 수집됨), aliyun(RAW 2025-02-02)은 버그난 증분으론 선택 불가한데 수집됨 = 풀 VCPKGVersionCrawler가 backlog 흡수. - 결론: 증분-only 운영이면 누락(증분 버그)이지만, 풀 run 주기 실행으로 실제론 완화됨. git-walk 최신commit 누락 우려(원래 #2)는 기각. - 수정 [구현완료 39a8d35]: conan식 per-product RECORD_UPDATED 비교를 시도했으나 vcpkg 에선 version 행 RECORD_UPDATED 가 재처리로 따로 전진해 0건 선택 → 실패. 직접 신호인 '(PRODUCT_KEY, VERSION) 미존재'로 교체(VCPKGVersionIncrementalCrawler.setup_iterator: RAW LEFT JOIN VERSION ON PRODUCT_KEY=PATH AND VERSION WHERE raw.VERSION IS NOT NULL AND v.VERSION IS NULL). 선택 포트는 scrape 시 git 히스토리 재walk → 현재+누락 버전 수집, 현재버전 수집된 포트는 제외(수렴, churn 없음). VERSION 부재 시 전체 RAW 폴백. 검증: 쿼리 read-only로 누락 스냅샷 정확 선택, py_compile OK, 쿼리만(포맷 불변). 교훈: RECORD_UPDATED-기반 증분은 derived 테이블 행이 재처리로 touch되면 신뢰 불가 — '현재값 미수집' 직접 비교가 안전. - 수정 위상(정정 반영): 활성 손실 차단이 아니라 증분 모드를 omission-safe하게 만드는 것 = 비싼 풀 run 의존을 줄이고 증분만으로 lag를 작게 유지. 풀 run이 이미 backlog를 흡수하므로 별도 일괄 복구는 불필요(풀이 돌고 있음). git-walk 정상 확인됐으므로 "최신 commit 못 잡음" 우려도 해소.
- vcpkg LICENSE 레거시 non-JSON 존재 여부(eval→json.loads 전 1회 DB 감사).
- 프레임워크 변경 blast radius:
labrador-sqlmodel은 모든 scraper 공유(P2 R2a/queueing 로그).
각 크롤러 main.py/ScraperRunner는 복사본이라 runner/로그는 크롤러별 따로.
데이터 포맷 안전 원칙 (모든 항목 공통)
- 컬럼/JSON 인코딩/row-shape/PK/SQLModel field 변경 금지.
- 허용: PROCESSED 정수값 추가(기존 tinyint), WHERE 필터, 트랜잭션 입도, 로그 severity,
스케줄/scraper-mode, 예외 propagation(abort vs skip), 기존 LAST_UPDATED 활용.
- 위험 플래그: vcpkg
eval→json.loads는 레거시 non-JSON을 reject→skip할 수 있음(감사 선행).
npm PATH/LICENSE 컬럼 폭은 절대 확장 금지(None-on-overflow가 포맷 계약).
진행 방식
- 크롤러별
<TYPE>/...feature 브랜치(scraper repo는 main 직접커밋 훅 차단). - 단계별 커밋, py_compile + (가능 시 이미지 내) 테스트, 포맷 불변 회귀.
- 순서: P0(npm ladder → pypi 폴백+ladder → clone-fail) → P1(로그) → P2/검증.