프로젝트

일반

사용자정보

Actions

이슈 #432

진행중
JL JL

Engine + ConnectNode 포팅 — Synth / Dispatcher / Scheduler / VNodeCreditor + bidi 스트림 완성

이슈 #432: Engine + ConnectNode 포팅 — Synth / Dispatcher / Scheduler / VNodeCreditor + bidi 스트림 완성

James Lee이(가) 약 6시간 전에 추가함.

상태:
해결
우선순위:
보통
담당자:
목표버전:
시작일:
2026/04/17
완료일:
2026/04/17 (약 7달 지연)
진척도:

0%

추정시간:

설명

배경

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.ConnectNode bidi 핸들러 작동 — 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_history row insert on assign, update on complete/fail.
  • job_receipts row + aggregated_earnings (또는 legacy rewards_log) 갱신.
  • BalanceUpdateMsg push to node.
  • 새 마이그레이션이 필요하면 0013_*.sql. DB 변경이라 #254 도 refs 에 포함 필수.

Phase 6 — 타임아웃 sweep + 취소

  • Dispatcher 가 주기적으로 assigned job 의 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 는 JWT sub 에서 추출 (== 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) 시나리오:
    1. 노드 등록 → 토큰 획득 → ConnectNode 연결 → ConnectedMsg 수신
    2. admin REST 로 demo job enqueue → JobAssignMsg 수신 → JobCompleteMsg 송신
    3. 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

연결된 일감 4 (4 건 진행 중 — 모두 미완료)

다음 일감과 관련됨: 이슈 #375: JWT 전역 인터셉터 구현 (alphea-ai-edge)해결James Lee2026/04/15

Actions
다음 일감과 관련됨: 이슈 #421: AGX 잔액을 Wallet 종속 → Account(GoTrue user_id) 종속으로 전환해결James Lee2026/04/27

Actions
다음 일감과 관련됨: 이슈 #254: 8-2. 데이터베이스 스키마 설계 (사용자. 앱. 일정)진행James Lee2026/04/282026/04/29

Actions
다음 일감을 우선 진행: 이슈 #379: NodeService.Heartbeat 구현 (alphea-ai-edge)해결James Lee2026/04/16

Actions

JL James Lee이(가) 6달 전에 변경 Actions #1

  • 완료일을(를) 2026/04/17(으)로 지정되었습니다.
  • 시작일을(를) 2026/05/08에서 2026/04/17(으)로 변경되었습니다.
  • 다음 일감을 우선 진행:에 이슈 #379: NodeService.Heartbeat 구현 (alphea-ai-edge)이(가) 추가되었습니다.

JL James Lee이(가) 6달 전에 변경 Actions #2

  • 다음 일감과 관련됨:에 이슈 #375: JWT 전역 인터셉터 구현 (alphea-ai-edge)이(가) 추가되었습니다.

JL James Lee이(가) 6달 전에 변경 Actions #3

  • 다음 일감과 관련됨:에 이슈 #421: AGX 잔액을 Wallet 종속 → Account(GoTrue user_id) 종속으로 전환이(가) 추가되었습니다.

JL James Lee이(가) 6달 전에 변경 Actions #4

  • 다음 일감과 관련됨:에 이슈 #254: 8-2. 데이터베이스 스키마 설계 (사용자. 앱. 일정)이(가) 추가되었습니다.

JL James Lee이(가) 6달 전에 변경 Actions #5

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 of node_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 use re-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 — 본문 구현:
    1. caller(req)?.token_type == "node" 강제 (불일치 시 permission_denied).
    2. nodes::get_by_id 로 tier / max_concurrent_jobs / owner_wallet_address 로드.
    3. StreamManager::register → mpsc Receiver 획득.
    4. ConnectedMsg { node_id, session_id (UUID v4), server_time } 송신.
    5. inbound reader task spawn — NodeHeartbeatMsg 수신 시 touch_heartbeat + DB nodes::touch_heartbeat + HeartbeatAckMsg 송신. StatusUpdate / JobComplete / JobFailed 는 Phase 2 까지 로그만.
    6. 스트림 종료 시 unregister.

인증

/alphea.v1.NodeService/ConnectNode 는 allow-list 에 추가하지 않음 — 글로벌 AuthLayer 가 토큰을 검증하고 핸들러 본문에서 node-token 을 강제하는 구조. JWT sub 가 곧 node_id.

검증

  • cargo check clean.
  • cargo clippy --lib Phase 1 신규 코드 경고 0.
  • cargo test --lib 62/62 통과 (StreamManager 단위 테스트 5건 포함):
    • register_then_send_delivers_message
    • reregister_drops_previous_session
    • send_to_unknown_node_returns_false
    • unregister_removes_node
    • touch_heartbeat_updates_running_jobs

남은 Phase

Phase 2~8 은 본 이슈에 그대로 — Phase 2 (QueuedJob + Dispatcher 코어) 가 다음 작업.

