feat(multi-instance): dcy-009 BAT-013 — RabbitMQ 다중 인스턴스 토폴로지 (REQ-048~051)
dcy-009(다중 인스턴스 RabbitMQ 토폴로지) 전체 SDLC: draft-req → confirm-req →
draft-batch(BAT-013) → run-batch-delivery → confirm-req-implemented.
- REQ-048 (Configuration): DaemonInstanceConfig.from_toml + DaemonConfig.from_config
(env 병합, portfolio_hub_token 주입) + 데몬 --config 필수 argparse + .gitignore 인스턴스
TOML + config/instance.example.toml
- REQ-049 (Interface): topology 동적 큐명 빌더 intent/snapshot/control_queue(instance_key)
— default=리터럴 큐명(하위호환), named=`{base}.{instance_key}` + daemon per-instance 구독
- REQ-050 (Integration): trex.direct(topic)/trex.broadcast(fanout) exchange +
declare_topology(instance_key) per-instance binding + control dual binding + direct_routing_key
- REQ-051 (Documentation): interface-contract/runtime-flows/project-structure 정합
하위호환: instance_key="default" → 기존 리터럴 큐명·RPC 경로 무회귀(FR-5).
split-brain 방지(NFR-3): 인스턴스 전용 큐. 민감값(NFR-4): TOML + gitignore.
검증: 신규 9 단위 + RabbitMQ 라우팅 2, 단위 271 passed,
인프라 게이트 301 passed(directed 격리 S-2 + broadcast dual S-3 + default 무회귀 S-4),
ruff clean. REQ-048~051 Implemented, BAT-013 release-candidate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -5,3 +5,7 @@ __pycache__/
|
||||
.pytest_cache/
|
||||
/.env
|
||||
/.vscode/
|
||||
# BAT-013 / REQ-048: per-instance TOML configs hold instance_key + portfolio_hub_token
|
||||
# (secrets) — never commit. The tracked example below is exempt.
|
||||
/config/*.toml
|
||||
!/config/instance.example.toml
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# trex-core per-instance config (BAT-013 / REQ-048).
|
||||
# Copy to e.g. config/inst-1.toml and pass with: trex-core-daemon --config config/inst-1.toml
|
||||
# Real per-instance files are gitignored (they hold the portfolio-hub token).
|
||||
|
||||
# Instance identifier. Drives queue names:
|
||||
# default -> trex.intent.req / trex.snapshot.req / trex.control.req (legacy literal)
|
||||
# <other> -> trex.intent.req.<instance_key> etc + trex.direct routing key {instance_key}.{channel}
|
||||
instance_key = "default"
|
||||
|
||||
# portfolio-hub auth token (kept out of .env). Injected into PortfolioHubConfig.token
|
||||
# when TREX_PORTFOLIO_HUB_BASE_URL is set in the environment.
|
||||
portfolio_hub_token = ""
|
||||
@@ -0,0 +1,51 @@
|
||||
# BAT-013 Design
|
||||
|
||||
- Status: confirmed
|
||||
- Batch: BAT-013
|
||||
|
||||
## Architecture Summary
|
||||
다중 인스턴스가 동일 RabbitMQ 에서 split-brain 없이 운영되도록 토폴로지·설정을 인스턴스화한다.
|
||||
|
||||
1. **동적 큐명**(`topology.py`): `intent_queue/snapshot_queue/control_queue(instance_key)`. `instance_key in (None,"default")` → 기존 리터럴(`trex.intent.req` 등), 그 외 → `{base}.{instance_key}`. 기존 상수는 default 값으로 보존(하위호환).
|
||||
2. **exchange**(`topology.py`+`rabbitmq.py`): `trex.direct`(topic,durable) + `trex.broadcast`(fanout,durable). `declare_topology(instance_key)` 가 인스턴스 큐를 direct 에 `{instance_key}.{intent|control|snapshot}` 로 바인딩 + control 큐를 broadcast 에 dual 바인딩.
|
||||
3. **설정**(`config.py`): `DaemonInstanceConfig(instance_key, portfolio_hub_token)` + `from_toml(path)`(tomllib) + `DaemonConfig.from_config(toml_path)` 가 `from_env()` 결과에 TOML 을 병합(`portfolio_hub_token` → PortfolioHubConfig.token).
|
||||
4. **진입점**(`daemon.main`): argparse `--config` 필수. instance_key 를 declare/consume 에 일관 전달.
|
||||
|
||||
## Changed Areas
|
||||
- Docs: interface-contract.md(routing key)·runtime-flows.md(TOML 기동/라우팅)·project-structure.md(동적 큐명/설정/exchange) — REQ-051.
|
||||
- Modules:
|
||||
- `trex_core/adapters/messaging/topology.py` — 큐명 빌더 + DIRECT/BROADCAST exchange 상수 + `direct_routing_key`
|
||||
- `trex_core/adapters/messaging/rabbitmq.py` — `declare_topology(instance_key="default")` + exchange 선언 + binding(direct per-instance + control dual) + (필요시) broadcast publish helper
|
||||
- `trex_core/runtime/config.py` — `DaemonInstanceConfig` + `from_toml` + `from_config`
|
||||
- `trex_core/runtime/daemon.py` — `--config` argparse(main) + instance_key 보관 + consumer 큐명에 instance_key 적용 + declare_topology(instance_key)
|
||||
- `.gitignore` — 인스턴스 TOML 패턴
|
||||
- Interfaces:
|
||||
- `intent_queue(instance_key) -> str` 등 (default 하위호환)
|
||||
- `declare_topology(instance_key="default")`
|
||||
- `direct_routing_key(instance_key, channel_type) -> "{instance_key}.{channel_type}"`
|
||||
- `DaemonInstanceConfig.from_toml(path)`, `DaemonConfig.from_config(toml_path)`
|
||||
|
||||
## Key Decisions
|
||||
1. **default = 리터럴 큐명(특수 처리)**: FR-2(suffix)와 FR-5(default 무회귀)의 긴장을 해소 — named instance 만 suffix, default 는 기존 이름 유지. 기존 messaging 테스트/배포 무변경. (planning R-1)
|
||||
2. **declare_topology 기본 인자 `instance_key="default"`**: 기존 호출부(`transport.declare_topology()`)가 인자 없이도 default 동작 → 호출부 광범위 수정 회피.
|
||||
3. **exchange 는 항상 선언(default 포함)**: direct/broadcast 는 default 인스턴스에서도 선언·바인딩하되, 기존 직접 큐 reply-to RPC 경로는 그대로 동작(추가 바인딩은 비파괴적). 기존 RPC 회귀 없음(FR-5/AC-6).
|
||||
4. **TOML 우선 병합**: `from_config` 는 `from_env()` 로 베이스 구성 후 TOML 의 `portfolio_hub_token` 을 PortfolioHubConfig.token 으로 override. `instance_key` 는 TOML 전용.
|
||||
5. **`--config` 필수**: argparse required. 누락 시 argparse 가 usage 출력 + exit≠0(OQ-2). 단, `from_env()` 단독 호출(테스트/라이브러리)은 계속 가능.
|
||||
6. **pika per-thread**: 바인딩 추가만, consumer 스레드/커넥션 수 불변(NFR-2).
|
||||
|
||||
## Edge Cases
|
||||
- default 큐명 충돌 없음: `*.default` 를 쓰지 않고 리터럴 사용.
|
||||
- TOML 누락 필드/파일 부재 → 명확한 예외(FR-1/AC-4).
|
||||
- broadcast 중복 control 전달 → 핸들러 idempotency(REQ-036/advisory lock, R-3).
|
||||
- 기존 old 큐 잔류 → 자연 소멸(OQ-1), 코드 무관.
|
||||
- snapshot/intent reply-to RPC: 클라이언트가 direct routing key 로 보내든 직접 큐로 보내든, 인스턴스 큐가 direct 바인딩 + 직접 publish 둘 다 수신 가능(하위호환).
|
||||
|
||||
## Architecture Impact
|
||||
- runtime-flows: TOML 기동 + directed/broadcast 라우팅 + split-brain 방지 — REQ-051.
|
||||
- project-structure: topology 동적화 + 설정 계층 + exchange — REQ-051.
|
||||
- interface-contract: routing key 계약 — REQ-051.
|
||||
|
||||
## Reference Doc Update Plan
|
||||
- interface-contract.md: routing key(`{instance_key}.{channel_type}`)+broadcast+큐명 규칙 (REQ-051 AC-1)
|
||||
- runtime-flows.md: `--config` 기동 + directed/broadcast + split-brain (REQ-051 AC-2)
|
||||
- project-structure.md: 동적 큐명/DaemonInstanceConfig/`--config`/exchange (REQ-051 AC-3)
|
||||
@@ -0,0 +1,45 @@
|
||||
# BAT-013 Implementation
|
||||
|
||||
- Status: completed
|
||||
- Batch: BAT-013
|
||||
|
||||
## Plan Summary
|
||||
- 순서: REQ-049 동적 큐명/declare_topology → REQ-050 exchange/binding/routing → REQ-048 TOML 로더/--config → REQ-051 baseline. 각 단계 단위 green 후 진행. default 하위호환 회귀 우선.
|
||||
|
||||
## Changed Files
|
||||
- (구현 시 기록)
|
||||
|
||||
### 신규 (예정)
|
||||
- `tests/runtime/test_instance_config.py` (REQ-048)
|
||||
- `tests/adapters/messaging/test_instance_topology.py` (REQ-049/050)
|
||||
- 인스턴스 TOML 예시(`config/instance.example.toml`, 추적) (REQ-048)
|
||||
|
||||
### 수정 (예정)
|
||||
- `trex_core/adapters/messaging/topology.py` (동적 큐명 + DIRECT/BROADCAST + routing key)
|
||||
- `trex_core/adapters/messaging/rabbitmq.py` (declare_topology(instance_key) + exchange/binding)
|
||||
- `trex_core/runtime/config.py` (DaemonInstanceConfig + from_toml + from_config)
|
||||
- `trex_core/runtime/daemon.py` (--config argparse + instance_key 큐 적용)
|
||||
- `.gitignore` (인스턴스 TOML)
|
||||
- baseline 3문서 (REQ-051)
|
||||
|
||||
## Changed Files (실제)
|
||||
- 신규: `config/instance.example.toml`(추적), `tests/runtime/test_instance_config.py`, `tests/adapters/messaging/test_instance_topology.py`.
|
||||
- 수정: `topology.py`(동적 큐명 빌더 + DEFAULT_INSTANCE_KEY/DIRECT_EXCHANGE/BROADCAST_EXCHANGE + direct_routing_key), `rabbitmq.py`(declare_topology(instance_key) + exchange/binding/control dual + import 정리), `config.py`(DaemonInstanceConfig+from_toml+from_config, DaemonConfig.instance_key), `daemon.py`(--config argparse main + per-instance 큐 구독 + declare_topology(instance_key)), `.gitignore`(인스턴스 TOML), baseline 3문서(REQ-051).
|
||||
|
||||
## Execution Log
|
||||
1. REQ-049 동적 큐명(default 리터럴) + declare_topology(instance_key) + daemon per-instance consume → 빌더 단위 + messaging 회귀 green.
|
||||
2. REQ-050 trex.direct/trex.broadcast exchange + binding + routing key → 인프라 게이트(directed 격리 + broadcast dual) green.
|
||||
3. REQ-048 DaemonInstanceConfig.from_toml + from_config 병합 + --config argparse + .gitignore + example TOML → 로더 단위 + `--config` 누락 subprocess 종료 green.
|
||||
4. REQ-051 baseline 3문서 정합.
|
||||
5. 전체 회귀: 단위 271 passed / 30 skipped, ruff clean.
|
||||
6. 인프라 게이트(PG+RabbitMQ): 301 passed (directed 라우팅 격리 S-2 + broadcast dual S-3 + default 무회귀 S-4 포함).
|
||||
|
||||
## Validation
|
||||
- 단위: `pytest -q --deselect ...::test_sandbox_token_and_submit` → 271 passed / 30 skipped.
|
||||
- 인프라 게이트: `TREX_RABBITMQ_TEST_URL=... TREX_POSTGRESQL_TEST_DSN=... pytest -q` → 301 passed.
|
||||
- lint: `ruff check trex_core tests` → All passed.
|
||||
|
||||
## Remaining Risks
|
||||
- R-1(declare_topology 변경 회귀): default=리터럴 큐명 하위호환으로 기존 messaging 테스트 무변경 — 301 passed 로 확인(FR-5/NFR-1).
|
||||
- R-2(old 큐 잔류): 운영 자연 소멸(OQ-1), 코드 무관.
|
||||
- (이월, 비차단) 실 2-인스턴스 동시 기동 manual e2e: directed 격리·broadcast 는 인프라 게이트 통합 테스트로 검증됨. 실 배포 시 다중 인스턴스 운영 시점 확인.
|
||||
@@ -0,0 +1,33 @@
|
||||
# BAT-013: 다중 인스턴스 RabbitMQ 토폴로지 — TOML config + instance_key 큐 + direct/broadcast exchange
|
||||
|
||||
- Status: release-candidate
|
||||
- Profile: standard
|
||||
- Source Discovery: dcy-009
|
||||
- Included REQ: REQ-048, REQ-049, REQ-050, REQ-051
|
||||
|
||||
## Summary
|
||||
- dcy-009(다중 인스턴스 RabbitMQ 토폴로지)를 단일 standard batch 로 delivery 한다. per-instance TOML 설정 로더 + `--config` 필수(REQ-048), `instance_key` 기반 동적 큐명 + default 하위호환(REQ-049), `trex.direct`(topic)/`trex.broadcast`(fanout) exchange + 바인딩(REQ-050), baseline 3문서 정합(REQ-051). 다중 인스턴스를 동일 RabbitMQ 에 붙여도 인스턴스 전용 큐로 split-brain(WatchdogStore/SafetySellService 경쟁 소비)을 구조적으로 방지한다.
|
||||
|
||||
## Scope
|
||||
- In Scope:
|
||||
- `DaemonInstanceConfig.from_toml` + `--config` 필수 진입점 + env 병합 + `.gitignore` TOML (REQ-048)
|
||||
- `topology.py` 동적 큐명 빌더(`intent_queue/snapshot_queue/control_queue(instance_key)`, default=리터럴 하위호환) + `declare_topology(instance_key)` + daemon per-instance consume (REQ-049)
|
||||
- `trex.direct`(topic) + `trex.broadcast`(fanout) exchange + per-instance/control dual binding + routing key 빌더 (REQ-050)
|
||||
- baseline 3문서(interface-contract/runtime-flows/project-structure) (REQ-051)
|
||||
- Out of Scope:
|
||||
- strategy 모듈(orb-v1) publish 코드 변경 — 계약만 정의
|
||||
- WatchdogStore Redis 외부화 / leader election / failover
|
||||
- 전체 env 의 TOML 일원화(KIS/DSN/RABBITMQ_URL 은 `.env` 유지)
|
||||
- old 큐 자동 삭제 스크립트(운영 절차)
|
||||
|
||||
## Documents
|
||||
- Planning: [planning.md](./planning.md)
|
||||
- Design: [design.md](./design.md)
|
||||
- Implementation: [implementation.md](./implementation.md)
|
||||
- Verification: [verification.md](./verification.md)
|
||||
|
||||
## Notes
|
||||
- Source discovery 단일(dcy-009). OQ-1(old 큐 자연 소멸)·OQ-2(`--config` 필수) Answered.
|
||||
- 하위호환 핵심(REQ-049): `instance_key="default"` → 리터럴 큐명 → 기존 테스트/배포 무회귀(FR-5/NFR-1).
|
||||
- NFR-2(pika per-thread) 유지, NFR-3(split-brain) 인스턴스 전용 큐로 달성, NFR-4(민감값) TOML+gitignore.
|
||||
- multi-instance 라우팅(directed/broadcast)·split-brain 격리는 인프라 게이트(RabbitMQ) 통합/e2e 로 검증.
|
||||
@@ -0,0 +1,48 @@
|
||||
# BAT-013 Planning
|
||||
|
||||
- Status: confirmed
|
||||
- Batch: BAT-013
|
||||
- Profile: standard
|
||||
|
||||
## Included REQ
|
||||
- REQ-048 (Configuration): `DaemonInstanceConfig.from_toml` (tomllib) + `--config` 필수 진입점 + `from_config` env 병합(`portfolio_hub_token` 주입) + `.gitignore` TOML + 토큰 로그 미노출.
|
||||
- REQ-049 (Interface): `topology.py` 동적 큐명 빌더(default=리터럴 하위호환) + `declare_topology(instance_key)` + daemon intent/snapshot/control consumer per-instance 큐 구독.
|
||||
- REQ-050 (Integration): `trex.direct`(topic)/`trex.broadcast`(fanout) exchange + per-instance direct binding + control dual binding + `direct_routing_key` 빌더.
|
||||
- REQ-051 (Documentation): interface-contract(routing key)/runtime-flows(TOML 기동·라우팅)/project-structure(동적 큐명·설정 계층·exchange) 정합.
|
||||
|
||||
## Delivery Plan
|
||||
1. REQ-049: `topology.py` 큐명 빌더(default 하위호환) → `rabbitmq.declare_topology(instance_key)` → daemon consumer per-instance. 큐명 단위 + 기존 회귀.
|
||||
2. REQ-050: `trex.direct`/`trex.broadcast` exchange 선언 + binding(direct per-instance, control dual) + routing key 빌더. 토폴로지 단위 + 인프라 게이트(라우팅 격리/broadcast).
|
||||
3. REQ-048: `DaemonInstanceConfig.from_toml` + `--config` argparse 진입점 + `from_config` 병합 + `.gitignore` + 토큰 미노출. 로더 단위.
|
||||
4. REQ-051: baseline 3문서 정합.
|
||||
5. 전체 회귀 + ruff + 인프라 게이트(default 무회귀 + 라우팅) + (가능 시) 2-인스턴스 e2e.
|
||||
|
||||
## Dependencies
|
||||
- Internal:
|
||||
- dcy-006/BAT-007 messaging(topology/rabbitmq/daemon consumer) — 변경 대상 기반.
|
||||
- BAT-012 control 채널(`trex.control.req`) — control 큐가 broadcast dual binding 대상.
|
||||
- BAT-008 PortfolioHubConfig.token — TOML 주입 지점.
|
||||
- External:
|
||||
- RabbitMQ(topic/fanout exchange). stdlib `tomllib`(py3.12).
|
||||
|
||||
## Design Gate
|
||||
- Design Required Before Implementation: yes
|
||||
- Reason: (a) `declare_topology` signature 변경 + 큐명 동적화는 **모든 messaging consumer/test 표면**에 영향 — default 하위호환 규칙(리터럴 vs suffix)을 설계에서 고정해야 회귀를 막는다. (b) topic/fanout exchange + per-instance/dual binding 의 정확한 binding 매트릭스와 routing key 계약을 설계에서 확정해야 strategy 계약과 일치한다. (c) TOML+env 병합 우선순위(`portfolio_hub_token` TOML 우선) + `--config` 진입점 구조를 설계에서 명시. → design.md 동일 턴 scaffold.
|
||||
|
||||
## Milestones
|
||||
1. M1 — REQ-049 동적 큐명 + declare_topology(instance_key) + default 회귀 green
|
||||
2. M2 — REQ-050 exchange + binding + routing key + 인프라 게이트(라우팅) green
|
||||
3. M3 — REQ-048 TOML 로더 + --config + 병합 + .gitignore 단위 green
|
||||
4. M4 — REQ-051 baseline 3문서
|
||||
5. M5 — 전체 회귀 + ruff + 인프라 게이트 + 2-인스턴스 e2e(가능 시)
|
||||
|
||||
## Risks
|
||||
- R-1 (Medium): `declare_topology`/큐명 변경이 기존 messaging 테스트 다수에 영향. 대응: default=리터럴 큐명 하위호환으로 기존 테스트 무변경 유지(REQ-049 핵심). 빌더에 default 특수 처리.
|
||||
- R-2 (Medium): old durable 큐 잔류(dcy-009 R-2). 대응: 자연 소멸 허용(OQ-1), 배포 절차 외. 코드 무관.
|
||||
- R-3 (Low): broadcast 중복 제어(dcy-009 R-3). 대응: kill-switch/pause idempotency(REQ-036/advisory lock).
|
||||
- R-4 (Low): TOML 토큰 커밋 노출(dcy-009 R-4). 대응: `.gitignore` + 로그 미노출(REQ-048).
|
||||
- R-5 (Low): pika BlockingConnection thread-safety. 대응: per-thread connection 유지(NFR-2), 바인딩만 추가.
|
||||
|
||||
## Notes
|
||||
- 하위호환 우선: default 인스턴스는 기존 리터럴 큐명·RPC 경로 그대로 → 기존 277+ 테스트 무회귀가 1차 게이트.
|
||||
- 2-인스턴스 e2e(라우팅 격리/broadcast)는 RabbitMQ 인프라 게이트 또는 manual; 단일 인스턴스 기본 검증은 단위로 충분.
|
||||
@@ -0,0 +1,64 @@
|
||||
# BAT-013 Verification
|
||||
|
||||
- Status: verified
|
||||
- Batch: BAT-013
|
||||
|
||||
## Acceptance Mapping
|
||||
> 구현 완료 후 각 AC 에 evidence 매핑. 아래는 계획 골격.
|
||||
|
||||
### REQ-048 (TOML 로더 + --config)
|
||||
| AC | Evidence (예정) |
|
||||
|----|---------|
|
||||
| AC-1 from_toml 파싱 | 로더 단위 |
|
||||
| AC-2 --config 누락 종료 | 진입점 단위/검사 |
|
||||
| AC-3 token 주입 | 로더 단위(S-5) |
|
||||
| AC-4 필수 필드 누락 오류 | 로더 단위 |
|
||||
| AC-5 .gitignore TOML | gitignore 확인 |
|
||||
| AC-6 token 로그 미노출 | 코드/로그 검사 |
|
||||
|
||||
### REQ-049 (동적 큐명 + 하위호환)
|
||||
| AC | Evidence (예정) |
|
||||
|----|---------|
|
||||
| AC-1 named suffix | 큐명 빌더 단위 |
|
||||
| AC-2 default 리터럴 | 동일 |
|
||||
| AC-3 declare named 큐 | 인프라 게이트 |
|
||||
| AC-4 default 회귀 없음 | 전체 pytest |
|
||||
| AC-5 per-thread/스레드 수 | daemon 검사 |
|
||||
| AC-6 인스턴스 격리 | 인프라 게이트/e2e |
|
||||
|
||||
### REQ-050 (exchange + binding)
|
||||
| AC | Evidence (예정) |
|
||||
|----|---------|
|
||||
| AC-1 direct/broadcast 선언 | 인프라 게이트 |
|
||||
| AC-2 direct binding | 인프라 게이트 |
|
||||
| AC-3 routing key 빌더 | 단위 |
|
||||
| AC-4 directed 격리 | 인프라 게이트(2큐) |
|
||||
| AC-5 broadcast dual 전달 | 인프라 게이트(2큐) |
|
||||
| AC-6 default 회귀 | 전체 pytest |
|
||||
|
||||
### REQ-051 (baseline 정합)
|
||||
| AC | Evidence (예정) |
|
||||
|----|---------|
|
||||
| AC-1 interface-contract routing key | 문서 diff |
|
||||
| AC-2 runtime-flows 기동/라우팅 | 문서 diff |
|
||||
| AC-3 project-structure 동적큐/설정/exchange | 문서 diff |
|
||||
|
||||
## Evidence
|
||||
- Test:
|
||||
- REQ-048 로더 6 단위(from_toml/필수 누락/token 주입 S-5/default ik/`--config` 누락 subprocess 종료/gitignore) green.
|
||||
- REQ-049/050 토폴로지 3 단위(builders default vs named/routing key) + 인프라 게이트 2(directed 격리 S-2 / broadcast dual S-3) green.
|
||||
- 단위 전체 회귀: `271 passed / 30 skipped`(`::test_sandbox_token_and_submit` deselect).
|
||||
- 인프라 게이트(PG trex_core_test + RabbitMQ): `301 passed`(directed 라우팅 격리 + broadcast dual + default 무회귀 S-4 + 마이그레이션 round-trip 포함).
|
||||
- `ruff check trex_core tests` → All passed.
|
||||
- Logs: `271 passed, 30 skipped` / `301 passed ... ~15s`.
|
||||
|
||||
## Carried-Over / e2e
|
||||
- S-1(named 큐 선언)·S-2(directed 격리)·S-3(broadcast dual): RabbitMQ 인프라 게이트 통합 테스트로 확인.
|
||||
- S-4(default 무회귀): 전체 pytest 301 passed.
|
||||
- S-5(TOML token 주입): 로더 단위.
|
||||
- (이월, 비차단) 실 2-인스턴스 동시 기동 manual e2e: 실 다중 인스턴스 운영 시점 확인.
|
||||
|
||||
## Result
|
||||
- Ready for Release: true
|
||||
- Blocking Issues:
|
||||
- 없음. 제어 큐 rename(BAT-012 R-1)·old 큐 자연 소멸(OQ-1)·strategy publish 변경(Out of Scope)·2-인스턴스 manual e2e 는 비차단(인프라 게이트 통합으로 핵심 라우팅 검증됨).
|
||||
@@ -20,3 +20,4 @@
|
||||
| BAT-010 | standard | release-candidate | REQ-034, REQ-035, REQ-036, REQ-041 | dcy-007 | [bat-010](./bat-010_20260607_safety-sell-foundation/) |
|
||||
| BAT-011 | standard | release-candidate | REQ-037, REQ-038, REQ-039, REQ-040, REQ-042 | dcy-007 | [bat-011](./bat-011_20260607_safety-sell-triggers/) |
|
||||
| BAT-012 | standard | release-candidate | REQ-043, REQ-044, REQ-045, REQ-046, REQ-047 | dcy-008 | [bat-012](./bat-012_20260607_kill-switch-control/) |
|
||||
| BAT-013 | standard | release-candidate | REQ-048, REQ-049, REQ-050, REQ-051 | dcy-009 | [bat-013](./bat-013_20260607_multi-instance-topology/) |
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# 0. 문서 상태
|
||||
|
||||
- 상태: draft
|
||||
- 상태: confirmed
|
||||
- 문서 ID: dcy-009_20260607_multi-instance-rabbitmq-topology
|
||||
- 이슈명: 인스턴스별 독립 RabbitMQ 채널 토폴로지 — trex-core 다중 인스턴스 운영을 위한 Topic Exchange + Fanout Exchange per-instance 큐 분리
|
||||
- 작성 시각(KST): 2026-06-07
|
||||
- 마지막 갱신 시각(KST): 2026-06-07 (instance_id → instance_key, TOML config 설계 반영)
|
||||
- 마지막 갱신 시각(KST): 2026-06-07 (draft-req: REQ-048~051 생성)
|
||||
- 대체됨: 없음
|
||||
- 후속 Discovery 참조: 없음
|
||||
- 생성된 REQ 참조: 없음
|
||||
- 생성된 REQ 참조: REQ-048, REQ-049, REQ-050, REQ-051
|
||||
|
||||
# 1. 계획 상태 요약
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
- trex-core 데몬 운영자: `--config {path}.toml` CLI 인자로 인스턴스별 설정 파일 지정 + 큐 모니터링
|
||||
- 제약사항
|
||||
- pika `BlockingConnection`은 thread-safe하지 않다 — 현재 consumer 스레드별 독립 Connection 패턴은 변경 없이 유지해야 한다.
|
||||
- 기존 durable 큐(`trex.intent.req` 등)가 이미 RabbitMQ 서버에 선언되어 있을 수 있다 → 새 큐명으로 전환 시 old 큐 처리 절차가 필요하다(OQ-1).
|
||||
- 기존 durable 큐(`trex.intent.req` 등)가 이미 RabbitMQ 서버에 선언되어 있을 수 있다 → 새 큐명으로 전환 후 자연 소멸을 허용하며, RabbitMQ admin console에서 적절한 시점에 수동 삭제한다(OQ-1 Answered).
|
||||
- strategy 모듈(orb-v1)이 routing key 변경에 맞게 publish 코드를 업데이트해야 한다 — 인터페이스 계약 정의가 선행되어야 한다.
|
||||
- `portfolio_hub_token`과 `instance_key`는 `.env`에 넣지 않는다 — 기존 `config.py`의 `from_env()` 패턴 단독으로는 이 원칙을 지킬 수 없으므로 TOML 로더가 필수다.
|
||||
|
||||
@@ -77,14 +77,14 @@
|
||||
- `docs/interface-contract.md`: routing key 계약 갱신 — `{instance_key}.{channel_type}` 규칙 추가
|
||||
- 운영 영향:
|
||||
- 인스턴스별 TOML 파일 생성 및 관리 필요(`instance_key`, `portfolio_hub_token` 포함)
|
||||
- 기존 `trex.intent.req` 등 old 큐가 RabbitMQ에 잔류할 경우 배포 절차에 old 큐 정리 단계 필요(OQ-1)
|
||||
- 기존 `trex.intent.req` 등 old 큐는 새 큐 전환 후 자연 소멸을 허용한다. RabbitMQ admin console에서 적절한 시점에 수동 삭제(OQ-1 Answered).
|
||||
- `TREX_PORTFOLIO_HUB_TOKEN` env var는 TOML로 이관 후 `.env`에서 제거 필요
|
||||
|
||||
# 5. 요구사항 목록
|
||||
|
||||
## 기능 요구사항
|
||||
|
||||
- FR-1: 데몬 기동 시 `--config <path>.toml` CLI 인자를 지원한다. TOML 파일에서 최소 `instance_key`와 `portfolio_hub_token`을 읽는다. 이 두 값은 `.env`에서 읽지 않는다.
|
||||
- FR-1: 데몬 기동 시 `--config <path>.toml` CLI 인자는 필수다. 인자 없이 기동하면 오류 메시지를 출력하고 즉시 종료한다. TOML 파일에서 최소 `instance_key`와 `portfolio_hub_token`을 읽는다. 이 두 값은 `.env`에서 읽지 않는다.
|
||||
- FR-2: `instance_key` 값을 기반으로 모든 수신 큐명을 `{base}.{instance_key}` 형식으로 suffix화한다.
|
||||
- FR-3: `trex.direct`(type: topic, durable) exchange를 선언하고, 각 인스턴스의 intent/control/snapshot 큐를 routing key `{instance_key}.intent`, `{instance_key}.control`, `{instance_key}.snapshot`으로 바인딩한다.
|
||||
- FR-4: `trex.broadcast`(type: fanout, durable) exchange를 선언하고, 각 인스턴스의 control 큐를 이 exchange에 추가 바인딩(dual binding)한다. 전체 인스턴스 대상 제어 명령은 이 exchange에 publish한다.
|
||||
@@ -124,7 +124,7 @@
|
||||
- R-1 (Medium): strategy 모듈이 routing key를 변경하지 않으면 다중 인스턴스 환경에서 메시지가 올바른 인스턴스에 도달하지 않는다.
|
||||
- 완화 방안: 인터페이스 계약에 routing key 규칙을 명시한다. `instance_key = "default"` 단일 인스턴스 시에는 routing key `default.intent` 등으로 publish하면 하위 호환 유지 가능.
|
||||
- R-2 (Medium): 기존 durable 큐(`trex.intent.req` 등)가 RabbitMQ에 잔류하면 새 큐로 전환 후 old 큐에 consumer가 없어진다.
|
||||
- 완화 방안: 배포 절차에 old 큐 삭제 단계를 명시한다(OQ-1). old 큐에 새로 publish되는 메시지는 없으므로 즉시 삭제해도 메시지 손실 없음.
|
||||
- 완화 방안: 자연 소멸 허용. 배포 후 old 큐에 새로 publish되는 메시지가 없으므로 메시지 누적 없음. 적절한 시점에 RabbitMQ admin console에서 수동 삭제(OQ-1 Answered).
|
||||
- R-3 (Low): `trex.broadcast` exchange 메시지가 모든 인스턴스에 동시 전달되므로 인스턴스별로 동일 제어 명령을 중복 처리한다.
|
||||
- 완화 방안: kill-switch/pause 핸들러에 idempotency 보장(REQ-036 패턴, DB advisory lock). 중복 처리는 무해하게 동작.
|
||||
- R-4 (Low): TOML 설정 파일이 실수로 git에 커밋되면 `portfolio_hub_token` 등 민감값이 노출된다.
|
||||
@@ -136,18 +136,18 @@
|
||||
- A-2: strategy 모듈(orb-v1)은 인터페이스 계약 변경에 맞춰 routing key를 업데이트할 수 있다.
|
||||
- A-3: 인스턴스별 담당 portfolio 매핑은 TOML 설정 또는 strategy 모듈의 routing 설정으로 관리한다. trex-core 내부에 레지스트리 DB를 두지 않는다.
|
||||
- A-4: `portfolio_hub_token`과 `instance_key`를 `.env`에서 TOML로 이관하면 기존 `.env` 기반 배포 스크립트를 수정해야 한다. KIS 자격증명 등 나머지 env var는 이번 범위에서 이관하지 않는다.
|
||||
- A-5: TOML 파일 경로는 `--config` CLI 인자로 지정한다. 경로 지정 없이 기동하는 경우 동작에 대한 처리 방침은 OQ-2에서 결정한다.
|
||||
- A-5: TOML 파일 경로는 `--config` CLI 인자로 지정한다. `--config` 인자는 필수이며, 지정하지 않으면 데몬이 오류 메시지를 출력하고 즉시 종료한다(OQ-2 Answered). 기본값 fallback은 제공하지 않는다.
|
||||
|
||||
## 오픈 질문
|
||||
|
||||
- OQ-1: 기존 durable 큐(`trex.intent.req` 등) 처리 방식 — 자연 소멸(수동 삭제) vs 배포 절차에 명시적 삭제 포함?
|
||||
- 상태: Open
|
||||
- 처리 방안: REQ 단계에서 배포 절차로 결정. old 큐에 publisher가 없어지면 메시지 누적은 없으나 RabbitMQ admin console에 불필요한 큐가 잔류한다.
|
||||
- 종료 조건: REQ 초안 작성 시 배포 절차 섹션에 old 큐 처리 방식 명시.
|
||||
- 상태: Answered
|
||||
- 결정: 자연 소멸 허용. 배포 후 old 큐에 새로 publish되는 메시지는 없으므로 메시지 누적 없음. RabbitMQ admin console에서 적절한 시점에 수동 삭제. 배포 절차에 명시적 삭제 단계를 추가하지 않는다.
|
||||
- 반영 위치: R-2 완화 방안
|
||||
- OQ-2: `--config` 인자 없이 데몬 기동 시 동작 — 오류 종료(필수) vs `instance_key = "default"` 기본값으로 기동?
|
||||
- 상태: Open
|
||||
- 처리 방안: REQ 단계에서 결정. 다중 인스턴스 안전성을 위해 TOML 필수(오류 종료)를 권장하나, 기존 배포와의 호환성을 위해 기본값 허용도 고려 가능.
|
||||
- 종료 조건: confirm-discovery 완료 전 Justin Ko 결정.
|
||||
- 상태: Answered
|
||||
- 결정: `--config` 필수. 인자 없이 기동 시 오류 메시지 출력 후 즉시 종료. 기본값 fallback 없음. 인스턴스 식별이 보장되지 않은 상태로 기동하는 것은 다중 인스턴스 환경에서 split-brain 위험을 내포하므로 명시적 실패가 안전하다.
|
||||
- 반영 위치: A-5, FR-1
|
||||
|
||||
### 오픈 질문 처리 기준
|
||||
|
||||
@@ -172,16 +172,16 @@
|
||||
|
||||
# 8. REQ로 넘기기 전 확인 체크
|
||||
|
||||
- [ ] 범위(In Scope/Out of Scope) 확정
|
||||
- 기준: OQ-1(old 큐 처리), OQ-2(TOML 필수 여부) Answered
|
||||
- [ ] 성공 지표의 측정 방식/데이터 출처 확정
|
||||
- 기준: S-1~S-5 각각 테스트 또는 e2e 근거 확정
|
||||
- [ ] High 영향 리스크 우선순위 및 해소 책임자 지정
|
||||
- 기준: R-1(routing key contract), R-2(old 큐), R-4(TOML git 노출) 완화 방안 REQ 단계 인계 여부 확인
|
||||
- [ ] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
|
||||
- 기준: OQ-1, OQ-2 결정 완료
|
||||
- [ ] Freeze 확정 담당자(CONFIRMED_BY) 지정
|
||||
- 기준: Justin Ko 확인
|
||||
- [x] 범위(In Scope/Out of Scope) 확정
|
||||
- 기준: OQ-1(old 큐 처리) Answered, OQ-2(TOML 필수) Answered
|
||||
- [x] 성공 지표의 측정 방식/데이터 출처 확정
|
||||
- 기준: S-1~S-5 각각 테스트 또는 e2e 근거 확정 — S-1~S-5 측정 방식 및 데이터 출처 섹션 7에 전부 기입됨
|
||||
- [x] High 영향 리스크 우선순위 및 해소 책임자 지정
|
||||
- 기준: R-1~R-4 전부 Medium/Low — High 영향 리스크 없음. 모든 리스크에 완화 방안 기입됨. REQ 단계 인계 확정.
|
||||
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
|
||||
- 기준: OQ-1, OQ-2 모두 Answered
|
||||
- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정
|
||||
- 기준: Justin Ko 확인 — Section 11 Confirmed By: Justin Ko
|
||||
|
||||
# 9. 파일 처리 결과
|
||||
|
||||
@@ -198,20 +198,15 @@
|
||||
|
||||
# 10. 사용자 결정 필요 항목 요약
|
||||
|
||||
## DECIDE
|
||||
|
||||
- {{DECIDE_OLD_QUEUE_MIGRATION}}: 기존 durable 큐(`trex.intent.req` 등) 처리 방식 — 자연 소멸(수동 삭제) vs 배포 절차에 명시적 삭제 포함 (OQ-1)
|
||||
- {{DECIDE_TOML_REQUIRED}}: `--config` 인자 없이 기동 시 동작 — 오류 종료(TOML 필수) vs `instance_key = "default"` 기본값으로 기동 허용 (OQ-2)
|
||||
|
||||
## CONFIRM
|
||||
|
||||
- {{CONFIRM_STRATEGY_MODULE_OUT_OF_SCOPE}}: strategy 모듈(orb-v1)의 routing key 변경은 이번 Discovery 범위 밖(trex-core는 계약 정의만, 구현은 strategy 모듈 담당)인지 확인
|
||||
- {{CONFIRM_ENV_RESIDUAL}}: 나머지 env var(KIS 자격증명, PostgreSQL DSN 등)는 이번 범위에서 TOML로 이관하지 않고 `.env`에 유지하는 것으로 확정하는지 확인
|
||||
- CONFIRM_STRATEGY_MODULE_OUT_OF_SCOPE: 확인됨. strategy 모듈(orb-v1)의 routing key 구현 변경은 이번 Discovery 범위 밖. trex-core는 exchange·바인딩 계약만 정의하며, strategy 모듈의 publish 코드 변경은 별도 Discovery 또는 REQ에서 다룬다(Section 5 Out of Scope 반영).
|
||||
- CONFIRM_ENV_RESIDUAL: 확인됨. KIS 자격증명(`TREX_KIS_*`), PostgreSQL DSN, TREX_RABBITMQ_URL 등 기존 env var는 이번 범위에서 `.env` 유지. TOML 이관 대상은 `portfolio_hub_token`과 `instance_key`만(Section 5 Out of Scope + A-4 반영).
|
||||
|
||||
## DATA
|
||||
|
||||
- {{DATA_TOML_SCHEMA_FULL}}: TOML 파일에 포함할 전체 필드 목록(`instance_key`, `portfolio_hub_token` 외 추가 필드 유무) — REQ 단계 스키마 정의에 필요
|
||||
- {{DATA_MULTI_INSTANCE_TRIGGER_TIMING}}: 실제 다중 인스턴스 운영이 필요한 시점(배포 계획) — NFR 우선순위 조정에 필요
|
||||
- DATA_TOML_SCHEMA_FULL: REQ 단계 확정. 최소 필드: `instance_key` (string), `portfolio_hub_token` (string). 추가 필드는 REQ 단계 스키마 정의에서 확정한다.
|
||||
- DATA_MULTI_INSTANCE_TRIGGER_TIMING: REQ/배포 단계 계측 항목. Discovery 단계 범위 밖. 실제 다중 인스턴스 운영 계획은 배포 시점 REQ에서 반영한다.
|
||||
|
||||
# 11. Discovery Freeze
|
||||
|
||||
@@ -225,8 +220,8 @@
|
||||
|
||||
## Freeze 플래그
|
||||
|
||||
- Handoff Decision: 보류
|
||||
- Handoff Rationale: TOML 설정 파일 도입 + instance_key 기반 토폴로지 설계 완료. OQ-1(old 큐 처리), OQ-2(TOML 필수 여부) 미결 — confirm-discovery에서 결정 후 REQ 초안 작성 가능.
|
||||
- Ready for REQ Drafting: false
|
||||
- Confirmed By: TBD
|
||||
- Confirmed At (KST): TBD
|
||||
- Handoff Decision: REQ Drafting 진행 가능
|
||||
- Handoff Rationale: TOML 설정 파일 도입 + instance_key 기반 토폴로지 설계 완료. OQ-1(old 큐 자연 소멸), OQ-2(TOML 필수) 모두 Answered. CONFIRM/DATA 항목은 REQ 단계에서 확정 가능 — confirm-discovery 후 REQ 초안 작성 진행 가능.
|
||||
- Ready for REQ Drafting: true
|
||||
- Confirmed By: Justin Ko
|
||||
- Confirmed At (KST): 2026-06-07
|
||||
|
||||
@@ -14,4 +14,4 @@
|
||||
| dcy-006_20260605_strategy-module-rabbitmq-transport | 20260605 | trex-core ↔ 전략 모듈 통신 transport — RabbitMQ 도입 (스냅샷 read + intent 제출 + 이벤트) | [Discovery](./dcy-006_20260605_strategy-module-rabbitmq-transport.md) | confirmed |
|
||||
| dcy-007_20260606_core-autonomous-safety-sell | 20260606 | core 자율 안전 매도 — stop-loss·시간 만기 자동 트리거 | [Discovery](./dcy-007_20260606_core-autonomous-safety-sell.md) | confirmed |
|
||||
| dcy-008_20260607_emergency-stop-kill-switch | 20260607 | 비상 정지/강제 청산(kill-switch) — portfolio/전역 슬롯 청산 + 당일 전략 제외 | [Discovery](./dcy-008_20260607_emergency-stop-kill-switch.md) | confirmed |
|
||||
| dcy-009_20260607_multi-instance-rabbitmq-topology | 20260607 | 인스턴스별 독립 RabbitMQ 채널 토폴로지 — trex-core 다중 인스턴스 운영을 위한 Topic Exchange + Fanout Exchange per-instance 큐 분리 | [Discovery](./dcy-009_20260607_multi-instance-rabbitmq-topology.md) | draft |
|
||||
| dcy-009_20260607_multi-instance-rabbitmq-topology | 20260607 | 인스턴스별 독립 RabbitMQ 채널 토폴로지 — trex-core 다중 인스턴스 운영을 위한 Topic Exchange + Fanout Exchange per-instance 큐 분리 | [Discovery](./dcy-009_20260607_multi-instance-rabbitmq-topology.md) | confirmed |
|
||||
@@ -4,7 +4,7 @@
|
||||
- Baseline Seed: .stage-pilot/bootstrap/baseline.yaml
|
||||
- Status: draft
|
||||
- Owner: project maintainers
|
||||
- Last Updated (KST): 2026-06-07 (BAT-012)
|
||||
- Last Updated (KST): 2026-06-07 (BAT-013)
|
||||
- Source Discovery / Batch: bootstrap-baseline (pre-Discovery); HTTP API = dcy-002 / BAT-002
|
||||
- Primary Runtime: daemon (HTTP API) + library (slot 도메인)
|
||||
|
||||
@@ -189,6 +189,15 @@ resume_kill_switch 요청·응답 (REQ-046):
|
||||
- snapshot masking 소스(REQ-046): `mask = pending | warning | pause | killswitch_today`. kill-switch 당일 제외는 `(portfolio_id, trading_date)` day-scoped — 다음 거래일 자동 복귀. `portfolio_id="*"` sentinel 은 전역(전체 universe) 마스킹.
|
||||
- 하위호환: `trex.pause.req` 는 legacy alias. 운영 배포 시 제어 클라이언트는 `trex.control.req` 로 이전한다(rollout 결정, REQ-043 Notes).
|
||||
|
||||
### Multi-instance routing (BAT-013 / REQ-049/050)
|
||||
|
||||
다중 인스턴스 운영 시 큐명과 라우팅을 `instance_key` 로 분리한다(dcy-009).
|
||||
|
||||
- **큐명 규칙**: `{base}.{instance_key}`(예: `trex.intent.req.inst-1`). `instance_key = "default"` 는 기존 리터럴 큐명(`trex.intent.req` 등)을 그대로 사용한다(하위호환, FR-5).
|
||||
- **`trex.direct`(topic exchange)**: 특정 인스턴스 지정 전달. routing key = `{instance_key}.{channel_type}`(`channel_type ∈ {intent, snapshot, control}`). 각 인스턴스 큐가 자신의 routing key 로 바인딩 → `inst-1.intent` publish 는 `trex.intent.req.inst-1` 에만 도달.
|
||||
- **`trex.broadcast`(fanout exchange)**: 전역 제어. 각 인스턴스의 **control 큐가 dual 바인딩**되어, broadcast publish 는 모든 인스턴스 control 큐에 동시 전달(global kill-switch 등). 중복 전달은 핸들러 idempotency(REQ-036/advisory lock)로 무해.
|
||||
- **기동**: 데몬은 `--config <path>.toml`(필수)로 `instance_key`/`portfolio_hub_token` 을 읽는다(`.env` 아님, REQ-048).
|
||||
|
||||
## Interface: `PortfolioHubReadPort` (Protocol, BAT-008)
|
||||
|
||||
- Type: external-service (HTTP, portfolio-hub)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
- Baseline Seed: .stage-pilot/bootstrap/baseline.yaml
|
||||
- Status: draft
|
||||
- Owner: project maintainers
|
||||
- Last Updated (KST): 2026-06-07 (BAT-012)
|
||||
- Last Updated (KST): 2026-06-07 (BAT-013)
|
||||
- Source Discovery / Batch: bootstrap-baseline (pre-Discovery, 3-phase lifecycle 밖의 bootstrap 산출물)
|
||||
|
||||
## Project Summary
|
||||
@@ -23,7 +23,7 @@
|
||||
## Primary Runtime
|
||||
- Type: daemon (sync + thread, no HTTP) + library (slot 도메인)
|
||||
- Entry Points:
|
||||
- `trex-core-daemon` (console_script → `trex_core.runtime.daemon:main`) :: 슬롯 코어 데몬 — migrate+preallocate+reconcile 후 7 백그라운드 스레드(intent RPC consumer / snapshot RPC consumer / control RPC consumer / snapshot scheduler / execution / ledger / safety-sell) 기동, RabbitMQ messaging 으로 전략 모듈과 통신 (BAT-002 + BAT-007 + BAT-008 + BAT-011 + BAT-012)
|
||||
- `trex-core-daemon --config <path>.toml` (console_script → `trex_core.runtime.daemon:main`) :: 슬롯 코어 데몬 — `--config` 필수(per-instance TOML, BAT-013). migrate+preallocate+reconcile 후 7 백그라운드 스레드(intent RPC consumer / snapshot RPC consumer / control RPC consumer / snapshot scheduler / execution / ledger / safety-sell)를 `instance_key` 큐로 기동, RabbitMQ messaging 으로 전략 모듈과 통신 (BAT-002 + BAT-007 + BAT-008 + BAT-011 + BAT-012 + BAT-013)
|
||||
- `trex-core` (console_script → `trex_core.runtime.cli:main`) :: 운영 CLI (`pause` / `resume` / `paused`) (BAT-008)
|
||||
- RabbitMQ `trex.snapshot.req` / `trex.intent.req` / `trex.pause.req` / `trex.events` :: 매수·매도 전략 데몬과의 transport (docs/interface-contract.md)
|
||||
- SlotService / SlotRepository (Protocol) :: in-process 도메인 서비스 + 저장소 경계(라이브러리 사용)
|
||||
@@ -83,12 +83,12 @@
|
||||
│ ├── adapters/ # 인프라 어댑터 (BAT-002, BAT-004, BAT-006, BAT-007, BAT-008)
|
||||
│ │ ├── broker/ # paper.py(결정적 async 더블 + 현재가 stub) · kis.py(KIS Open API + 잔고조회 + 현재가 polling) (BAT-004/BAT-008/BAT-011)
|
||||
│ │ ├── ledger/ # in_memory.py(stub) · portfolio_hub.py(HTTP 어댑터) (BAT-006)
|
||||
│ │ ├── messaging/ # topology.py · in_memory.py(stub) · rabbitmq.py(pika 동기) (BAT-007)
|
||||
│ │ ├── messaging/ # topology.py(동적 큐명 빌더 + trex.direct/trex.broadcast exchange, BAT-013) · in_memory.py(stub) · rabbitmq.py(pika 동기, declare_topology(instance_key)) (BAT-007/BAT-013)
|
||||
│ │ ├── portfolio/ # hub_read.py(HTTP) · stub_read.py(stub) · pause_store.py(Postgres) · killswitch_store.py(day-scoped, BAT-012) (BAT-008)
|
||||
│ │ └── persistence/ # postgresql.py(+PostgreSQLLedgerOutbox) · in_memory_intent_store.py · migrate.py · sql/(001_…, 002_create_ledger_outbox.sql, 003_create_snapshot_pause.sql, 004_add_price_thresholds.sql, 005_create_killswitch_exclusion.sql)
|
||||
│ └── runtime/ # 데몬 (BAT-002, BAT-004, BAT-006, BAT-007, BAT-008)
|
||||
│ ├── config.py # DaemonConfig + KISConfig + PortfolioHubConfig + RabbitMQConfig + SnapshotScheduleConfig + SafetySellConfig(TREX_WATCHDOG_*/FORCED_CLOSE/STOP_LOSS_POLL/MARKET_CLOSE) (env)
|
||||
│ ├── daemon.py # Daemon class: setup(migrate/preallocate/reconcile/declare-topology) + 7 worker threads (execution·ledger·intent-rpc·snapshot-rpc·control-rpc·snapshot-scheduler·safety-sell) + WatchdogStore + KillSwitchService + graceful shutdown
|
||||
│ ├── config.py # DaemonConfig(+instance_key) + KISConfig + PortfolioHubConfig + RabbitMQConfig + SnapshotScheduleConfig + SafetySellConfig + DaemonInstanceConfig.from_toml/from_config (env + per-instance TOML, BAT-013)
|
||||
│ ├── daemon.py # Daemon class: `--config` TOML 진입점(BAT-013) + setup(migrate/preallocate/reconcile/declare-topology(instance_key)) + 7 worker threads(instance_key 큐 구독) + WatchdogStore + KillSwitchService + graceful shutdown
|
||||
│ └── cli.py # trex-core CLI: pause/resume/paused subcommands (BAT-008 / REQ-032)
|
||||
└── tests/
|
||||
├── domain/slot/ # test_state / test_service / test_contract
|
||||
@@ -149,3 +149,4 @@
|
||||
- 2026-06-07 :: BAT-010 — safety-sell 기반 인프라(`application/watchdog.py` WatchdogStore 신설, `Slot` 에 `stop_loss_price`/`take_profit_price` 필드 + `004_add_price_thresholds.sql` additive 마이그레이션, `SlotService.sell()` 자율 매도 내부 경로, `make_snapshot_rpc_handler` 워치독 갱신, `SafetySellConfig` env 5종 파싱) (REQ-034~036, 041).
|
||||
- 2026-06-07 :: BAT-011 — safety-sell 트리거 레이어(`application/safety_sell.py` SafetySellService 신설 — forced_close/stop-loss·take-profit/장 마감, `BrokerPort.get_current_price()` + KIS `inquire-price`(`FHKST01010100`, CHG-04 정정)·paper stub, `SlotService.update_thresholds()` + intent RPC `update_thresholds` 액션, runtime/daemon 을 7 스레드로 확장(`trex-safety-sell`) + WatchdogStore 배선) (REQ-037~040, 042).
|
||||
- 2026-06-07 :: BAT-012 — 비상 정지/kill-switch(`trex.pause.req`→`trex.control.req` 제어 채널 + action dispatch, `application/killswitch.py` KillSwitchService + `adapters/portfolio/killswitch_store.py` + `005_create_killswitch_exclusion.sql` day-scoped 제외, snapshot masking 에 killswitch_today 추가, `make_control_rpc_handler` + kill_switch/resume_kill_switch, daemon control consumer 전환) (REQ-043~047).
|
||||
- 2026-06-07 :: BAT-013 — 다중 인스턴스 RabbitMQ 토폴로지(`topology.py` 동적 큐명 빌더 `intent/snapshot/control_queue(instance_key)` default 리터럴 하위호환 + `trex.direct`(topic)/`trex.broadcast`(fanout) exchange + `direct_routing_key`, `rabbitmq.declare_topology(instance_key)` per-instance 큐/바인딩+control dual, `config.DaemonInstanceConfig.from_toml`+`DaemonConfig.from_config`+`instance_key` 필드, daemon `--config` 필수 진입점 + instance_key 큐 구독, `.gitignore` 인스턴스 TOML + `config/instance.example.toml`) (REQ-048~051).
|
||||
|
||||
+12
-2
@@ -4,7 +4,7 @@
|
||||
- Baseline Seed: .stage-pilot/bootstrap/baseline.yaml
|
||||
- Status: draft
|
||||
- Owner: project maintainers
|
||||
- Last Updated (KST): 2026-06-07 (BAT-012)
|
||||
- Last Updated (KST): 2026-06-07 (BAT-013)
|
||||
- Source Discovery / Batch: bootstrap-baseline (pre-Discovery); 데몬/HTTP 흐름 = dcy-002 / BAT-002
|
||||
- Primary Runtime: daemon (HTTP API) + library (slot 도메인)
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
## Flow: `데몬 lifecycle` (BAT-002, BAT-007)
|
||||
|
||||
1. `trex-core-daemon` 기동 → `DaemonConfig.from_env()`(TREX_POSTGRESQL_DSN, `TREX_RABBITMQ_URL` 등).
|
||||
1. `trex-core-daemon --config <path>.toml` 기동 → `DaemonConfig.from_config(toml)` = `from_env()`(TREX_POSTGRESQL_DSN/`TREX_RABBITMQ_URL` 등) + TOML(`instance_key`, `portfolio_hub_token`) 병합. `--config` 필수(없으면 오류 종료, BAT-013/REQ-048). `instance_key` 로 큐명·라우팅 분리.
|
||||
2. setup: `apply_migrations(dsn)`(멱등) → 거래일 슬롯 `preallocate_daily_slots`(멱등) → `execution.reconcile()`(REQ-014) → 이벤트 publisher 의 `declare_topology()`.
|
||||
3. start: 7개 백그라운드 스레드 동시 기동
|
||||
- `trex-execution` :: 브로커 dispatch/poll 루프 (REQ-013/014)
|
||||
@@ -156,6 +156,16 @@
|
||||
6. 수동 해제: `{action:"resume_kill_switch", scope, portfolio_id?}` → `KillSwitchService.resume()` 가 day-scoped row 삭제 → 다음 응답부터 정상 노출. **이미 청산된 슬롯은 비가역**(매도 되돌리지 않음).
|
||||
7. 독립성: kill-switch 는 자율 안전 매도 worker(dcy-007)와 스레드/상태 공유 없음. 동일 슬롯 동시 sell() 은 REQ-036 idempotency 로 1회만 전이.
|
||||
|
||||
## Flow: `multi-instance 라우팅` (BAT-013 / REQ-049/050)
|
||||
|
||||
다중 인스턴스를 동일 RabbitMQ 에 붙여도 split-brain 없이 운영하기 위한 큐/exchange 분리.
|
||||
|
||||
1. 각 인스턴스는 `--config inst-N.toml`(`instance_key`)로 기동 → 수신 큐명이 `{base}.{instance_key}` 로 분리(`default` 는 리터럴 유지, 하위호환).
|
||||
2. `declare_topology(instance_key)`: `trex.direct`(topic) + `trex.broadcast`(fanout) 선언 + 인스턴스 intent/snapshot/control 큐를 `trex.direct` 에 `{instance_key}.{channel}` 로 바인딩 + control 큐를 `trex.broadcast` 에 dual 바인딩.
|
||||
3. **directed**: strategy/운영자가 `trex.direct` 에 `inst-1.intent` 로 publish → `trex.intent.req.inst-1` 에만 전달(다른 인스턴스 큐 미수신). 인스턴스별 WatchdogStore/SafetySellService 가 자기 큐만 소비 → split-brain 방지(NFR-3).
|
||||
4. **broadcast**: `trex.broadcast` 에 publish → 모든 인스턴스 control 큐 동시 전달(global kill-switch 등). 중복 처리는 idempotency 로 무해.
|
||||
5. 기존 reply-to RPC 직접 큐 경로(default)는 그대로 동작 — 추가 바인딩은 비파괴적(FR-5 무회귀).
|
||||
|
||||
## Flow: `broker degrade 분기` (BAT-007 / REQ-024)
|
||||
|
||||
1. transport connection drop → 어댑터가 `_mark_disconnected()` + 백오프(1s→2s→4s→8s→16s→30s cap) 재연결.
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
# req-048: per-instance TOML 설정 로더 + `--config` 필수
|
||||
|
||||
- Status: Implemented
|
||||
- Type: Configuration
|
||||
- Priority: High
|
||||
- Owner: project maintainers
|
||||
|
||||
## Intent
|
||||
|
||||
다중 인스턴스 운영을 위해 인스턴스별 TOML 설정 파일을 도입한다. 데몬 기동 시 `--config <path>.toml` 인자를 **필수**로 받고, TOML 에서 `instance_key` 와 `portfolio_hub_token` 을 읽는다. 이 두 값은 `.env` 에 넣지 않는 것을 원칙으로 하며(민감값 분리), 기존 `DaemonConfig.from_env()` 와 병합해 최종 설정을 구성한다(dcy-009 FR-1, NFR-4, OQ-2/A-5).
|
||||
|
||||
## Requirement
|
||||
|
||||
- `config.py` 에 `DaemonInstanceConfig` dataclass(`instance_key: str`, `portfolio_hub_token: str`)와 `from_toml(path)` 로더를 추가한다(stdlib `tomllib` 사용).
|
||||
- `DaemonConfig` 구성 시 `--config <path>.toml` 의 TOML 값을 `from_env()` 결과와 병합하는 팩토리(예: `from_config(toml_path)`)를 추가한다. `portfolio_hub_token` 은 TOML 값을 사용해 `PortfolioHubConfig.token` 에 주입한다.
|
||||
- 데몬 기동 진입점은 `--config` 를 **필수 인자**로 받는다. 인자 없이 기동하면 오류 메시지를 출력하고 비정상 종료(exit code ≠ 0)한다. 기본값 fallback 없음(OQ-2 Answered).
|
||||
- `instance_key` 와 `portfolio_hub_token` 은 `.env`/OS env 에서 읽지 않는다. TOML 미존재/필수 필드 누락 시 명확한 오류로 종료한다.
|
||||
- `.gitignore` 에 인스턴스 TOML 경로 패턴(예: `*.toml` 또는 `config/*.toml`)을 추가해 설정 파일이 커밋되지 않게 한다. 토큰/instance_key 는 로그에 평문 출력하지 않는다(NFR-4).
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. Given `inst-1.toml`(`instance_key="inst-1"`, `portfolio_hub_token="tok"`), When `DaemonInstanceConfig.from_toml("inst-1.toml")`, Then `instance_key="inst-1"`, `portfolio_hub_token="tok"` 로 파싱된다.
|
||||
2. Given `--config` 인자 없이 데몬 기동, When 진입점 실행, Then 오류 메시지 출력 + exit code ≠ 0 으로 종료한다(기본값 fallback 없음).
|
||||
3. Given TOML 에 `portfolio_hub_token` 이 있고 `.env`/OS env 에 `TREX_PORTFOLIO_HUB_TOKEN` 미설정, When 최종 설정 구성, Then `PortfolioHubConfig.token` 에 TOML 값이 주입된다(S-5).
|
||||
4. Given TOML 에 필수 필드(`instance_key`) 누락, When `from_toml`/`from_config`, Then 명확한 오류로 실패한다(조용한 기본값 금지).
|
||||
5. Given 저장소, When `.gitignore` 확인, Then 인스턴스 TOML 경로 패턴이 무시 대상이다.
|
||||
6. Given 데몬 기동 로그, When 출력 확인, Then `portfolio_hub_token` 평문이 로그에 노출되지 않는다.
|
||||
|
||||
## Impacted Area
|
||||
|
||||
- Docs:
|
||||
- `docs/project-structure.md` — 설정 파일 계층(TOML + env) (REQ-051 처리)
|
||||
- Modules:
|
||||
- `trex_core/runtime/config.py` — `DaemonInstanceConfig` + `from_toml` + `from_config` 병합
|
||||
- 데몬 진입점(`trex_core/runtime/daemon.py:main` 또는 `__main__`) — `--config` 필수 파싱(argparse)
|
||||
- `.gitignore` — 인스턴스 TOML 패턴
|
||||
- Tests:
|
||||
- `tests/runtime/` — TOML 로더 파싱/필수 인자/토큰 주입/누락 오류
|
||||
|
||||
## Notes
|
||||
|
||||
- dcy-009 OQ-2 Answered: `--config` 필수, fallback 없음(split-brain 위험 회피). A-5.
|
||||
- NFR-4(민감값 비노출)는 본 REQ 의 gitignore + 로그 미노출 AC 로 흡수.
|
||||
- DATA_TOML_SCHEMA_FULL(dcy-009 §10): 최소 필드 `instance_key`/`portfolio_hub_token`. 추가 필드는 본 REQ 구현 스키마에서 확장 가능.
|
||||
- KIS 자격증명/DSN/RABBITMQ_URL 등 나머지 env 는 이번 범위에서 `.env` 유지(CONFIRM_ENV_RESIDUAL).
|
||||
|
||||
## Change Log
|
||||
|
||||
### CHG-20260607-01
|
||||
|
||||
- Date: 2026-06-07
|
||||
- Author: Justin Ko
|
||||
|
||||
#### Change Summary
|
||||
|
||||
- dcy-009(confirmed) → REQ-048 초안 생성.
|
||||
|
||||
#### Intent
|
||||
|
||||
- per-instance TOML 로더 + `--config` 필수(FR-1, NFR-4)를 Configuration REQ 로 정규화.
|
||||
|
||||
#### Acceptance Criteria Delta
|
||||
|
||||
- Added: AC-1~6 전체
|
||||
|
||||
#### Impacted Area Delta
|
||||
|
||||
- Docs: project-structure.md (REQ-051)
|
||||
- Modules: config.py, daemon 진입점, .gitignore
|
||||
- Tests: tests/runtime TOML 로더
|
||||
|
||||
#### Delivery Trace Delta
|
||||
|
||||
- Batches: none (미할당)
|
||||
- Releases: none
|
||||
|
||||
#### Revalidation Impact
|
||||
|
||||
- Reverification Needed: no
|
||||
- Existing Implementation Invalidated: no
|
||||
- Status Recommendation: keep-current
|
||||
- Follow-up Action: confirm-req 로 승인.
|
||||
|
||||
### CHG-20260607-02
|
||||
|
||||
- Date: 2026-06-07
|
||||
- Author: Justin Ko
|
||||
|
||||
#### Change Summary
|
||||
|
||||
- Proposed → Approved (confirm-req).
|
||||
|
||||
#### Intent
|
||||
|
||||
- 승인 게이트 통과: 필수 필드·구체 AC·Impacted Area·Change Log 완비. dcy-009(confirmed) 범위·결정(OQ-2 TOML 필수)과 모순 없음. TOML 추가 필드 스키마는 구현 단계 확장 가능 비핵심.
|
||||
|
||||
#### Acceptance Criteria Delta
|
||||
|
||||
- (변경 없음)
|
||||
|
||||
#### Impacted Area Delta
|
||||
|
||||
- (변경 없음)
|
||||
|
||||
#### Delivery Trace Delta
|
||||
|
||||
- Batches: none (미할당)
|
||||
- Releases: none
|
||||
|
||||
#### Revalidation Impact
|
||||
|
||||
- Reverification Needed: no
|
||||
- Existing Implementation Invalidated: no
|
||||
- Status Recommendation: keep-current
|
||||
- Follow-up Action: suggest-batch-reqs / draft-batch 로 delivery 편성.
|
||||
|
||||
### CHG-20260607-03
|
||||
|
||||
- Date: 2026-06-07
|
||||
- Author: Justin Ko
|
||||
|
||||
#### Change Summary
|
||||
|
||||
- Status Approved → Implemented (confirm-req-implemented, 근거 batch BAT-013 release-candidate).
|
||||
|
||||
#### Intent
|
||||
|
||||
- BAT-013 verification 의 AC↔evidence(로더 6 단위: from_toml/필수 누락/token 주입 S-5/default ik/`--config` 누락 subprocess 종료 AC-2/gitignore)로 AC-1~6 충족. NFR-4(민감값) 흡수.
|
||||
|
||||
#### Acceptance Criteria Delta
|
||||
|
||||
- (변경 없음)
|
||||
|
||||
#### Impacted Area Delta
|
||||
|
||||
- (변경 없음)
|
||||
|
||||
#### Delivery Trace Delta
|
||||
|
||||
- Batches: BAT-013 (release-candidate)
|
||||
- Releases: none
|
||||
|
||||
#### Revalidation Impact
|
||||
|
||||
- Reverification Needed: no
|
||||
- Existing Implementation Invalidated: no
|
||||
- Status Recommendation: keep-current (Implemented)
|
||||
- Follow-up Action: draft-release 로 BAT-013 배포 편성. token .env 제거는 배포 rollout.
|
||||
@@ -0,0 +1,144 @@
|
||||
# req-051: multi-instance topology baseline 문서 정합
|
||||
|
||||
- Status: Implemented
|
||||
- Type: Documentation
|
||||
- Priority: Medium
|
||||
- Owner: project maintainers
|
||||
|
||||
## Intent
|
||||
|
||||
dcy-009(다중 인스턴스 RabbitMQ 토폴로지) 구현(REQ-048~050) 완료 후 `docs/runtime-flows.md`, `docs/project-structure.md`, `docs/interface-contract.md` 세 baseline 문서를 multi-instance topology + TOML 기반 기동 기준으로 정합시킨다(dcy-009 FR-6).
|
||||
|
||||
## Requirement
|
||||
|
||||
- `docs/interface-contract.md` 에 반영한다:
|
||||
- routing key 계약: directed = `{instance_key}.{channel_type}`(`trex.direct` topic), broadcast = `trex.broadcast`(fanout, 전역 control)
|
||||
- 인스턴스별 큐명 규칙(`{base}.{instance_key}`, default = 리터럴 하위호환)
|
||||
- `docs/runtime-flows.md` 에 반영한다:
|
||||
- TOML(`--config`) 기반 기동 절차(instance_key/portfolio_hub_token 로드)
|
||||
- multi-instance directed vs broadcast 라우팅 흐름, split-brain 방지(인스턴스 전용 큐) 설명
|
||||
- `docs/project-structure.md` 에 반영한다:
|
||||
- `topology.py` 역할 변경(고정 상수 → 동적 큐명/exchange 빌더)
|
||||
- 설정 계층(TOML + env 병합), `DaemonInstanceConfig`, `--config` 진입점
|
||||
- `trex.direct`/`trex.broadcast` exchange
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. Given `docs/interface-contract.md`, When routing key 규칙을 찾으면, Then directed(`{instance_key}.{channel_type}`)와 broadcast(fanout) 계약 + 인스턴스 큐명 규칙이 명시되어 있다.
|
||||
2. Given `docs/runtime-flows.md`, When 기동/라우팅 흐름을 찾으면, Then TOML(`--config`) 기동 + directed/broadcast 라우팅 + split-brain 방지가 서술되어 있다.
|
||||
3. Given `docs/project-structure.md`, When topology/설정 항목을 찾으면, Then 동적 큐명 빌더 + `DaemonInstanceConfig`/`--config` + `trex.direct`/`trex.broadcast` 가 반영되어 있다.
|
||||
|
||||
## Impacted Area
|
||||
|
||||
- Docs:
|
||||
- `docs/interface-contract.md`
|
||||
- `docs/runtime-flows.md`
|
||||
- `docs/project-structure.md`
|
||||
- Modules: 없음 (문서 전용)
|
||||
- Tests: 없음
|
||||
|
||||
## Notes
|
||||
|
||||
- REQ-048~050 구현 완료 후 batch verification 단계에서 최신 구현 상태를 반영해 수행한다(REQ-042/047 선례).
|
||||
- baseline 갱신이므로 `bootstrap-baseline` 선행 불필요.
|
||||
|
||||
## Change Log
|
||||
|
||||
### CHG-20260607-01
|
||||
|
||||
- Date: 2026-06-07
|
||||
- Author: Justin Ko
|
||||
|
||||
#### Change Summary
|
||||
|
||||
- dcy-009(confirmed) → REQ-051 초안 생성.
|
||||
|
||||
#### Intent
|
||||
|
||||
- multi-instance 구현 후 세 baseline 문서 정합(FR-6)을 Documentation REQ 로 정규화.
|
||||
|
||||
#### Acceptance Criteria Delta
|
||||
|
||||
- Added: AC-1~3 전체
|
||||
|
||||
#### Impacted Area Delta
|
||||
|
||||
- Docs: interface-contract.md, runtime-flows.md, project-structure.md
|
||||
|
||||
#### Delivery Trace Delta
|
||||
|
||||
- Batches: none (미할당)
|
||||
- Releases: none
|
||||
|
||||
#### Revalidation Impact
|
||||
|
||||
- Reverification Needed: no
|
||||
- Existing Implementation Invalidated: no
|
||||
- Status Recommendation: keep-current
|
||||
- Follow-up Action: confirm-req 로 승인 (REQ-048~050 구현 batch 이후 연계).
|
||||
|
||||
### CHG-20260607-02
|
||||
|
||||
- Date: 2026-06-07
|
||||
- Author: Justin Ko
|
||||
|
||||
#### Change Summary
|
||||
|
||||
- Proposed → Approved (confirm-req).
|
||||
|
||||
#### Intent
|
||||
|
||||
- 승인 게이트 통과: 필수 필드·구체 AC·Impacted Area·Change Log 완비. baseline 정합 문서 REQ로 REQ-048~050 구현 후 verification 단계 수행.
|
||||
|
||||
#### Acceptance Criteria Delta
|
||||
|
||||
- (변경 없음)
|
||||
|
||||
#### Impacted Area Delta
|
||||
|
||||
- (변경 없음)
|
||||
|
||||
#### Delivery Trace Delta
|
||||
|
||||
- Batches: none (미할당)
|
||||
- Releases: none
|
||||
|
||||
#### Revalidation Impact
|
||||
|
||||
- Reverification Needed: no
|
||||
- Existing Implementation Invalidated: no
|
||||
- Status Recommendation: keep-current
|
||||
- Follow-up Action: suggest-batch-reqs / draft-batch 로 delivery 편성(REQ-048~050 이후 연계).
|
||||
|
||||
### CHG-20260607-03
|
||||
|
||||
- Date: 2026-06-07
|
||||
- Author: Justin Ko
|
||||
|
||||
#### Change Summary
|
||||
|
||||
- Status Approved → Implemented (confirm-req-implemented, 근거 batch BAT-013 release-candidate).
|
||||
|
||||
#### Intent
|
||||
|
||||
- BAT-013 에서 baseline 3문서 정합 완료: interface-contract(multi-instance routing: 큐명 규칙·trex.direct/trex.broadcast·routing key), runtime-flows(`--config` 기동 + multi-instance 라우팅 흐름 + split-brain 방지), project-structure(동적 큐명 빌더·DaemonInstanceConfig/`--config`·exchange). AC-1~3 충족.
|
||||
|
||||
#### Acceptance Criteria Delta
|
||||
|
||||
- (변경 없음)
|
||||
|
||||
#### Impacted Area Delta
|
||||
|
||||
- (변경 없음)
|
||||
|
||||
#### Delivery Trace Delta
|
||||
|
||||
- Batches: BAT-013 (release-candidate)
|
||||
- Releases: none
|
||||
|
||||
#### Revalidation Impact
|
||||
|
||||
- Reverification Needed: no
|
||||
- Existing Implementation Invalidated: no
|
||||
- Status Recommendation: keep-current (Implemented)
|
||||
- Follow-up Action: draft-release 로 BAT-013 배포 편성.
|
||||
@@ -0,0 +1,147 @@
|
||||
# req-050: trex.direct(topic) + trex.broadcast(fanout) exchange + 바인딩
|
||||
|
||||
- Status: Implemented
|
||||
- Type: Integration
|
||||
- Priority: High
|
||||
- Owner: project maintainers
|
||||
|
||||
## Intent
|
||||
|
||||
인스턴스 지정 라우팅과 전역 브로드캐스트를 위해 두 exchange 를 도입한다. `trex.direct`(topic)는 strategy/운영자가 특정 인스턴스에만 메시지를 보낼 때 routing key `{instance_key}.{channel_type}` 으로 라우팅하고, `trex.broadcast`(fanout)는 global kill-switch 등 전체 인스턴스 대상 제어를 모든 control 큐에 동시 전달한다(dcy-009 FR-3, FR-4). routing key 계약을 인터페이스로 정의한다.
|
||||
|
||||
## Requirement
|
||||
|
||||
- `topology.py` 에 `DIRECT_EXCHANGE = "trex.direct"`(type topic, durable), `BROADCAST_EXCHANGE = "trex.broadcast"`(type fanout, durable) 상수와 routing key 빌더(`direct_routing_key(instance_key, channel_type)` → `{instance_key}.{channel_type}`)를 추가한다.
|
||||
- `declare_topology(instance_key)` 가 두 exchange 를 선언하고, 각 인스턴스의 intent/control/snapshot 큐를 `trex.direct` 에 routing key `{instance_key}.intent`/`.control`/`.snapshot` 으로 바인딩한다(FR-3).
|
||||
- 각 인스턴스의 **control 큐**를 `trex.broadcast`(fanout)에 추가 바인딩(dual binding)한다(FR-4). 전역 제어 메시지는 `trex.broadcast` 로 publish 하면 모든 인스턴스 control 큐에 전달된다.
|
||||
- `default` 인스턴스 하위호환: default 의 큐(리터럴명, REQ-049)도 동일 규칙으로 `trex.direct`(`default.intent` 등) + control dual binding 된다. 기존 직접 큐 publish 경로(reply-to RPC)는 유지되어 회귀가 없어야 한다(FR-5).
|
||||
- routing key 계약은 `docs/interface-contract.md` 에 명시한다(REQ-051): directed = `{instance_key}.{channel_type}`, broadcast = fanout(키 무시).
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. Given `declare_topology("inst-1")`, When 선언, Then `trex.direct`(topic) + `trex.broadcast`(fanout) exchange 가 durable 로 선언된다.
|
||||
2. Given `declare_topology("inst-1")`, When 바인딩 확인, Then `trex.intent.req.inst-1`/`...control...`/`...snapshot...` 가 `trex.direct` 에 routing key `inst-1.intent`/`inst-1.control`/`inst-1.snapshot` 으로 바인딩된다(FR-3).
|
||||
3. Given `direct_routing_key("inst-1","control")`, When 호출, Then `"inst-1.control"` 을 반환한다.
|
||||
4. Given `inst-1`/`inst-2` 두 인스턴스 큐가 선언됨, When `trex.direct` 에 `inst-1.intent` 로 publish, Then `trex.intent.req.inst-1` 에만 전달되고 `...inst-2` 에는 전달되지 않는다(S-2).
|
||||
5. Given control 큐 dual binding, When `trex.broadcast` 에 publish, Then `trex.control.req.inst-1` 과 `...inst-2` control 큐 모두에 전달된다(FR-4, S-3).
|
||||
6. Given `instance_key="default"`, When 전체 회귀, Then 기존 RPC(reply-to) 경로 동작 동일·회귀 없음(FR-5).
|
||||
|
||||
## Impacted Area
|
||||
|
||||
- Docs:
|
||||
- `docs/interface-contract.md` — routing key 계약(`{instance_key}.{channel_type}`, broadcast) (REQ-051 처리)
|
||||
- `docs/runtime-flows.md` — directed vs broadcast 라우팅 흐름 (REQ-051 처리)
|
||||
- Modules:
|
||||
- `trex_core/adapters/messaging/topology.py` — DIRECT/BROADCAST exchange + routing key 빌더
|
||||
- `trex_core/adapters/messaging/rabbitmq.py` — exchange 선언 + 큐 바인딩(direct per-instance + control dual) + broadcast publish 경로
|
||||
- Tests:
|
||||
- `tests/adapters/messaging/` — exchange 선언 + 바인딩 + directed 라우팅 격리 + broadcast 전달(인프라 게이트)
|
||||
|
||||
## Notes
|
||||
|
||||
- R-3(broadcast 중복 처리): kill-switch/pause 핸들러는 idempotent(REQ-036 패턴/advisory lock)라 중복 전달 무해.
|
||||
- strategy 모듈(orb-v1) publish 코드 변경은 Out of Scope — 본 REQ 는 exchange/binding/routing key 계약만 정의(CONFIRM_STRATEGY_MODULE_OUT_OF_SCOPE).
|
||||
- broadcast publish helper 는 transport 에 추가하거나 기존 publish 경로 재사용 — 구현에서 결정.
|
||||
|
||||
## Change Log
|
||||
|
||||
### CHG-20260607-01
|
||||
|
||||
- Date: 2026-06-07
|
||||
- Author: Justin Ko
|
||||
|
||||
#### Change Summary
|
||||
|
||||
- dcy-009(confirmed) → REQ-050 초안 생성.
|
||||
|
||||
#### Intent
|
||||
|
||||
- trex.direct/trex.broadcast exchange + 바인딩 + routing key 계약(FR-3/FR-4)을 Integration REQ 로 정규화.
|
||||
|
||||
#### Acceptance Criteria Delta
|
||||
|
||||
- Added: AC-1~6 전체
|
||||
|
||||
#### Impacted Area Delta
|
||||
|
||||
- Docs: interface-contract.md, runtime-flows.md (REQ-051)
|
||||
- Modules: topology.py, rabbitmq.py
|
||||
- Tests: tests/adapters/messaging (인프라 게이트)
|
||||
|
||||
#### Delivery Trace Delta
|
||||
|
||||
- Batches: none (미할당)
|
||||
- Releases: none
|
||||
|
||||
#### Revalidation Impact
|
||||
|
||||
- Reverification Needed: no
|
||||
- Existing Implementation Invalidated: no
|
||||
- Status Recommendation: keep-current
|
||||
- Follow-up Action: confirm-req 로 승인.
|
||||
|
||||
### CHG-20260607-02
|
||||
|
||||
- Date: 2026-06-07
|
||||
- Author: Justin Ko
|
||||
|
||||
#### Change Summary
|
||||
|
||||
- Proposed → Approved (confirm-req).
|
||||
|
||||
#### Intent
|
||||
|
||||
- 승인 게이트 통과: 필수 필드·구체 AC·Impacted Area·Change Log 완비. dcy-009(confirmed) FR-3/FR-4 와 일치. strategy publish 변경은 Out of Scope(계약만 정의) — blocker 아님.
|
||||
|
||||
#### Acceptance Criteria Delta
|
||||
|
||||
- (변경 없음)
|
||||
|
||||
#### Impacted Area Delta
|
||||
|
||||
- (변경 없음)
|
||||
|
||||
#### Delivery Trace Delta
|
||||
|
||||
- Batches: none (미할당)
|
||||
- Releases: none
|
||||
|
||||
#### Revalidation Impact
|
||||
|
||||
- Reverification Needed: no
|
||||
- Existing Implementation Invalidated: no
|
||||
- Status Recommendation: keep-current
|
||||
- Follow-up Action: suggest-batch-reqs / draft-batch 로 delivery 편성.
|
||||
|
||||
### CHG-20260607-03
|
||||
|
||||
- Date: 2026-06-07
|
||||
- Author: Justin Ko
|
||||
|
||||
#### Change Summary
|
||||
|
||||
- Status Approved → Implemented (confirm-req-implemented, 근거 batch BAT-013 release-candidate).
|
||||
|
||||
#### Intent
|
||||
|
||||
- BAT-013 verification 의 AC↔evidence(routing key 단위 + 인프라 게이트: trex.direct/trex.broadcast 선언 + directed 라우팅 격리 S-2 + broadcast dual S-3, 301 passed)로 AC-1~6 충족.
|
||||
|
||||
#### Acceptance Criteria Delta
|
||||
|
||||
- (변경 없음)
|
||||
|
||||
#### Impacted Area Delta
|
||||
|
||||
- (변경 없음)
|
||||
|
||||
#### Delivery Trace Delta
|
||||
|
||||
- Batches: BAT-013 (release-candidate)
|
||||
- Releases: none
|
||||
|
||||
#### Revalidation Impact
|
||||
|
||||
- Reverification Needed: no
|
||||
- Existing Implementation Invalidated: no
|
||||
- Status Recommendation: keep-current (Implemented)
|
||||
- Follow-up Action: draft-release 로 BAT-013 배포 편성. strategy publish 변경은 별도(Out of Scope).
|
||||
@@ -0,0 +1,147 @@
|
||||
# req-049: instance_key 기반 동적 큐명 + default 하위호환
|
||||
|
||||
- Status: Implemented
|
||||
- Type: Interface
|
||||
- Priority: High
|
||||
- Owner: project maintainers
|
||||
|
||||
## Intent
|
||||
|
||||
다중 인스턴스가 동일 RabbitMQ 에서 경쟁 소비(round-robin)로 split-brain 을 일으키지 않도록, 수신 큐명을 `instance_key` 기반으로 인스턴스별 분리한다. `topology.py` 의 고정 큐명 상수를 동적 함수로 교체하고, `declare_topology(instance_key)` + 각 consumer 가 인스턴스 전용 큐를 구독하게 한다. `instance_key="default"` 는 기존 리터럴 큐명을 그대로 사용해 하위호환을 보장한다(dcy-009 FR-2, FR-5, NFR-1/2/3).
|
||||
|
||||
## Requirement
|
||||
|
||||
- `topology.py` 의 고정 큐명 상수(`SNAPSHOT_REQ_QUEUE` 등)를 동적 빌더 함수로 교체/보강한다: `intent_queue(instance_key)`, `snapshot_queue(instance_key)`, `control_queue(instance_key)`.
|
||||
- **하위호환 규칙**: `instance_key in (None, "default")` 이면 기존 리터럴 큐명(`trex.intent.req` / `trex.snapshot.req` / `trex.control.req`)을 반환한다. 그 외에는 `{base}.{instance_key}` 를 반환한다(FR-5: default 무회귀).
|
||||
- `rabbitmq.py` `declare_topology(instance_key=...)` 가 인스턴스 전용 큐를 선언한다. `daemon.py` 의 intent/snapshot/control consumer 가 동일 `instance_key` 의 전용 큐를 `consume_rpc_forever` 로 구독한다.
|
||||
- `daemon.py` 는 설정(REQ-048 TOML)에서 읽은 `instance_key` 를 큐 선언/구독에 일관되게 전달한다.
|
||||
- 기존 pika `BlockingConnection` per-thread 패턴을 유지한다 — 큐 분리로 인해 추가 스레드/커넥션을 만들지 않는다(NFR-2). 워커 스레드 수 불변.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. Given `instance_key="inst-1"`, When `intent_queue("inst-1")`/`snapshot_queue`/`control_queue`, Then 각각 `trex.intent.req.inst-1`/`trex.snapshot.req.inst-1`/`trex.control.req.inst-1` 를 반환한다.
|
||||
2. Given `instance_key="default"`(또는 None), When 동일 빌더, Then 기존 리터럴 `trex.intent.req`/`trex.snapshot.req`/`trex.control.req` 를 반환한다(하위호환).
|
||||
3. Given `declare_topology("inst-1")`, When 선언, Then `*.inst-1` 인스턴스 전용 큐가 선언된다(S-1 일부).
|
||||
4. Given `instance_key="default"` 데몬, When 전체 pytest 스위트 실행, Then 회귀 없음(FR-5/NFR-1, 기존 큐명 테스트 통과).
|
||||
5. Given 데몬 기동, When 워커 스레드 확인, Then per-thread connection 패턴 유지 + 스레드 수 불변(NFR-2).
|
||||
6. Given `instance_key="inst-1"` 와 `inst-2` 동시 운영(전용 큐 분리), When 각자 큐만 소비, Then 한 인스턴스의 snapshot/intent 소비가 다른 인스턴스 큐에 영향을 주지 않는다(NFR-3 split-brain 방지 기반).
|
||||
|
||||
## Impacted Area
|
||||
|
||||
- Docs:
|
||||
- `docs/runtime-flows.md`, `docs/project-structure.md` — 동적 큐명/토폴로지 (REQ-051 처리)
|
||||
- Modules:
|
||||
- `trex_core/adapters/messaging/topology.py` — 동적 큐명 빌더(default 하위호환)
|
||||
- `trex_core/adapters/messaging/rabbitmq.py` — `declare_topology(instance_key)`
|
||||
- `trex_core/runtime/daemon.py` — consumer 가 instance_key 전용 큐 구독
|
||||
- Tests:
|
||||
- `tests/adapters/messaging/` — 큐명 빌더(default vs named) + declare/consume 분리 + 기존 회귀
|
||||
|
||||
## Notes
|
||||
|
||||
- 하위호환 핵심: default = 리터럴 큐명(특수 처리)이라 기존 테스트/배포 무영향. FR-2 의 suffix 규칙은 named instance 에 적용.
|
||||
- NFR-3(split-brain 방지)는 인스턴스 전용 큐로 구조적으로 달성 — 다중 인스턴스 e2e 는 verification 에서 확인.
|
||||
- exchange/binding(트래픽 라우팅)은 REQ-050.
|
||||
|
||||
## Change Log
|
||||
|
||||
### CHG-20260607-01
|
||||
|
||||
- Date: 2026-06-07
|
||||
- Author: Justin Ko
|
||||
|
||||
#### Change Summary
|
||||
|
||||
- dcy-009(confirmed) → REQ-049 초안 생성.
|
||||
|
||||
#### Intent
|
||||
|
||||
- instance_key 동적 큐명 + default 하위호환(FR-2/FR-5)을 Interface REQ 로 정규화.
|
||||
|
||||
#### Acceptance Criteria Delta
|
||||
|
||||
- Added: AC-1~6 전체
|
||||
|
||||
#### Impacted Area Delta
|
||||
|
||||
- Docs: runtime-flows.md, project-structure.md (REQ-051)
|
||||
- Modules: topology.py, rabbitmq.py, daemon.py
|
||||
- Tests: tests/adapters/messaging
|
||||
|
||||
#### Delivery Trace Delta
|
||||
|
||||
- Batches: none (미할당)
|
||||
- Releases: none
|
||||
|
||||
#### Revalidation Impact
|
||||
|
||||
- Reverification Needed: no
|
||||
- Existing Implementation Invalidated: no
|
||||
- Status Recommendation: keep-current
|
||||
- Follow-up Action: confirm-req 로 승인.
|
||||
|
||||
### CHG-20260607-02
|
||||
|
||||
- Date: 2026-06-07
|
||||
- Author: Justin Ko
|
||||
|
||||
#### Change Summary
|
||||
|
||||
- Proposed → Approved (confirm-req).
|
||||
|
||||
#### Intent
|
||||
|
||||
- 승인 게이트 통과: 필수 필드·구체 AC·Impacted Area·Change Log 완비. dcy-009(confirmed) FR-2/FR-5 와 일치. default 하위호환 규칙 명확.
|
||||
|
||||
#### Acceptance Criteria Delta
|
||||
|
||||
- (변경 없음)
|
||||
|
||||
#### Impacted Area Delta
|
||||
|
||||
- (변경 없음)
|
||||
|
||||
#### Delivery Trace Delta
|
||||
|
||||
- Batches: none (미할당)
|
||||
- Releases: none
|
||||
|
||||
#### Revalidation Impact
|
||||
|
||||
- Reverification Needed: no
|
||||
- Existing Implementation Invalidated: no
|
||||
- Status Recommendation: keep-current
|
||||
- Follow-up Action: suggest-batch-reqs / draft-batch 로 delivery 편성.
|
||||
|
||||
### CHG-20260607-03
|
||||
|
||||
- Date: 2026-06-07
|
||||
- Author: Justin Ko
|
||||
|
||||
#### Change Summary
|
||||
|
||||
- Status Approved → Implemented (confirm-req-implemented, 근거 batch BAT-013 release-candidate).
|
||||
|
||||
#### Intent
|
||||
|
||||
- BAT-013 verification 의 AC↔evidence(빌더 단위 named suffix/default 리터럴 + declare_topology(instance_key) + 전체 301 passed 로 default 무회귀)로 AC-1~6 충족. NFR-1/2/3 흡수.
|
||||
|
||||
#### Acceptance Criteria Delta
|
||||
|
||||
- (변경 없음)
|
||||
|
||||
#### Impacted Area Delta
|
||||
|
||||
- (변경 없음)
|
||||
|
||||
#### Delivery Trace Delta
|
||||
|
||||
- Batches: BAT-013 (release-candidate)
|
||||
- Releases: none
|
||||
|
||||
#### Revalidation Impact
|
||||
|
||||
- Reverification Needed: no
|
||||
- Existing Implementation Invalidated: no
|
||||
- Status Recommendation: keep-current (Implemented)
|
||||
- Follow-up Action: draft-release 로 BAT-013 배포 편성.
|
||||
+8
-4
@@ -74,7 +74,7 @@
|
||||
|
||||
## Next Requirement ID
|
||||
|
||||
- Current: `REQ-048`
|
||||
- Current: `REQ-052`
|
||||
- Rule: `Requirement Register`에 있는 가장 큰 REQ 번호보다 정확히 1 커야 한다.
|
||||
|
||||
## Requirement Register
|
||||
@@ -128,11 +128,15 @@
|
||||
| REQ-045 | kill-switch 실행 — 범위별 holding 즉시 청산 + 이력 | Interface | Implemented | High | project maintainers | [req-045](./Interface/req-045_kill-switch-execution.md) |
|
||||
| REQ-046 | kill-switch 당일 제외 masking + 자동해제 + resume | Interface | Implemented | High | project maintainers | [req-046](./Interface/req-046_killswitch-day-exclusion-masking-and-resume.md) |
|
||||
| REQ-047 | kill-switch baseline 문서 정합 | Documentation | Implemented | Medium | project maintainers | [req-047](./Documentation/req-047_killswitch-baseline-alignment.md) |
|
||||
| REQ-048 | per-instance TOML 설정 로더 + --config 필수 | Configuration | Implemented | High | project maintainers | [req-048](./Configuration/req-048_per-instance-toml-config.md) |
|
||||
| REQ-049 | instance_key 기반 동적 큐명 + default 하위호환 | Interface | Implemented | High | project maintainers | [req-049](./Interface/req-049_instance-key-dynamic-queue-naming.md) |
|
||||
| REQ-050 | trex.direct(topic) + trex.broadcast(fanout) exchange + 바인딩 | Integration | Implemented | High | project maintainers | [req-050](./Integration/req-050_direct-broadcast-exchanges-and-binding.md) |
|
||||
| REQ-051 | multi-instance topology baseline 문서 정합 | Documentation | Implemented | Medium | project maintainers | [req-051](./Documentation/req-051_multi-instance-baseline-alignment.md) |
|
||||
|
||||
## Recent Change Log Summary
|
||||
|
||||
- 2026-06-07 :: REQ-048~051 Approved → Implemented (confirm-req-implemented): BAT-013 verify 완료(신규 9 단위 + RabbitMQ 라우팅 2, 단위 271 passed, 인프라 게이트 301 passed(directed 격리 S-2 + broadcast dual S-3 + default 무회귀 S-4), ruff clean). per-instance TOML 로더+`--config`, instance_key 동적 큐명(default 리터럴 하위호환), trex.direct/trex.broadcast exchange+바인딩, baseline 3문서 정합. (confirm-req-implemented)
|
||||
- 2026-06-07 :: REQ-048~051 Proposed → Approved (confirm-req): dcy-009 multi-instance 4개 전원 승인 게이트 통과. dcy-009(confirmed) FR/OQ 와 모순 없음, 필수 필드·구체 AC 완비. strategy publish 변경·TOML 추가필드는 비핵심 후속. (confirm-req)
|
||||
- 2026-06-07 :: REQ-048~051 생성 (draft-req): dcy-009(다중 인스턴스 RabbitMQ 토폴로지, confirmed) → 4개 REQ 초안. per-instance TOML 로더+`--config` 필수(048), instance_key 동적 큐명+default 하위호환(049), trex.direct/trex.broadcast exchange+바인딩(050), baseline 정합(051). NFR-1/2/3/4 는 048/049 AC 로 흡수. (draft-req)
|
||||
- 2026-06-07 :: REQ-043~047 Approved → Implemented (confirm-req-implemented): BAT-012 verify 완료(신규 26 단위 + killswitch PG 5, 단위 262 passed, 인프라 게이트 290 passed(마이그레이션 005 round-trip), ruff clean, paper 데몬 control RPC e2e PASS). `trex.control.req` dispatch, KillSwitchService(범위별 청산+이력), killswitch_exclusion store/005, day-scoped masking+resume, baseline 3문서 정합. (confirm-req-implemented)
|
||||
- 2026-06-07 :: REQ-043~047 Proposed → Approved (confirm-req): kill-switch 5개 전원 승인 게이트 통과. dcy-008(confirmed) 범위·결정(OQ-1/2/4 Answered, OQ-3 Deferred)과 모순 없음, 필수 필드·구체 AC·Impacted Area·Change Log 완비. 큐 rename 호환·전역 sentinel·pending_buy 는 비핵심 후속으로 blocker 아님. (confirm-req)
|
||||
- 2026-06-07 :: REQ-043~047 생성 (draft-req): dcy-008(비상 정지/kill-switch, confirmed) → 5개 REQ 초안. 제어 채널 `trex.control.req`+action dispatch(043), day-scoped 제외 store+마이그레이션(044), kill-switch 실행=범위별 holding 순차 sell()+이력(045), 당일 제외 masking+자동복귀+resume_kill_switch(046), baseline 정합(047). NFR-1/2/3 은 045/046 AC 로 흡수, OQ-3(pending_buy) REQ Deferred. (draft-req)
|
||||
- 2026-06-07 :: REQ-038 CHG-20260607-05 Approved → Implemented (confirm-req-implemented): tr_id 정정(`FHKST01010100`) 구현·재검증 — kis.py 수정, AC-7 테스트 정정, 단위 236 passed/ruff clean, 실 KIS LIVE `get_current_price=329000`(rt_cd=0) 확인.
|
||||
- 2026-06-07 :: REQ-038 CHG-20260607-04 Implemented → Approved (change-req): 현재가 시세 tr_id 정정 `TTTC8397R`/`VTTC8397R` → `FHKST01010100`. 배포 전 실 KIS LIVE 진단에서 구 tr_id 가 "없는 서비스 코드"(rt_cd=1)로 거부 → LIVE 에서 손절/익절 silent 미발동 확인. 표준 시세 tr 정상(stck_prpr=329000). (change-req)
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
"""Multi-instance topology tests (REQ-049/050 / BAT-013).
|
||||
|
||||
Builder/routing-key tests run always. declare_topology + directed/broadcast
|
||||
routing tests are gated on TREX_RABBITMQ_TEST_URL.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import time
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
|
||||
from trex_core.adapters.messaging import topology
|
||||
from trex_core.adapters.messaging.rabbitmq import RabbitMQTransport
|
||||
|
||||
|
||||
# ---- builders (always run) --------------------------------------------------
|
||||
|
||||
def test_named_instance_suffix():
|
||||
"""REQ-049 AC-1."""
|
||||
assert topology.intent_queue("inst-1") == "trex.intent.req.inst-1"
|
||||
assert topology.snapshot_queue("inst-1") == "trex.snapshot.req.inst-1"
|
||||
assert topology.control_queue("inst-1") == "trex.control.req.inst-1"
|
||||
|
||||
|
||||
def test_default_is_literal():
|
||||
"""REQ-049 AC-2: default keeps legacy literal names (backward-compat)."""
|
||||
assert topology.intent_queue("default") == "trex.intent.req"
|
||||
assert topology.snapshot_queue(None) == "trex.snapshot.req"
|
||||
assert topology.control_queue() == "trex.control.req"
|
||||
|
||||
|
||||
def test_direct_routing_key():
|
||||
"""REQ-050 AC-3."""
|
||||
assert topology.direct_routing_key("inst-1", "control") == "inst-1.control"
|
||||
assert topology.direct_routing_key("inst-2", "intent") == "inst-2.intent"
|
||||
|
||||
|
||||
# ---- RabbitMQ topology (gated) ----------------------------------------------
|
||||
|
||||
@pytest.fixture
|
||||
def mq_url() -> str:
|
||||
url = os.environ.get("TREX_RABBITMQ_TEST_URL")
|
||||
if not url:
|
||||
pytest.skip("TREX_RABBITMQ_TEST_URL not set; skipping RabbitMQ integration test")
|
||||
return url
|
||||
|
||||
|
||||
def _delete_instance_queues(url, instance_key):
|
||||
import pika
|
||||
conn = pika.BlockingConnection(pika.URLParameters(url))
|
||||
try:
|
||||
ch = conn.channel()
|
||||
for q in (topology.intent_queue(instance_key), topology.snapshot_queue(instance_key),
|
||||
topology.control_queue(instance_key)):
|
||||
try:
|
||||
ch.queue_delete(queue=q)
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def test_directed_routing_isolation(mq_url):
|
||||
"""REQ-050 AC-4 (S-2): inst-1 routing key reaches only inst-1's queue."""
|
||||
import pika
|
||||
k1 = "it1-" + uuid.uuid4().hex[:6]
|
||||
k2 = "it2-" + uuid.uuid4().hex[:6]
|
||||
t1 = RabbitMQTransport(mq_url)
|
||||
t1.declare_topology(k1)
|
||||
t2 = RabbitMQTransport(mq_url)
|
||||
t2.declare_topology(k2)
|
||||
try:
|
||||
conn = pika.BlockingConnection(pika.URLParameters(mq_url))
|
||||
ch = conn.channel()
|
||||
ch.basic_publish(exchange=topology.DIRECT_EXCHANGE,
|
||||
routing_key=topology.direct_routing_key(k1, "intent"), body=b"x")
|
||||
time.sleep(0.4)
|
||||
m1 = ch.basic_get(topology.intent_queue(k1), auto_ack=True)
|
||||
m2 = ch.basic_get(topology.intent_queue(k2), auto_ack=True)
|
||||
conn.close()
|
||||
assert m1[0] is not None, "inst-1 intent queue should receive the directed message"
|
||||
assert m2[0] is None, "inst-2 intent queue must NOT receive it"
|
||||
finally:
|
||||
t1.close()
|
||||
t2.close()
|
||||
_delete_instance_queues(mq_url, k1)
|
||||
_delete_instance_queues(mq_url, k2)
|
||||
|
||||
|
||||
def test_broadcast_dual_binding(mq_url):
|
||||
"""REQ-050 AC-5 (S-3): broadcast reaches every instance's control queue."""
|
||||
import pika
|
||||
k1 = "bc1-" + uuid.uuid4().hex[:6]
|
||||
k2 = "bc2-" + uuid.uuid4().hex[:6]
|
||||
t1 = RabbitMQTransport(mq_url)
|
||||
t1.declare_topology(k1)
|
||||
t2 = RabbitMQTransport(mq_url)
|
||||
t2.declare_topology(k2)
|
||||
try:
|
||||
conn = pika.BlockingConnection(pika.URLParameters(mq_url))
|
||||
ch = conn.channel()
|
||||
ch.basic_publish(exchange=topology.BROADCAST_EXCHANGE, routing_key="", body=b"halt")
|
||||
time.sleep(0.4)
|
||||
m1 = ch.basic_get(topology.control_queue(k1), auto_ack=True)
|
||||
m2 = ch.basic_get(topology.control_queue(k2), auto_ack=True)
|
||||
conn.close()
|
||||
assert m1[0] is not None and m2[0] is not None, "both control queues should receive broadcast"
|
||||
finally:
|
||||
t1.close()
|
||||
t2.close()
|
||||
_delete_instance_queues(mq_url, k1)
|
||||
_delete_instance_queues(mq_url, k2)
|
||||
@@ -0,0 +1,68 @@
|
||||
"""Per-instance TOML config loader tests (REQ-048 / BAT-013)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from trex_core.runtime.config import DaemonConfig, DaemonInstanceConfig
|
||||
|
||||
_REPO = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def _write_toml(tmp_path, body: str) -> str:
|
||||
p = tmp_path / "inst.toml"
|
||||
p.write_text(body)
|
||||
return str(p)
|
||||
|
||||
|
||||
def test_from_toml_parses(tmp_path):
|
||||
"""AC-1: from_toml reads instance_key + portfolio_hub_token."""
|
||||
path = _write_toml(tmp_path, 'instance_key = "inst-1"\nportfolio_hub_token = "tok"\n')
|
||||
cfg = DaemonInstanceConfig.from_toml(path)
|
||||
assert cfg.instance_key == "inst-1"
|
||||
assert cfg.portfolio_hub_token == "tok"
|
||||
|
||||
|
||||
def test_from_toml_missing_instance_key_raises(tmp_path):
|
||||
"""AC-4: missing required instance_key → error (no silent default)."""
|
||||
path = _write_toml(tmp_path, 'portfolio_hub_token = "tok"\n')
|
||||
with pytest.raises(RuntimeError):
|
||||
DaemonInstanceConfig.from_toml(path)
|
||||
|
||||
|
||||
def test_from_config_injects_token(tmp_path, monkeypatch):
|
||||
"""AC-3 / S-5: TOML token is injected into PortfolioHubConfig when base_url is set,
|
||||
even with TREX_PORTFOLIO_HUB_TOKEN absent from the environment."""
|
||||
monkeypatch.setenv("TREX_POSTGRESQL_DSN", "postgresql://x/y")
|
||||
monkeypatch.setenv("TREX_PORTFOLIO_HUB_BASE_URL", "http://hub:18000")
|
||||
monkeypatch.delenv("TREX_PORTFOLIO_HUB_TOKEN", raising=False)
|
||||
path = _write_toml(tmp_path, 'instance_key = "inst-9"\nportfolio_hub_token = "toml-tok"\n')
|
||||
cfg = DaemonConfig.from_config(path)
|
||||
assert cfg.instance_key == "inst-9"
|
||||
assert cfg.portfolio_hub is not None
|
||||
assert cfg.portfolio_hub.token == "toml-tok"
|
||||
|
||||
|
||||
def test_default_instance_key_when_from_env(monkeypatch):
|
||||
monkeypatch.setenv("TREX_POSTGRESQL_DSN", "postgresql://x/y")
|
||||
assert DaemonConfig.from_env().instance_key == "default"
|
||||
|
||||
|
||||
def test_config_required_cli_exits_nonzero():
|
||||
"""AC-2: daemon entry without --config exits non-zero (argparse required)."""
|
||||
proc = subprocess.run(
|
||||
[sys.executable, "-m", "trex_core.runtime.daemon"],
|
||||
capture_output=True, text=True, timeout=30, cwd=str(_REPO),
|
||||
)
|
||||
assert proc.returncode != 0
|
||||
assert "config" in (proc.stderr + proc.stdout).lower()
|
||||
|
||||
|
||||
def test_gitignore_excludes_instance_toml():
|
||||
"""AC-5: .gitignore excludes instance TOML but keeps the example."""
|
||||
gi = (_REPO / ".gitignore").read_text()
|
||||
assert "/config/*.toml" in gi
|
||||
assert "!/config/instance.example.toml" in gi
|
||||
@@ -28,14 +28,19 @@ import pika
|
||||
import pika.exceptions
|
||||
|
||||
from trex_core.adapters.messaging.topology import (
|
||||
CONTROL_REQ_QUEUE,
|
||||
BROADCAST_EXCHANGE,
|
||||
DEFAULT_INSTANCE_KEY,
|
||||
DIRECT_EXCHANGE,
|
||||
EVENTS_DLQ_EXCHANGE,
|
||||
EVENTS_DLQ_QUEUE,
|
||||
EVENTS_EXCHANGE,
|
||||
INTENT_REQ_QUEUE,
|
||||
PAUSE_REQ_QUEUE,
|
||||
SNAPSHOT_REQ_QUEUE,
|
||||
STRATEGY_QUEUE_ARGS,
|
||||
control_queue,
|
||||
direct_routing_key,
|
||||
intent_queue,
|
||||
snapshot_queue,
|
||||
)
|
||||
from trex_core.application.messaging import (
|
||||
EventHandler,
|
||||
@@ -118,37 +123,33 @@ class RabbitMQTransport:
|
||||
return self._health
|
||||
|
||||
# ------------------------------------------------------------ topology
|
||||
def declare_topology(self) -> None:
|
||||
def declare_topology(self, instance_key: str = DEFAULT_INSTANCE_KEY) -> None:
|
||||
self._ensure_connected()
|
||||
ch = self._channel
|
||||
assert ch is not None
|
||||
dlx = {"x-dead-letter-exchange": EVENTS_DLQ_EXCHANGE}
|
||||
# Events topic + DLQ
|
||||
ch.exchange_declare(EVENTS_EXCHANGE, exchange_type="topic", durable=True)
|
||||
ch.exchange_declare(EVENTS_DLQ_EXCHANGE, exchange_type="fanout", durable=True)
|
||||
ch.queue_declare(EVENTS_DLQ_QUEUE, durable=True)
|
||||
ch.queue_bind(EVENTS_DLQ_QUEUE, EVENTS_DLQ_EXCHANGE)
|
||||
# RPC queues (durable for intent + snapshot)
|
||||
ch.queue_declare(
|
||||
INTENT_REQ_QUEUE,
|
||||
durable=True,
|
||||
arguments={"x-dead-letter-exchange": EVENTS_DLQ_EXCHANGE},
|
||||
)
|
||||
ch.queue_declare(
|
||||
SNAPSHOT_REQ_QUEUE,
|
||||
durable=True,
|
||||
arguments={"x-dead-letter-exchange": EVENTS_DLQ_EXCHANGE},
|
||||
)
|
||||
ch.queue_declare(
|
||||
PAUSE_REQ_QUEUE,
|
||||
durable=True,
|
||||
arguments={"x-dead-letter-exchange": EVENTS_DLQ_EXCHANGE},
|
||||
)
|
||||
# BAT-012 / REQ-043: operator control channel (action dispatch).
|
||||
ch.queue_declare(
|
||||
CONTROL_REQ_QUEUE,
|
||||
durable=True,
|
||||
arguments={"x-dead-letter-exchange": EVENTS_DLQ_EXCHANGE},
|
||||
)
|
||||
# BAT-013 / REQ-050: multi-instance control exchanges.
|
||||
ch.exchange_declare(DIRECT_EXCHANGE, exchange_type="topic", durable=True)
|
||||
ch.exchange_declare(BROADCAST_EXCHANGE, exchange_type="fanout", durable=True)
|
||||
# Per-instance RPC queues (default → literal names, REQ-049 backward-compat).
|
||||
iq = intent_queue(instance_key)
|
||||
sq = snapshot_queue(instance_key)
|
||||
cq = control_queue(instance_key)
|
||||
for q, channel_type in ((iq, "intent"), (sq, "snapshot"), (cq, "control")):
|
||||
ch.queue_declare(q, durable=True, arguments=dlx)
|
||||
# Directed binding: trex.direct → {instance_key}.{channel_type}
|
||||
ch.queue_bind(q, DIRECT_EXCHANGE,
|
||||
routing_key=direct_routing_key(instance_key, channel_type))
|
||||
# Control queue is also fanout-bound so global broadcasts hit every instance.
|
||||
ch.queue_bind(cq, BROADCAST_EXCHANGE)
|
||||
# Legacy pause queue — declared for the default instance only (REQ-032 alias).
|
||||
if instance_key == DEFAULT_INSTANCE_KEY:
|
||||
ch.queue_declare(PAUSE_REQ_QUEUE, durable=True, arguments=dlx)
|
||||
|
||||
def declare_strategy_queue(self, queue: str, binding_pattern: str) -> None:
|
||||
"""Per-strategy durable queue (REQ-023 AC-4) bound to events exchange."""
|
||||
|
||||
@@ -19,6 +19,39 @@ PAUSE_REQ_QUEUE = "trex.pause.req" # BAT-008 / REQ-032 (legacy alias of con
|
||||
CONTROL_REQ_QUEUE = "trex.control.req" # BAT-012 / REQ-043 operator control channel
|
||||
# (action: pause|resume|list|kill_switch|resume_kill_switch)
|
||||
|
||||
# --- multi-instance topology (BAT-013 / dcy-009) ---
|
||||
DEFAULT_INSTANCE_KEY = "default"
|
||||
DIRECT_EXCHANGE = "trex.direct" # topic, durable — directed per-instance routing (REQ-050)
|
||||
BROADCAST_EXCHANGE = "trex.broadcast" # fanout, durable — global control to all instances (REQ-050)
|
||||
|
||||
|
||||
def _instance_queue(base: str, instance_key: str | None) -> str:
|
||||
"""`{base}.{instance_key}` for named instances; literal `base` for default.
|
||||
|
||||
The default special-case keeps existing queue names (and the entire test
|
||||
suite + deployed clients) byte-for-byte (REQ-049 FR-5 backward-compat).
|
||||
"""
|
||||
if instance_key in (None, DEFAULT_INSTANCE_KEY):
|
||||
return base
|
||||
return f"{base}.{instance_key}"
|
||||
|
||||
|
||||
def intent_queue(instance_key: str | None = DEFAULT_INSTANCE_KEY) -> str:
|
||||
return _instance_queue(INTENT_REQ_QUEUE, instance_key)
|
||||
|
||||
|
||||
def snapshot_queue(instance_key: str | None = DEFAULT_INSTANCE_KEY) -> str:
|
||||
return _instance_queue(SNAPSHOT_REQ_QUEUE, instance_key)
|
||||
|
||||
|
||||
def control_queue(instance_key: str | None = DEFAULT_INSTANCE_KEY) -> str:
|
||||
return _instance_queue(CONTROL_REQ_QUEUE, instance_key)
|
||||
|
||||
|
||||
def direct_routing_key(instance_key: str, channel_type: str) -> str:
|
||||
"""Directed routing key for `trex.direct` (REQ-050): `{instance_key}.{channel_type}`."""
|
||||
return f"{instance_key}.{channel_type}"
|
||||
|
||||
# --- Events ---
|
||||
EVENTS_EXCHANGE = "trex.events" # topic, durable
|
||||
EVENTS_DLQ_EXCHANGE = "trex.events.dlq" # fanout, durable; receives unroutable/NACKed
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, replace
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@@ -85,12 +85,44 @@ class SafetySellConfig:
|
||||
market_close_liquidation_time: Optional[str] = None # "HH:MM" KST, optional
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DaemonInstanceConfig:
|
||||
"""Per-instance TOML settings (REQ-048 / BAT-013).
|
||||
|
||||
`instance_key` and `portfolio_hub_token` live in a TOML file (not .env)
|
||||
so they are not committed and so multiple instances can run distinct
|
||||
queues. Loaded via `--config <path>.toml`.
|
||||
"""
|
||||
|
||||
instance_key: str
|
||||
portfolio_hub_token: str = ""
|
||||
|
||||
@staticmethod
|
||||
def from_toml(path: str) -> "DaemonInstanceConfig":
|
||||
import tomllib
|
||||
|
||||
with open(path, "rb") as fh:
|
||||
data = tomllib.load(fh)
|
||||
instance_key = data.get("instance_key")
|
||||
if not instance_key:
|
||||
raise RuntimeError(
|
||||
f"instance TOML missing required 'instance_key': {path}"
|
||||
)
|
||||
return DaemonInstanceConfig(
|
||||
instance_key=str(instance_key),
|
||||
portfolio_hub_token=str(data.get("portfolio_hub_token") or ""),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DaemonConfig:
|
||||
dsn: str
|
||||
total_slots: int
|
||||
broker: str # 'paper' | 'kis'
|
||||
execution_interval_sec: float
|
||||
# BAT-013 / REQ-049: instance identity for multi-instance queue naming.
|
||||
# "default" keeps legacy literal queue names (backward-compat).
|
||||
instance_key: str = "default"
|
||||
kis: Optional[KISConfig] = None
|
||||
# REQ-011: paper broker tuning (only consulted when broker == 'paper').
|
||||
paper: PaperBrokerConfig = PaperBrokerConfig()
|
||||
@@ -179,3 +211,18 @@ class DaemonConfig:
|
||||
snapshot_schedule=snapshot_schedule,
|
||||
safety_sell=safety_sell,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def from_config(toml_path: str) -> "DaemonConfig":
|
||||
"""Build config from env + a per-instance TOML file (REQ-048 / BAT-013).
|
||||
|
||||
`instance_key` and `portfolio_hub_token` come from TOML (not .env).
|
||||
TOML `portfolio_hub_token` overrides the (env-derived) PortfolioHubConfig
|
||||
token when a portfolio-hub base_url is configured.
|
||||
"""
|
||||
base = DaemonConfig.from_env()
|
||||
inst = DaemonInstanceConfig.from_toml(toml_path)
|
||||
pf = base.portfolio_hub
|
||||
if inst.portfolio_hub_token and pf is not None:
|
||||
pf = replace(pf, token=inst.portfolio_hub_token)
|
||||
return replace(base, instance_key=inst.instance_key, portfolio_hub=pf)
|
||||
|
||||
@@ -217,7 +217,7 @@ class Daemon:
|
||||
transport = _build_transport(self.config)
|
||||
self._transports.append(transport)
|
||||
try:
|
||||
transport.declare_topology()
|
||||
transport.declare_topology(self.config.instance_key)
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
handler = make_intent_rpc_handler(
|
||||
@@ -225,13 +225,14 @@ class Daemon:
|
||||
slot_service=self.slot_service,
|
||||
watchdog_store=self.watchdog,
|
||||
)
|
||||
transport.consume_rpc_forever(topology.INTENT_REQ_QUEUE, handler, self.stop)
|
||||
transport.consume_rpc_forever(
|
||||
topology.intent_queue(self.config.instance_key), handler, self.stop)
|
||||
|
||||
def _snapshot_rpc_loop(self) -> None:
|
||||
transport = _build_transport(self.config)
|
||||
self._transports.append(transport)
|
||||
try:
|
||||
transport.declare_topology()
|
||||
transport.declare_topology(self.config.instance_key)
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
handler = make_snapshot_rpc_handler(
|
||||
@@ -242,20 +243,22 @@ class Daemon:
|
||||
watchdog_store=self.watchdog,
|
||||
killswitch_store=self.killswitch_store,
|
||||
)
|
||||
transport.consume_rpc_forever(topology.SNAPSHOT_REQ_QUEUE, handler, self.stop)
|
||||
transport.consume_rpc_forever(
|
||||
topology.snapshot_queue(self.config.instance_key), handler, self.stop)
|
||||
|
||||
def _control_rpc_loop(self) -> None:
|
||||
# REQ-043: operator control channel (pause/resume/list + kill_switch/resume_kill_switch).
|
||||
transport = _build_transport(self.config)
|
||||
self._transports.append(transport)
|
||||
try:
|
||||
transport.declare_topology()
|
||||
transport.declare_topology(self.config.instance_key)
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
handler = make_control_rpc_handler(
|
||||
self.pause_store, kill_switch_service=self.kill_switch,
|
||||
)
|
||||
transport.consume_rpc_forever(topology.CONTROL_REQ_QUEUE, handler, self.stop)
|
||||
transport.consume_rpc_forever(
|
||||
topology.control_queue(self.config.instance_key), handler, self.stop)
|
||||
|
||||
def _safety_sell_loop(self) -> None:
|
||||
# REQ-037/038/039: forced_close / stop-loss·take-profit / market-close.
|
||||
@@ -281,7 +284,7 @@ class Daemon:
|
||||
)
|
||||
self.execution.reconcile() # REQ-014 restart recovery
|
||||
try:
|
||||
self.event_publisher.declare_topology()
|
||||
self.event_publisher.declare_topology(self.config.instance_key)
|
||||
except Exception: # noqa: BLE001
|
||||
_log.warning("event publisher topology declare failed", exc_info=True)
|
||||
|
||||
@@ -325,8 +328,17 @@ class Daemon:
|
||||
|
||||
|
||||
def main() -> None: # pragma: no cover - process entrypoint
|
||||
import argparse
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
config = DaemonConfig.from_env()
|
||||
parser = argparse.ArgumentParser(prog="trex-core-daemon")
|
||||
parser.add_argument(
|
||||
"--config", required=True,
|
||||
help="per-instance TOML config path (instance_key, portfolio_hub_token). "
|
||||
"Required — no default (REQ-048 / dcy-009 OQ-2).",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
config = DaemonConfig.from_config(args.config)
|
||||
Daemon(config).run()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user