feat(anchoring): 설정 파일 환경 분리 — APP_ENV 로 config.{local,dev,prod}.toml 선택
- config.py: APP_ENV(기본 local) 기반 config.{APP_ENV}.toml 로드 — backend/negodata 와 동일 관례
- dev/prod 는 파일 부재 시 기동 즉시 중단(FileNotFoundError) — 오타·미배치 상태로
코드 기본값(로컬 DB)에 붙는 무증상 사고 방지. local 만 파일 없이 기본값 허용(개발 편의)
- main 기동 로그에 APP_ENV·설정 파일 명시
- 루트 compose: anchoring 서비스에 APP_ENV=local + config.local.toml 마운트
- config.toml.example 을 3환경 공용 템플릿으로 갱신, .gitignore 에 config.*.toml
(env별 실파일은 gitignore — dev/prod 는 배포 시 CHANGE_ME 채움)
- TODO.md 삭제(주요 과제 전부 종결) + README·개발용.md 참조 정리
- 검증: 모듈 20 테스트 통과, APP_ENV=staging fail-fast(exit 1)·local dry-run 기동 확인
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
5f9dec66cc
commit
c0d6723fe4
@ -85,11 +85,12 @@ services:
|
|||||||
build: ./schedules/anchoring
|
build: ./schedules/anchoring
|
||||||
container_name: anchoring
|
container_name: anchoring
|
||||||
environment:
|
environment:
|
||||||
|
APP_ENV: local # config.{APP_ENV}.toml 선택 (local/dev/prod)
|
||||||
DB_HOST: host.docker.internal # 컨테이너→호스트 DB
|
DB_HOST: host.docker.internal # 컨테이너→호스트 DB
|
||||||
REDIS_HOST: anchoring-redis
|
REDIS_HOST: anchoring-redis
|
||||||
TZ: Asia/Seoul
|
TZ: Asia/Seoul
|
||||||
volumes:
|
volumes:
|
||||||
- ./schedules/anchoring/config.toml:/app/config.toml:ro # 시크릿은 마운트 — up 전에 파일 필요
|
- ./schedules/anchoring/config.local.toml:/app/config.local.toml:ro # 시크릿은 마운트 — up 전에 파일 필요
|
||||||
depends_on:
|
depends_on:
|
||||||
- anchoring-redis
|
- anchoring-redis
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
|
|||||||
1
schedules/anchoring/.gitignore
vendored
1
schedules/anchoring/.gitignore
vendored
@ -1,4 +1,5 @@
|
|||||||
config.toml
|
config.toml
|
||||||
|
config.*.toml
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
|
|||||||
@ -5,7 +5,7 @@ WORKDIR /app
|
|||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
# config.toml 은 이미지에 넣지 않는다(시크릿이 이미지 레이어에 남음) — 런타임 마운트 또는 env 로 주입
|
# config.{env}.toml 은 이미지에 넣지 않는다(시크릿이 이미지 레이어에 남음) — 런타임 마운트 또는 env 로 주입
|
||||||
COPY src ./src
|
COPY src ./src
|
||||||
|
|
||||||
ENV PYTHONPATH=/app/src \
|
ENV PYTHONPATH=/app/src \
|
||||||
|
|||||||
@ -6,7 +6,6 @@
|
|||||||
|
|
||||||
> 규범 문서: **`docs/개발용.md`** (정책: `docs/기획용.md`, 흐름 해설: `docs/워크플로우.md`, 타 팀 적용: `docs/인수인계.md`)
|
> 규범 문서: **`docs/개발용.md`** (정책: `docs/기획용.md`, 흐름 해설: `docs/워크플로우.md`, 타 팀 적용: `docs/인수인계.md`)
|
||||||
> **처음 오신 분 / 운영 담당자** → **`docs/운영및유지보수.md`** 부터 보세요 (설치·실행·로그 읽기·트러블슈팅).
|
> **처음 오신 분 / 운영 담당자** → **`docs/운영및유지보수.md`** 부터 보세요 (설치·실행·로그 읽기·트러블슈팅).
|
||||||
> 과제 이력·백로그 → **`TODO.md`** (주요 과제는 전부 종결)
|
|
||||||
|
|
||||||
## 경계
|
## 경계
|
||||||
|
|
||||||
@ -41,12 +40,12 @@ psql -h 127.0.0.1 -U postgres -d negosium_db -f ../../postgres-init/05-anchoring
|
|||||||
|
|
||||||
# 로컬(가상환경)
|
# 로컬(가상환경)
|
||||||
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
|
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
|
||||||
cp config.toml.example config.toml # DB/Redis 채우기 (env 로 대체 가능)
|
cp config.toml.example config.local.toml # DB/Redis 채우기 (APP_ENV 로 local/dev/prod 선택, env 로 대체 가능)
|
||||||
PYTHONPATH=src .venv/bin/python -m anchoring.main --once --dry-run # 예행 연습(DB/Redis 무변경)
|
PYTHONPATH=src .venv/bin/python -m anchoring.main --once --dry-run # 예행 연습(DB/Redis 무변경)
|
||||||
PYTHONPATH=src .venv/bin/python -m anchoring.main --once # 수동 1회(격주 게이트 무시)
|
PYTHONPATH=src .venv/bin/python -m anchoring.main --once # 수동 1회(격주 게이트 무시)
|
||||||
PYTHONPATH=src .venv/bin/python -m anchoring.main # 스케줄러 상주
|
PYTHONPATH=src .venv/bin/python -m anchoring.main # 스케줄러 상주
|
||||||
|
|
||||||
# 도커 — 루트 docker-compose.yml 에 통합됨(anchoring + anchoring-redis). config.toml 은
|
# 도커 — 루트 docker-compose.yml 에 통합됨(anchoring + anchoring-redis). config.local.toml 은
|
||||||
# 이미지에 안 들어가고 마운트되므로 up 전에 파일이 먼저 있어야 한다 (레포 루트에서 실행)
|
# 이미지에 안 들어가고 마운트되므로 up 전에 파일이 먼저 있어야 한다 (레포 루트에서 실행)
|
||||||
(cd ../.. && docker compose up -d --build anchoring anchoring-redis)
|
(cd ../.. && docker compose up -d --build anchoring anchoring-redis)
|
||||||
|
|
||||||
|
|||||||
@ -1,50 +0,0 @@
|
|||||||
# TODO — 앵커링 모듈 후속 과제
|
|
||||||
|
|
||||||
> 남은 과제는 **정책 재확정 + 스펙(v1.3) 개정 사안**이다. 현재 규범(`docs/개발용.md` §12)은
|
|
||||||
> 정적 테이블 변경·파라미터 조정을 봉인하고 있으므로, 착수 전 정책 확정 → 문서 개정 → 구현 순서를 지킨다.
|
|
||||||
> **운영 데이터(조정 이력)가 쌓이기 전에 확정하는 것이 가장 저렴하다** — negodata 적용 전이 적기.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ~~1. 정적 기본 테이블의 가격구간을 계단식으로 재설계~~ ✅ 완료 (2026-07-02)
|
|
||||||
|
|
||||||
**자릿수 계단식 사다리(46칸)로 확정·구현 완료.** 폭 = 구간 상한의 10%(선행 자릿수 밴드):
|
|
||||||
[0, 1,000) 통일 1칸 + 자릿수(1천~1억, 5개)당 9칸 — "1천 원대·2천 원대 … 9천만 원대".
|
|
||||||
1억 초과는 마지막 인덱스(45) 클램프. 사다리 단일 소스 = `constants.UPPER_BOUNDS`,
|
|
||||||
산식 = `bisect_right`. 조정 이력 0건 시점에 적용해 마이그레이션 없음.
|
|
||||||
상세: `docs/개발용.md` §2·§4.1.
|
|
||||||
|
|
||||||
## ~~2. `anchoring_records` — 회사별 앵커링 값 전체 조회 테이블~~ ✅ 뷰로 종결 (2026-07-02)
|
|
||||||
|
|
||||||
**신규 테이블 없이 조회용 뷰 2개로 해결.** 요구(회사별 값 업데이트 리스트업 + 이전 값 판별)는
|
|
||||||
`adjustments` 한 행에 before→after 가 박제되어 있어 이미 충족 — 테이블 추가는 사본만 만든다고
|
|
||||||
판단해 기각하고, 조회를 제품화하는 뷰를 추가했다:
|
|
||||||
|
|
||||||
- `anchoring.value_history` — 회사별 값 변경 이력(이전→새 값, 변화폭, 성공률, 시각)
|
|
||||||
- `anchoring.current_values` — 칸별 현재값(없는 칸 = 시작값 10‰)
|
|
||||||
|
|
||||||
상세: `docs/개발용.md` §6.3, 사용법: `docs/운영및유지보수.md` §8.
|
|
||||||
추후 대시보드에서 "전체 칸 나열(무조정 칸 포함)·페이징" 요구가 생기면 그때 스냅샷 테이블로 승격을 재검토한다.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 백로그 (저우선 — 리뷰에서 식별, 착수 조건 명시)
|
|
||||||
|
|
||||||
- [ ] **전환기 점프 정책 결정 (negodata 적용 직전 필수)**: backend 배포~negodata 적용 사이에
|
|
||||||
학습된 rate 가 적용 순간 한 번에 반영된다("한 계단" 원칙의 1회 예외).
|
|
||||||
적용 직전 `SELECT max(anchoring_value_after) FROM anchoring.current_values` 로 폭 확인 후
|
|
||||||
점프 감수 vs 이력 아카이브·리셋을 결정할 것 — 절차는 `docs/인수인계.md` 적용 순서 ③.
|
|
||||||
|
|
||||||
- [ ] **percent 입력 모드 대비**: `chat_service.send` 는 `user_input_type == "price"` 만 가격으로
|
|
||||||
파싱한다. agent 에 percent 스크립트가 도입되면 percent 턴이 가격 흔적 없이 지나가
|
|
||||||
학습에서 조용히 빠진다(현재 agent 스크립트에 percent 없음 — 잠복). 도입 시
|
|
||||||
percent→price 변환(`target*(100-pct)//100`) 후 동일 경로로 태울 것.
|
|
||||||
- [ ] **스캔 스트리밍**: 배치 스캔이 pending 전량을 메모리에 올린다. 레거시 수백만 행
|
|
||||||
규모 DB 에 첫 적용할 때는 keyset 페이지네이션으로 전환 검토(제외 마킹은 이미 청크
|
|
||||||
커밋이라 트랜잭션 장기화 없음).
|
|
||||||
- [ ] **Redis 통합 테스트**: 자동 스위트는 무Redis(폴백 경로)로 돈다. CI 에 redis 컨테이너가
|
|
||||||
생기면 §11.5 의 re-SET 회복·TTL·오염 값 방어(get_value 범위 검증) 케이스를 자동화.
|
|
||||||
- [ ] **config 오류 메시지**: config.toml 의 오타 키가 TypeError 로 죽는다 — 파일/섹션명을
|
|
||||||
알려주는 검증 메시지로 개선.
|
|
||||||
- [ ] **운영 Redis 인증**: compose 는 127.0.0.1 바인딩으로 방어했지만, 운영 네트워크에서
|
|
||||||
negodata 가 원격 접속하는 구성이면 `requirepass` + `REDIS_PASSWORD` 설정을 적용할 것.
|
|
||||||
@ -1,5 +1,9 @@
|
|||||||
# anchoring 모듈 설정 — config.toml 로 복사 후 채운다 (config.toml 은 gitignore).
|
# anchoring 모듈 설정 템플릿 — 실행 환경에 맞게 복사 후 채운다 (전부 gitignore):
|
||||||
# 우선순위: env(DB_*/REDIS_*/LOG_LEVEL) > 이 파일 > 코드 기본값.
|
# cp config.toml.example config.local.toml # APP_ENV=local (기본)
|
||||||
|
# cp config.toml.example config.dev.toml # APP_ENV=dev
|
||||||
|
# cp config.toml.example config.prod.toml # APP_ENV=prod
|
||||||
|
# 우선순위: env(DB_*/REDIS_*/LOG_LEVEL) > 해당 파일 > 코드 기본값.
|
||||||
|
# local 은 파일이 없어도 기본값으로 동작, dev/prod 는 파일이 없으면 기동 중단.
|
||||||
|
|
||||||
log_level = "info"
|
log_level = "info"
|
||||||
|
|
||||||
|
|||||||
@ -377,14 +377,14 @@ anchoring.current_values -- 칸별 현재값(최신 조정 행). 여기 없는
|
|||||||
- 캐시는 파생값이다. Redis flush가 발생해도 조정 이력에서 완전 복구 가능해야 한다 (MUST).
|
- 캐시는 파생값이다. Redis flush가 발생해도 조정 이력에서 완전 복구 가능해야 한다 (MUST).
|
||||||
- ⚠️ **stale 키는 "미스"가 나지 않는다**: 배치의 DB 커밋 후 SET 실패, 또는 Redis 가 옛 스냅샷(RDB/AOF)으로 재기동하면 옛 rate 가 계속 서빙된다. 그래서 TTL + 주간 re-SET 이중 방어가 MUST 다.
|
- ⚠️ **stale 키는 "미스"가 나지 않는다**: 배치의 DB 커밋 후 SET 실패, 또는 Redis 가 옛 스냅샷(RDB/AOF)으로 재기동하면 옛 rate 가 계속 서빙된다. 그래서 TTL + 주간 re-SET 이중 방어가 MUST 다.
|
||||||
- 멀티 인스턴스 동시 미스 → 결과 동일(최신 조정 행은 하나)하므로 락 불필요.
|
- 멀티 인스턴스 동시 미스 → 결과 동일(최신 조정 행은 하나)하므로 락 불필요.
|
||||||
- 클라이언트: `redis.asyncio` — 사용 주체는 **anchoring 서비스**(배치 SET/re-SET)뿐이다. backend 는 Redis 를 쓰지 않고, **negodata 도 쓰지 않는다**(2026-07-04 적용된 이식판 reader 는 `current_values` 뷰 직조회 — 인수인계 §1. Redis 캐시 전체 제거가 후속 백로그로 확정됨). 설정은 모듈 `config.toml` + `REDIS_HOST/PORT/PASSWORD` env 오버라이드.
|
- 클라이언트: `redis.asyncio` — 사용 주체는 **anchoring 서비스**(배치 SET/re-SET)뿐이다. backend 는 Redis 를 쓰지 않고, **negodata 도 쓰지 않는다**(2026-07-04 적용된 이식판 reader 는 `current_values` 뷰 직조회 — 인수인계 §1. Redis 캐시 전체 제거가 후속 백로그로 확정됨). 설정은 모듈 `config.{APP_ENV}.toml`(local/dev/prod, 기본 local) + `REDIS_HOST/PORT/PASSWORD` env 오버라이드.
|
||||||
- 보안: 무인증 Redis 를 외부 네트워크에 노출 **MUST NOT** — 오염된 rate 는 실제 제안가를 왜곡한다. compose(루트 docker-compose.yml)는 포트를 `127.0.0.1` 로만 바인딩한다. negodata 가 다른 호스트에서 접근해야 하는 배치라면 인증(requirepass)·네트워크 격리 적용 후 개방한다(TODO 백로그).
|
- 보안: 무인증 Redis 를 외부 네트워크에 노출 **MUST NOT** — 오염된 rate 는 실제 제안가를 왜곡한다. compose(루트 docker-compose.yml)는 포트를 `127.0.0.1` 로만 바인딩한다. negodata 가 다른 호스트에서 접근해야 하는 배치라면 인증(requirepass)·네트워크 격리 적용 후 개방한다.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. 배치 잡 명세
|
## 8. 배치 잡 명세
|
||||||
|
|
||||||
- **러너**: `schedules/anchoring` **자립 컨테이너**의 APScheduler(AsyncIOScheduler, `Asia/Seoul`) — 자체 Dockerfile·config.toml 보유(compose 는 루트 docker-compose.yml 에 통합), backend 코드 import 없음. 단일 컨테이너가 곧 스케줄러라 중복 실행이 원천 차단되며(`coalesce=True`, `max_instances=1`, `misfire_grace_time=3600`), 진입점은 `python -m anchoring.main`(상주) / `python -m anchoring.main --once`(수동 1회, 게이트 무시) / `--once --dry-run`(예행 — 아래 dry-run 모드). 플래그는 argparse 로 검증한다 — `--dry-run` 단독(상주에 dry-run 은 없음)·미지의 플래그(오타)는 **기동 전 즉시 에러(종료코드 2)**: 예행인 줄 알고 실제 변경 상주 스케줄러가 뜨는 사고를 차단. `--once` 는 종료 상태가 `done`/`skipped`/`dry_run` 이 아니면(부분 실패 포함) **종료코드 1** 로 끝난다(cron·수동 실행 실패 감지). config.toml 은 이미지에 넣지 않는다(.dockerignore 포함) — compose 가 읽기 전용 마운트하거나 env 로 주입.
|
- **러너**: `schedules/anchoring` **자립 컨테이너**의 APScheduler(AsyncIOScheduler, `Asia/Seoul`) — 자체 Dockerfile·config.{APP_ENV}.toml 보유(APP_ENV 로 local/dev/prod 선택, compose 는 루트 docker-compose.yml 에 통합), backend 코드 import 없음. 단일 컨테이너가 곧 스케줄러라 중복 실행이 원천 차단되며(`coalesce=True`, `max_instances=1`, `misfire_grace_time=3600`), 진입점은 `python -m anchoring.main`(상주) / `python -m anchoring.main --once`(수동 1회, 게이트 무시) / `--once --dry-run`(예행 — 아래 dry-run 모드). 플래그는 argparse 로 검증한다 — `--dry-run` 단독(상주에 dry-run 은 없음)·미지의 플래그(오타)는 **기동 전 즉시 에러(종료코드 2)**: 예행인 줄 알고 실제 변경 상주 스케줄러가 뜨는 사고를 차단. `--once` 는 종료 상태가 `done`/`skipped`/`dry_run` 이 아니면(부분 실패 포함) **종료코드 1** 로 끝난다(cron·수동 실행 실패 감지). config.{env}.toml 은 이미지에 넣지 않는다(.dockerignore 포함) — compose 가 읽기 전용 마운트하거나 env 로 주입. dev/prod 는 파일 부재 시 기동 즉시 중단(local 만 기본값 허용).
|
||||||
- **스케줄**: 매주 토 00:00 KST 트리거(`CronTrigger(day_of_week="sat", hour=0, minute=0)`) + 잡 내부에서 **ISO 주차 % 2 == EVAL_WEEK_PARITY** 격주 게이트 (기준 패리티는 상수 고정 MUST).
|
- **스케줄**: 매주 토 00:00 KST 트리거(`CronTrigger(day_of_week="sat", hour=0, minute=0)`) + 잡 내부에서 **ISO 주차 % 2 == EVAL_WEEK_PARITY** 격주 게이트 (기준 패리티는 상수 고정 MUST).
|
||||||
- **멱등성**: 소비 마킹이 담당 — 같은 배치가 2회 실행돼도 1회차가 마킹한 세션은 2회차 pending에서 빠져 n < 10 스킵. 마킹 UPDATE의 `AND used_by_adjustment_id IS NULL` 조건 + **rowcount = n 검증(불일치 시 전체 롤백) MUST** 가 경합을 차단한다 — 유니크 가드가 없는 구조에서 이중 조정(+2δ)을 막는 유일한 방어선이므로 SHOULD 가 아니라 MUST 다.
|
- **멱등성**: 소비 마킹이 담당 — 같은 배치가 2회 실행돼도 1회차가 마킹한 세션은 2회차 pending에서 빠져 n < 10 스킵. 마킹 UPDATE의 `AND used_by_adjustment_id IS NULL` 조건 + **rowcount = n 검증(불일치 시 전체 롤백) MUST** 가 경합을 차단한다 — 유니크 가드가 없는 구조에서 이중 조정(+2δ)을 막는 유일한 방어선이므로 SHOULD 가 아니라 MUST 다.
|
||||||
- **원자성**: 조정 INSERT 와 세션 마킹은 **같은 DB 세션의 한 트랜잭션**에서 실행한다(MUST). 모듈은 자체 async 엔진(`session_scope`)을 쓰므로 자연 충족된다. (참고: backend 의 `DB_SESSION_MNG.execute_lambda_run`은 db_type 2개 이상을 거부하므로, 이 로직을 backend 로 옮길 경우 단일 DBType 세션으로 실행해야 한다.)
|
- **원자성**: 조정 INSERT 와 세션 마킹은 **같은 DB 세션의 한 트랜잭션**에서 실행한다(MUST). 모듈은 자체 async 엔진(`session_scope`)을 쓰므로 자연 충족된다. (참고: backend 의 `DB_SESSION_MNG.execute_lambda_run`은 db_type 2개 이상을 거부하므로, 이 로직을 backend 로 옮길 경우 단일 DBType 세션으로 실행해야 한다.)
|
||||||
|
|||||||
@ -64,16 +64,19 @@ psql -h <DB호스트> -U <계정> -d negosium_db -f postgres-init/05-anchoring-s
|
|||||||
### STEP 2 — 설정 채우기
|
### STEP 2 — 설정 채우기
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp config.toml.example config.toml
|
# 실행 환경(APP_ENV, 기본 local)에 맞는 파일을 만든다 — local/dev/prod
|
||||||
# config.toml 열어서 [db] 호스트/계정/비밀번호 채우기
|
cp config.toml.example config.local.toml
|
||||||
|
# config.local.toml 열어서 [db] 호스트/계정/비밀번호 채우기 (dev/prod 는 config.dev.toml/config.prod.toml)
|
||||||
```
|
```
|
||||||
|
|
||||||
환경변수로 덮어쓸 수도 있습니다(우선순위: env > config.toml > 기본값):
|
- 환경 선택은 `APP_ENV` 환경변수(기본 `local` → `config.local.toml`). **dev/prod 는 파일이 없으면 기동이 즉시 중단**됩니다(오타·미배치 상태로 로컬 기본값에 붙는 사고 방지). local 은 파일 없이도 코드 기본값으로 뜹니다.
|
||||||
|
|
||||||
|
환경변수로 덮어쓸 수도 있습니다(우선순위: env > config.{APP_ENV}.toml > 기본값):
|
||||||
`DB_HOST` `DB_PORT` `DB_USER` `DB_PASSWORD` `DB_NAME` / `REDIS_HOST` `REDIS_PORT` `REDIS_DB` `REDIS_PASSWORD` / `LOG_LEVEL`
|
`DB_HOST` `DB_PORT` `DB_USER` `DB_PASSWORD` `DB_NAME` / `REDIS_HOST` `REDIS_PORT` `REDIS_DB` `REDIS_PASSWORD` / `LOG_LEVEL`
|
||||||
|
|
||||||
> config.toml 은 **이미지에 들어가지 않습니다**(시크릿이 이미지 레이어에 남는 것을 방지 — .dockerignore 로도 차단).
|
> config.{env}.toml 은 **이미지에 들어가지 않습니다**(시크릿이 이미지 레이어에 남는 것을 방지 — .dockerignore 로도 차단).
|
||||||
> 도커 실행 시 compose 가 읽기 전용 마운트하므로, **`docker compose up` 전에 config.toml 파일이 반드시 존재해야 합니다**
|
> 도커 실행 시 compose 가 읽기 전용 마운트하므로, **`docker compose up` 전에 해당 환경 파일이 반드시 존재해야 합니다**
|
||||||
> (없이 up 하면 docker 가 같은 이름의 디렉터리를 만들어 기동에 실패합니다).
|
> (없이 up 하면 docker 가 같은 이름의 디렉터리를 만들어 기동에 실패합니다). 루트 compose 는 APP_ENV=local + config.local.toml 마운트.
|
||||||
|
|
||||||
### STEP 3-A — 도커로 실행 (운영 권장)
|
### STEP 3-A — 도커로 실행 (운영 권장)
|
||||||
|
|
||||||
@ -180,7 +183,7 @@ docker logs anchoring | tail -20 # 최근 상태
|
|||||||
| **예행 연습** (DB/Redis 무변경, 예상 결과만 로그) | `docker exec anchoring python -m anchoring.main --once --dry-run` — 첫 운영 실행 전 필수 권장 |
|
| **예행 연습** (DB/Redis 무변경, 예상 결과만 로그) | `docker exec anchoring python -m anchoring.main --once --dry-run` — 첫 운영 실행 전 필수 권장 |
|
||||||
| 재기동 | `docker compose restart anchoring` |
|
| 재기동 | `docker compose restart anchoring` |
|
||||||
| 서비스 중지/시작 | `docker compose stop anchoring anchoring-redis` / `docker compose up -d anchoring anchoring-redis` (레포 루트에서) |
|
| 서비스 중지/시작 | `docker compose stop anchoring anchoring-redis` / `docker compose up -d anchoring anchoring-redis` (레포 루트에서) |
|
||||||
| 설정 변경 반영 | config.toml 수정 → `docker compose restart anchoring` (파일은 마운트라 리빌드 불필요) |
|
| 설정 변경 반영 | config.{env}.toml 수정 → `docker compose restart anchoring` (파일은 마운트라 리빌드 불필요) |
|
||||||
| 다음 실행 예정 시각 확인 | `docker logs anchoring \| grep "다음 실행 예정"` |
|
| 다음 실행 예정 시각 확인 | `docker logs anchoring \| grep "다음 실행 예정"` |
|
||||||
| 로그 레벨 올리기(디버깅) | env `LOG_LEVEL=debug` 로 재기동 |
|
| 로그 레벨 올리기(디버깅) | env `LOG_LEVEL=debug` 로 재기동 |
|
||||||
|
|
||||||
@ -189,8 +192,9 @@ docker logs anchoring | tail -20 # 최근 상태
|
|||||||
| 증상 (로그 메시지) | 원인 | 조치 |
|
| 증상 (로그 메시지) | 원인 | 조치 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 기동 실패 + `BaseTableError: 정적 테이블 …` | `resources/anchoring_base.json` 손상/수정됨 | **의도된 안전장치** — git 으로 파일 원복 후 재기동. 이 파일은 절대 수정 금지 |
|
| 기동 실패 + `BaseTableError: 정적 테이블 …` | `resources/anchoring_base.json` 손상/수정됨 | **의도된 안전장치** — git 으로 파일 원복 후 재기동. 이 파일은 절대 수정 금지 |
|
||||||
| 기동 실패 + DB 연결 예외 | config.toml/env 의 DB 접속 정보 오류 | 접속 정보 확인, `psql` 로 직접 접속 테스트 |
|
| 기동 실패 + DB 연결 예외 | config.{env}.toml/env 의 DB 접속 정보 오류(dev/prod 의 CHANGE_ME 미기입 포함) | 접속 정보 확인, `psql` 로 직접 접속 테스트 |
|
||||||
| 기동 실패 + `config.toml` 이 **디렉터리**로 생겨 있음 | config.toml 없이 `docker compose up` — 마운트 대상이 없어 docker 가 디렉터리를 만듦 | `docker compose down` → 디렉터리 삭제 → `cp config.toml.example config.toml` 채우고 재기동 |
|
| 기동 실패 + `설정 파일이 없습니다 (APP_ENV=…)` | dev/prod 인데 config.{env}.toml 미배치 | `cp config.toml.example config.{env}.toml` 채우고 재기동 |
|
||||||
|
| 기동 실패 + `config.local.toml` 이 **디렉터리**로 생겨 있음 | 파일 없이 `docker compose up` — 마운트 대상이 없어 docker 가 디렉터리를 만듦 | `docker compose down` → 디렉터리 삭제 → `cp config.toml.example config.local.toml` 채우고 재기동 |
|
||||||
| `[redis] GET/SET 실패 … DB 폴백` WARN | Redis 다운/네트워크 | **서비스는 계속 정상 동작**(DB 폴백). `docker compose up -d anchoring-redis` 로 복구하면 다음 실행 때 캐시 자동 재적재 |
|
| `[redis] GET/SET 실패 … DB 폴백` WARN | Redis 다운/네트워크 | **서비스는 계속 정상 동작**(DB 폴백). `docker compose up -d anchoring-redis` 로 복구하면 다음 실행 때 캐시 자동 재적재 |
|
||||||
| `redis 실패 누계 get=… set=…` WARN | 위와 동일(회차 요약) | 위와 동일 |
|
| `redis 실패 누계 get=… set=…` WARN | 위와 동일(회차 요약) | 위와 동일 |
|
||||||
| `[redis] 범위 밖 캐시 값 무시(오염 의심)` WARN | 누군가/다른 프로세스가 Redis 에 비정상 값을 씀 | 동작엔 문제 없음(자동 무시 + DB 폴백 + 재적재로 자가 교정). 반복되면 Redis 접근 경로 점검 — 포트가 외부에 열려 있지 않은지(`127.0.0.1` 바인딩) 확인 |
|
| `[redis] 범위 밖 캐시 값 무시(오염 의심)` WARN | 누군가/다른 프로세스가 Redis 에 비정상 값을 씀 | 동작엔 문제 없음(자동 무시 + DB 폴백 + 재적재로 자가 교정). 반복되면 Redis 접근 경로 점검 — 포트가 외부에 열려 있지 않은지(`127.0.0.1` 바인딩) 확인 |
|
||||||
|
|||||||
@ -1,13 +1,17 @@
|
|||||||
"""설정 — config.toml + env 오버라이드(env > toml > 기본값).
|
"""설정 — config.{APP_ENV}.toml + env 오버라이드(env > toml > 기본값).
|
||||||
|
|
||||||
자립 모듈: backend config 체계를 쓰지 않는다. 시크릿은 config.toml(.gitignore) 또는 env 로.
|
자립 모듈: backend config 체계를 쓰지 않지만 선택 방식은 동일 — 환경변수 APP_ENV(기본 local)로
|
||||||
|
config.local.toml / config.dev.toml / config.prod.toml 을 고른다(전부 .gitignore, 시크릿은 파일 또는 env 로).
|
||||||
|
local 은 파일이 없어도 코드 기본값으로 뜨지만(개발 편의), 그 외 환경은 파일이 없으면
|
||||||
|
즉시 중단한다 — 오타/미배치 상태로 기본값(로컬 DB)에 붙는 사고 방지.
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
import tomllib
|
import tomllib
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
_CONFIG_PATH = Path(__file__).resolve().parents[2] / "config.toml"
|
APP_ENV = os.environ.get("APP_ENV", "local")
|
||||||
|
_CONFIG_PATH = Path(__file__).resolve().parents[2] / f"config.{APP_ENV}.toml"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -45,6 +49,11 @@ def _env(name: str, current, cast=str):
|
|||||||
|
|
||||||
def load_config() -> Config:
|
def load_config() -> Config:
|
||||||
cfg = Config()
|
cfg = Config()
|
||||||
|
if not _CONFIG_PATH.exists() and APP_ENV != "local":
|
||||||
|
raise FileNotFoundError(
|
||||||
|
f"설정 파일이 없습니다: {_CONFIG_PATH} (APP_ENV={APP_ENV}). "
|
||||||
|
f"config.toml.example 을 복사해 채우거나 APP_ENV=local 로 실행하세요."
|
||||||
|
)
|
||||||
if _CONFIG_PATH.exists():
|
if _CONFIG_PATH.exists():
|
||||||
data = tomllib.loads(_CONFIG_PATH.read_text())
|
data = tomllib.loads(_CONFIG_PATH.read_text())
|
||||||
db = data.get("db", {})
|
db = data.get("db", {})
|
||||||
|
|||||||
@ -16,7 +16,7 @@ import sys
|
|||||||
|
|
||||||
from anchoring.base_table import load_base_table
|
from anchoring.base_table import load_base_table
|
||||||
from anchoring.batch import run_evaluation_batch
|
from anchoring.batch import run_evaluation_batch
|
||||||
from anchoring.config import load_config
|
from anchoring.config import APP_ENV, load_config
|
||||||
from anchoring.constants import PRICE_RANGE_COUNT
|
from anchoring.constants import PRICE_RANGE_COUNT
|
||||||
from anchoring.db import dispose_engine, init_engine
|
from anchoring.db import dispose_engine, init_engine
|
||||||
from anchoring.log import LOG, configure
|
from anchoring.log import LOG, configure
|
||||||
@ -44,6 +44,7 @@ async def _run(once: bool, dry_run: bool = False) -> None:
|
|||||||
cfg = load_config()
|
cfg = load_config()
|
||||||
configure(cfg.log_level)
|
configure(cfg.log_level)
|
||||||
|
|
||||||
|
LOG.info(f"[main] 설정 로드 — APP_ENV={APP_ENV} (config.{APP_ENV}.toml, env 오버라이드 우선)")
|
||||||
load_base_table() # 검증 실패 시 BaseTableError → 기동 중단
|
load_base_table() # 검증 실패 시 BaseTableError → 기동 중단
|
||||||
LOG.info(f"[main] 정적 기본 테이블 로드·검증 완료 ({PRICE_RANGE_COUNT}칸 사다리)")
|
LOG.info(f"[main] 정적 기본 테이블 로드·검증 완료 ({PRICE_RANGE_COUNT}칸 사다리)")
|
||||||
init_engine(cfg)
|
init_engine(cfg)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user