From 4ea99b4881e6a3f5e653dc0cdda9188a46b8a5cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EA=B8=B0=EB=AF=BC?= Date: Sat, 8 Aug 2026 15:03:06 +0900 Subject: [PATCH 1/6] =?UTF-8?q?docs:=20#124=20=EA=B3=B5=EC=8B=9D=20OpenSQL?= =?UTF-8?q?=20=EA=B2=80=EC=A6=9D=20=EC=84=A4=EA=B3=84=20=EB=AC=B8=EC=84=9C?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...-#124-opensql-compatibility-performance.md | 222 ++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 docs/design/gimin-#124-opensql-compatibility-performance.md diff --git a/docs/design/gimin-#124-opensql-compatibility-performance.md b/docs/design/gimin-#124-opensql-compatibility-performance.md new file mode 100644 index 0000000..9a79ce9 --- /dev/null +++ b/docs/design/gimin-#124-opensql-compatibility-performance.md @@ -0,0 +1,222 @@ +# Issue #124 공식 OpenSQL 17.8 호환성 및 성능 검증 상세 설계 + +## 1. 배경과 목적 + +DocGrid 로컬 개발 환경은 PostgreSQL 17.8과 pgvector 0.8.1에서 실제 문서 업로드, BGE-M3 임베딩, +`vector(1024)` 저장과 검색까지 검증됐다. 대회 지정 공식 환경은 Rocky Linux 9.7 x86-64 Single 구성의 +OpenSQL 17.8이므로 PostgreSQL 호환성만으로 최종 판정할 수 없다. + +이번 작업은 제품 기능을 변경하지 않고 공식 DB에서 다음 계약을 반복 실행 가능한 Test와 Runbook으로 +고정한다. + +```text +공식 Host Preflight +→ Flyway 전체 Migration·Hibernate Validation +→ pgvector·vector(1024)·HNSW·Cosine Operator +→ SKIP LOCKED·Claim·Lease·Retry +→ 실제 문서 인덱싱 E2E +→ Claim·Vector 검색 성능 측정 +→ 환경 Fingerprint와 결과 기록 +``` + +### 1.1 성공 기준 + +- Rocky Linux 9.7 x86-64, OpenSQL 17.8, pgvector 0.8.1을 자동 확인한다. +- 전용 Test Schema에 Flyway 전체 Migration과 Hibernate Validation이 성공한다. +- 제품 `embeddings.vector`가 `vector(1024)`이고 Cosine HNSW Index가 존재한다. +- Vector 저장, `vector_dims`, `<=>`, HNSW 실행 계획이 실제 DB에서 동작한다. +- `FOR UPDATE SKIP LOCKED`, 다중 Worker Claim, Lease 갱신·복구와 Retry가 통과한다. +- MinIO·BGE-M3 전체 문서 인덱싱 E2E가 공식 DB 연결에서도 통과한다. +- Claim과 Vector 검색의 처리량·p50·p95·p99와 Lock·Pool 대기를 기록한다. +- Secret, 설치 번들, 라이선스와 개인 절대 경로가 Git 또는 Test Log에 포함되지 않는다. + +## 2. 범위 + +### 2.1 포함 + +- 공식 Host OS·Architecture·설치 모드 Preflight Script +- 공식 접속 환경 변수의 Fail-fast Validation +- 공식 DB 전용 Gradle Test Task와 집계 Task +- Flyway·Schema·Extension·Vector·HNSW Compatibility Test +- HNSW 실행 계획과 Vector 검색 지연 측정 Probe +- 기존 Claim 동시성·성능 Test의 공식 DB 재사용 +- 기존 로컬 전체 관통 E2E의 공식 DB 재사용 +- 실행 Runbook, 결과 Template, 로컬 기준선과 공식 실측 결과 + +### 2.2 제외 + +- OpenSQL 설치 자동화와 설치 파일 재배포 +- 라이선스 발급·변경·복사 자동화 +- HA, Patroni, etcd와 OpenProxy 구성 +- 운영 Database 또는 운영 Schema 대상 Test +- 제품 Entity·API·검색 순위 정책 변경 +- 절대 성능값을 모든 장비에 적용하는 SLO 확정 + +## 3. 안전 경계 + +### 3.1 Git 비반입 + +다음은 Source, 문서, Test Log와 PR에 절대 기록하지 않는다. + +- OpenSQL 설치 압축·압축 해제 파일 +- 라이선스 XML과 라이선스 내용 +- 다운로드 URL·비밀번호·메일 원문 +- DB Password, IP, SSH Key와 내부 Host 식별자 + +`.gitignore`의 공급사 번들 보호 패턴을 유지하고 새 파일은 추가하지 않는다. + +### 3.2 접속 Fail-fast + +공식 Task는 다음 환경 변수가 모두 존재할 때만 Test JVM을 시작한다. + +```text +OPENSQL_DB_HOST +OPENSQL_DB_PORT +OPENSQL_DB_NAME +OPENSQL_DB_USER +OPENSQL_DB_PASSWORD +OPENSQL_DB_SSLMODE +``` + +Gradle은 이를 기존 Test Profile의 `DB_*`로 전달한다. 명령행 Argument나 결과 문서에는 값을 출력하지 +않는다. Password가 없거나 빈 값이면 Connection 시도 전에 실패한다. + +### 3.3 Schema 격리 + +- 각 Test Class가 `docgrid_opensql_*_test` Schema를 사용한다. +- `public`, Application 운영 Schema 또는 기존 개발 Schema를 변경하지 않는다. +- Flyway가 전용 Schema만 생성·Migration한다. +- 종료 시 해당 Test Schema만 `CASCADE` 삭제한다. +- `KEEP_*_SCHEMA=true`를 명시한 수동 진단 실행만 Schema를 보존한다. + +## 4. 실행 구조 + +### 4.1 Host Preflight + +Rocky Host에서 Script를 실행해 다음을 확인한다. + +1. `/etc/os-release`의 ID가 `rocky`, VERSION_ID가 `9.7`이다. +2. `uname -m`이 `x86_64`다. +3. OpenSQL은 Single 구성으로 동작한다. +4. DB 연결 뒤 `server_version`이 `17.8`로 시작한다. +5. `vector` Extension Version이 `0.8.1`이다. + +OS·Architecture가 다르면 로컬 기준선은 실행할 수 있어도 공식 검증은 즉시 실패한다. + +### 4.2 Gradle Task + +| Task | 역할 | +|---|---| +| `openSqlCompatibilityTest` | Migration, Schema, Vector, HNSW, SKIP LOCKED, 검색 지연 | +| `openSqlClaimConcurrencyTest` | 기존 다중 Worker Claim·Lease 정합성 | +| `openSqlDocumentE2eTest` | 기존 MinIO·BGE-M3 전체 문서 인덱싱 E2E | +| `openSqlClaimPerformanceTest` | 기존 Claim 처리량·경합 Benchmark | +| `openSqlVerification` | 위 네 Task 전체 집계 | + +모든 Task는 공식 환경 변수를 같은 방식으로 검증·전달하며 일반 `test`와 CI에서는 실행되지 않는다. + +## 5. Compatibility Test + +### 5.1 Environment Fingerprint + +- `current_database`, `current_schema` +- `server_version`, `server_version_num` +- pgvector Extension Version +- Flyway 성공 Migration과 최신 Version +- Product Vector Column Type +- Product HNSW Index Definition + +Fingerprint는 Password·Host·IP·Username 없이 구조·Version만 Log로 남긴다. + +### 5.2 Vector·HNSW Probe + +1. 전용 Schema에 `vector(1024)` Probe Table을 만든다. +2. 고정 Seed로 정규화한 2,000개 Vector를 Batch Insert한다. +3. Cosine HNSW Index를 만들고 `ANALYZE`한다. +4. 동일 Seed Query Vector로 `<=>` Top-K를 30회 실행한다. +5. Warm-up을 제외하고 p50·p95·p99와 최대 지연을 계산한다. +6. `EXPLAIN`에서 HNSW Index Scan이 선택되는지 확인한다. +7. 거리 값이 유한하고 오름차순인지 확인한다. + +절대 지연값은 Hardware 의존성이 있으므로 자동 실패 기준으로 삼지 않는다. Index 미사용, 오류, 비유한 +거리와 결과 순서 위반만 실패시킨다. + +### 5.3 SKIP LOCKED Probe + +두 독립 Connection을 사용한다. + +1. 첫 Connection이 Queue Row를 `FOR UPDATE`로 잠근다. +2. 둘째 Connection이 `FOR UPDATE SKIP LOCKED`로 같은 Queue를 조회한다. +3. 둘째 조회가 대기하지 않고 빈 결과를 반환하는지 확인한다. +4. 첫 Transaction Rollback 뒤 Row가 다시 조회되는지 확인한다. + +## 6. 기존 검증 재사용 + +### 6.1 Claim·Lease + +기존 PostgreSQL Test를 공식 DB 주소로 실행한다. + +- ACTIVE Worker 100개 단일 Job 경쟁 +- Worker 20개, Job 1,000개 Queue 소진 +- Claim Token·소유권·LOCKED Event 단일성 +- Lease 갱신 중 복구 차단과 갱신 중단 뒤 단일 복구 + +### 6.2 전체 문서 인덱싱 + +기존 `local-e2e` Test의 DB 연결만 공식 OpenSQL로 전환한다. + +```text +PDF·DOCX HTTP 업로드 +→ MinIO +→ Worker 자동 실행 +→ BGE-M3 Batch +→ 공식 OpenSQL vector(1024) +→ INDEXED·current_version +→ Query Embedding·Cosine 검색 +``` + +Embedding Provider 장애의 부분 저장 방지와 지연 재시도도 같은 공식 DB에서 재검증한다. + +### 6.3 Claim Benchmark + +기존 Benchmark의 기본 Profile을 그대로 사용할 수 있고, 사전 Smoke는 축소 Parameter로 실행한다. + +```text +Warm-up Job = 100 +측정 Job = 500 +반복 = 2 +Worker = 1, 10, 20 +``` + +최종 공식 결과는 기본 Profile 또는 대회 제출에 합의한 고정 Profile로 다시 측정한다. + +## 7. 결과 문서 + +`docs/test-results/gimin-#124-opensql-compatibility-performance.md`에 다음을 기록한다. + +- 공개 가능한 환경 Version과 Architecture +- 실행한 Commit SHA와 명령 Template +- Test별 건수·시간·성공 여부 +- Vector HNSW 실행 계획의 민감정보 제거 요약 +- Vector 검색 p50·p95·p99·최대 지연 +- Claim TPS·p50·p95·p99·Hikari·PG Lock 대기 +- 로컬 PostgreSQL 기준선과 공식 OpenSQL 차이 +- 미실행 또는 실패 항목과 재현 절차 + +공식 x86-64 실행 전 로컬 PostgreSQL 결과는 `LOCAL BASELINE`으로만 표시하고 공식 합격으로 쓰지 않는다. + +## 8. 커밋 분할 + +1. `docs: #124 공식 OpenSQL 검증 설계 문서 추가` +2. `build: #124 공식 OpenSQL 검증 실행 경계 추가` +3. `test: #124 Vector·HNSW·SKIP LOCKED 호환성 검증 추가` +4. `docs: #124 공식 OpenSQL 검증 Runbook 추가` +5. `docs: #124 공식 OpenSQL 호환성·성능 결과 기록` + +## 9. 완료 조건 + +- 일반 `./gradlew test`는 공식 환경 없이 계속 통과한다. +- 로컬 PostgreSQL 17.8 기준선에서 새 Compatibility Test가 통과한다. +- 공식 Rocky Linux 9.7 x86-64 OpenSQL 17.8에서 `openSqlVerification`이 통과한다. +- 공식 Claim·Vector 성능 실측과 실행 계획이 결과 문서에 기록된다. +- 공식 실행이 불가능하면 PR은 도구·Runbook까지 검증하되 공식 완료로 잘못 표시하지 않는다. From 9b474533ee733fc216ed8b7a87d8184b2f0b1329 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EA=B8=B0=EB=AF=BC?= Date: Sat, 8 Aug 2026 15:04:58 +0900 Subject: [PATCH 2/6] =?UTF-8?q?build:=20#124=20=EA=B3=B5=EC=8B=9D=20OpenSQ?= =?UTF-8?q?L=20=EA=B2=80=EC=A6=9D=20=EC=8B=A4=ED=96=89=20=EA=B2=BD?= =?UTF-8?q?=EA=B3=84=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 95 ++++++++++++++++++++++++++++++++++ scripts/opensql/verify-host.sh | 64 +++++++++++++++++++++++ 2 files changed, 159 insertions(+) create mode 100755 scripts/opensql/verify-host.sh diff --git a/build.gradle b/build.gradle index 7e4494e..88410f4 100644 --- a/build.gradle +++ b/build.gradle @@ -115,3 +115,98 @@ tasks.register('claimPerformanceTest', Test) { } outputs.upToDateWhen { false } } + +def configureOpenSqlDatabase = { Test task -> + task.maxParallelForks = 1 + task.outputs.upToDateWhen { false } + task.doFirst { + def environmentMappings = [ + OPENSQL_DB_HOST: 'DB_HOST', + OPENSQL_DB_PORT: 'DB_PORT', + OPENSQL_DB_NAME: 'DB_NAME', + OPENSQL_DB_USER: 'DB_USER', + OPENSQL_DB_PASSWORD: 'DB_PASSWORD', + OPENSQL_DB_SSLMODE: 'DB_SSLMODE' + ] + environmentMappings.each { sourceName, targetName -> + def value = System.getenv(sourceName) + if (value == null || value.trim().isEmpty()) { + throw new GradleException("필수 공식 OpenSQL 환경 변수가 비어 있습니다: ${sourceName}") + } + task.environment(targetName, value) + } + // 공식 접속 정보와 분리된 Test 전용 JWT만 사용하고 값은 Task 출력에 기록하지 않는다. + task.environment( + 'JWT_SECRET', + System.getenv('OPENSQL_JWT_SECRET') ?: 'docgrid-opensql-verification-test-secret-key-2026' + ) + } +} + +def openSqlCompatibilityTest = tasks.register('openSqlCompatibilityTest', Test) { + group = 'verification' + description = '공식 OpenSQL의 Migration, Vector, HNSW와 SKIP LOCKED 호환성을 검증합니다.' + testClassesDirs = sourceSets.test.output.classesDirs + classpath = sourceSets.test.runtimeClasspath + useJUnitPlatform { + includeTags 'opensql-verification' + } + configureOpenSqlDatabase(delegate as Test) +} + +def openSqlClaimConcurrencyTest = tasks.register('openSqlClaimConcurrencyTest', Test) { + group = 'verification' + description = '공식 OpenSQL에서 다중 Worker Claim과 Lease 정합성을 검증합니다.' + testClassesDirs = sourceSets.test.output.classesDirs + classpath = sourceSets.test.runtimeClasspath + useJUnitPlatform { + includeTags 'claim-concurrency' + } + configureOpenSqlDatabase(delegate as Test) +} + +def openSqlDocumentE2eTest = tasks.register('openSqlDocumentE2eTest', Test) { + group = 'verification' + description = '공식 OpenSQL DB와 실제 MinIO·BGE-M3를 연결한 문서 인덱싱 E2E를 실행합니다.' + testClassesDirs = sourceSets.test.output.classesDirs + classpath = sourceSets.test.runtimeClasspath + useJUnitPlatform { + includeTags 'local-e2e' + } + configureOpenSqlDatabase(delegate as Test) +} + +def openSqlClaimPerformanceTest = tasks.register('openSqlClaimPerformanceTest', Test) { + group = 'verification' + description = '공식 OpenSQL에서 Embedding Job Claim 처리량과 경합을 측정합니다.' + testClassesDirs = sourceSets.test.output.classesDirs + classpath = sourceSets.test.runtimeClasspath + useJUnitPlatform { + includeTags 'claim-performance' + } + systemProperties System.properties.findAll { key, value -> + key.toString().startsWith('claim.performance.') + } + configureOpenSqlDatabase(delegate as Test) +} + +openSqlClaimConcurrencyTest.configure { + mustRunAfter(openSqlCompatibilityTest) +} +openSqlDocumentE2eTest.configure { + mustRunAfter(openSqlClaimConcurrencyTest) +} +openSqlClaimPerformanceTest.configure { + mustRunAfter(openSqlDocumentE2eTest) +} + +tasks.register('openSqlVerification') { + group = 'verification' + description = '공식 OpenSQL 호환성, Claim 정합성, 문서 E2E와 성능 측정을 순서대로 실행합니다.' + dependsOn( + openSqlCompatibilityTest, + openSqlClaimConcurrencyTest, + openSqlDocumentE2eTest, + openSqlClaimPerformanceTest + ) +} diff --git a/scripts/opensql/verify-host.sh b/scripts/opensql/verify-host.sh new file mode 100755 index 0000000..5aa14a1 --- /dev/null +++ b/scripts/opensql/verify-host.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# 공식 판정은 공급사 지원 Matrix와 Single 설치 계약을 모두 만족한 Host에서만 진행한다. +required_variables=( + OPENSQL_DB_HOST + OPENSQL_DB_PORT + OPENSQL_DB_NAME + OPENSQL_DB_USER + OPENSQL_DB_PASSWORD + OPENSQL_DB_SSLMODE + OPENSQL_INSTALL_MODE +) + +for variable_name in "${required_variables[@]}"; do + if [[ -z "${!variable_name:-}" ]]; then + echo "필수 공식 OpenSQL 환경 변수가 비어 있습니다: ${variable_name}" >&2 + exit 1 + fi +done + +# 1. 공식 지원 OS와 Architecture가 아니면 PostgreSQL 호환 DB여도 공식 결과로 인정하지 않는다. +source /etc/os-release +if [[ "${ID:-}" != "rocky" || "${VERSION_ID:-}" != "9.7" ]]; then + echo "지원되지 않는 OS입니다. Rocky Linux 9.7이 필요합니다." >&2 + exit 1 +fi +if [[ "$(uname -m)" != "x86_64" ]]; then + echo "지원되지 않는 Architecture입니다. x86_64가 필요합니다." >&2 + exit 1 +fi + +# 2. 대회 안내의 Single 설치만 허용하고 HA 관련 구성을 공식 검증 범위에서 제외한다. +if [[ "${OPENSQL_INSTALL_MODE}" != "single" ]]; then + echo "공식 검증은 OpenSQL Single 설치에서만 실행할 수 있습니다." >&2 + exit 1 +fi + +# 3. Password는 표준 libpq 환경으로만 전달하고 Command·성공 출력에는 포함하지 않는다. +export PGHOST="${OPENSQL_DB_HOST}" +export PGPORT="${OPENSQL_DB_PORT}" +export PGDATABASE="${OPENSQL_DB_NAME}" +export PGUSER="${OPENSQL_DB_USER}" +export PGPASSWORD="${OPENSQL_DB_PASSWORD}" +export PGSSLMODE="${OPENSQL_DB_SSLMODE}" + +server_version="$(psql --tuples-only --no-align --command "SHOW server_version")" +vector_version="$(psql --tuples-only --no-align --command \ + "SELECT extversion FROM pg_extension WHERE extname = 'vector'")" + +# 4. 지원 기준 Version을 정확히 확인해 로컬 PostgreSQL 결과와 공식 OpenSQL 결과를 구분한다. +if [[ "${server_version}" != 17.8* ]]; then + echo "지원되지 않는 OpenSQL PostgreSQL Version입니다: ${server_version}" >&2 + exit 1 +fi +if [[ "${vector_version}" != "0.8.1" ]]; then + echo "지원되지 않는 pgvector Version입니다: ${vector_version}" >&2 + exit 1 +fi + +echo "OpenSQL 공식 Host preflight 통과" +echo "OS=Rocky Linux ${VERSION_ID}, architecture=$(uname -m), mode=${OPENSQL_INSTALL_MODE}" +echo "server_version=${server_version}, pgvector=${vector_version}" From 42586c28d1c31efcc68cee3a41d1b28fef002277 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EA=B8=B0=EB=AF=BC?= Date: Sat, 8 Aug 2026 15:08:04 +0900 Subject: [PATCH 3/6] =?UTF-8?q?test:=20#124=20Vector=C2=B7HNSW=C2=B7SKIP?= =?UTF-8?q?=20LOCKED=20=ED=98=B8=ED=99=98=EC=84=B1=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OpenSqlCompatibilityIntegrationTest.java | 383 ++++++++++++++++++ 1 file changed, 383 insertions(+) create mode 100644 src/test/java/com/opensource/docgrid/opensql/OpenSqlCompatibilityIntegrationTest.java diff --git a/src/test/java/com/opensource/docgrid/opensql/OpenSqlCompatibilityIntegrationTest.java b/src/test/java/com/opensource/docgrid/opensql/OpenSqlCompatibilityIntegrationTest.java new file mode 100644 index 0000000..3a92bea --- /dev/null +++ b/src/test/java/com/opensource/docgrid/opensql/OpenSqlCompatibilityIntegrationTest.java @@ -0,0 +1,383 @@ +package com.opensource.docgrid.opensql; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.Timeout; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; + +import lombok.extern.slf4j.Slf4j; + +/** + * 공식 OpenSQL 17.8의 Flyway·pgvector·HNSW와 SKIP LOCKED PostgreSQL 호환 경계를 검증한다. + * + *

