22 KiB
id, status
| id | status |
|---|---|
| dcy-0009 | confirmed |
Persistent strategy-instance state lifecycle and identity
Problem
slot_strategy_state는 trex-core가 내용을 해석하지 않는 opaque 전략 상태이지만, 현재 저장·조회·삭제 계약은 그 상태의 lifecycle과 소유권을 core의 slot occupancy 및 strategy_id에 결합한다. 슬롯이 어떤 경로로든 INACTIVE가 되면 core가 해당 슬롯의 모든 전략 상태를 삭제하고, 저장된 occupancy_seq가 현재 슬롯과 다르면 조회에서 숨긴다. 이 계약은 TSL peak처럼 occupancy마다 새로 시작하는 상태에는 맞지만, TVR-v2 reserve처럼 close와 재점유를 넘어 같은 전략 실행 instance가 이어서 사용해야 하는 상태를 표현하지 못한다. Opaque data의 의미와 만료 시점을 모르는 core가 모든 전략에 동일한 삭제 정책을 강제한다는 점도 책임 경계와 맞지 않는다.
현재 row와 RPC namespace는 (slot_id, strategy_id)를 사용한다. 여기서 strategy_id는 tvr_v2 같은 전략 종류를 나타내므로, 같은 종류의 서로 다른 설정·실행 instance가 같은 슬롯을 사용하면 하나의 row와 version counter를 공유한다. 한 instance의 상태가 다른 instance에 상속되거나 서로의 CAS가 충돌할 수 있다.
strategy_instance_id는 state namespace이면서 전략 실행 instance의 안정적인 identity다. 현재 slots.req는 한 슬롯에서 조회된 모든 strategy-state namespace를 map으로 반환하지만, 일반 strategy가 필요한 것은 자기 strategy_instance_id에 해당하는 state뿐이다. 다른 strategy instance의 state를 함께 노출하면 consumer가 잘못된 entry를 선택할 여지가 있고 wire가 불필요하게 전체 namespace를 공개한다. strategy_instance_id는 매 요청에서 임의로 선택하는 값이 아니라 strategy runtime의 필수·불변 non-empty string 설정이어야 한다. 누락·비문자열·빈 값처럼 잘못 설정된 instance가 실행되면 state namespace 오염보다 잘못된 slot·queue·주문 경계에서 전략이 동작하는 문제가 더 크므로, 그런 runtime은 worker나 decision cycle을 시작할 수 없어야 한다.
기대 상태는 lifecycle과 identity 변경을 함께 다루는 것이다. Strategy state는 slot occupancy에서 완전히 분리되어 close와 재점유를 넘어 보존되고, 하나의 slot-scoped data map 안에서 안정적인 strategy_instance_id별 entry와 monotonic version을 가져야 한다. State persistence와 write RPC에서 occupancy_seq를 제거하고 동시 write는 요청 instance entry의 version CAS만으로 조정한다. 하나의 core instance_key에는 여러 strategy instance가 연결될 수 있으며 core가 그 목록을 별도 registry로 저장하지 않는다. 각 strategy runtime은 필수 설정으로 확정한 자기 strategy_instance_id만 slots.req와 state write에 사용하고, core는 전체 map이 아니라 요청 ID와 정확히 일치하는 entry 하나만 slots 응답에 넣어야 한다. 잘못된 설정이면 strategy는 실행 전에 fail-closed해야 한다.
Evidence
-
GitHub issue #77은 close 시 자동 삭제와 read-side occupancy filtering을 제거하고 state 의미·session 만료·갱신 판단을 전략에 돌리도록 요구한다. 본문에는 이전 설계의
expected_occupancy_seqCAS 유지가 남아 있다. -
GitHub issue #78은 그 뒤 확정된 persistence/write contract로서 state namespace와 RPC의 소유 키를 안정적인
strategy_instance_id로 바꾸고, 저장 요청을{slot_id, strategy_instance_id, expected_version, data}로 정의한다. 사용자는 #78이occupancy_seq에 관한 마지막 결정이며 이 계약이 #77의expected_occupancy_seq유지 문구보다 우선한다고 확인했다. 또한 slot persistence는data[strategy_instance_id] = {version, data, updated_at}형태의 slot-scoped map으로 저장한다고 확인했다. 따라서 이 draft는 strategy-state persistence·application model·write RPC·CAS에서occupancy_seq를 제거하고 map entry별 version CAS만 유지한다. -
GitHub issue #79는
slots.reqresponse에 관한 후속 최종 계약이다. 저장소에는 여러strategy_instance_identry가 함께 존재하지만 일반 전략 응답에는 요청의strategy_instance_id와 정확히 일치하는 entry 하나만{version, data, updated_at}단일 object로 반환하고 다른 instance의 ID와 state를 포함하지 않는다. 사용자는 응답 필터 기준이strategy_id집합이 아니라 요청의 정확한strategy_instance_id라고 확인했다. 이 Discovery에서는 core-side instance 목록 registry나 caller 인증을 두지 않으며,strategy_instance_id를 strategy runtime의 필수·불변 설정으로 사용한다. -
현재
mainrevisionddf7d6710bed9d0b8b91a3ce9c96ad17e129bd78의trex_core/adapters/persistence/sql/015_create_slot_strategy_state.sql은PRIMARY KEY (slot_id, strategy_id)를 선언하고occupancy_seq,version, opaquedata를 저장한다. -
trex_core/application/strategy_state.py의StrategyStateEntry,StrategyStateStore,InMemoryStrategyStateStore는 state를(slot_id, strategy_id)로 식별한다.get_for_slots()는 저장된occupancy_seq가 현재 값과 다르면 row를 숨기고,delete_all(slot_id)는 해당 slot의 모든 namespace를 삭제한다._cas()는expected_occupancy_seq == current_occupancy_seq와 expected/stored version 일치를 독립적으로 검사한다. -
trex_core/adapters/persistence/postgresql.py의PgStrategyStateStore도(slot_id, strategy_id)로SELECT ... FOR UPDATE및 upsert를 수행한다. Version은 기존 row가 없으면 0에서 시작하고 성공한 write마다 1 증가한다. -
trex_core/domain/slot/service.py의SlotService.transit_slot()은 모든to_state is INACTIVE전이에서strategy_state_store.delete_all(slot_id)을 호출한다. Strategy state 삭제는 slot update 및 transition-log append와 같은 transition 경로 안에서 실행된다. -
trex_core/application/messaging_handlers.py의make_slots_rpc_handler()는 현재 occupancy와 일치하는 모든 state를 읽어strategy_state[strategy_id]map 전체를 슬롯 row에 넣는다.make_strategy_state_set_handler()는 payload의strategy_id를 필수로 받아 그대로 저장소에 전달하며 caller와 namespace의 소유 관계를 검사하지 않는다. -
실제 write 호출은 strategy daemon이 AMQP direct-reply-to RPC로 instance별
trex.slots.strategy_state.set[.{instance_key}]queue에 요청하는 방식이다. 현재 TSL client는{slot_id, strategy_id, expected_occupancy_seq, expected_version, data}를 보내며data를 opaque JSON string으로 직렬화한다. 현재 구현의correlation_id는 reply matching에만 사용되고expected_occupancy_seq와expected_versionCAS가 각각 occupancy 전환과 version 충돌을 검사한다. 이 중 occupancy 결합은 #78의 최종 payload로 대체할 현재 baseline이다. -
trex_core/runtime/daemon.py는slots.req와strategy_state.setconsumer를config.instance_key별 queue에 연결하지만 두 handler에 strategy-instance registry를 주입하지 않는다.trex_core/runtime/config.py와 repository 전체를strategy_instance_id로 검색한 결과 정의나 사용처는 0건이고, per-instance TOML에는instance_key만 있다. -
tests/domain/slot/test_occupancy_seq_and_close_delete.py는 모든 close 경로의 자동 삭제와 재점유 후 빈 상태를 기대한다.tests/application/test_slots_handler_strategy_state.py는 재점유 시 이전 상태가 노출되지 않는 현재 계약을,tests/application/test_strategy_state_set_handler.py와tests/application/test_strategy_state_store.py는 occupancy mismatch 거부와(slot_id, strategy_id)별 version CAS를 검증한다. -
위 네 focused test와 PostgreSQL 관련 test를 현재 revision에서
strategy_state or occupancy_seq or close_delete로 실행한 결과33 passed, 1 skipped, 9 deselected였다. 이는 issue가 바꾸려는 현재 lifecycle·key·visibility baseline이 실행 가능한 상태임을 확인한다. -
Git history의
e0d4e7130400bbfe576d5a6fc94b87dcc26f0734는 issue #26과 legacydcy-028의 “strategy state lifecycle은 slot occupancy와 일치한다”는 전제 아래 close-delete, occupancy-filtered read,(slot_id, strategy_id)key와 occupancy/version CAS를 함께 도입했다. 현재docs/runtime-flows.md349359행과541행도 같은 C-1/C-2/C-3 계약을 설명한다.docs/interface-contract.md527 -
현재 ProofLine active criteria 전체에서
strategy_state,occupancy_seq,delete_all을 검색한 결과 직접 소유하는 AC는 없었다. 가장 가까운AC-0023은 C-case reconciliation warning에 따른 strategy-facingslots.req제외와 intent blocking을 소유하며 state persistence와 namespace는 다루지 않는다.
Scope
-
이번 Line은 strategy state의 lifecycle을 slot occupancy에서 분리하고, state namespace의 identity를
strategy_instance_id로 전환하는 변경을 함께 다룬다. -
이 변경의 Acceptance Criteria는 version-independent 동작을 검증하는 서로 독립적인 신규 AC 세 건으로 분리한다.
- close·재점유를 넘어 strategy state와 version을 삭제하지 않고 보존하는 lifecycle criterion
- 슬롯별 state map에서
strategy_instance_id를 각 state entry의 key로 사용하고 같은strategy_id의 instance별 state·version 격리를 보장하는 identity criterion strategy_instance_id를 전략용 request-scoped identity로 사용하여spy=false인slots.req가 해당 ID의 state만 반환하고 누락·비문자열·빈 ID 요청을 fail-closed하되,spy=true인 operator 조회는strategy_instance_id를 요구하지 않고 watchdog을 갱신하지 않으며 각 슬롯의 전체 strategy-state map을strategy_instance_id별 entry로 반환하는 criterion
-
기존 active AC의
update,retire,satisfy,release evidence대상은 없다. 특히AC-0023의 reconciliation warning 판정·slot 제외·intent blocking·clear 권한은 변경하지 않는다. -
Legacy Discovery와 REQ는 업데이트 대상이 아니다. 현재
docs/runtime-flows.md와docs/interface-contract.md의 strategy-state baseline 문구만 새 AC가 승인된 뒤 approved change로 정합화한다. -
어떤 경로의
→ INACTIVE전이에서도 일반 lifecycle hook이 slot-scoped strategy-state map이나 그 entry를 자동 삭제하지 않게 한다. Map은 inactive와 이후 재점유 상태에서도 유지하며 현재 occupancy와 무관하게 다시 볼 수 있게 한다. -
Core는 opaque
data를 파싱하거나 전략별 session·reserve·peak의 유효성을 판단하지 않는다. State의 session identity와 내용을 이어 쓸지 초기화할지는 전략이 결정한다. -
occupancy_seq는 slot-scoped strategy-state persistence schema,StrategyStateEntry, store protocol, read filtering,strategy_state.setrequest/response와 state CAS에서 제거한다. Slot 자체의occupancy_seq와 증가 규칙은 이 Line에서 변경하지 않지만 strategy-state persistence와 write 승인에는 사용하지 않는다. -
State write는
expected_version과 slot map의data[strategy_instance_id].version만 비교한다. 해당 entry가 없을 때expected_version=0, 성공 시 그 entry의 version만 기존 값에서 1 증가하며 close·reopen은 version을 초기화하지 않는다. 다른 instance entry와 version은 변경하지 않는다. 같은 slot에서 서로 다른 instance entry를 동시에 갱신해도 각 성공 write가 모두 보존되고 어느 entry도 유실되지 않아야 한다. Slot active-state 검사와 inner opaque data size limit는 유지한다. -
일반 lifecycle에는 strategy-state 삭제 동작을 두지 않는다. 기존의 파괴적 전체 DB reset은 별도 운영 기능으로 유지한다.
-
Persistence, in-memory model, dataclass, store protocol과 PostgreSQL query/update는 슬롯별로 하나의 state map을 소유하고
strategy_instance_id를 각 state entry의 key로 사용하도록 전환한다. 각 entry는{version, data, updated_at}를 가진다.strategy_instance_id가 entry의 저장·조회 key이며 별도의strategy_idmetadata를 entry에 중복 저장하지 않는다. 여기서 innerdata만 전략이 소유하는 opaque string이며 outer map과 entry envelope은 core contract다. 아래 JSON은 설명용 응답 예시가 아니라 entry 전체를 그대로 보존하는 canonical persistence shape이다.{ "data": { "tvr-v2-prod-01": { "version": 8, "data": "{...}", "updated_at": "..." }, "new-v1-prod-01": { "version": 7, "data": "{...}", "updated_at": "..." } } } -
같은
strategy_id를 사용하는 서로 다른strategy_instance_id는 동일 slot map에서도 독립된 entry와 version을 가져야 한다. 재시작 후 같은 instance ID를 사용하는 owner는 기존 entry를 이어 읽고, 다른 instance ID는 이를 상속하지 않는다. -
strategy_state.set은strategy_instance_id를 필수 field로 받으며, core는 누락·비문자열·빈 값을 state 저장 없이 non-retryablebad_request로 거부한다. 별도의 core-side registry나instance_key ↔ strategy_instance_id목록 검증은 도입하지 않는다. -
저장 순서는
slots.reqread → 전략의 opaque state 계산 →strategy_state.setwrite다. 변경 후 write payload는{slot_id, strategy_instance_id, expected_version, data}이며,expected_version은 직전slots.req의 자기strategy_state.version을 사용하고 entry가 없을 때만0을 사용한다. Core는 active slot·해당 entry의 version CAS·inner data size limit를 모두 통과한 경우에만 slot map의data[strategy_instance_id]를 create/update하고 증가한 version을 반환한다. -
version_conflict이면 전략은 추정한 값으로 재전송하지 않고 다음slots.req에서 자기 state를 다시 읽어 재계산한다.occupancy_changedreason은 계약에서 제거한다.slot_inactive,state_too_large,bad_request:*는 해당 write를 적용하지 않는다. RPCcorrelation_id는 저장 멱등성 key로 사용하지 않는다. -
spy=false인 전략용slots.req요청은strategy_instance_id를 필수 field로 받는다. 누락·비문자열·빈 값이면 core는 RPC-level error나 새 envelope을 만들지 않고 기존slots.reqresponse envelope으로 다음 non-retryablebad_request를 반환한다.reason="bad_request:strategy_instance_id"가 retry하지 않을 configuration error의 machine-readable 판별값이며,slots=[]이므로 어떤 slot이나strategy_state도 노출하지 않는다.{ "slots": [], "available_slots": null, "degraded": false, "reason": "bad_request:strategy_instance_id", "message": "strategy_instance_id must be a non-empty string" }정상 전략 요청에서는 각 slot의 전체 map을 wire에 넣지 않고
data[request.strategy_instance_id]만 선택하여 응답의strategy_state에{version, data, updated_at}단일 object로 넣는다. 해당 key가 없으면 기존 absent-state 의미를 유지해strategy_statefield를 생략하며, 다른 strategy instance의 state를 대신 반환해서는 안 된다. 다음 JSON은 전체slots.reqenvelope이 아니라slots[]내부 row 예시다.strategy_instance_id="tvr-v2-prod-01"요청에는 해당 state만 포함한다.{ "slot_id": "slot-001", "strategy_state": { "version": 8, "data": "{...}", "updated_at": "..." } } -
이 single-object response는 canonical wire contract다. 응답의
strategy_state안에는strategy_instance_idkey나 다른 instance를 열거하는 collection을 두지 않는다. -
같은
instance_key에 여러 strategy instance가 연결되어도 요청 A에는data[A]만, 요청 B에는data[B]만 반환한다. Core는 이를 위해 strategy-instance 목록 registry를 유지하지 않고 요청의 필수strategy_instance_id를 map lookup key로만 사용한다. -
spy=true인 operatorslots.req는strategy_instance_id를 요구하지 않고 기존 filter·slot 조회와 watchdog 비갱신 동작을 유지한다. Operator 응답의 각slots[]row에는 해당 슬롯의 전체 strategy-state map을strategy_instance_id별 entry로 구성한strategy_statefield를 포함한다. 요청에strategy_instance_id가 있더라도 operator 조회 결과를 특정 entry로 제한하지 않는다. 이 동작은 기존slots.req를 사용하며 별도 operator/diagnostic API를 신설하지 않는다. -
기존 state에 대해 별도의
strategy_id → strategy_instance_idmapping, backfill 또는 instance 귀속 정책을 두지 않는다. 변경된 slot-scopeddata[strategy_instance_id]persistence contract를 그대로 적용하며, 이를 별도 migration 결정 사항으로 다시 열지 않는다. -
Verification은 close 후 map 보존, inactive/read visibility, 재점유 후 entry version 연속 증가, state persistence·model·RPC에서
occupancy_seq부재, entry별 version conflict 거부와 다른 entry 무변경, 같은 종류의 두 instance 격리, 같은 slot의 서로 다른 A·B entry에 대한 동시 성공 write가 모두 보존되는 조건, 동일 slot map에 A·B state가 있을 때 전략 요청 A에는data[A]만/전략 요청 B에는data[B]만 반환되고 다른 ID와 전체 map이 비노출되는 조건, 요청 ID에 해당하는 state가 없을 때strategy_statefield 생략, 누락·비문자열·빈spy=false요청 ID가 기존 envelope의slots=[],degraded=false,reason="bad_request:strategy_instance_id"로 반환되어 slot·state를 노출하지 않는 조건과 같은 malformed state-write ID의 저장 거부,strategy_instance_id없는spy=trueoperator CLI의 기존 slot 조회 성공·watchdog 비갱신·각 슬롯의 전체 strategy-state map 반환, InMemory/PostgreSQL parity 및 production daemon wiring을 포함한다.
Out of Scope
profit_buffer, portfolio 공통 모델, slot occupancy state machine 또는 Slot 자체의occupancy_seq와 증가 규칙 변경.- TVR-v2 reserve·cost basis·processed execution 계산, TSL peak/session 해석 또는 어떤 전략의 opaque
dataschema 정의. - Core가 strategy state의 session 만료나 occupancy 귀속을 판단하는 기능.
- Core-side strategy-instance registry,
instance_key별 strategy 목록 저장, caller 인증 또는 AMQP producer 권한 변경. - Operator/diagnostic용 전체 strategy-state 조회 API 신설.
- Strategy-state를 audit/event store로 전환하거나 row history, exactly-once write, multi-writer merge를 도입하는 기능.
AC-0023·AC-0024·AC-0025의 reconciliation warning 및 알림 lifecycle 변경.- Strategy consumer 통합. Consumer는 통합 전제 조건으로 runtime 설정에 required non-empty string
strategy_instance_id를 두고 startup에서 누락·비문자열·빈 값을 검증해야 한다. 검증 실패 시 RPC worker와 decision cycle을 시작하지 않고 configuration error로 종료하며, 시작 후에는 ID를 정규화·변형하거나 요청별로 override하지 않아야 한다. 이 동작과 검증은 각 consumer repository가 소유한다. - TVR-v2 daemon 또는 다른 전략 repository의 구현·배포. 이 Line은 trex-core persistence와 wire contract만 소유한다.
Risks and Unknowns
Risks
- Close-delete와 occupancy-filtered read를 제거하면 slot-scoped map과 그 entry가 장기간 남는다. 같은
strategy_instance_id의 반복 write는 새 entry를 늘리지 않고 기존 entry를 갱신한다. - Lifecycle, key/wire와 strategy configuration 변경을 부분적으로 배포하면 기존 consumer와 새 core의 contract가 어긋날 수 있으므로 core와 strategy consumer를 lockstep으로 배포해야 한다.
- Consumer가 통합 전제 조건인
strategy_instance_idstartup 검증과 불변 사용을 구현하지 않으면 잘못된 namespace로 요청할 수 있다. 이 위험의 완화와 contract test는 각 consumer repository가 소유한다. slots.reqhandler가 storage의 전체 map을 먼저 조립한 뒤 consumer 쪽에서 필터링하면 wire 노출은 해소되지 않는다. Core의 조회 또는 response 조립 단계에서 요청 ID 하나만 선택하고 serialized payload에 다른 ID가 없는지 검증해야 한다.- Strategy-state에서 occupancy fence를 제거하므로 close·reopen 자체는 이전 read에서 계산된 write를 거부하는 근거가 아니다. Core는 state 내용을 해석하지 않으며
strategy_instance_idnamespace와 version CAS만 보장한다. Occupancy/session 전환에 따라 data를 초기화해야 하는 전략은 opaque state 내부 규칙으로 처리해야 한다. SlotService.transit_slot()의 close-delete hook을 제거할 때 slot transition의 나머지 원자성과 실패 동작이 바뀌지 않아야 한다.
Assumptions
strategy_instance_id는 process PID나 매 restart마다 달라지는 runtime ID가 아니라 배포 재시작 뒤에도 같은 논리적 전략 instance가 재사용하는 안정적인 식별자다.- 하나의 trex-core
instance_key를 여러 strategy instance가 소비할 수 있으며, 각 strategy instance는 자기 runtime 설정에서 고유하고 안정적인strategy_instance_id를 소유한다. - 하나의 strategy instance가 같은 slot에서 소유하는 current state는 map entry 하나이며, version은 그 entry의 successful write마다 증가하는 CAS counter다. State history나 mergeable multi-writer log는 요구하지 않는다.
- Strategy-state row와 wire contract는 slot occupancy identity를 저장하거나 비교하지 않는다.