JL James Lee이(가) 6달 전에 변경 Actions #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 송신 → assignments map 등록.
    • handle_job_complete / handle_job_failed — DashMap<job_id, Assignment> 에서 제거 + 로그. Phase 5 에서 receipt + AGX 정산 + BalanceUpdateMsg push 추가 예정.
    • 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> + fallback JobQueue 초기화.
  • 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 호출. count 1..=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 check clean.
  • cargo clippy --lib Phase 2 신규 코드 경고 0.
  • cargo test --lib 71/71 통과 (engine 9건 추가):
    • engine::job::enqueue_then_dequeue_returns_in_order
    • engine::job::enqueue_past_capacity_increments_dropped
    • engine::job::dequeue_more_than_present_returns_what_exists
    • engine::job::generate_job_id_unique_and_well_formed
    • engine::dispatcher::dispatch_with_no_nodes_falls_back_to_queue
    • engine::dispatcher::dispatch_picks_eligible_node_and_records_assignment
    • engine::dispatcher::dispatch_skips_nodes_below_required_tier
    • engine::dispatcher::handle_complete_clears_assignment
    • engine::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_queue consumer 없음 — Scheduler 가 Phase 4 에서 drain 시작.
  • Synth 자동 루프 없음 — admin REST 로만 트리거 (Phase 3).

JL James Lee이(가) 6달 전에 변경 Actions #7

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 build clean.
  • cargo clippy --lib Phase 4 신규 코드 경고 0.
  • cargo test --lib 78/78 통과 (engine 14건, 5건 추가):
    • engine::edge_node::derive_edge_wallet_is_deterministic_and_well_formed
    • engine::edge_node::from_salt_uses_hardcoded_constants
    • engine::scheduler::tick_drains_queue_and_increments_counters
    • engine::scheduler::tick_respects_jobs_per_tick_cap
    • engine::scheduler::pause_disables_tick_processing
    • engine::scheduler::set_tick_ms_clamps_to_range
    • engine::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달 전에 변경 Actions #8

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, Scenario enum, scenario 별 가중치 테이블, BurstState, SynthSnapshot.
    • 기본 100ms tick. update_burst (확률 3% 진입, 20–50 tick × 2–3× multiplier).
    • calculate_job_count — bootstrap rate max(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 경계를 넘지 않도록.
  • 운영 안전 기본값: enabled=false 로 시작 — 프로덕션이 부팅 즉시 데모 부하를 자동 생성하지 않도록. admin REST resume 으로 명시 활성.
  • 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 build clean.
  • cargo clippy --lib Phase 3 신규 코드 경고 0.
  • cargo test --lib 85/85 통과 (engine 21건, synth 7건 추가):
    • engine::synth::scenario_parse_round_trip
    • engine::synth::pick_weighted_only_returns_eligible_types
    • engine::synth::rate_factor_clamps_and_scales_max
    • engine::synth::set_tick_ms_clamps
    • engine::synth::pause_resume_toggle_enabled
    • engine::synth::tick_pushes_into_fallback_queue_when_no_nodes
    • engine::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달 전에 변경 Actions #9

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_history ASSIGNED row insert.
    • handle_job_complete → 보상 계산 → job_receipts insert + aggregated_earnings.credit (daily upsert) + job_history COMPLETED 갱신 + BalanceUpdateMsg push (현재 잔액/누적 수입/마지막 보상).
    • handle_job_failed → reward=0 receipt insert + failed_jobs 카운터 증가 + job_history FAILED 갱신.
    • 모든 영속화 best-effort: 실패는 WARN 로그만, bidi stream 은 끊지 않음 (graceful degradation).
  • src/engine/scheduler.rs — db: PgPool 필드 + tick 비동기화.
    • roll_outcomes (sync 헬퍼) 로 !Send ThreadRng 가 await 경계를 넘지 않도록 격리.
    • tick 당 단일 aggregated_earnings.credit upsert (집계 단위).
    • 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 — uuid features 에 v5 추가.

Edge 합성 계정

EDGE_USER_NAMESPACE (고정 UUID v4 한 번 생성 후 코드에 pin) + edge wallet → UUID v5 결정적 도출. accounts 행 + nodes.owner_user_id 모두 이 UUID 로 참조 → edge 의 receipts/earnings 가 일반 사용자와 동일 query 경로로 노출.

검증

  • cargo build clean.
  • cargo clippy --lib Phase 5 신규 코드 경고 0.
  • cargo test --lib 88/88 통과 (engine 24건, 5건 추가):
    • engine::reward::score_multiplier_buckets
    • engine::reward::calculate_reward_matches_legacy_compute
    • engine::reward::breakdown_ratios_sum_to_one
    • engine::edge_node::derive_edge_user_id_is_stable_v5
    • engine::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달 전에 변경 Actions #10

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 처리:
      1. assignments 에서 제거
      2. JobCancelMsg { job_id, reason: "expired" } 송신 (할당 노드의 슬롯 해제 유도)
      3. jobs::mark_timed_out (best-effort, 실패 시 WARN)
      4. 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 build clean.
  • cargo clippy --lib Phase 6 신규 코드 경고 0.
  • cargo test --lib 89/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달 전에 변경 Actions #11

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 (신규) — VirtualNodeAssignment struct + 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 build clean.
  • cargo clippy --lib Phase 7 신규 코드 경고 0.
  • cargo test --lib 94/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달 전에 변경 Actions #12

  • 상태을(를) 신규에서 해결(으)로 변경되었습니다.

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 카드:
    1. Engine Snapshot — Refresh 버튼 + 3초 auto-refresh 체크박스. 5섹션 grid (Overview / Edge node / Scheduler / Synth / Creditor). bool 값은 색상으로 즉각 구분 (on=녹색 / off=빨강).
    2. 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.
    3. Synth Control — Resume / Pause / Trigger Burst + scenario / tick_ms / rate_factor 입력 + Apply 버튼 → POST /api/engine/synth/control.
    4. 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 build clean.
  • cargo clippy --lib Phase 8 신규 코드 경고 0.
  • cargo test --lib 94/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 마무리.

→ 상태를 해결로 전환.

Actions

내보내기 PDF Atom