제품 Table은 Metadata만 확인하고 측정 Data는 전용 Test Schema의 Probe Table에 격리한다. Host, + * Username과 Password는 수집하거나 출력하지 않으며 공개 가능한 Version·지연 집계만 Log로 남긴다. + */ +@Slf4j +@Tag("integration") +@Tag("opensql-verification") +@ActiveProfiles("test") +@SpringBootTest +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +@DisplayName("공식 OpenSQL 17.8 호환성·Vector 성능 통합 테스트") +class OpenSqlCompatibilityIntegrationTest { + + private static final String TEST_SCHEMA = "docgrid_opensql_compatibility_test"; + private static final String VECTOR_PROBE_TABLE = "opensql_vector_probe"; + private static final String QUEUE_PROBE_TABLE = "opensql_queue_probe"; + private static final String EXPECTED_SERVER_VERSION_PREFIX = "17.8"; + private static final String EXPECTED_PGVECTOR_VERSION = "0.8.1"; + private static final String EXPECTED_VECTOR_TYPE = "vector(1024)"; + private static final int VECTOR_DIMENSION = 1024; + private static final int VECTOR_ROW_COUNT = 2_000; + private static final int INSERT_BATCH_SIZE = 100; + private static final int WARM_UP_QUERY_COUNT = 5; + private static final int MEASURED_QUERY_COUNT = 30; + private static final int TOP_K = 10; + + @Autowired private JdbcTemplate jdbcTemplate; + @Autowired private DataSource dataSource; + + @DynamicPropertySource + static void configureEnvironment(DynamicPropertyRegistry registry) { + registry.add("TEST_DB_SCHEMA", () -> TEST_SCHEMA); + registry.add("jwt.secret", () -> "docgrid-opensql-compatibility-test-secret-key-2026"); + registry.add( + "spring.datasource.hikari.data-source-properties.ApplicationName", + () -> "docgrid-opensql-compatibility-test" + ); + } + + @AfterAll + void dropIsolatedSchema() { + if (Boolean.parseBoolean(System.getenv("KEEP_OPENSQL_COMPATIBILITY_SCHEMA"))) { + log.warn("수동 진단을 위해 공식 OpenSQL 호환성 Test Schema를 유지합니다: {}", TEST_SCHEMA); + return; + } + jdbcTemplate.execute("DROP SCHEMA IF EXISTS " + TEST_SCHEMA + " CASCADE"); + } + + @Test + @Order(1) + @DisplayName("Flyway 전체 적용 뒤 OpenSQL 17.8·pgvector 0.8.1·제품 HNSW 구조가 일치한다") + void migration_createsExpectedOpenSqlVectorSchema() { + String currentSchema = jdbcTemplate.queryForObject("SELECT current_schema()", String.class); + String serverVersion = jdbcTemplate.queryForObject("SHOW server_version", String.class); + Integer serverVersionNumber = jdbcTemplate.queryForObject( + "SELECT current_setting('server_version_num')::integer", + Integer.class + ); + String vectorVersion = jdbcTemplate.queryForObject( + "SELECT extversion FROM pg_extension WHERE extname = 'vector'", + String.class + ); + + assertThat(currentSchema).isEqualTo(TEST_SCHEMA); + assertThat(serverVersion).startsWith(EXPECTED_SERVER_VERSION_PREFIX); + assertThat(serverVersionNumber).isBetween(170_000, 179_999); + assertThat(vectorVersion).isEqualTo(EXPECTED_PGVECTOR_VERSION); + assertThat(count( + "SELECT COUNT(*) FROM flyway_schema_history WHERE version = '35' AND success = TRUE" + )).isOne(); + assertThat(count( + "SELECT COUNT(*) FROM flyway_schema_history WHERE success = FALSE" + )).isZero(); + + assertThat(jdbcTemplate.queryForObject(""" + SELECT format_type(attribute.atttypid, attribute.atttypmod) + FROM pg_attribute attribute + JOIN pg_class table_definition ON table_definition.oid = attribute.attrelid + JOIN pg_namespace namespace ON namespace.oid = table_definition.relnamespace + WHERE namespace.nspname = current_schema() + AND table_definition.relname = 'embeddings' + AND attribute.attname = 'vector' + AND attribute.attnum > 0 + AND NOT attribute.attisdropped + """, String.class)).isEqualTo(EXPECTED_VECTOR_TYPE); + assertThat(jdbcTemplate.queryForList(""" + SELECT indexdef + FROM pg_indexes + WHERE schemaname = current_schema() + AND tablename = 'embeddings' + AND indexdef ILIKE '%USING hnsw%' + AND indexdef ILIKE '%vector_cosine_ops%' + """, String.class)).hasSize(1); + + log.info( + "OPENSQL_COMPATIBILITY_ENV serverVersion={}, serverVersionNumber={}, pgvector={}, " + + "flywayVersion=35, vectorType={}", + serverVersion, + serverVersionNumber, + vectorVersion, + EXPECTED_VECTOR_TYPE + ); + } + + @Test + @Order(2) + @Timeout(180) + @DisplayName("2,000개 Vector에서 HNSW 실행 계획과 Cosine Top-K 지연을 측정한다") + void vectorSearch_usesHnswAndReportsLatency() throws Exception { + jdbcTemplate.execute("DROP TABLE IF EXISTS " + VECTOR_PROBE_TABLE); + jdbcTemplate.execute( + "CREATE TABLE " + VECTOR_PROBE_TABLE + + " (id BIGSERIAL PRIMARY KEY, vector vector(1024) NOT NULL)" + ); + List vectors = deterministicVectors(VECTOR_ROW_COUNT, VECTOR_DIMENSION); + + try (Connection connection = dataSource.getConnection()) { + // 1. 고정 Seed Vector를 Batch Insert해 실행마다 같은 검색 분포를 만든다. + insertVectors(connection, vectors); + + // 2. 실제 Cosine HNSW Index를 구축하고 Planner Statistics를 갱신한다. + execute(connection, + "CREATE INDEX opensql_vector_probe_hnsw ON " + VECTOR_PROBE_TABLE + + " USING hnsw (vector vector_cosine_ops)" + ); + execute(connection, "ANALYZE " + VECTOR_PROBE_TABLE); + + // 3. Seq Scan을 비활성화한 같은 Connection에서 HNSW 실행 계획을 확인한다. + String queryVector = vectors.get(0); + String plan = explainVectorSearch(connection, queryVector); + assertThat(plan.toLowerCase(Locale.ROOT)) + .contains("index scan") + .contains("opensql_vector_probe_hnsw"); + + // 4. Warm-up을 분리하고 실제 Top-K 결과·거리 순서와 지연 분포를 수집한다. + for (int index = 0; index < WARM_UP_QUERY_COUNT; index++) { + queryVector(connection, queryVector); + } + List latenciesMillis = new ArrayList<>(); + for (int index = 0; index < MEASURED_QUERY_COUNT; index++) { + long startedAt = System.nanoTime(); + List results = queryVector(connection, queryVector); + latenciesMillis.add(nanosToMillis(System.nanoTime() - startedAt)); + assertVectorResults(results); + } + + VectorLatencySummary summary = VectorLatencySummary.from(latenciesMillis); + log.info( + "OPENSQL_VECTOR_RESULT rows={}, queries={}, topK={}, p50Ms={}, p95Ms={}, p99Ms={}, maxMs={}", + VECTOR_ROW_COUNT, + MEASURED_QUERY_COUNT, + TOP_K, + summary.p50Millis(), + summary.p95Millis(), + summary.p99Millis(), + summary.maxMillis() + ); + } finally { + jdbcTemplate.execute("DROP TABLE IF EXISTS " + VECTOR_PROBE_TABLE); + } + } + + @Test + @Order(3) + @Timeout(30) + @DisplayName("잠긴 Queue Row를 SKIP LOCKED가 대기 없이 건너뛰고 해제 뒤 다시 조회한다") + void skipLocked_skipsLockedRowWithoutWaiting() throws Exception { + jdbcTemplate.execute("DROP TABLE IF EXISTS " + QUEUE_PROBE_TABLE); + jdbcTemplate.execute( + "CREATE TABLE " + QUEUE_PROBE_TABLE + + " (id BIGSERIAL PRIMARY KEY, status VARCHAR(20) NOT NULL)" + ); + jdbcTemplate.update("INSERT INTO " + QUEUE_PROBE_TABLE + " (status) VALUES ('PENDING')"); + + try ( + Connection lockingConnection = dataSource.getConnection(); + Connection skippingConnection = dataSource.getConnection() + ) { + lockingConnection.setAutoCommit(false); + skippingConnection.setAutoCommit(false); + + // 1. 첫 Connection이 유일한 Queue Row의 Lock을 Transaction 종료까지 보유한다. + assertThat(selectQueueRow(lockingConnection, false)).isEqualTo(1L); + + // 2. 둘째 Connection은 같은 Row를 기다리지 않고 즉시 건너뛴다. + long startedAt = System.nanoTime(); + assertThat(selectQueueRow(skippingConnection, true)).isNull(); + Duration skippedIn = Duration.ofNanos(System.nanoTime() - startedAt); + assertThat(skippedIn).isLessThan(Duration.ofSeconds(5)); + + // 3. 첫 Lock 해제와 둘째 Transaction 갱신 뒤 같은 Row를 정상 조회한다. + lockingConnection.rollback(); + skippingConnection.rollback(); + assertThat(selectQueueRow(skippingConnection, true)).isEqualTo(1L); + skippingConnection.rollback(); + + log.info("OPENSQL_SKIP_LOCKED_RESULT skippedInMs={}, recovered=true", skippedIn.toMillis()); + } finally { + jdbcTemplate.execute("DROP TABLE IF EXISTS " + QUEUE_PROBE_TABLE); + } + } + + private void insertVectors(Connection connection, List vectors) throws SQLException { + try (PreparedStatement statement = connection.prepareStatement( + "INSERT INTO " + VECTOR_PROBE_TABLE + " (vector) VALUES (CAST(? AS vector))" + )) { + for (int index = 0; index < vectors.size(); index++) { + statement.setString(1, vectors.get(index)); + statement.addBatch(); + if ((index + 1) % INSERT_BATCH_SIZE == 0) { + statement.executeBatch(); + } + } + if (vectors.size() % INSERT_BATCH_SIZE != 0) { + statement.executeBatch(); + } + } + } + + private void execute(Connection connection, String sql) throws SQLException { + try (Statement statement = connection.createStatement()) { + statement.execute(sql); + } + } + + private String explainVectorSearch(Connection connection, String queryVector) throws SQLException { + execute(connection, "SET enable_seqscan = off"); + try (PreparedStatement statement = connection.prepareStatement( + "EXPLAIN (COSTS OFF) SELECT id FROM " + VECTOR_PROBE_TABLE + + " ORDER BY vector <=> CAST(? AS vector) LIMIT " + TOP_K + )) { + statement.setString(1, queryVector); + try (ResultSet resultSet = statement.executeQuery()) { + StringBuilder plan = new StringBuilder(); + while (resultSet.next()) { + plan.append(resultSet.getString(1)).append('\n'); + } + return plan.toString(); + } + } finally { + execute(connection, "RESET enable_seqscan"); + } + } + + private List queryVector(Connection connection, String queryVector) throws SQLException { + try (PreparedStatement statement = connection.prepareStatement( + "SELECT id, vector <=> CAST(? AS vector) AS distance FROM " + VECTOR_PROBE_TABLE + + " ORDER BY vector <=> CAST(? AS vector) LIMIT " + TOP_K + )) { + statement.setString(1, queryVector); + statement.setString(2, queryVector); + try (ResultSet resultSet = statement.executeQuery()) { + List results = new ArrayList<>(); + while (resultSet.next()) { + results.add(new VectorResult(resultSet.getLong("id"), resultSet.getDouble("distance"))); + } + return List.copyOf(results); + } + } + } + + private void assertVectorResults(List results) { + assertThat(results).hasSize(TOP_K); + assertThat(results.get(0).id()).isEqualTo(1L); + assertThat(results.get(0).distance()).isEqualTo(0.0); + assertThat(results).allMatch(result -> Double.isFinite(result.distance())); + assertThat(results).isSortedAccordingTo(Comparator.comparingDouble(VectorResult::distance)); + } + + private Long selectQueueRow(Connection connection, boolean skipLocked) throws SQLException { + String suffix = skipLocked ? " SKIP LOCKED" : ""; + try (PreparedStatement statement = connection.prepareStatement( + "SELECT id FROM " + QUEUE_PROBE_TABLE + + " WHERE status = 'PENDING' ORDER BY id FOR UPDATE" + suffix + )) { + statement.setQueryTimeout(5); + try (ResultSet resultSet = statement.executeQuery()) { + return resultSet.next() ? resultSet.getLong(1) : null; + } + } + } + + private List deterministicVectors(int count, int dimension) { + Random random = new Random(124L); + List vectors = new ArrayList<>(count); + for (int row = 0; row < count; row++) { + float[] values = new float[dimension]; + double squaredNorm = 0.0; + for (int index = 0; index < dimension; index++) { + values[index] = random.nextFloat() - 0.5F; + squaredNorm += values[index] * values[index]; + } + double norm = Math.sqrt(squaredNorm); + StringBuilder vector = new StringBuilder(dimension * 12).append('['); + for (int index = 0; index < dimension; index++) { + if (index > 0) { + vector.append(','); + } + vector.append(values[index] / norm); + } + vector.append(']'); + vectors.add(vector.toString()); + } + return List.copyOf(vectors); + } + + private double nanosToMillis(long nanos) { + return nanos / (double) TimeUnit.MILLISECONDS.toNanos(1); + } + + private int count(String sql) { + return jdbcTemplate.queryForObject(sql, Integer.class); + } + + /** Top-K 한 행의 식별자와 Cosine Distance를 보존한다. */ + private record VectorResult(long id, double distance) { + } + + /** Hardware 의존 임계값 없이 공개 가능한 Vector 검색 지연 Percentile만 집계한다. */ + private record VectorLatencySummary( + double p50Millis, + double p95Millis, + double p99Millis, + double maxMillis + ) { + private static VectorLatencySummary from(List values) { + List sorted = values.stream().sorted().toList(); + return new VectorLatencySummary( + percentile(sorted, 0.50), + percentile(sorted, 0.95), + percentile(sorted, 0.99), + sorted.get(sorted.size() - 1) + ); + } + + private static double percentile(List sorted, double percentile) { + int rank = Math.max(1, (int) Math.ceil(percentile * sorted.size())); + return sorted.get(rank - 1); + } + } +} From e98903202ed83c9784afb5d1908cda7938ca3799 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EA=B8=B0=EB=AF=BC?= Date: Sat, 8 Aug 2026 15:09:44 +0900 Subject: [PATCH 4/6] =?UTF-8?q?docs:=20#124=20=EA=B3=B5=EC=8B=9D=20OpenSQL?= =?UTF-8?q?=20=EA=B2=80=EC=A6=9D=20Runbook=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...gimin-#124-opensql-verification-runbook.md | 251 ++++++++++++++++++ 1 file changed, 251 insertions(+) create mode 100644 docs/test-results/gimin-#124-opensql-verification-runbook.md diff --git a/docs/test-results/gimin-#124-opensql-verification-runbook.md b/docs/test-results/gimin-#124-opensql-verification-runbook.md new file mode 100644 index 0000000..2907917 --- /dev/null +++ b/docs/test-results/gimin-#124-opensql-verification-runbook.md @@ -0,0 +1,251 @@ +# Issue #124 공식 OpenSQL 17.8 호환성·성능 검증 Runbook + +## 1. 목적 + +이 문서는 Rocky Linux 9.7 x86-64 Single 구성의 공식 OpenSQL 17.8에서 DocGrid DB 호환성, 문서 +인덱싱 E2E와 성능 측정을 재현하는 절차다. 일반 PostgreSQL 17.8 실행은 로컬 기준선일 뿐 공식 결과가 +아니다. + +## 2. 사전 조건 + +### 2.1 공식 Host + +| 항목 | 필수 값 | +|---|---| +| OS | Rocky Linux 9.7 | +| Architecture | x86-64 | +| OpenSQL | PostgreSQL 17.8 기반 공식 배포본 | +| pgvector | 0.8.1 | +| 설치 모드 | Single | +| License | 해당 Hostname·CPU 조건으로 발급된 유효 License | + +Apple Silicon의 일반 UTM Virtualization VM은 `aarch64`일 수 있다. `uname -m`이 `x86_64`가 아니면 +공식 판정에 사용하지 않는다. Hostname 또는 CPU Topology 변경이 필요한 경우 기존 License를 임의로 +재사용하지 않고 공급사 재발급 절차를 따른다. + +### 2.2 저장소 밖 보관 + +다음 파일은 Repository, 작업 Branch, PR 첨부와 Test 결과 폴더로 복사하지 않는다. + +- OpenSQL 설치 압축과 압축 해제 디렉터리 +- License XML +- 공급사 다운로드 URL·비밀번호 +- SSH Key와 DB 접속 Secret + +공식 설치와 License 적용은 공급사 통합 설치 가이드의 Single 절차로 완료한 상태여야 한다. 이 +Runbook은 설치 자체를 자동화하지 않는다. + +### 2.3 검증 전용 Database 권한 + +검증 계정은 운영 계정과 분리하고 다음 권한만 준비한다. + +- 검증 Database 연결 +- `docgrid_opensql_*_test` Schema 생성·삭제 +- Schema 안 Table·Index·Sequence 생성 +- `vector` Extension 조회와, 초기 설치 시 필요한 경우 관리자에 의한 Extension 생성 +- `pg_stat_activity`, `pg_locks`, `pg_stat_database` 성능 관측 + +운영 Database나 운영 Schema에서는 실행하지 않는다. + +## 3. 환경 변수 준비 + +값은 Shell Session에만 주입한다. `.env`, Markdown, Shell History와 CI Log에 실제 Password를 추가하지 +않는다. + +```bash +export OPENSQL_DB_HOST= +export OPENSQL_DB_PORT= +export OPENSQL_DB_NAME= +export OPENSQL_DB_USER= +read -s OPENSQL_DB_PASSWORD +export OPENSQL_DB_PASSWORD +export OPENSQL_DB_SSLMODE=require +export OPENSQL_INSTALL_MODE=single +``` + +공식 환경이 TLS를 제공하지 않는 격리 Network라면 공급사 설정을 확인한 뒤 `OPENSQL_DB_SSLMODE`만 +조정한다. Password와 Host 값은 성공 출력에 포함되지 않는다. + +## 4. Rocky Host Preflight + +이 단계는 Application을 실행하는 macOS가 아니라 OpenSQL이 설치된 Rocky Host에서 수행한다. + +```bash +scripts/opensql/verify-host.sh +``` + +정상 출력 예시는 Version과 공개 가능한 환경 정보만 포함한다. + +```text +OpenSQL 공식 Host preflight 통과 +OS=Rocky Linux 9.7, architecture=x86_64, mode=single +server_version=17.8..., pgvector=0.8.1 +``` + +다음 중 하나라도 다르면 공식 Test를 실행하지 않는다. + +- Rocky 9.7이 아님 +- x86_64가 아님 +- Single 모드가 아님 +- `server_version`이 17.8로 시작하지 않음 +- pgvector가 없거나 0.8.1이 아님 + +## 5. Application 측 연결 점검 + +공식 Host에서 직접 Gradle을 실행하거나, 접근 제한 Network에서는 승인된 SSH Tunnel을 사용한다. + +```bash +ssh -N -L :127.0.0.1: +``` + +Tunnel을 사용할 때 `OPENSQL_DB_HOST=127.0.0.1`, `OPENSQL_DB_PORT=`로 현재 Shell만 +변경한다. SSH Host, IP와 Key 경로는 결과 문서에 기록하지 않는다. + +환경 변수 누락 차단은 DB 접속 전에 확인할 수 있다. + +```bash +env -u OPENSQL_DB_HOST ./gradlew openSqlCompatibilityTest +``` + +이 명령은 `OPENSQL_DB_HOST` 누락 오류로 실패해야 하며 DB Connection을 시도하지 않는다. + +## 6. 단계별 검증 + +### 6.1 Migration·Vector·HNSW·SKIP LOCKED + +```bash +./gradlew openSqlCompatibilityTest +``` + +기대 결과: + +- 3 tests passed +- Flyway V35와 모든 Migration 성공 +- `vector(1024)`, pgvector 0.8.1, Cosine HNSW Index 확인 +- 2,000개 Vector Insert와 Top-K 30회 성공 +- HNSW Index Scan 실행 계획 확인 +- `SKIP LOCKED` 비대기와 Lock 해제 뒤 재조회 성공 + +공개 가능한 Log Marker: + +```text +OPENSQL_COMPATIBILITY_ENV +OPENSQL_VECTOR_RESULT +OPENSQL_SKIP_LOCKED_RESULT +``` + +### 6.2 다중 Worker Claim·Lease + +```bash +./gradlew openSqlClaimConcurrencyTest +``` + +기대 결과: + +- ACTIVE Worker 100개가 단일 Job에 경쟁해도 Claim 한 건 +- Worker 20개가 Job 1,000개를 중복 없이 소진 +- Claim Token·Worker 소유권·LOCKED Event 단일성 +- Lease 갱신 중 복구 차단, 중단 뒤 단일 복구 + +### 6.3 실제 문서 인덱싱 E2E + +로컬 MinIO와 현재 Source의 BGE-M3 Batch Server를 먼저 기동한다. + +```bash +docker compose up -d minio embedding-server +./gradlew openSqlDocumentE2eTest +``` + +DB만 공식 OpenSQL을 사용하며 MinIO Bucket과 Test Schema는 실행마다 격리된다. + +- PDF·DOCX 실제 HTTP 업로드 +- Worker 자동 Claim·Attempt·Lease +- BGE-M3 Batch와 Query Embedding +- 공식 DB의 `vector(1024)` 저장과 Cosine 검색 +- 관리자 Job·Attempt·Event 인증 HTTP +- Provider 장애와 부분 Vector 방지·지연 재시도 + +### 6.4 Claim 성능 Smoke + +최종 장시간 측정 전에 축소 Profile로 환경과 권한을 점검한다. + +```bash +./gradlew openSqlClaimPerformanceTest \ + -Dclaim.performance.warm-up-jobs=100 \ + -Dclaim.performance.job-count=500 \ + -Dclaim.performance.repetitions=2 \ + -Dclaim.performance.workers=1,10,20 +``` + +### 6.5 전체 공식 검증 + +```bash +./gradlew openSqlVerification +``` + +이 Task는 Compatibility, Claim 정합성, 문서 E2E와 기본 Claim Benchmark를 순서대로 실행한다. 기본 +Benchmark는 장시간·대량 Data를 사용하므로 공식 측정 시간과 DB 자원을 확보한 뒤 실행한다. + +## 7. 결과 수집 + +결과 문서에는 다음만 기록한다. + +- Rocky Version, x86-64, Single 모드 +- OpenSQL `server_version`, pgvector Version +- Commit SHA +- Test Suite별 건수·실행 시간·결과 +- Vector 행 수·질의 수·p50·p95·p99·max +- HNSW Index Scan 사용 여부 +- Claim Worker·Job·반복 수, TPS·p50·p95·p99 +- Hikari 대기와 PostgreSQL Lock 대기 최대값 +- Deadlock·Rollback과 정합성 판정 + +다음은 삭제하거나 ``로 치환한다. + +- Host·IP·Database Username +- JDBC URL +- Password·Token·License 내용 +- SSH Command의 실제 Host와 Key 경로 + +## 8. Schema 정리 + +정상 종료 시 Test가 자신의 Schema를 자동 삭제한다. JVM 강제 종료 등으로 남은 경우 정확한 대상 이름을 +먼저 조회하고 검증 계정으로 해당 Schema만 제거한다. + +```sql +SELECT nspname +FROM pg_namespace +WHERE nspname LIKE 'docgrid_opensql_%_test'; +``` + +예상하지 않은 이름이 함께 조회되면 삭제하지 않는다. 공식 결과 보존을 위해 `KEEP_*_SCHEMA=true`를 +사용했다면 결과 수집 뒤 같은 원칙으로 수동 정리한다. + +## 9. 로컬 기준선 + +공식 접속 전에 같은 Task를 PostgreSQL 17.8 + pgvector 0.8.1 로컬 DB에 연결해 Test Code 자체를 +검증할 수 있다. 이 결과는 반드시 `LOCAL BASELINE`으로 표시한다. + +```bash +OPENSQL_DB_HOST= \ +OPENSQL_DB_PORT= \ +OPENSQL_DB_NAME= \ +OPENSQL_DB_USER= \ +OPENSQL_DB_PASSWORD= \ +OPENSQL_DB_SSLMODE=disable \ +./gradlew openSqlCompatibilityTest +``` + +로컬 기준선 통과는 Rocky Linux 9.7 x86-64, 공급사 OpenSQL Binary와 License 검증을 대신하지 않는다. + +## 10. Git 안전 확인 + +공식 실행 전후에 작업 Tree를 확인한다. + +```bash +git status --short +git diff --check +``` + +설치 번들, License, `.env` 또는 결과 원시 Log가 표시되면 Stage·Commit하지 않는다. 이 작업의 Git +변경은 Source Test, 실행 경계와 민감정보를 제거한 Markdown 결과만 포함해야 한다. From 59a202a34dd02509af7d143b95d58b6fe2b873d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EA=B8=B0=EB=AF=BC?= Date: Sat, 8 Aug 2026 16:13:12 +0900 Subject: [PATCH 5/6] =?UTF-8?q?docs:=20#124=20=EA=B3=B5=EC=8B=9D=20OpenSQL?= =?UTF-8?q?=20=ED=98=B8=ED=99=98=EC=84=B1=C2=B7=EC=84=B1=EB=8A=A5=20?= =?UTF-8?q?=EA=B2=B0=EA=B3=BC=20=EA=B8=B0=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...-#124-opensql-compatibility-performance.md | 176 ++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 docs/test-results/gimin-#124-opensql-compatibility-performance.md diff --git a/docs/test-results/gimin-#124-opensql-compatibility-performance.md b/docs/test-results/gimin-#124-opensql-compatibility-performance.md new file mode 100644 index 0000000..7cc9560 --- /dev/null +++ b/docs/test-results/gimin-#124-opensql-compatibility-performance.md @@ -0,0 +1,176 @@ +# Issue #124 공식 OpenSQL 17.8 호환성·성능 검증 결과 + +## 1. 결과 요약 + +공급사가 제공한 OpenSQL 3.17.8.7 배포본을 Rocky Linux 9.7 x86-64 Single 구성에 설치하고 DocGrid의 +Migration, Vector 저장·검색, Worker Claim·Lease, 실제 문서 인덱싱과 Claim 성능을 검증했다. + +| 검증 항목 | 결과 | +|---|---| +| Rocky Linux 9.7·x86-64·Single 사전 점검 | PASS | +| OpenSQL PostgreSQL 17.8·pgvector 0.8.1 | PASS | +| Flyway V1~V35·Hibernate Schema Validation | PASS | +| `vector(1024)`·Cosine HNSW·`<=>` 검색 | PASS | +| `FOR UPDATE SKIP LOCKED` | PASS | +| 다중 Worker Claim·Lease·자동 실행 | PASS, 10 tests | +| 실제 문서·BGE-M3·Vector 전체 관통 | PASS, 2 tests | +| 기본 Claim 성능 Profile | PASS, 25 measurements | +| 일반 Test 회귀 | PASS, 709 tests | +| 공급사 지원 VM·원격 Server 최종 인수 판정 | PENDING | + +마지막 항목은 이번 실행이 Apple Silicon Host의 Docker에서 `linux/amd64`로 실행한 Container 검증이기 +때문이다. 공식 배포 Binary와 Database 기능의 호환성은 확인했지만, Container는 Rocky Linux 9.7 +x86-64 VM 또는 물리·원격 Server와 동일한 운영 경계가 아니다. 특히 아래 성능값은 제품 SLO나 최종 +대회 제출 성능으로 사용하지 않고, 공급사 지원 Host에서 같은 Runbook을 한 번 더 실행해야 한다. + +## 2. 공개 가능한 실행 환경 + +| 항목 | 값 | +|---|---| +| Database 배포본 | OpenSQL 3.17.8.7 | +| Database 호환 Version | PostgreSQL 17.8 | +| OS | Rocky Linux 9.7 (Blue Onyx) | +| Database Architecture | x86-64 | +| 설치 모드 | Single | +| pgvector | 0.8.1 | +| pgvectorscale | 0.9.0 | +| Database Container 자원 | 4 CPU, 6 GiB Memory | +| Application JVM | Java 17, Apple Silicon Host | +| DB 실행 경계 | Docker `linux/amd64` Emulation | +| Test 대상 Commit | `e98903202ed83c9784afb5d1908cda7938ca3799` | +| 실행 일자 | 2026-08-08 KST | + +License, 설치 파일, 다운로드 정보, Database 접속 정보와 개인 절대 경로는 결과에 포함하지 않았다. +격리된 Local 검증 Network여서 TLS는 사용하지 않았으며 운영 연결 정책을 의미하지 않는다. + +## 3. 설치와 사전 점검 + +### 3.1 설치 결과 + +공식 설치기의 Single Mode 설치는 완료됐고 etcd, Patroni와 OpenSQL PostgreSQL이 정상 기동했다. +검증 Database에는 Runbook의 사전 조건에 따라 관리자가 `vector` Extension을 생성했다. 그 후 +Application 계정으로 실행한 Host Preflight가 다음 조건을 모두 확인했다. + +```text +OS=Rocky Linux 9.7 +architecture=x86_64 +mode=single +server_version=17.8 +pgvector=0.8.1 +``` + +### 3.2 설치 중 확인한 공급사 설치기 주의점 + +1. 설치기가 참조한 `EL-9.6-x86_64` PGDG Repository RPM URL은 실행 시점에 404를 반환했다. +2. Rocky 기본·EPEL Repository의 SFCGAL은 설치기가 요구한 2.0 이상보다 낮았다. +3. 현재 공식 PGDG EL9 Repository를 등록하고 PGDG의 SFCGAL 2.2.0 Package를 설치한 뒤 설치가 + 진행됐다. +4. 이는 DocGrid Source 변경이 아니라 검증 Host의 설치 사전 작업이며, 공급사 원본 설치기는 + 수정하지 않았다. + +실제 VM·원격 Server 설치 전 공급사에 Repository URL과 SFCGAL 선행 조건을 확인하는 것이 필요하다. + +## 4. 호환성·Vector 검색 결과 + +`openSqlCompatibilityTest`의 3개 Test가 통과했다. + +| 항목 | 결과 | +|---|---| +| Flyway 최신 Version | 35 | +| 제품 Vector Column | `vector(1024)` | +| Probe Vector | 2,000 rows | +| Query | 30회, Top-K 10 | +| Cosine HNSW Index Scan | 사용 확인 | +| Vector p50 | 4.207 ms | +| Vector p95 | 4.706 ms | +| Vector p99 | 5.087 ms | +| Vector max | 5.087 ms | +| `SKIP LOCKED` 잠금 건너뛰기 | 4 ms | +| 잠금 해제 후 재조회 | 성공 | + +같은 Test를 Local PostgreSQL 17.8 + pgvector 0.8.1에서 실행한 기준선은 p50 2.086 ms, p95 +2.344 ms, p99·max 3.875 ms였다. 공식 배포본의 이번 측정은 x86-64 Emulation과 Container Network를 +포함하므로 두 값의 차이를 Database Engine만의 차이로 해석할 수 없다. + +## 5. Claim·Lease 정합성 결과 + +`openSqlClaimConcurrencyTest`의 10개 Test가 통과했다. + +- Worker 100개의 단일 Job 경쟁에서 Claim 한 건만 생성 +- Worker 20개의 Queue 소진에서 중복 Claim 없음 +- Claim Token·Worker 소유권·LOCKED Event 정합성 유지 +- Lease 갱신 중 복구 차단과 갱신 중단 뒤 단일 복구 +- Worker 자동 Polling·실행·Graceful Shutdown 계약 유지 + +## 6. 실제 문서 인덱싱 전체 관통 결과 + +`openSqlDocumentE2eTest`의 2개 Test가 통과했다. + +```text +PDF·DOCX HTTP 업로드 +→ MinIO 저장 +→ Worker Claim·Attempt·Lease +→ Parsing·Chunk 저장 +→ 실제 BGE-M3 Batch +→ OpenSQL vector(1024) 저장 +→ INDEXED·current_version 전환 +→ Query Embedding·Cosine 검색 +``` + +Embedding Provider 장애 시 부분 Vector가 저장되지 않고 지연 재시도로 연결되는 시나리오도 통과했다. + +## 7. Claim 성능 결과 + +### 7.1 기본 정식 Profile + +- Warm-up: 500 Jobs +- 측정: Worker별 5,000 Jobs +- 반복: 5회 +- Worker: 1, 5, 10, 20, 40 +- Hikari Pool: 20 + +| Worker | 중앙 TPS | p50 (ms) | p95 (ms) | p99 (ms) | max (ms) | Hikari 대기 | PG Lock 대기 | +|---:|---:|---:|---:|---:|---:|---:|---:| +| 1 | 273.61 | 3.619 | 5.525 | 6.077 | 37.349 | 0 | 0 | +| 5 | 748.45 | 6.524 | 9.872 | 13.187 | 32.063 | 0 | 0 | +| 10 | 563.07 | 9.451 | 61.265 | 67.780 | 90.505 | 0 | 0 | +| 20 | 591.32 | 18.662 | 79.235 | 89.913 | 162.080 | 0 | 0 | +| 40 | 591.89 | 67.921 | 166.329 | 212.795 | 398.824 | 20 | 1 | + +25회 측정 전체에서 Worker 오류, 불완전 소유권, 중복 Claim Token, 잘못된 LOCKED Event, Rollback과 +Deadlock은 모두 0건이었다. 이번 환경에서는 Worker 5개가 가장 높은 처리량을 보였고, Worker 40개는 +20개 Connection Pool 한계로 대기 수가 20까지 증가했다. Worker 수를 Pool보다 크게 늘리는 것은 +처리량을 높이지 않고 지연만 증가시켰다. + +### 7.2 Smoke Profile + +동일 환경에서 500 Jobs, 2회 반복, Worker 1·10·20으로 먼저 실행한 Smoke도 통과했다. 중앙 TPS는 +각각 387.77, 1,431.85, 1,047.78이었다. 짧은 실행은 Cache·초기 상태의 영향을 크게 받아 정식 +Profile보다 높은 수치가 나왔으므로 최종 비교에는 기본 정식 Profile을 사용한다. + +## 8. 전체 실행 결과 + +| Task | Test 수 | 결과 | +|---|---:|---| +| `openSqlCompatibilityTest` | 3 | PASS | +| `openSqlClaimConcurrencyTest` | 10 | PASS | +| `openSqlDocumentE2eTest` | 2 | PASS | +| `openSqlClaimPerformanceTest` | 1 | PASS | +| `openSqlVerification` | 위 4개 Task 집계 | PASS, 6m 59s | +| 일반 `test --rerun-tasks` | 709 | PASS, failure/error/skipped 0 | + +일반 Test의 강제 재실행에는 Local DB의 `sslmode=disable`과 Test 전용 JWT Secret을 Process 환경으로만 +주입했다. 실제 값은 Source, 결과 문서 또는 Git에 저장하지 않았다. + +## 9. 남은 최종 인수 절차 + +공급사 지원 범위를 최종 충족하려면 Rocky Linux 9.7 x86-64 VM 또는 원격 Server에 같은 Single 구성을 +설치하고 다음을 다시 실행한다. + +1. `scripts/opensql/verify-host.sh` +2. `./gradlew openSqlVerification` +3. 이 문서의 Container 결과와 VM·Server 결과를 분리해 기록 +4. 운영에 가까운 CPU·Memory·Network 조건으로 성능값 재측정 + +이 재실행 전까지 본 결과는 공식 OpenSQL 배포본의 기능 호환성 증거이며, 공급사 지원 운영 Host의 +최종 인수 완료 증거는 아니다. From e105314b1f38aea09371d78ecf4060a6b657a8dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EA=B8=B0=EB=AF=BC?= Date: Sat, 8 Aug 2026 16:28:35 +0900 Subject: [PATCH 6/6] =?UTF-8?q?fix:=20#124=20=EA=B3=B5=EC=8B=9D=20OpenSQL?= =?UTF-8?q?=20=EA=B2=80=EC=A6=9D=20=EA=B2=BD=EA=B3=84=20=EB=B3=B4=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 20 +++++++++++ ...-#124-opensql-compatibility-performance.md | 19 ++++++---- ...-#124-opensql-compatibility-performance.md | 5 +-- ...gimin-#124-opensql-verification-runbook.md | 28 ++++++++++----- scripts/opensql/verify-host.sh | 35 ++++++++++++++----- .../OpenSqlCompatibilityIntegrationTest.java | 5 ++- 6 files changed, 86 insertions(+), 26 deletions(-) diff --git a/build.gradle b/build.gradle index 88410f4..603cc33 100644 --- a/build.gradle +++ b/build.gradle @@ -143,6 +143,25 @@ def configureOpenSqlDatabase = { Test task -> } } +def configureOpenSqlExternalServices = { Test task -> + task.doFirst { + // 실제 외부 Service 값이 없으면 공식 E2E가 Local 기본값으로 통과하는 것을 차단한다. + def environmentMappings = [ + OPENSQL_MINIO_ENDPOINT: 'MINIO_ENDPOINT', + OPENSQL_MINIO_ACCESS_KEY: 'MINIO_ACCESS_KEY', + OPENSQL_MINIO_SECRET_KEY: 'MINIO_SECRET_KEY', + OPENSQL_EMBEDDING_SERVER_URL: 'EMBEDDING_SERVER_URL' + ] + environmentMappings.each { sourceName, targetName -> + def value = System.getenv(sourceName) + if (value == null || value.trim().isEmpty()) { + throw new GradleException("필수 공식 OpenSQL E2E 환경 변수가 비어 있습니다: ${sourceName}") + } + task.environment(targetName, value) + } + } +} + def openSqlCompatibilityTest = tasks.register('openSqlCompatibilityTest', Test) { group = 'verification' description = '공식 OpenSQL의 Migration, Vector, HNSW와 SKIP LOCKED 호환성을 검증합니다.' @@ -174,6 +193,7 @@ def openSqlDocumentE2eTest = tasks.register('openSqlDocumentE2eTest', Test) { includeTags 'local-e2e' } configureOpenSqlDatabase(delegate as Test) + configureOpenSqlExternalServices(delegate as Test) } def openSqlClaimPerformanceTest = tasks.register('openSqlClaimPerformanceTest', Test) { diff --git a/docs/design/gimin-#124-opensql-compatibility-performance.md b/docs/design/gimin-#124-opensql-compatibility-performance.md index 9a79ce9..8e5c42a 100644 --- a/docs/design/gimin-#124-opensql-compatibility-performance.md +++ b/docs/design/gimin-#124-opensql-compatibility-performance.md @@ -10,7 +10,7 @@ OpenSQL 17.8이므로 PostgreSQL 호환성만으로 최종 판정할 수 없다. 고정한다. ```text -공식 Host Preflight +공식 Host 지원 환경 호환성 Preflight → Flyway 전체 Migration·Hibernate Validation → pgvector·vector(1024)·HNSW·Cosine Operator → SKIP LOCKED·Claim·Lease·Retry @@ -34,7 +34,7 @@ OpenSQL 17.8이므로 PostgreSQL 호환성만으로 최종 판정할 수 없다. ### 2.1 포함 -- 공식 Host OS·Architecture·설치 모드 Preflight Script +- 공식 Host 지원 OS·Architecture·선언된 설치 모드 Preflight Script - 공식 접속 환경 변수의 Fail-fast Validation - 공식 DB 전용 Gradle Test Task와 집계 Task - Flyway·Schema·Extension·Vector·HNSW Compatibility Test @@ -76,10 +76,15 @@ OPENSQL_DB_NAME OPENSQL_DB_USER OPENSQL_DB_PASSWORD OPENSQL_DB_SSLMODE +OPENSQL_MINIO_ENDPOINT +OPENSQL_MINIO_ACCESS_KEY +OPENSQL_MINIO_SECRET_KEY +OPENSQL_EMBEDDING_SERVER_URL ``` -Gradle은 이를 기존 Test Profile의 `DB_*`로 전달한다. 명령행 Argument나 결과 문서에는 값을 출력하지 -않는다. Password가 없거나 빈 값이면 Connection 시도 전에 실패한다. +Gradle은 Database 값은 기존 Test Profile의 `DB_*`로, 전체 관통 E2E 값은 MinIO·Embedding Server +환경 변수로 전달한다. 명령행 Argument나 결과 문서에는 값을 출력하지 않는다. Password·Secret 또는 +외부 Service 주소가 없거나 빈 값이면 Connection 시도 전에 실패한다. ### 3.3 Schema 격리 @@ -91,7 +96,7 @@ Gradle은 이를 기존 Test Profile의 `DB_*`로 전달한다. 명령행 Argume ## 4. 실행 구조 -### 4.1 Host Preflight +### 4.1 Host 지원 환경 호환성 Preflight Rocky Host에서 Script를 실행해 다음을 확인한다. @@ -101,7 +106,9 @@ Rocky Host에서 Script를 실행해 다음을 확인한다. 4. DB 연결 뒤 `server_version`이 `17.8`로 시작한다. 5. `vector` Extension Version이 `0.8.1`이다. -OS·Architecture가 다르면 로컬 기준선은 실행할 수 있어도 공식 검증은 즉시 실패한다. +OS·Architecture가 다르면 로컬 기준선은 실행할 수 있어도 공식 검증은 즉시 실패한다. 이 Script만으로 +OpenSQL 제품 식별, License 적용 또는 실제 Single Topology를 판정하지 않고 공급사 설치 기록으로 별도 +확인한다. ### 4.2 Gradle Task diff --git a/docs/test-results/gimin-#124-opensql-compatibility-performance.md b/docs/test-results/gimin-#124-opensql-compatibility-performance.md index 7cc9560..da61562 100644 --- a/docs/test-results/gimin-#124-opensql-compatibility-performance.md +++ b/docs/test-results/gimin-#124-opensql-compatibility-performance.md @@ -7,7 +7,7 @@ Migration, Vector 저장·검색, Worker Claim·Lease, 실제 문서 인덱싱 | 검증 항목 | 결과 | |---|---| -| Rocky Linux 9.7·x86-64·Single 사전 점검 | PASS | +| Rocky Linux 9.7·x86-64 지원 환경 호환성 사전 점검 | PASS | | OpenSQL PostgreSQL 17.8·pgvector 0.8.1 | PASS | | Flyway V1~V35·Hibernate Schema Validation | PASS | | `vector(1024)`·Cosine HNSW·`<=>` 검색 | PASS | @@ -49,7 +49,8 @@ License, 설치 파일, 다운로드 정보, Database 접속 정보와 개인 공식 설치기의 Single Mode 설치는 완료됐고 etcd, Patroni와 OpenSQL PostgreSQL이 정상 기동했다. 검증 Database에는 Runbook의 사전 조건에 따라 관리자가 `vector` Extension을 생성했다. 그 후 -Application 계정으로 실행한 Host Preflight가 다음 조건을 모두 확인했다. +Application 계정으로 실행한 지원 환경 호환성 Preflight가 다음 조건을 모두 확인했다. OpenSQL 제품, +License와 실제 Single Topology는 공급사 설치 기록으로 별도 확인했다. ```text OS=Rocky Linux 9.7 diff --git a/docs/test-results/gimin-#124-opensql-verification-runbook.md b/docs/test-results/gimin-#124-opensql-verification-runbook.md index 2907917..755def3 100644 --- a/docs/test-results/gimin-#124-opensql-verification-runbook.md +++ b/docs/test-results/gimin-#124-opensql-verification-runbook.md @@ -57,18 +57,28 @@ export OPENSQL_DB_HOST= export OPENSQL_DB_PORT= export OPENSQL_DB_NAME= export OPENSQL_DB_USER= -read -s OPENSQL_DB_PASSWORD +read -r -s OPENSQL_DB_PASSWORD +printf '\n' export OPENSQL_DB_PASSWORD export OPENSQL_DB_SSLMODE=require export OPENSQL_INSTALL_MODE=single +export OPENSQL_CONNECT_TIMEOUT_SECONDS=10 +export OPENSQL_MINIO_ENDPOINT= +export OPENSQL_MINIO_ACCESS_KEY= +read -r -s OPENSQL_MINIO_SECRET_KEY +printf '\n' +export OPENSQL_MINIO_SECRET_KEY +export OPENSQL_EMBEDDING_SERVER_URL= ``` 공식 환경이 TLS를 제공하지 않는 격리 Network라면 공급사 설정을 확인한 뒤 `OPENSQL_DB_SSLMODE`만 -조정한다. Password와 Host 값은 성공 출력에 포함되지 않는다. +조정한다. 연결 Timeout은 1~30초 정수만 허용한다. Password와 Host 값은 성공 출력에 포함되지 않는다. -## 4. Rocky Host Preflight +## 4. Rocky Host 호환성 Preflight -이 단계는 Application을 실행하는 macOS가 아니라 OpenSQL이 설치된 Rocky Host에서 수행한다. +이 단계는 Application을 실행하는 macOS가 아니라 OpenSQL이 설치된 Rocky Host에서 수행한다. Script는 +OS·Architecture·실행자가 선언한 Mode·호환 Version만 확인한다. OpenSQL 제품 식별, License 적용과 실제 +Single Topology는 공급사 설치 기록으로 별도 확인해야 한다. ```bash scripts/opensql/verify-host.sh @@ -77,8 +87,8 @@ scripts/opensql/verify-host.sh 정상 출력 예시는 Version과 공개 가능한 환경 정보만 포함한다. ```text -OpenSQL 공식 Host preflight 통과 -OS=Rocky Linux 9.7, architecture=x86_64, mode=single +OpenSQL 지원 환경 호환성 preflight 통과 +OS=Rocky Linux 9.7, architecture=x86_64, declared_mode=single server_version=17.8..., pgvector=0.8.1 ``` @@ -215,7 +225,7 @@ Benchmark는 장시간·대량 Data를 사용하므로 공식 측정 시간과 D ```sql SELECT nspname FROM pg_namespace -WHERE nspname LIKE 'docgrid_opensql_%_test'; +WHERE nspname LIKE 'docgrid\_opensql\_%\_test%' ESCAPE E'\\'; ``` 예상하지 않은 이름이 함께 조회되면 삭제하지 않는다. 공식 결과 보존을 위해 `KEEP_*_SCHEMA=true`를 @@ -227,11 +237,13 @@ WHERE nspname LIKE 'docgrid_opensql_%_test'; 검증할 수 있다. 이 결과는 반드시 `LOCAL BASELINE`으로 표시한다. ```bash +read -r -s OPENSQL_DB_PASSWORD +printf '\n' +export OPENSQL_DB_PASSWORD OPENSQL_DB_HOST= \ OPENSQL_DB_PORT= \ OPENSQL_DB_NAME= \ OPENSQL_DB_USER= \ -OPENSQL_DB_PASSWORD= \ OPENSQL_DB_SSLMODE=disable \ ./gradlew openSqlCompatibilityTest ``` diff --git a/scripts/opensql/verify-host.sh b/scripts/opensql/verify-host.sh index 5aa14a1..6e0d4e1 100755 --- a/scripts/opensql/verify-host.sh +++ b/scripts/opensql/verify-host.sh @@ -2,7 +2,7 @@ set -euo pipefail -# 공식 판정은 공급사 지원 Matrix와 Single 설치 계약을 모두 만족한 Host에서만 진행한다. +# 이 Script는 공개 가능한 지원 환경·호환 Version만 확인하며 제품 식별·License·Topology 판정을 대신하지 않는다. required_variables=( OPENSQL_DB_HOST OPENSQL_DB_PORT @@ -31,9 +31,9 @@ if [[ "$(uname -m)" != "x86_64" ]]; then exit 1 fi -# 2. 대회 안내의 Single 설치만 허용하고 HA 관련 구성을 공식 검증 범위에서 제외한다. +# 2. 실행자가 공급사 설치 기록으로 확인한 Single Mode 표식을 요구하고 HA 검증과 구분한다. if [[ "${OPENSQL_INSTALL_MODE}" != "single" ]]; then - echo "공식 검증은 OpenSQL Single 설치에서만 실행할 수 있습니다." >&2 + echo "호환성 검증은 OpenSQL Single 설치 표식에서만 실행할 수 있습니다." >&2 exit 1 fi @@ -44,12 +44,29 @@ export PGDATABASE="${OPENSQL_DB_NAME}" export PGUSER="${OPENSQL_DB_USER}" export PGPASSWORD="${OPENSQL_DB_PASSWORD}" export PGSSLMODE="${OPENSQL_DB_SSLMODE}" +connect_timeout_seconds="${OPENSQL_CONNECT_TIMEOUT_SECONDS:-10}" +if [[ ! "${connect_timeout_seconds}" =~ ^[1-9][0-9]*$ ]] || (( connect_timeout_seconds > 30 )); then + echo "OPENSQL_CONNECT_TIMEOUT_SECONDS는 1~30초 정수여야 합니다." >&2 + exit 1 +fi +export PGCONNECT_TIMEOUT="${connect_timeout_seconds}" -server_version="$(psql --tuples-only --no-align --command "SHOW server_version")" -vector_version="$(psql --tuples-only --no-align --command \ - "SELECT extversion FROM pg_extension WHERE extname = 'vector'")" +# 4. 사용자 psqlrc와 Pager를 배제하고 실패 상세가 접속 식별자를 Log에 노출하지 않게 격리한다. +psql_error_file="$(mktemp)" +trap 'rm -f "${psql_error_file}"' EXIT +psql_options=(-X --pset=pager=off --tuples-only --no-align) + +if ! server_version="$(psql "${psql_options[@]}" --command "SHOW server_version" 2>"${psql_error_file}")"; then + echo "OpenSQL 연결 또는 server_version 조회에 실패했습니다." >&2 + exit 1 +fi +if ! vector_version="$(psql "${psql_options[@]}" --command \ + "SELECT extversion FROM pg_extension WHERE extname = 'vector'" 2>"${psql_error_file}")"; then + echo "OpenSQL 연결 또는 pgvector Version 조회에 실패했습니다." >&2 + exit 1 +fi -# 4. 지원 기준 Version을 정확히 확인해 로컬 PostgreSQL 결과와 공식 OpenSQL 결과를 구분한다. +# 5. 지원 기준 Version을 정확히 확인하되 OpenSQL 제품 판정은 외부 설치 증거로 별도 수행한다. if [[ "${server_version}" != 17.8* ]]; then echo "지원되지 않는 OpenSQL PostgreSQL Version입니다: ${server_version}" >&2 exit 1 @@ -59,6 +76,6 @@ if [[ "${vector_version}" != "0.8.1" ]]; then exit 1 fi -echo "OpenSQL 공식 Host preflight 통과" -echo "OS=Rocky Linux ${VERSION_ID}, architecture=$(uname -m), mode=${OPENSQL_INSTALL_MODE}" +echo "OpenSQL 지원 환경 호환성 preflight 통과" +echo "OS=Rocky Linux ${VERSION_ID}, architecture=$(uname -m), declared_mode=${OPENSQL_INSTALL_MODE}" echo "server_version=${server_version}, pgvector=${vector_version}" diff --git a/src/test/java/com/opensource/docgrid/opensql/OpenSqlCompatibilityIntegrationTest.java b/src/test/java/com/opensource/docgrid/opensql/OpenSqlCompatibilityIntegrationTest.java index 3a92bea..1c504c0 100644 --- a/src/test/java/com/opensource/docgrid/opensql/OpenSqlCompatibilityIntegrationTest.java +++ b/src/test/java/com/opensource/docgrid/opensql/OpenSqlCompatibilityIntegrationTest.java @@ -13,6 +13,7 @@ import java.util.List; import java.util.Locale; import java.util.Random; +import java.util.UUID; import java.util.concurrent.TimeUnit; import javax.sql.DataSource; @@ -53,7 +54,9 @@ @DisplayName("공식 OpenSQL 17.8 호환성·Vector 성능 통합 테스트") class OpenSqlCompatibilityIntegrationTest { - private static final String TEST_SCHEMA = "docgrid_opensql_compatibility_test"; + // 별도 Gradle 실행이 겹쳐도 한 JVM의 Cleanup이 다른 실행의 Schema를 제거하지 않게 격리한다. + private static final String TEST_SCHEMA = "docgrid_opensql_compatibility_test_" + + UUID.randomUUID().toString().replace("-", "").substring(0, 12); private static final String VECTOR_PROBE_TABLE = "opensql_vector_probe"; private static final String QUEUE_PROBE_TABLE = "opensql_queue_probe"; private static final String EXPECTED_SERVER_VERSION_PREFIX = "17.8";