Actions
이슈 #432
진행중
JL
JL
Engine + ConnectNode 포팅 — Synth / Dispatcher / Scheduler / VNodeCreditor + bidi 스트림 완성
이슈 #432:
Engine + ConnectNode 포팅 — Synth / Dispatcher / Scheduler / VNodeCreditor + bidi 스트림 완성
설명
배경¶
alphea-ai-edge (Rust) 의 NodeService.ConnectNode 는 protobuf 시그니처만 있고 현재 핸들러는 Status::unimplemented 를 반환한다 (src/handler/node.rs:288-299). 이 RPC 가 작동하려면 legacy Go 프로젝트의 internal/engine/ 트리 전체(2,462 LoC) 를 Rust 로 포팅해야 한다 — 노드 운영 자체가 가능해지는 임팩트 큰 작업이다.
레거시 구성요소:
| 컴포넌트 | 파일 | LoC | 역할 |
|---|---|---|---|
| Engine | engine.go |
170 | Synth/Dispatcher/Scheduler/Creditor 통합 + provider adapter |
| Dispatcher | dispatcher.go |
381 | 실 노드(StreamManager) 로 job 라우팅, fallback 시 Scheduler, 타임아웃 sweep, 완료/실패 처리 |
| Synth | synth.go |
381 | 합성 job 생성기 (시나리오/burst/rate 제어, scheduler 백프레셔 반영) |
| Scheduler | scheduler.go |
489 | built-in edge node 시뮬레이터 (실제 노드가 없을 때 fallback 처리) |
| EdgeNode | edge_node.go |
68 | 부트스트랩 시 edge 노드 row 생성/캐시 |
| Job | job.go |
269 |
QueuedJob 모델 + 큐 자료구조 |
| RPCBridge | rpcbridge.go |
62 | 핸들러 ↔ 엔진 양방향 호출 어댑터 |
| VirtualNodeCreditor | virtual_node_creditor.go |
151 | 가상 노드 보유자에게 1분마다 AGX 적립 |
| VirtualNodeStats | virtual_node_stats.go |
250 | 가상 노드 메트릭 |
레거시 노드 도메인 (internal/domain/node/) 의 StreamManager (실 노드 bidi 세션 인덱싱) + RedisBridge (분산 환경에서 stream session 공유) 도 함께 필요.
목표¶
-
NodeService.ConnectNodebidi 핸들러 작동 — handshake, heartbeat ack, job assign, job complete/fail, balance update push 흐름 full round-trip. - 합성 job 이 dispatcher → 실 노드 (또는 Scheduler fallback) → 결과 수신 → DB 적립 + AGX 정산까지 자동 흐름.
- 가상 노드 보유자에게 1분 cron 으로 AGX 자동 적립.
- legacy 와 기능 동등하지만 코드 구조는 Rust idiomatic (tokio task, mpsc/broadcast,
Arc<Mutex<...>>최소화).
제외 범위 (별도 이슈)¶
-
MonitorService server-streaming
StreamStats— Engine snapshot 을 소비하는 dashboard. 본 이슈는 snapshot API 까지만,StreamStats핸들러는 Monitor 이슈에서. - AisService edge console exec/shell — 별도 서비스.
- PoolService, AgxService, FriendsService, VfsService — 본 이슈와 무관.
Phase 분해 (각 Phase 독립 커밋 + 회귀 테스트)¶
Phase 1 — StreamManager + ConnectNode handshake¶
범위: 실제 job dispatch 없이 bidi 세션 자체만.
-
src/domain/node/stream.rs—StreamManager(node_id → tx 매핑), 등록/해제,ConnectedCount. -
src/handler/node.rs::connect_node— 첫 메시지(없음, JWT 의typ=node강제) 검증 →ConnectedMsg송신 → ack/heartbeat loop 처리. - node token (
typ=node) 강제 —auth_middleware::caller(req)?.token_type == "node"체크. - E2E 시나리오:
tools/client.py의 메뉴 6 (ConnectNode 시뮬) 이 ConnectedMsg 를 받고 heartbeat ack 가 돌아오는지.
Phase 2 — QueuedJob 모델 + Dispatcher 코어¶
-
src/engine/job.rs—QueuedJob,JobQueue(BinaryHeap-based priority queue). -
src/engine/dispatcher.rs—Dispatcher::run루프 (jobCh 수신 → 적합 노드 선택 →JobAssignMsg송신),handle_job_complete/handle_job_failed콜백. - node → server 메시지가 dispatcher 까지 routing 되도록 wiring.
- Phase 1 과 결합되어 외부에서 admin REST 로 임의 job 을 enqueue 하면 연결된 노드에 dispatch 되는 흐름까지.
Phase 3 — Synth (합성 job 생성기)¶
-
src/engine/synth.rs— tick 루프,calculate_job_count, scenario/rate/burst 제어. - admin REST
/api/engine/synth/{pause,resume,burst,scenario}— Phase 8 의 일부지만 synth 단위에서 미리 노출. - backpressure: scheduler queue 크기/active node 수를 callback 으로 읽어 합성률 조절.
Phase 4 — Scheduler (built-in edge fallback)¶
-
src/engine/scheduler.rs— built-in edge 노드 시뮬레이터. dispatcher 가 실 노드 매칭 실패 시 여기로 fallback. -
src/engine/edge_node.rs— startup 시 edge 노드 row 보장. - 시뮬 결과를 dispatcher 로 push (job_complete) → DB 적립.
Phase 5 — Job 영속화 + AGX 정산¶
-
job_historyrow insert on assign, update on complete/fail. -
job_receiptsrow +aggregated_earnings(또는 legacyrewards_log) 갱신. -
BalanceUpdateMsgpush to node. - 새 마이그레이션이 필요하면
0013_*.sql. DB 변경이라 #254 도 refs 에 포함 필수.
Phase 6 — 타임아웃 sweep + 취소¶
- Dispatcher 가 주기적으로
assignedjob 의 deadline 초과 검사 →JobCancelMsg송신, 다른 노드로 reassign 또는 drop. -
job_history.status = 'TIMED_OUT'.
Phase 7 — VirtualNodeCreditor¶
-
src/engine/virtual_node_creditor.rs— 1분 tick 으로 가상 노드 보유자에게 AGX 적립. - legacy 의
virtual_node_assignments/virtual_node_credits/virtual_node_packages테이블 미생성 상태 — 마이그레이션 추가 필요 (#254 refs).
Phase 8 — Engine 통합 + admin 컨트롤¶
-
src/engine/mod.rs—Engine구조체로 4 컴포넌트 묶고start(ctx)에서 tokio task 들 spawn. -
AppState에engine: Arc<Engine>추가,main.rs에서 GoTrue/admin server 와 함께 부팅. - admin REST API 확장 — engine snapshot, synth/scheduler 컨트롤 (Pause/Resume/SetTick/SetRate/SetScenario/TriggerBurst).
- Dev Console UI 에 Engine 탭 (snapshot 표시 + 컨트롤 버튼).
DB 변경 예상¶
- Phase 5:
job_history/job_receipts가 이미 0001~0005 에 일부 정의되어 있으므로 컬럼 보강 수준. 새 마이그 1개. - Phase 7:
virtual_node_assignments,virtual_node_credits,virtual_node_packages— 신규 마이그.
→ 두 phase 모두 commit 시 refs 에 #254 포함.
인증¶
- Phase 1 에서
auth_middleware::public_path_set()에/alphea.v1.NodeService/ConnectNode추가하지 말 것 — node token 필수. - 핸들러 본문에서
caller(&request)?로AuthContext가져와token_type == "node"검증. -
node_id는 JWTsub에서 추출 (==caller_user_id).
완료 기준 (DoD — 이슈 전체)¶
- Phase 1~8 모든 phase 가 별도 커밋으로 main 에 머지됨.
-
cargo test/cargo clippy --all-targets -- -D warnings전부 통과. -
examples/connect_node_roundtrip.rs(또는tools/client.py메뉴 6) 시나리오:- 노드 등록 → 토큰 획득 → ConnectNode 연결 → ConnectedMsg 수신
- admin REST 로 demo job enqueue → JobAssignMsg 수신 → JobCompleteMsg 송신
- BalanceUpdateMsg 수신, DB 의
aggregated_earnings가 갱신됨
- CLAUDE.md 의 "Engine (background tasks)" 섹션이 "still target architecture" 가 아니라 실제 구현으로 갱신됨.
관련 이슈¶
-
#379 (해결) —
Heartbeat포팅. 본 이슈의 선행 작업이며, ConnectNode 의NodeHeartbeatMsg처리에 같은 store 함수 재사용. -
#375 (해결) — JWT 전역 인터셉터.
connect_node핸들러에서caller(req)헬퍼 사용. - #421 (해결) — AGX 잔액 account scope 전환. Phase 5/7 정산이 이 위에서 작동.
- #254 (진행) — DB 스키마. Phase 5/7 마이그레이션 시 함께 refs.
참고 코드¶
- 레거시:
~/Works/alphea-rpc-next/alphea-rpc/internal/engine/(전체) - 레거시:
~/Works/alphea-rpc-next/alphea-rpc/internal/domain/node/stream.go,redis_bridge.go - 레거시:
~/Works/alphea-rpc-next/alphea-rpc/internal/handler/node.go:218(ConnectNode핸들러)
예상 작업량¶
- Phase 1: 3~4h (StreamManager + handshake)
- Phase 2: 5~6h (Dispatcher + JobQueue + bidi message routing)
- Phase 3: 3~4h (Synth)
- Phase 4: 4~5h (Scheduler)
- Phase 5: 3~4h (Persistence + AGX 정산 + balance push)
- Phase 6: 2~3h (Timeout sweep)
- Phase 7: 2~3h (VNodeCreditor + 마이그)
- Phase 8: 3~4h (Engine 통합 + admin 컨트롤 + Dev Console UI)
- 총: 25~33h
Actions