# crawler-lib-java
AI Summary
Purpose:
- Durable note for the Maven/Java package crawler at
~/labrador/crawler/crawler-lib-java. - Tracks the Python migration effort on branch
python-migration-maven.
Key points:
- Original implementation is Java (Spring/MyBatis). The Python migration replicates the exact column order and JSON encoding of every DB insert row so the two implementations are byte-compatible.
- Python source root:
src/ai/labradorlabs/inside the repo. Tests:tests/. venv:.venv/. Activate:. .venv/bin/activate. LANGUAGE='java',REPOSITORY='MAVEN'. (live DB 저장값은 대문자MAVEN— 2026-06-16 확인. 코드/노트에 보이던'Maven'은 REPOSITORY collation 이 ci 라 PK 매칭은 되지만 저장 표기는MAVEN.)- Key dependency files already in place (as of 2026-06-09):
sw/common.py(GetData, getNowDate),input/crawlerInfo.py(CrawlerInfo, LIBRARY_INFO),util/constants.py,db/base.py,db/cdbvdb.py. conftest.py addssrc/to sys.path for tests. - Row VO contract:
src/ai/labradorlabs/db/labradorDBVo.py—LabradorDBVoclass.
- getVersionVo(extracted) → 24-column VERSION_JAVA row (TB_COMP_LIB_VERSION_JAVA). JSON via jsonUtil.to_json (Jackson-exact: compact, ensure_ascii=False). ALWAYS-set columns (empty→"[]" / full-null dict): LICENSE, SCM, ISSUE_MANAGEMENT, ORGANIZATION, PARENT, ORIGINAL_INFO, DEPENDENCIES, DEPENDENCY_MANAGEMENT. CONDITIONAL (None when absent): STANDARD_LICENSE, LICENSE_IDS, LICENSE_AI. - getProductVo(version_row) → 9-column PRODUCT row (TB_COMP_LIB_PRODUCT). LICENSE = version_row's LICENSE (raw POM license JSON), matching Java product.setLicense(last.getLicense()) — NOT STANDARD_LICENSE (corrected P2b Task 1). - getMavenArchiveVo(record) → 9-column MAVEN_ARCHIVE row. ORIGINAL_INFO always json.dumps (never None). - getDependencyVo(version_row) → generator of DEPENDENCY rows. ID = sha256(PRODUCT_KEY + VERSION + origin_key + version_range). Yields nothing when DEPENDENCIES is None. - getCrawlerStatusVo(name, json_raw) → STATUS row (TYPE, RAW_DATA, TIMESTAMP).
MavenUrlUtil(P2a Task 4):src/ai/labradorlabs/util/mavenUrlUtil.py.groupIdToPath,getPomUrl,getArtifactSha1Url,getFallbackRepos. Java source-of-truth corrections: MavenCentral POM baseUrl =LINK_FILE(http://search.maven.org/remotecontent?filepath=); MAVEN_ATLASSIAN_E name="AtlassianExternal"baseUrl=https://packages.atlassian.com/mvn/maven-external/; MAVEN_CLOJARS name="Clojars". Fallback order: MavenCentral→Jenkins→Clojars→Cloudera→Eclipse→AtlassianExternal→MavenGoogle.MAVEN_REPOSlist added tocrawlerInfo.LIBRARY_INFO.- Test suite: 162 tests, all green as of 2026-06-10 (P4 Task 4).
BaseCrawler(Phase1 + P2d Task 1):src/ai/labradorlabs/crawler/baseCralwer.py. Routes inserts to correct maven tables viaCrawlerInfo.LIBRARY_INFOkeys. INSERT INTO … ON DUPLICATE KEY UPDATE viasetInsertIntoDB; INSERT IGNORE viasetInsertIgnoreDB.getLimitSelectsDBbuildsWHERE … ORDER BY PRODUCT_KEY LIMIT :start, 1000. Class-leveldb = LabradorDB()can be overridden per-instance for testing. No component/gatheringDBVo/versionSort — those are Phase2.
- P2d Task 1 work-list methods (all LANGUAGE='java', REPOSITORY='Maven'): - selectNotProcessedProducts(limit) → labradordb.TB_COMP_LIB_PRODUCT … ORDER BY PRODUCT_KEY LIMIT 0, :limit - selectNotProcessedArchives(repository_name, limit) → gatheringdb.TB_COMP_LIB_MAVEN_ARCHIVE … REPOSITORY_NAME=:repo AND PROCESSED IS NULL LIMIT 0, :limit - selectPriorityVersions(limit) / selectNotProcessedVersions(limit) → TB_COMP_LIB_VERSION_JAVA - selectVersionsOfProduct(product_key) → TB_COMP_LIB_VERSION_JAVA … ORDER BY FIXED_SORT_ORDER ASC, SORT_ORDER ASC, VERSION ASC - updateMavenArchiveProcessed(record) → UPDATE gatheringdb.TB_COMP_LIB_MAVEN_ARCHIVE SET PROCESSED = NOW() WHERE … - updateVersionSortOrder(row) → UPDATE TB_COMP_LIB_VERSION_JAVA SET SORT_ORDER = :so WHERE … - Test suite after P2d Task 1: 115/115 passed. Commit 44fb45a.
LicenseAnalysis(P2c Tasks 2-4):src/ai/labradorlabs/crawler/licenseAnalysis.py.convertLicenseNameToSpdxCode(name)→ regex + Levenshtein min-distance SPDX match (None if no regex match).convertToStandardLicense(licenses)→ dedup list with key order fixed at{name,url,id}; special cases: CDDL+GPL, APL+apache/2.0, CPE+GPL (no 3); " or " split; unmatched keeps original{name,url}; empty/None → None. LICENSE_IDS path (Task 4):normalizeLicenseName(lowercases, unifies license/licence, appliesapache-/mit-/bsd-/gpl-/lgpl-/agpl-licensepatterns,version X.Y→vX.Y; cleanup[^a-z0-9\s-]— strips.sov2.0→v20, faithful to Java LicenseAnalysisComponent:175 (the earlier.-preserving draft was reverted ina7c9d78));findSimilarLicenseId(Levenshtein ≤ 3 against all DB licenses, normalized both sides);extractLicenseIdFromUrl(apache/aws/spdx via MavenCrawlingUtil stubs, opensource.org last-path segment direct lookup);convertLicenseIds(3-step: normalize→DB lookup → URL → similarity);convertLicenseIdsWithName(direct DB lookup by raw name).MavenCrawlingUtilinjectable vialicenseAnalysis.mavenCrawlingUtil.LicenseDaoinjectable vialicenseAnalysis.licenseDao = FakeLicenseDao().MavenCrawlingUtil(P2c Task 4):src/ai/labradorlabs/util/mavenCrawlingUtil.py.convertApacheSiteByUrl,convertAwsSiteByUrl,convertSpdxSiteByUrl,getMavenCrawlingInfo— all return None (network stubs, integration-env only).LicenseeUtil(P2c Task 5):src/ai/labradorlabs/util/licenseeUtil.py.getAiLibraryLicense(license_datas, url=None)— mirrors dotnet/golanggetAiLibraryLicense. POSThttp://211.115.125.171/api/v1/licenses, body{"texts":[{"text":…}]}, 30s timeout. Normalises string/dict/None inputs. Response key lookup case-insensitive (AI_LICENSEthenai_license). Empty list → None. Non-200 → None. Output viato_json(compact). Stub methods (executeGitClone,getLicenseFromTag,getLicenseFileTextsFromClone,getTagFromVersion) deferred to Phase 2d Docker integration.AI_LICENSE_URLadded tocrawlerInfo.LIBRARY_INFO.LicenseResolver(P2c Task 6):src/ai/labradorlabs/crawler/licenseResolver.py. Wires the full license pipeline (mirrorsMavenPomCrawlerService:295-354). Output:{standardLicense, licenseIds, licenseAi}. Flow: (1) AI call over POM license NAMES first (empty names → skip → None); (2)convertToStandardLicense→ None → return early (ids also None); (3) ids from standard entries withidkey; fallback to per-namegetLicenseIdFromName; final fallbackconvertLicenseIds; [] → None.licenseAiuses AI API per user decision (Java used mvnrepository scrape). git-clone path is Phase 2d. Class-levelanalysis = LicenseAnalysis()andlicensee = LicenseeUtil()— both injectable as instance attrs for tests. Test suite: 108/108 passed.MavenCrawler(P3 Task 4):src/ai/labradorlabs/crawler/mavenCrawler.py. Ports JavaMavenCrawlerService.doCreateAllGAV+startCrawl.ingestRepo(repo, clean):clean=True→deleteMavenArchivesInRepofirst; streams viaIndexHelper.streamRecords; skipsclassifier != null; buildsgetMavenArchiveVorows; flushesinsertMavenArchiveevery BATCH=10000; writesinsertCrawlerStatusRowwith_msToDate(indexTimestamp); callsinsertAllProductsFromArchivefor MavenCentral only.startCrawl(): MavenCentral →clean=True; others →clean=False(incremental_lastUpdatedMillisviaselectCrawlerStatus). Per-repo exceptions isolated.indexHelperinjectable. Test suite: 144/144 passed (8 new tests). Commitc93f61c.app/main_index.py(P3 Task 5):src/ai/labradorlabs/app/main_index.py.main()instantiatesMavenCrawlerand callsstartCrawl(). Under__main__: runsmain()once then loopsschedule.every(1).days.do(main)/time.sleep(1). Mirrorsmain.py/main_resync.py. Test suite: 145/145 passed (1 new test). Commit051951d.GoogleMavenCrawler(P4 Task 4):src/ai/labradorlabs/crawler/googleMavenCrawler.py. Archive INTAKE only (POM crawl done byEtcMavenPomCrawlervia MavenGoogle archives).perform(): bootstrap (no STATUS) → fetch master-index + write STATUS RAW_DATA={groupId:null,…} TIMESTAMP=master Last-Modified ms. Non-bootstrap →updatedMasterIndex+updatedGroupIndex.updatedMasterIndex(): if master Last-Modified changed, merges new groupIds into existing RAW_DATA (preserves old timestamps) and upserts STATUS.updatedGroupIndex(): per groupId: known==null → collect all versions; known==ms → skip; else → diff new versions vsselectArchiveVersionsOfProduct. STATUS row built directly as{'TYPE':…,'RAW_DATA':to_json(group_map),'TIMESTAMP':str(status['timestamp'])}(bypassesgetCrawlerStatusVoto avoid TIMESTAMP=now override). REPO_NAME='MavenGoogle'. Test suite: 162/162 passed (6 new tests). Commitde6cd04.
- 로그레벨 정책 정렬 (2026-06-23, master Python, 푸시
8c04e42..0d1436d): 누락 위험 아닌error→warning강등(ruby 기준). 구.java(폐기중)는 미수정. master Python error 10건 전부 강등, .py error 유지 0(남은 94건은 전부.java). 특히transient … keep retryable(etcMavenPomCrawler/mavenPomCrawler 3곳)이 error로 찍히던 것을 교정 — Python 마이그레이션의 omission-safe 설계와 일치시킴. 저수준 DB(base/cdbvdb)는 log 후 re-raise라 warning. 사용자 작업트리(fix 브랜치)를 안 건드리려고 master worktree에서 수행 후HEAD:master푸시(worktree 정리 완료). W26 worklog 참조. - POM-메타 pending 재수집 escalation (2026-06-23, master Python, merge
b37eea3): 진단 —cn.com.antcloud.api:*등에서 메인 POM 200 인데도transient archive crawl, keep retryable폭주. 원인은 메인 POM 이 아니라 2차 fetch/일시 transient 가crawlVersion에서 raise →createVersionFromArchive가 archive 미마킹 → 매 run 동일 항목 무한 재시도. (참고: 브라우저의 "Encoding error"는 브라우저 XML 뷰어가 엄격한 것; 크롤러는recover=True라 그 POM 정상 파싱 — 파싱은 원인 아님.) 설계(사용자 결정): version+sha1 은 durable core, POM-메타(license/scm/deps)는 나중에 보강.
- crawlVersion 은 더는 transient 를 raise 하지 않음 → 어떤 transient/POM 미제공이든 version+sha1 으로 pending 행 반환. createVersionFromArchive/_processVersion 의 transient try/except 제거(archive 는 version 생성 시점에 처리완료 마킹 → 무한 재처리 제거). - PROCESSED 코드 확장: 90=재수집 대기(version+sha1 확보, 메타 미완), 90~97=재수집 재시도 카운터(매 실패 +1), 98=포기 terminal(version+sha1 유지, 자동 재시도 종료·조회 가능), 99=레거시(6.9M, 미사용 유지), 88=배치 백필 시드. 갓 배포(grace window) 404/200-비POM → pending(90); window 지난 미존재 → 기존대로 minimal(1). - run() 에 pending 재수집 1패스 추가(selectPendingVersions 90~97, _recollectVersion): 성공→1, 실패→+1, 97 다음→98. drain 아닌 run 당 1패스 — 재시도를 run 간격으로 분산해 host 쿨다운 회복 시간 확보(한 run 에서 90→98 소진 방지). - 로그: 200 인데 끝내 파싱 불가(POM/XML 아님)는 WARNING(common.getXmlRequests, lenient 재파싱 실패 시 None+경고). transient→pending 전환도 WARNING. (앞선 be7d818=transient 로그에 원인 host 노출, 8c04e42=인코딩 깨진 200 lenient 재파싱.) - tests 221 green(옛 transient-propagate/keep-retryable 단언을 pending 동작으로 갱신, _recollectVersion 증분/포기 테스트 추가). EtcMavenPomCrawler 는 자체 except 보유라 미적용(해당 repo 대부분 index:False).
- 아카이브 루프 스켈레톤-only 전환 (2026-06-23, master Python,
264a650): "최대한 누락 없게"(사용자) — 위 pending 설계를 한 단계 더.createVersionFromArchive에서 POM(network) 제거 → 아카이브를 보면 g:a:v 로 version 스켈레톤(_pendingRow, PROCESSED=90) insert + product LATEST compare-and-set(_ensureProductLatest) + archive 마킹만 함. 네트워크가 없어 아카이브 루프가 transient/쿨다운으로 막히거나 누락될 수 없음(antcloud 류 폭주 구조적으로 불가). POM-메타 보강은 전부 run 의 pending 패스로 일원화.
- _ensureProductLatest(pk, version, row): selectProductLatest 로 기존 LATEST_VERSION 읽어 — 없으면 insert, 이 version 이 더 최신(versionSort.sortVersions([latest,version])[-1]==version)이면 교체, 아니면 무변경. 전체 버전 SELECT+정렬 없이 O(1)(사용자 요청 방식). insertProduct 는 ON DUP 라 무조건 덮어쓰므로 "최신일 때만"은 코드 판단 필수 → selectProductLatest 신설. sort_order 는 보강 경로 rebuildProductAndSort 가 유지. - run() pending 보강 2분할: fresh(PROCESSED=90)는 같은 run 에서 drain(selectFreshPendingVersions, 90→1 성공 / 91 실패)로 즉시 보강, 재시도(91~97)는 run 당 1패스(selectPendingVersions 범위 90~97→91~97 로 조정, 쿨다운 회복 분산, →98 포기). - 데이터 포맷 불변 보장: 스켈레톤=getVersionVo/_emptyExtracted, product=getProductVo 재사용 → 24컬럼·JSON 바이트 동일, PROCESSED 값(90)만 minimal(1)과 차이(스크립트로 직접 검증: keys 동일, 차이 PROCESSED 1개뿐). tests 221 green.
insertAllProductsFromArchive1052 모호컬럼 수정 (2026-06-23, master Python,e4c19d5): central index ingest 의 product 벌크 프리시드INSERT INTO labradordb.TB_COMP_LIB_PRODUCT (...) SELECT DISTINCT ... FROM gatheringdb.TB_COMP_LIB_MAVEN_ARCHIVE ... ON DUPLICATE KEY UPDATE PRODUCT_KEY = PRODUCT_KEY에서PRODUCT_KEY가 타깃(PRODUCT)·소스(MAVEN_ARCHIVE) 양쪽에 있어 MySQL 1052 ambiguous → 매 ingest 실패(maven index ingest failed for MavenCentral, 0d1436d 이후 WARNING). 제 스켈레톤 변경과 무관한 기존 SQL 버그.ingestRepo순서가 아카이브 insert→증분 anchor(insertCrawlerStatusRow) 기록→(central) 프리시드라서 아카이브·증분 연속성은 무영향(anchor 가 먼저 커밋됨), product 도_ensureProductLatest가 버전 처리 시 생성하므로 실제 누락 0. 수정: no-op 대상을 타깃에만 있는 컬럼으로 —ON DUPLICATE KEY UPDATE REPOSITORY = REPOSITORY(archive 는REPOSITORY_NAME이라 충돌 없음). 동작 동일, 모호성 제거. tests 221 green.
Relevant when:
- Continuing the Python migration (Task 10+).
- Debugging row format mismatch between Python and Java outputs.
- Adding new VO methods.
Do not read full document unless:
- Exact column lists or method signatures are needed.
Linked documents:
ai/worklog/2026/2026-W24.mdai/workspace/repos.md
Open Questions
- Phase 2a COMPLETE (47 tests green incl. live Java-oracle sort check): comparableVersion.py (faithful Maven ComparableVersion port, differential-verified vs maven-artifact-3.6.2), versionSort.py (pure-Python sortVersionUsingMaven, no JPype runtime dep), mavenUrlUtil.py, getXmlRequests/getSha1Requests. Next: Phase 2b (component POM parsing).
- requirements.txt still pins JPype1 — now unused for sort (kept for Phase 3 index-helper jar). Remove if Phase 3 drops JPype.
- Phase 2b COMPLETE (78 tests green):
util/jsonUtil.py(Jackson-exact compact JSON:separators=(',',':'),ensure_ascii=False, empty list→[], always-full dicts) + Phase 1 VO byte-exactness correction (incl. PRODUCT.LICENSE = version LICENSE, not STANDARD_LICENSE);crawler/component.pyComponentGetter(POM lxml ns-strip,${}substitution port of TagNodeUtil, scm/issue/org/parent/name/desc/url/license extractors, properties+parent resolution, dependencies/DM/exclusions with exact per-method key order,extractPomFieldsassembler). Real commons-lang3 POM parses. Code-review fix: unresolved dependency${}version → null (Java parity; getDependencyManagement keeps literal). - Deferred POM edge cases (commented in code, low risk):
_replaceOnceunclosed-${restore vs Java drop;getNameparent-element${}fallback simplified. Revisit only if a real POM diverges. - Phase 2c COMPLETE (108 tests green, code-reviewed): LicenseDao (2 tables), LicenseAnalysis (SPDX match/standard/ids), LicenseeUtil (AI API client + Docker stubs), MavenCrawlingUtil (network stubs), LicenseResolver (wiring → standardLicense/licenseIds/licenseAi). Decisions: license map key order fixed
{name,url,id}(semantic eq); LICENSE_AI = AI API result (changed from Java mvnrepository scrape). - Phase 2c deferred to Docker/2d (documented stubs): git clone + licensee (
executeGitClone/getLicenseFromTag/getLicenseFileTextsFromClone/getTagFromVersion), mvnrepository scrape + apache/aws/spdx URL→name conversion. - Phase 2d COMPLETE (128 tests green, code-reviewed Approved):
crawler/mavenPomCrawler.pyMavenPomCrawler(BaseCrawler)—fetchPom(central+fallback, transient propagates),crawlVersion(POM→extractPomFields→resolveLicense→sha1→getVersionVo row; not-found→minimal row),createVersionFromArchive(SNAPSHOT skip, empty-file sha1→None, releaseDate=lastModified),rebuildProductAndSort(sort + product from highest version, LICENSE=version LICENSE),run()4-loop (products→central archives→priority versions→remaining),_drainno-progress break.app/main.py(schedule 5min) +app/main_resync.py. DECISION: unified on parent-resolution extraction (Phase 2b). Integration test parses real commons-lang3 POM → byte-exact row. - Phase 2d intentional deviations (improve omission-safety, reviewed sound): transient → keep retryable (Java marked processed=1 on any error → omission); no-progress break replaces Java's mark-on-error loop termination (stuck items stay processed=0, retried next run — never dropped).
- Phase 3 COMPLETE (incl. jar):
index-helper/standalone Maven module →maven-index-helper.jar(12.8MB shaded, built with mvnw 3.5.4 on JDK 25; fixes: ComponentsXmlResourceTransformer for plexus components.xml merge, runtime--add-opens java.base/java.lang[.reflect]=ALL-UNNAMEDfor indexer-core 6.0.0 CGLIB). Jar committed atsrc/ai/labradorlabs/util/jars/. Smoke-verified: Plexus init + Wagon + Maven Central index download (full first-run processing ~15-20min — live NDJSON record output NOT verified end-to-end; verify on server). Known: Clojars index fails on Lucene 32KB term limit (indexer-core 6.0.0 limitation, per-repo exception isolated). Python side: IndexHelper subprocess NDJSON, MavenCrawler ingestRepo/startCrawl, main_index. - Phase 4 COMPLETE (164 tests at gate):
getLastModified(HEAD Last-Modified→millis),selectVersionExists/selectArchiveVersionsOfProduct,EtcMavenPomCrawler(single-repo POM fetch viacrawlVersion(record, repo=)param, new-versions-only, central excluded),GoogleMavenCrawler(master/group-index.xml intake, STATUS rawData {gid:lastModified} map, Last-Modified diff; status persisted only when archives inserted — Java parity),app/main_etc.py+main_google.py. - Phase 5 COMPLETE (191 tests final, code-reviewed Approved): real
LicenseeUtil(git clone https no-token — Java's hardcoded ghp_ token NOT carried over; tag match incl. fixing Java'ssplit(".")regex bug; licensee CLI as argv list+cwd — de-shelled after review; license-file texts walk), git-clone license fallback wired intocrawlVersion(POM licenses empty + scm/url → clone → getLicenseFromTag → recompute; clone-no-license → STANDARD_LICENSE[]; null-POM path never clones), realMavenCrawlingUtil(mvnrepository License cell scrape + apache/aws/spdx URL converters, CSS selectors match Java jsoup), resync mode (run(mode='resync')ordered offset pagination over ALL archives, idempotent upsert; ORDER BY added vs Java's unordered resync select),Dockerfile.python(ubuntu22.04 + openjdk-11 + rbenv ruby/licensee /tools/licensee + python; PYTHONPATH=/workspace/src; mkdir /workspace/output; docker build untested — no docker in env) +.dockerignore. - Remaining for production (server-side verification): docker build + live run (index jar full NDJSON output, live DB writes, licensee/git/mvnrepository/dl.google.com/AI-API live calls), license SPDX accuracy vs license DB, abnormal mode (Java had a separate abnormal profile — not ported; decide if needed). First live run should target a staging DB (full index ingest DELETEs+reinserts the repo's archive rows).
Live DB facts (verified 2026-06-16, via read-only MySQL)
Source of truth for the physical tables (the VO column orders above are the crawler insert contracts, which differ from physical column order).
- 테이블 위치:
gatheringdb.TB_COMP_LIB_VERSION_JAVA,
gatheringdb.TB_COMP_LIB_MAVEN_ARCHIVE, labradordb.TB_COMP_LIB_PRODUCT. (PRODUCT 만 labradordb. host 211.115.125.165:43316.)
VERSION_JAVA.PROCESSED=tinyint(nullable) — 컬럼 존재함. (구
collect_maven_missing.py 주석의 "PROCESSED 컬럼 없음" 전제는 outdated.) 현재 값 분포: 1=8.60M(완료), 99=6.93M, 2=4.30M, 0=734K, null=8,959, 10=4. → 88 미사용: 배치 백필 worklist 마커로 안전.
PRODUCT.PROCESSED=tinyint(default null),LATEST_VERSIONvarchar(250) nullable.- Collation gotcha (백필 시 치명적):
- VERSION_JAVA.PRODUCT_KEY = utf8mb4_bin(대소문자 구분). - VERSION_JAVA.VERSION, PRODUCT.PRODUCT_KEY = utf8mb4_0900_ai_ci(구분 안 함). - → 소문자로 정규화한 좌표를 VERSION_JAVA 에 INSERT 하면 대문자 좌표 product 가 bin 기준 별개 키로 쪼개지고, POM 경로(case-sensitive)는 404. 백필 입력은 원본 case 를 유지해야 한다.
- 인덱스→DB 누락 추출은 양쪽
LOWER()비교(extract_maven_missing_versions.py).
원본 case 보존판: extract_maven_missing_versions_origcase.py (인덱스 재파싱 lower\torig → join -v1 로 DB 누락만, orig 출력).
Maven version 백필 (2026-06-16, 진행 중)
~/labrador/tool/maven_missing_products/2026-06-16/maven_missing_versions.txt (인덱스에는 있으나 VERSION_JAVA 에 없는 g:a:v 1,045,271건 / distinct product_key 140,221개) 백필. product-level 누락은 146건뿐 → product 는 대부분 LATEST_VERSION UPDATE.
- Phase A1 (시드): 원본 case g:a:v 를 VERSION_JAVA 에 thin
INSERT IGNORE
(ORIGINAL_INFO={groupId,artifactId,version}, SORT_ORDER=0, FIXED_SORT_ORDER=0, PROCESSED=88, 나머지 NULL). 네트워크 없음, 빠름.
- Phase A2 (product latest): distinct product_key 별
maven-metadata.xml1요청
→ <latest>(없으면 <release>/정렬 max) → PRODUCT upsert, UPDATE 는 LATEST_VERSION 만(기존 PROCESSED 보존). rate limit 3/s → ~13h.
- Phase B (상세 채우기):
WHERE PROCESSED=88배치 SELECT → POM → 상세 컬럼
채움 → 성공 시 PROCESSED=1(정상 파이프라인 완료 코드와 합류), 일시오류는 88 유지(재시도), POM 404 별도 마킹. rate limit 3/s → ~4일. PROCESSED 컬럼 자체가 worklist(파일 done-set 불필요, resumable).
- 쓰기는 프로덕션 DB 직접 (MCP 아님,
.env스크립트).
Java master crawler — 라이선스 버그 수정 + 누락 감사 (2026-06-18)
운영 로그(/data/logs/crawler-lib-java/logs/all.log)에 ~3초마다 두 에러가 반복(myScheduler-2, maven 라이선스 분석 단계). 둘 다 master(운영 Java) 코드 문제. 수정 브랜치 fix/maven-license-omissions(master 기반, 미커밋) 에 반영.
수정한 2건
LicenseDao.getAllLicenses()→ MyBatis PersistenceException:LicenseDao.java가 statement
...LicenseDao.getAllLicenses를 호출하나 LicenseDao.xml에 해당 <select>가 없었음 (getLicenseIdFromName, getSPDXLicenseIdFromName만 존재). 463464a(version table split/ license upgrade)에서 Java 메서드만 추가하고 매퍼 누락. → 유사 라이선스(Levenshtein) 매칭 무력화. Fix: LicenseDao.xml에 getAllLicenses 추가(SELECT ID,NAME_SHORT,NAME,MAPPING,URL FROM TB_LICENSE_V2 WHERE MAPPING is Not NULL — getSPDXLicenseIdFromName 미러).
- licensee
/tools/licensee/license.txt (No such file or directory): `license.detect.dir=
/tools/licensee인데 Dockerfile은 licensee를 **전역 gem**(gem install licensee)으로 깔고 /tools/licensee 디렉토리는 안 만듦. 게다가 코드가 bundle exec bin/licensee(번들 체크아웃 방식)를 기대 → 전역 gem과 불일치. new FileWriter가 부모 디렉토리 없어 실패. **Fix**(2곳): LicenseAnalysisComponent.analyzeLicenseWithLicensee + LicenseeUtil(:187, hardcoded licenseDir="/tools/licensee") — mkdirs()로 디렉토리 보장 + bundle exec bin/licensee → 전역 licensee` 명령으로 교정.
⚠️ master 자체가 컴파일 안 됨 (배포 차단)
./mvnw compile 시 5개 파일 "cannot find symbol": php/crawler/PackagistCrawler.java, php/service/PackagistCrawlerService.java, utils/db/dao/StatusLicenseDao.java, utils/runtime/util/FileUtil.java, HttpUtil.java. version-table-split이 ProductVo/VersionVo 필드(@Getter/@Setter)를 바꿨는데 호출부 미갱신(예: ProductVo.setCreated/setProcessed, VersionVo.getVersion). 운영 jar는 이 breakage 이전 커밋에서 빌드된 것으로 추정. → 위 라이선스 수정을 배포하려면 이 컴파일 에러부터 해결 필요(내 수정 파일은 에러 목록 밖, 무관).
누락(omission) 감사 — 미수정, 권장순
- [HIGH] H1
MavenPomCrawlerService.manageNewAddedVersion(~1175-1182): POM 크롤 중 **어떤
예외든 versionVo.setProcessed(1) 후 insert → 재스캔 쿼리(selectVersionListNotProcessed*, PROCESSED IS NULL OR =0)가 다시 안 잡음 → name/license/deps 빈 채 영구 누락. 메인 maven 경로의 최대 omission. Python 마이그레이션은 이걸 의도적으로 고침(transient는 processed 안 함). 권장 Fix**: 에러 경로에서 processed=0 유지(또는 실패 전용 상태), 성공 후에만 1.
- [MED] M1
VersionDao.updateVersionIdx(:516): 매퍼에updateVersionIdx없음(#1과 동류).
현재 호출자 없어 latent. 다른 DAO↔매퍼는 모두 정상 확인됨.
- [LOW] L1
insertVersionList(VersionDao:279-285): 배치 insert가 try/catch 삼킴 + **split-retry
없음** → 한 행이 제약/폭 위반이면 그 배치 전체 드롭. (archive insert는 1/10 split-retry 있음.)
- [LOW] L2
FileUtil.downloadFileFromUrl(:64):if (backupDir.exists()) mkdirs()— 로직 반대.
commons copyURLToFile가 부모 생성해줘 현재는 무해.
- [보안] L3
LicenseeUtil(:91): GitHub PAT(ghp_...) 하드코딩. 만료 시 clone 실패→라이선스 누락.
로테이트+config화 필요. (Python 이식은 이 토큰 미반영 — getAiLibraryLicense 노트 참고.)
SHA1 null 보정 백필 (2026-06-18, 진행 중)
동기: Java→Python 전환 분석 중 라이브 DB에서 VERSION_JAVA의 SHA1_VALUE 결손이 큼을 확인. java-era PROCESSED=1 859만 중 SHA1 null 4,086,790 (47.5%) / set 4,509,113. 일부는 pom-only (바이너리 없음, 정상)이나 상당수는 Java sha1 수집 누락/H1 에러-마킹 결과. Python은 "이미 존재하는 version 재크롤 skip"(createVersionFromArchive→selectVersionExists)이라 전환해도 기존 null sha1을 안 고침. sha1 중요 → 별도 1회 데이터 보정.
도구: ~/labrador/tool/maven_missing_products/backfill_sha1_from_index.py (+ sha1_chain.sh). 3단계: targets(null-sha1 행의 distinct product_key 덤프, 스트리밍 커서) → index (collect_maven_missing.build_sha1_index로 그 product들의 g:a:v→sha1 추출) → apply (매칭 UPDATE).
원칙 (사용자 요구):
- 포맷 불변:
SHA1_VALUE컬럼만 UPDATE, 다른 컬럼/포맷 무손. - 밴 위험 0: 소스는 로컬 Maven 인덱스 캐시(
maven_index_cache.gz)의'1'필드 —
Maven HTTP 요청 0. 진행 중 PROCESSED=88 fill 과 병행 안전.
- idempotent/안전: UPDATE 에 항상
AND SHA1_VALUE IS NULL가드 → 덮어쓰기 없음, fill 무충돌. - 정확성: 메인 바이너리만(classifier=NA, ext∈{jar,aar,war,ear,zip}), 대소문자 안전
매칭(lower(g:a:v)). pom-only/바이너리 없는 행은 인덱스에 없어 자동으로 null 유지(정상).
- empty-file sha1(
da39a3ee…)은 None 처리(Java 동작).
규모/결과 (2026-06-18 완료): distinct null-sha1 product_key ≈ 470k → 인덱스 sha1 수집 8,738,661 (sha1_backfill.tsv). null-sha1 행 스캔 6,999,679 중 매칭 2,622,179 → SHA1_VALUE 채움. 최종 java/MAVEN sha1 커버리지 set 17,240,577 / null 4,377,498 (총 21,618,075, ~79.7% set). 잔여 null 은 pom-only/바이너리 없음(정상) + 인덱스에 sha1 없는 것 + 아직 PROCESSED=88 시드(POM fill 이 채울 것). 신규 insert 0(ON DUP, 기존 PK 갱신만), 총행수 증가분은 fill 시드로 설명.
- apply 가속 교훈: 처음
executemany(UPDATE)는 드라이버가 행별 왕복 → ~60/s(ETA ~12h). PK 가 이미
존재하므로 다중행 INSERT … ON DUPLICATE KEY UPDATE SHA1_VALUE(배치당 1왕복)로 바꿔 ~2700/s (전체 ~19분)로 단축. 또 스트리밍 SELECT 중 UPDATE 끼우면 2013 Lost connection(net_write_timeout) → read(매칭→파일) ↔ write(파일→UPDATE) 분리로 해결.
참고 — Python 런타임 sha1 경로 비판(mavenPomCrawler.crawlVersion):
- 인덱스 sha1 우선(archive.SHA1_VALUE) → 신규분은 대체로 OK.
- HTTP 폴백이 jar→aar 만(
:97-101) → 인덱스 sha1 없는 war/ear/zip 은 null. (Java는 5종 시도.) - POM 못 찾으면(
_emptyExtracted) sha1 HTTP 폴백 없음 / 이미 존재 version 재크롤 skip. - → 전환 전 위 폴백 확장(+war/ear/zip) 권장. 본 백필은 그 갭까지 인덱스로 메움.
RELEASE_DATE 백필 (2026-06-18 완료)
sha1 백필과 동일 패턴(backfill_releasedate_from_index.py, targets→index→apply, HTTP 0, 포맷 불변). 소스 = Maven 인덱스 i 필드 lastModified(ms, classifier=NA, g:a:v당 최소) → YYYY-MM-DD HH:MM:SS(UTC). 크롤러의 archive 경로가 쓰는 값(archive.LAST_MODIFIED=같은 인덱스 lastModified)과 동일 소스 → 일관.
- targets: null-RD 2,188,146 / distinct product_key 216,274
- rd-index: 13,165,402 g:a:v→ms 수집
- apply: 매칭 1,981,048 (~85%) UPDATE → RELEASE_DATE null 2,188,362 → 207,098 (set 21,410,977, ~99%)
- 잔여 null = 인덱스에 없는 좌표(아직 PROCESSED=88 시드 등) + non-index repo. 다중행 INSERT…ON DUP,
read↔write 분리(2013 회피). 신규 insert 0.
Python 컷오버 수정 (2026-06-18, master 푸시됨)
Java 정지 → Python(master) 전환 중 발견·수정한 버그/구성 (origin/master 98440ec 시점):
- status 앵커 이전(증분 연속성): Java 가 쓴 repo별 앵커(MavenCentral/Jenkins/Clojars/Cloudera/Eclipse/
AtlassianExternal + GoogleMavenCrawler 14KB 그룹맵)는 labradordb.TB_CRAWLER_STATUS, Python 은 gatheringdb 를 읽음 → copy_status_anchors.py 로 7개 복사(컷오버 시 1회). 그래야 Python 이 Java 마지막 인덱스 timestamp 에서 증분 재개(안 하면 전 repo full 재인입).
- Dockerfile.python → 기본
Dockerfile승격(구 Java 용은Dockerfile.java), composedockerfile:갱신.
Python 이미지 빌드에 maven jar 불필요. 인덱서는 jar(maven-index-helper.jar)가 아니라 순수 파이썬 MavenIndexFetcher(HTTP+gzip, JVM 불필요)로 동작 — jar/openjdk-11 은 orphan(레거시).
- 로그
logs정렬: configLogPath=./logs(=Java logback./logs/all.log모니터링 경로) 인데
Dockerfile/compose 는 /workspace/log(단수)였음 → 핸들러 init 크래시/모니터링 누락. Dockerfile mkdir /workspace/logs + compose ./logs:/workspace/logs. logger when='D'→'midnight'(자정 분할).
- 인덱스 없는 repo skip: Cloudera/Eclipse/AtlassianExternal/MavenGoogle 은
.index/...properties404
(리다이렉트 따라가도 404). MAVEN_REPOS 에 "index": False + startCrawl skip. POM 폴백/EtcPom 은 유지(기존 archive 접근 손실 없음).
- MavenCentral 인덱스 404:
_index_base가indexUrl우선인데 MavenCentral 엔트리에 없어 POM용
baseUrl(search.maven.org)로 잘못된 인덱스 URL. "indexUrl": "https://repo1.maven.org/maven2" 추가.
- 빈 배치 insert 버그(
bind parameter 'LANGUAGE'):setInsertIntoDB/setInsertIgnoreDB의
while limit==LIMIT 루프가 행수=LIMIT 배수일 때 빈 배치로 한 번 더 돌아 executeSql(stmt, []) → 에러. while datas: 로 교정.
- worklist select 보존:
selectNotProcessedVersions/selectPriorityVersions에
RELEASE_DATE/SORT_ORDER/FIXED_SORT_ORDER/SHA1_VALUE 추가 → 재크롤 시 백필값 유실 방지.
- 증분 잔여 보정(
mavenPomCrawler): POM 404 가 grace window(7d) 내 신규면 retryable(조기 minimal
방지); sha1 HTTP 폴백 jar/aar→jar/aar/war/ear/zip. 보정잡 app/main_repair.py(불완전 행 재크롤, --recent-days/--limit, format 불변) — R1(에러로 processed=1+name null, ~60만) 등 점진 보정용.
MavenCentral 인입 정지 원인 + archive VERSION varchar(100) 하드닝 (2026-07-14)
증상: TB_CRAWLER_STATUS.MavenCentral 앵커가 2026-06-25 18:30 에 정지(같은 인덱서가 쓰는 Jenkins/Clojars/MavenIndexFullSweep 는 07-12~13 로 최신). 06-25 이후 Central 신규 g:a:v 가 MAVEN_ARCHIVE 에 안 들어와 다운스트림 전부 누락(관측: 75건 리포트 중 65 실누락).
근본원인 2중:
- 인덱서 미가동(labCrawl1
crawler-lib-maven-indexer):[indexer]로그가
07-06~07-12 전무. 컨테이너 StartedAt 07-13T01:11(RestartCount=0) → 약 1주+ 정지.
- 1406 DataError:
MAVEN_ARCHIVE.VERSION= varchar(100) (VERSION_JAVA 는 250).
Central 인덱스에 100자 초과 버전 1행이 있으면 1000행 배치 INSERT 가 통째로 (1406, "Data too long for column 'VERSION'") 로 실패 → startCrawl 의 repo별 예외격리가 WARNING(maven index ingest failed for MavenCentral)만 찍고 anchor 미전진 → 매 사이클 같은 지점 재정지. startCrawl(full=True)(full-sweep)도 같은 격리라 Central 만 실패해도 MavenIndexFullSweep 앵커는 갱신됨(오해 소지).
archive 컬럼 폭(2026-07-14 확인, gatheringdb.TB_COMP_LIB_MAVEN_ARCHIVE): LANGUAGE/REPOSITORY_NAME varchar(20), PRODUCT_KEY varchar(400), VERSION varchar(100), NAME varchar(200), DESCRIPTION longtext, SHA1_VALUE varchar(100). (VERSION_JAVA.VERSION 은 varchar(250) — 두 테이블 폭이 다르다.)
수정(스키마 불변, 코드만 — working tree, 배포 필요):
getMavenArchiveVo: NAME_clip_chars(...,200)클램프. VERSION 은 클램프 안 함
(좌표 절단 시 POM 경로 깨짐).
mavenCrawler.ingestRepo:len(VERSION)>100행을 아카이브에서 제외 →
_ingestOversizedVersions 가 version_java(250) 스켈레톤으로 우회 적재(>250 하드 스킵). 앵커를 우회 적재보다 먼저 기록(우회 실패가 커서 전진을 막지 않게; 실패분은 다음 run overlap 60일이 회수).
mavenPomCrawler.insertVersionSkeletonDirect(record): 아카이브 우회 version_java
스켈레톤(PROCESSED=90) 직접 적재(idempotent, archive 마킹 없음). 오버사이즈+백필 공용.
- 테스트 254 passed(신규 6, stale
test_start_crawl_default_no_clean을 index:False
repo skip 반영으로 교정).
백필(2026-07-14 완료, host 165): 65 실누락 g:a 를 원본 case 로 repo1 maven-metadata → 전 버전 → insertVersionSkeletonDirect. new=207 버전 / product 생성, worker 가 POM 보강. 도구 scratchpad/backfill_missing_java.py(1회성).
남은 조치: (1) 코드 커밋 + labCrawl1 재배포(배포 전엔 앵커 06-25 고정·상시 인입 미재개), (2) 인덱서 liveness + 앵커 staleness 알람, (3) 1406 유발 100자 초과 버전 GAV 특정(선택).
Phase 2 Decisions (2026-06-09, user-confirmed)
- Version sort = pure Python reproducing Java
VersionSortUtil.sortVersionUsingMaven
(Apache ComparableVersion + preprocessing _→-, +→-, pr→rc; insertion-sort ascending, incl. the equal-version drop quirk). No JPype / sort-gen jar at runtime. The existing jar/Java is used only as a test oracle (differential check, skipped when no JVM).
- License AI API =
http://211.115.125.171/api/v1/licenses(POST), body
{"texts":[{"text": <str>}, ...]}, 30s timeout, no auth. Response {"NAMES":[{NAME,CONFIDENCE}], "AI_LICENSE":[ids]}; extract AI_LICENSE/ai_license (case-insensitive) → json.dumps(list) into LICENSE_AI (None if empty). Input priority: cloned license-file text → GitHub API license → POM <licenses> name. Independent of the TB_LICENSE SPDX match that fills STANDARD_LICENSE/LICENSE_IDS. Same client as crawler-lib-dotnet/golang (licenseeUtil.getAiLibraryLicense).
- DEPENDENCY table NOT used. Dependencies live only in the VERSION row
DEPENDENCIES
(and DEPENDENCY_MANAGEMENT) JSON columns. baseCralwer.insertDependency / VERSION_DEPENDENCY_TABLE stay dormant (not called).
Details
Branch
python-migration-maven (working branch for all Python migration work).
Run tests
cd ~/labrador/crawler/crawler-lib-java
. .venv/bin/activate
python -m pytest tests/ -vColumn order: VERSION_JAVA (TB_COMP_LIB_VERSION_JAVA)
LANGUAGE, REPOSITORY, PRODUCT_KEY, VERSION, NAME, DESCRIPTION, LICENSE, LICENSE_AI, STANDARD_LICENSE, LICENSE_IDS, URL, SCM, ISSUE_MANAGEMENT, ORGANIZATION, PARENT, ORIGINAL_INFO, SHA1_VALUE, CREATED, RELEASE_DATE, PROCESSED, SORT_ORDER, FIXED_SORT_ORDER, DEPENDENCIES, DEPENDENCY_MANAGEMENT
Column order: PRODUCT (TB_COMP_LIB_PRODUCT)
LANGUAGE, REPOSITORY, PRODUCT_KEY, LATEST_VERSION, NAME, DESCRIPTION, LICENSE, CREATED, PROCESSED
Column order: MAVEN_ARCHIVE (TB_COMP_LIB_MAVEN_ARCHIVE)
LANGUAGE, REPOSITORY_NAME, PRODUCT_KEY, VERSION, NAME, DESCRIPTION, SHA1_VALUE, ORIGINAL_INFO, LAST_MODIFIED
Column order: DEPENDENCY (TB_COMP_LIB_VERSION_JAVA_DEPENDENCY)
ID, LANGUAGE, REPOSITORY, PRODUCT_KEY, VERSION, ORIGIN_PRODUCT_KEY, ORIGIN_VERSION_RANGE, RECORD_CREATED
Column order: STATUS (TB_CRAWLER_STATUS)
TYPE, RAW_DATA, TIMESTAMP