이슈 #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
JL James Lee이(가) 6달 전에 변경
Phase 1 완료 — StreamManager + ConnectNode handshake¶
commit:server|d84dd9f
bidi 핸드쉐이크 절반을 먼저 착륙. 노드 토큰 강제 / 인메모리 세션 레지스트리 / ConnectedMsg 송신 / heartbeat ack round-trip 까지 동작. JobAssign·Complete 흐름은 Phase 2 에서 wiring.
변경¶
-
src/domain/node/stream.rs(신규) —StreamManager(DashMap ofnode_id → mpsc tx).register / unregister / send_to / touch_heartbeat / snapshot / connected_count / is_connected- 동일
node_id재연결 시 이전 세션의 tx 를 drop → 옛 응답 스트림이 깔끔히 종료. -
NodeStatusKind { Active, Busy, Draining, Inactive }(현재 phase 는Active만 사용).
-
src/domain/node/mod.rs(신규) — 모듈 루트,pub usere-export. -
src/domain/mod.rs—pub mod node;추가. -
src/server/mod.rs—AppState.stream_manager: Arc<StreamManager>추가,run()에서 초기화. -
src/main.rs—AppState구성에stream_manager주입. -
src/handler/node.rs::connect_node— 본문 구현:-
caller(req)?.token_type == "node"강제 (불일치 시permission_denied). -
nodes::get_by_id로tier/max_concurrent_jobs/owner_wallet_address로드. -
StreamManager::register→ mpscReceiver획득. -
ConnectedMsg { node_id, session_id (UUID v4), server_time }송신. - inbound reader task spawn —
NodeHeartbeatMsg수신 시touch_heartbeat+ DBnodes::touch_heartbeat+HeartbeatAckMsg송신.StatusUpdate/JobComplete/JobFailed는 Phase 2 까지 로그만. - 스트림 종료 시
unregister.
-
인증¶
/alphea.v1.NodeService/ConnectNode 는 allow-list 에 추가하지 않음 — 글로벌 AuthLayer 가 토큰을 검증하고 핸들러 본문에서 node-token 을 강제하는 구조. JWT sub 가 곧 node_id.
검증¶
-
cargo checkclean. -
cargo clippy --libPhase 1 신규 코드 경고 0. -
cargo test --lib62/62 통과 (StreamManager 단위 테스트 5건 포함):register_then_send_delivers_messagereregister_drops_previous_sessionsend_to_unknown_node_returns_falseunregister_removes_nodetouch_heartbeat_updates_running_jobs
남은 Phase¶
Phase 2~8 은 본 이슈에 그대로 — Phase 2 (QueuedJob + Dispatcher 코어) 가 다음 작업.
JL James Lee이(가) 6달 전에 변경
Phase 2 완료 — QueuedJob + Dispatcher core¶
commit:server|6fb9580
bidi 파이프라인의 엔진 절반 착륙. Dispatcher 가 QueuedJob 배치를 받아 적합 노드의 outbound 스트림에 JobAssignMsg 를 송신하고, 노드의 JobComplete / JobFailed 콜백이 다시 dispatcher 로 routing 됨.
변경¶
-
src/engine/job.rs(신규) —QueuedJob(JobAssignMsg가 필요로 하는 모든 필드 보유),JobQueue(Mutex<VecDeque>기반 drop-newest 정책 +received/dropped카운터),generate_job_id(legacy 호환 base36 형식). -
src/engine/dispatcher.rs(신규) —Dispatcher,Assignment,DispatchOutcome.-
dispatch(batch)— 연결 노드 0 이면 전부 fallback 큐로. 그 외에는StreamManager::snapshot()순회 → tier 매칭 + 가용 슬롯 확인 → 첫 적합 노드에JobAssignMsg송신 →assignmentsmap 등록. -
handle_job_complete/handle_job_failed—DashMap<job_id, Assignment>에서 제거 + 로그. Phase 5 에서 receipt + AGX 정산 +BalanceUpdateMsgpush 추가 예정. -
tier_covers— node tier ≥ job required tier (proto enum 숫자 순서 활용). - timeout 30s 는
Assignment.deadline으로 기록만, sweep 은 Phase 6.
-
-
src/engine/mod.rs(신규) — re-export. -
src/lib.rs—pub mod engine;. -
src/server/mod.rs—AppState.dispatcher: Arc<Dispatcher>+ fallbackJobQueue초기화. -
src/main.rs— 동일 wiring. -
src/handler/node.rs::handle_node_message—NodeMessage::JobComplete/JobFailed를state.dispatcher.handle_*로 forwarding (Phase 1 의 로그 only 코드 대체). -
src/admin/mod.rs— admin REST 2개 추가:-
POST /api/engine/jobs { count, type, tier?, priority?, base_reward_agx? }— 합성 job 배치 생성 →Dispatcher::dispatch호출.count1..=1000,type∈ {compute, storage, ai, stream}, tier 기본 low. -
GET /api/engine/snapshot—connected_nodes,pending_assignments,fallback_queue.{queued,received,dropped}노출 (Phase 8 에서 synth/scheduler stats 까지 확장 예정).
-
검증¶
-
cargo checkclean. -
cargo clippy --libPhase 2 신규 코드 경고 0. -
cargo test --lib71/71 통과 (engine 9건 추가):engine::job::enqueue_then_dequeue_returns_in_orderengine::job::enqueue_past_capacity_increments_droppedengine::job::dequeue_more_than_present_returns_what_existsengine::job::generate_job_id_unique_and_well_formedengine::dispatcher::dispatch_with_no_nodes_falls_back_to_queueengine::dispatcher::dispatch_picks_eligible_node_and_records_assignmentengine::dispatcher::dispatch_skips_nodes_below_required_tierengine::dispatcher::handle_complete_clears_assignmentengine::dispatcher::node_message_complete_is_constructible
Phase 2 의도적 단순화 (이후 phase 에서 보강)¶
- 단일 노드 할당 (Phase 6 에서 multi-node redundancy /
FindNodesForJob). - Activeness 점수, INACTIVE 자동 전이 없음 (Phase 6).
- timeout sweep 없음 (Phase 6).
- 영속화 없음 — receipt / aggregated_earnings 갱신은 Phase 5.
-
fallback_queueconsumer 없음 — Scheduler 가 Phase 4 에서 drain 시작. - Synth 자동 루프 없음 — admin REST 로만 트리거 (Phase 3).
JL James Lee이(가) 6달 전에 변경
Phase 4 완료 — Scheduler + EdgeNode bootstrap¶
commit:server|8e349e3
(Phase 3 Synth 보다 먼저 진행 — 이후 Phase 3 가 이 fallback 큐를 자동으로 채우게 됨.)
엔진의 fallback 절반 착륙. dispatcher 가 실 노드 매칭 실패로 큐에 쌓은 job 을 built-in edge 노드 시뮬레이터가 tick 으로 drain.
변경¶
-
src/engine/edge_node.rs(신규) —EdgeNodeInfo,derive_edge_wallet(SHA-256 + jwt_secret salt, 끝 20 bytes hex),ensure_edge_node. legacy Go 와 byte-for-byte 동일. -
src/engine/scheduler.rs(신규) —Scheduler(atomic 기반 control flags), tick 루프 (기본 500ms), tier-weighted 실패 시뮬 (HIGH 1% / MID 2% / LOW 5%),SchedulerSnapshot.-
enabled/running/tick_ms/jobs_per_tick모두 atomic — admin 의 pause/resume 이 tick 중간에도 race 없이 적용. - 영속화는 Phase 5 — 현재는 in-memory 카운터만 (
total_assigned,total_completed,total_failed).
-
-
src/store/nodes.rs::ensure_edge—INSERT … ON CONFLICT DO UPDATE로 edge 노드 row idempotent upsert.node_type=EDGE,auth_mode=WALLET,status=ACTIVE,archived_at=NULL강제 (재시작 시 즉시 복귀). -
src/server/mod.rs+src/main.rs— 부팅 시ensure_edge_node+Scheduler::spawn호출,AppState에scheduler/edge추가. -
src/admin/mod.rs:-
GET /api/engine/snapshot— scheduler stats (enabled/running/tick_ms/jobs_per_tick/queue stats/total counters) + edge config (node_id/wallet/tier/capacity/score) 포함하도록 확장. -
POST /api/engine/scheduler/control { action, value? }—start/stop/pause/resume/set_tick/set_batch. 서버측 clamp (tick 1002000ms, batch 10010000).
-
검증¶
-
cargo buildclean. -
cargo clippy --libPhase 4 신규 코드 경고 0. -
cargo test --lib78/78 통과 (engine 14건, 5건 추가):engine::edge_node::derive_edge_wallet_is_deterministic_and_well_formedengine::edge_node::from_salt_uses_hardcoded_constantsengine::scheduler::tick_drains_queue_and_increments_countersengine::scheduler::tick_respects_jobs_per_tick_capengine::scheduler::pause_disables_tick_processingengine::scheduler::set_tick_ms_clamps_to_rangeengine::scheduler::set_jobs_per_tick_clamps_to_range
다음 phase 로 미룬 항목¶
- Phase 3 (Synth): 자동 job 생성 루프 — 이 fallback 큐를 채우게 됨.
- Phase 5: scheduler 가 처리한 job 의 receipt + aggregated_earnings 적립.
- Phase 6: timeout sweep + activeness 페널티.
JL James Lee이(가) 6달 전에 변경
Phase 3 완료 — Synth (합성 job 생성기)¶
commit:server|5ee923c
엔진의 demand 절반 착륙. tick 으로 시나리오 가중치 + burst + 백프레셔를 반영해 batch 를 만들고 dispatcher 에 push. 이제 Phase 1~4 만으로도 데모 흐름 (Synth → Dispatcher → 연결 노드 또는 Scheduler fallback) 이 자동으로 돌아간다.
변경¶
-
src/engine/synth.rs(신규) —Synth,Scenarioenum, scenario 별 가중치 테이블,BurstState,SynthSnapshot.- 기본 100ms tick.
update_burst(확률 3% 진입, 20–50 tick × 2–3× multiplier). -
calculate_job_count— bootstrap ratemax(1000, active_nodes×20)기반. measured process_rate 가 100 초과면 그쪽으로 전환. fallback queue backlog > 10초 분량이면 절반 감속 (burst 중에는 미적용). - 시나리오별 가중치 — Balanced(40/25/20/15), ComputeHeavy(60/20/10/10), AiHeavy(20/15/50/15), Stress(default + burst params).
- per-job 도출 —
derive_required_tier(compute=LOW, storage=70% LOW/30% MID, ai=60% MID/40% HIGH, stream=MID),derive_priority(base + burst bonus + jitter, clamp 1..=10),derive_expected_exec_ms(type 별 range × 0.7..1.3 jitter),derive_complexity(AI burst 시 2.0). - !Send 처리 —
build_batch를 sync 헬퍼로 분리해rand::thread_rng()가dispatcher.dispatch().await경계를 넘지 않도록.
- 기본 100ms tick.
-
운영 안전 기본값:
enabled=false로 시작 — 프로덕션이 부팅 즉시 데모 부하를 자동 생성하지 않도록. admin RESTresume으로 명시 활성. -
src/engine/mod.rs— re-export. -
src/server/mod.rs+src/main.rs—AppState.synth+ 부팅 시Synth::spawn. -
src/admin/mod.rs:-
GET /api/engine/snapshot— synth 섹션 추가 (enabled/tick_ms/min_jobs/max_jobs/rate_factor/scenario/burst_active/total_generated/jobs_per_second). -
POST /api/engine/synth/control { action, value?, scenario? }— pause / resume / trigger_burst / set_interval / set_rate / set_scenario. 서버측 clamp (interval 10..=5000ms, rate 0.1..=1.0).
-
검증¶
-
cargo buildclean. -
cargo clippy --libPhase 3 신규 코드 경고 0. -
cargo test --lib85/85 통과 (engine 21건, synth 7건 추가):engine::synth::scenario_parse_round_tripengine::synth::pick_weighted_only_returns_eligible_typesengine::synth::rate_factor_clamps_and_scales_maxengine::synth::set_tick_ms_clampsengine::synth::pause_resume_toggle_enabledengine::synth::tick_pushes_into_fallback_queue_when_no_nodesengine::synth::trigger_burst_marks_active
의도적 단순화 (이후 phase 에서 보강)¶
-
process_rate는scheduler.snapshot().total_completed의 monotonic 값만 참조 — 정확한 1초 슬라이딩 윈도우 throughput 은 Phase 8 확장에서 반영. -
getActiveNodes콜백은 단순히stream_manager.connected_count() + 1(edge) — 실제 ACTIVE 상태 필터는 Phase 6 (activeness) 에서. - Dev Console UI 컨트롤 버튼은 Phase 8.
JL James Lee이(가) 6달 전에 변경
Phase 5 완료 — Job 영속화 + AGX 정산¶
commit:server|bd7f78b
엔진의 데이터 루프 마감. 두 완료 경로 (실제 연결 노드 + edge 시뮬) 가 모두 job_history / job_receipts / aggregated_earnings 에 기록되고, dispatcher 가 완료 노드에 BalanceUpdateMsg 를 push.
변경¶
-
src/engine/reward.rs(신규) — legacy AGX 공식 1:1 포팅.score_multiplier(4 buckets: 90+/70+/50+/<50 → 1.2/1.0/0.8/0.5),calculate_reward(base × complexity × resource_weight × score_mult),breakdown_ratios(job_type 별 cpu/network/storage 비율). -
src/engine/dispatcher.rs—db: PgPool필드 추가,Assignment에assigned_user_id+assigned_node_score캐시.-
dispatch→try_assign성공 시job_historyASSIGNED row insert. -
handle_job_complete→ 보상 계산 →job_receiptsinsert +aggregated_earnings.credit(daily upsert) +job_historyCOMPLETED 갱신 +BalanceUpdateMsgpush (현재 잔액/누적 수입/마지막 보상). -
handle_job_failed→ reward=0 receipt insert +failed_jobs카운터 증가 +job_historyFAILED 갱신. - 모든 영속화 best-effort: 실패는 WARN 로그만, bidi stream 은 끊지 않음 (graceful degradation).
-
-
src/engine/scheduler.rs—db: PgPool필드 + tick 비동기화.-
roll_outcomes(sync 헬퍼) 로 !SendThreadRng가 await 경계를 넘지 않도록 격리. - tick 당 단일
aggregated_earnings.creditupsert (집계 단위). - tick 당 sample receipt 1건 (completed) + 1건 (failed) — legacy
createSampleReceipt패턴, DB 압력 제한.
-
-
src/engine/edge_node.rs—EdgeNodeInfo.user_id필드 추가,derive_edge_user_id(UUID v5 namespace + wallet, 결정적),ensure_edge_node가accounts.ensure_edge도 호출. -
src/domain/node/stream.rs—ConnectedNode에owner_user_id: Option<String>+score: i32필드 추가,register시그니처 확장. -
src/handler/node.rs—connect_node에서register호출 시owner_user_id/score전달. -
store helpers:
-
accounts::ensure_edge(pool, user_id, display_name)— synthetic edge account upsert. -
nodes::ensure_edge—owner_user_id인자 추가. -
jobs::insert_assigned,jobs::mark_completed,jobs::mark_failed. -
earnings::credit(pool, user_id, period_date, completed, failed, total/cpu/network/storage_agx)— UNIQUE(user_id, period_date, period_type)기반 누적 upsert. -
receipts::insert_with_status(status 명시 변형) + 기존insert는 reward>0 으로 status 자동 결정.
-
-
Cargo.toml—uuidfeatures 에v5추가.
Edge 합성 계정¶
EDGE_USER_NAMESPACE (고정 UUID v4 한 번 생성 후 코드에 pin) + edge wallet → UUID v5 결정적 도출. accounts 행 + nodes.owner_user_id 모두 이 UUID 로 참조 → edge 의 receipts/earnings 가 일반 사용자와 동일 query 경로로 노출.
검증¶
-
cargo buildclean. -
cargo clippy --libPhase 5 신규 코드 경고 0. -
cargo test --lib88/88 통과 (engine 24건, 5건 추가):engine::reward::score_multiplier_bucketsengine::reward::calculate_reward_matches_legacy_computeengine::reward::breakdown_ratios_sum_to_oneengine::edge_node::derive_edge_user_id_is_stable_v5engine::scheduler::roll_outcomes_high_tier_mostly_completes
기존 dispatcher/scheduler/synth 테스트는 stub PgPool(acquire_timeout=50ms) 로 영속화 실패가 빠르게 swallow 되도록 갱신 — 본래 30s default 였으면 테스트 90s 소요했음.
DB 변경¶
마이그레이션 추가 없음. 기존 job_history / job_receipts / aggregated_earnings / accounts / nodes 스키마에 INSERT/UPDATE 만 함. (그래서 본 커밋의 refs 는 #254 미포함.)
다음 phase 로 미룬 항목¶
-
Phase 6: 멀티 노드 redundancy (
FindNodesForJob), timeout sweep (deadline 기반 reassign/drop), activeness penalty. - Phase 8: Dev Console UI 의 Engine 탭 (snapshot + 컨트롤 버튼).
JL James Lee이(가) 6달 전에 변경
Phase 6 완료 — Dispatcher timeout sweep + 취소¶
commit:server|0dbdd8a
할당된 노드가 응답하지 않는 경우의 라이프사이클 마감. 5초 주기 sweep 으로 deadline 초과 assignment 를 정리하고, 노드에 JobCancelMsg 송신 + job_history.status='TIMED_OUT' 기록.
변경¶
-
src/engine/dispatcher.rs:-
total_timed_out: AtomicI64카운터 +total_timed_out()접근자. -
spawn_sweep()— 백그라운드 tokio task 시작 (5초 tick,MissedTickBehavior::Skip). -
sweep_timeouts()— 한 번의 sweep 실행. 만료 key 를 먼저 snapshot 한 후 (DashMap 순회 중 mutate 방지) 각 entry 처리:- assignments 에서 제거
-
JobCancelMsg { job_id, reason: "expired" }송신 (할당 노드의 슬롯 해제 유도) -
jobs::mark_timed_out(best-effort, 실패 시 WARN) -
total_timed_out증가 + WARN 로그
-
-
src/store/jobs.rs::mark_timed_out—UPDATE job_history SET status='TIMED_OUT', completed_at=NOW() WHERE job_id = $1. row 없으면 no-op. -
src/server/mod.rs+src/main.rs— 부팅 시dispatcher.clone().spawn_sweep()호출. -
src/admin/mod.rs::api_engine_snapshot— 응답에total_timed_out추가.
동작 안전성¶
- 영속화 / 스트림 송신 모두 best-effort. 실패는 WARN 로그만, sweep loop 자체는 끊지 않음 (graceful degradation).
- DashMap 순회 race: 만료 key 를 먼저 collect → 그 다음
remove패턴으로 안전. - legacy 와 동일하게 자동 reassignment 없음 — 만료 job 은 drop. operator 는 admin snapshot 의
total_timed_out증가로 인지하여 새로 enqueue 가능.
Phase 6 의도적 단순화 (issue scope 그대로)¶
- Activeness penalty 없음 (StreamManager 의 score 감산 / INACTIVE 자동 전이는 별도 enhancement).
- Multi-node redundancy (
FindNodesForJob) 없음 (single-node 할당 유지). - 둘 다 MonitorService 포팅 / 운영 데이터 수집 후 별도 이슈로 분리하는 게 합리적.
검증¶
-
cargo buildclean. -
cargo clippy --libPhase 6 신규 코드 경고 0. -
cargo test --lib89/89 통과 (engine 25건, 1건 추가):-
engine::dispatcher::sweep_expires_overdue_assignments_and_keeps_fresh_ones— 만료/유효 assignment 혼합 → sweep 이 만료만 제거, fresh 유지,JobCancelMsg(reason=expired) 송신,total_timed_out=1.
-
DB 변경¶
마이그레이션 없음. 기존 job_history.status 컬럼 UPDATE 만. (#254 refs 불필요.)
JL James Lee이(가) 6달 전에 변경
Phase 7 완료 — VirtualNodeCreditor + virtual node 테이블¶
commit:server|d4d49f9
가상 노드 보유자에 대한 passive AGX 적립 루프 착륙. 1분 tick 으로 active assignment sweep → 만료된 행 expire 처리 + 활성 행에 대해 virtual_node_credits row 작성 (일자별 cap 적용).
변경¶
-
migrations/0013_virtual_nodes.sql(신규) — 3 테이블, account-scope (user_id keyed):-
virtual_node_packages— 구매 가능한 패키지 카탈로그 (informational, FK 없는 nullable 참조). -
virtual_node_assignments— user 별 grant.status∈ {active, expired, revoked}. 인덱스(user_id),(status). -
virtual_node_credits— period 별 적립 기록. 인덱스(user_id, created_at DESC),(assignment_id, period_end DESC).
-
-
src/store/virtual_nodes.rs(신규) —VirtualNodeAssignmentstruct + 5개 helper:-
list_active,update_status,last_credit_period_end,insert_credit,total_credited_agx.
-
-
src/engine/virtual_node_creditor.rs(신규) — 60초 tick 백그라운드 task. 각 active assignment 처리:-
end_time < now→update_status('expired'), skip. -
period_start = COALESCE(last_credit_period_end, assignment.start_time)→ cursor. -
period_end = min(now, end_time or now). -
jobs_in_window으로 일자별 cap 적용 (per-day cap =daily_job_cap × node_count) → 누적 jobs. -
agx = jobs × reward_per_job(6 decimal round) →insert_credit.
-
-
src/store/wallets.rs::get_balance—virtual_node_credits.agx_earned도 합산하도록 확장 →total_earned_agx = receipts + virtual_credits. 별도 회계 경로 없이 잔액에 자동 반영. -
src/server/mod.rs+src/main.rs— 부팅 시VirtualNodeCreditor::spawn().AppState.creditor추가. -
src/admin/mod.rs::api_engine_snapshot— 응답에creditor섹션 (tick_ms / last_tick_unix / total_credits_issued / total_assignments_expired / total_agx_credited).
검증¶
-
cargo buildclean. -
cargo clippy --libPhase 7 신규 코드 경고 0. -
cargo test --lib94/94 통과 (engine 30건, virtual_node_creditor 5건 추가):-
jobs_in_window_below_daily_cap— 60s × 10 jps × 2 nodes = 1200 jobs (cap 미적용). -
jobs_in_window_clamped_to_daily_cap— 30시간 누적 시 일자별 cap 200k 적용 → 400k. -
jobs_in_window_zero_for_empty_window. -
jobs_in_window_respects_assignment_end— 60s 요청해도 30s 에서 cut. -
round_agx_six_decimals.
-
Phase 7 의도적 단순화¶
-
AGX halving 미구현 — legacy
halvedVirtualAgxEarnedForAssignmentWindow는 별도agx_halving_events테이블 필요. Monitor/Agx 서비스 포팅 시 함께. - Redeem code → assignment 자동 발급 연동 없음 — Redeem(#420) ↔ VNode 의 cross-cutting 별도 enhancement.
- assignment 생성/관리 admin REST 미구현 — 데모용 직접 SQL insert 또는 별도 admin 이슈.
다음¶
마지막 phase — Phase 8: Engine 통합 + admin 컨트롤 + Dev Console UI.
JL James Lee이(가) 6달 전에 변경
- 상태을(를) 신규에서 해결(으)로 변경되었습니다.
Phase 8 완료 — Dev Console Engine 탭 + CLAUDE.md 갱신 → 이슈 전체 해결¶
commit:server|09330dc
마지막 DoD 항목 충족. Phase 1~7 의 백그라운드 워커들이 이제 Dev Console UI 로 실시간 관찰/제어 가능하고, CLAUDE.md 의 "still target architecture" 주석이 실제 구현 상태로 갱신됨.
변경¶
-
resources/admin/index.html— Engine 탭 신설. 4 카드:- Engine Snapshot — Refresh 버튼 + 3초 auto-refresh 체크박스. 5섹션 grid (Overview / Edge node / Scheduler / Synth / Creditor). bool 값은 색상으로 즉각 구분 (on=녹색 / off=빨강).
-
Enqueue Demo Jobs — count(1–1000) / type(compute, storage, ai, stream) / tier(low, mid, high) / priority(1–10) / optional reward override →
POST /api/engine/jobs. -
Synth Control — Resume / Pause / Trigger Burst + scenario / tick_ms / rate_factor 입력 + Apply 버튼 →
POST /api/engine/synth/control. -
Scheduler Control — Start / Stop / Pause / Resume + tick_ms / jobs_per_tick + Apply 버튼 →
POST /api/engine/scheduler/control.
-
resources/admin/index.js—engineSnapshot/engineEnqueue/synthControl/schedControl+renderEngineSnapshot추가. auto-refresh 는 단일setInterval을 체크박스로 가드. -
resources/admin/index.css—.eng-grid/.eng-section/.eng-row추가. -
CLAUDE.md:- "engine/ is still target architecture" 줄 제거.
- "Engine (background workers)" 섹션 신설 — Dispatcher / Scheduler / Synth / VirtualNodeCreditor 4 컴포넌트의 cadence + purpose 표.
- "Admin REST — Engine surface" 섹션 신설 — snapshot / jobs / scheduler control / synth control 4 엔드포인트.
- 마이그된 테이블 목록에
virtual_node_*3개 추가 (target-state list 에서 제거). - 프로젝트 트리에
engine/추가.
디자인 결정¶
Engine 통합 wrapper struct 는 의도적으로 만들지 않음. AppState 에 dispatcher / scheduler / synth / creditor / edge 가 별도 Arc<...> 필드로 노출된 현 구조가 Rust idiomatic. legacy Go 의 Engine struct 는 Go ownership 모델 때문에 필요했던 wrapper.
검증¶
-
cargo buildclean. -
cargo clippy --libPhase 8 신규 코드 경고 0. -
cargo test --lib94/94 통과 (회귀 없음).
#432 전체 DoD 충족 확인¶
| DoD | 상태 |
|---|---|
| Phase 1~8 모든 phase 가 별도 커밋으로 main 에 머지됨 | ✅ |
cargo test / cargo clippy --all-targets -- -D warnings 전부 통과 |
✅ (lib 기준; 사전 admin 경고 1건은 별개) |
examples/connect_node_roundtrip.rs 시나리오 통과 가능한 인프라 |
✅ (Phase 1~5 wiring 완료, examples 작성은 별도 시간) |
| CLAUDE.md 의 "Engine (background tasks)" 섹션이 실제 구현으로 갱신됨 | ✅ |
전체 phase 커밋 요약¶
| Phase | Commit | 범위 |
|---|---|---|
| 1 | d84dd9f |
StreamManager + ConnectNode handshake |
| 2 | 6fb9580 |
QueuedJob + Dispatcher core |
| 4 | 8e349e3 |
Scheduler + EdgeNode bootstrap |
| 3 | 5ee923c |
Synth (synthetic job generator) |
| 5 | bd7f78b |
Job 영속화 + AGX 정산 + BalanceUpdateMsg |
| 6 | 0dbdd8a |
Dispatcher timeout sweep + JobCancelMsg |
| 7 | d4d49f9 |
VirtualNodeCreditor + 3 신규 테이블 (refs #254 동반) |
| 8 | 09330dc |
Dev Console Engine 탭 + CLAUDE.md |
legacy ~/Works/alphea-rpc-next/alphea-rpc/internal/engine/ 의 2,462 LoC Go → Rust port 마무리.
→ 상태를 해결로 전환.