프로젝트

일반

사용자정보

Actions

이슈 #379

진행중
JL JL

NodeService.Heartbeat 구현 (alphea-ai-edge)

이슈 #379: NodeService.Heartbeat 구현 (alphea-ai-edge)

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

상태:
해결
우선순위:
높음
담당자:
목표버전:
시작일:
2026/04/16
완료일:
진척도:

0%

추정시간:

설명

배경

alphea-ai-edge 의 NodeService 8개 RPC 중 현재 Heartbeat 가 Status::unimplemented 스텁 상태입니다. 노드 라이브니스 추적은 이후 Dispatcher/Scheduler 포팅의 전제 조건이고, 모니터링 대시보드(#327~#332 영역) 와 풀 관리에서도 필요한 기본 시그널이라 먼저 포팅합니다.

레거시 Go 구현 (alphea-rpc-next/internal/handler/node.go:185) 은 매우 단순합니다:

func (h *NodeHandler) Heartbeat(...) (*HeartbeatResponse, error) {
    if msg.NodeId == "" { return nil, InvalidArgument }
    _ = h.db.UpdateNodeHeartbeat(msg.NodeId, msg.CpuUsage, msg.MemoryUsage, msg.ActiveJobs)
    return &HeartbeatResponse{Acknowledged: true, ServerTime: nowUnix()}, nil
}

DB 업데이트는 last_heartbeat_at, updated_at 두 컬럼만 갱신하고 cpu/memory/active_jobs 는 SQL 에서 무시합니다 (스키마에 컬럼이 없음).

목표 (legacy 대비 강화)

  • 인증 필수 — 글로벌 AuthLayer (#375) 가 JWT 를 검증, 핸들러는 caller_wallet(&request) 만 호출
  • 소유권 체크 — caller_wallet 이 nodes.owner_wallet_address 와 일치해야 함 (case-insensitive). 다른 사람이 남의 노드 heartbeat 을 위조하지 못하도록 차단 (legacy 에는 없는 강화)
  • 소프트 삭제 준수 — archived_at IS NOT NULL 인 노드는 NotFound
  • 타임스탬프 갱신 — last_heartbeat_at = NOW(), updated_at = NOW()
  • 메트릭 처리 — cpu_usage / memory_usage / active_jobs 는 현재 스키마에 컬럼이 없으므로 debug! 로깅만 하고 저장은 하지 않음 (legacy 와 동일)
  • 응답 — acknowledged=true, server_time=Utc::now().timestamp()

구현 항목

  • src/store/nodes.rs — pub async fn touch_heartbeat(pool, node_id) -> sqlx::Result<bool> (rows affected 가 0 이면 false 반환 → 핸들러가 NotFound 매핑)
  • src/handler/node.rs — Heartbeat 메서드 본체:
    1. caller = caller_wallet(&request)?
    2. node_id 빈문자열 → InvalidArgument
    3. 노드 조회 (get_by_id) → 없으면 NotFound
    4. owner 비교 → 불일치면 PermissionDenied
    5. touch_heartbeat 실행
    6. cpu/mem/active_jobs 를 debug! 로 기록
    7. HeartbeatResponse { acknowledged: true, server_time: now_unix } 반환
  • examples/node_heartbeat.rs — 노드 등록 → heartbeat 호출 → GetNode 로 last_heartbeat_at 시간이 진행되었는지 확인. 또한:
    • stranger JWT → PermissionDenied
    • 토큰 없음 → Unauthenticated
    • 존재하지 않는 node_id → NotFound

완료 기준 (DoD)

  • nodes::touch_heartbeat 추가 + 단위 테스트 (DB 없이 SQL 문법만 컴파일 단계에서 확인 가능한 수준)
  • NodeService::heartbeat 핸들러 (인증 + 소유권 + DB 갱신 + 응답)
  • examples/node_heartbeat.rs 4개 시나리오 통과
  • cargo clippy --all-targets -- -D warnings 경고 0
  • cargo test 전체 통과 (회귀 없음)

관련 이슈

  • #310, #311, #312, #313, #314 — NodeService / WalletService 의 read/register/delete 가 이미 포팅됨. Heartbeat 는 그 위에 얹는 추가 RPC.
  • #375 — JWT 전역 인터셉터. 이번 핸들러는 그 위에서 caller_wallet 만 호출.
  • #350~#373 (모니터링/베타 영역) — 노드 라이브니스 데이터의 후속 소비처.

후속 (이 이슈 범위 외)

  • 메트릭 컬럼(last_cpu_usage, last_memory_usage, last_active_jobs) 추가 및 저장 — 모니터링 대시보드 포팅 시 별도 이슈에서.
  • INACTIVE 노드가 heartbeat 보내면 자동으로 ACTIVE 로 전환하는 status 머신 — Dispatcher 포팅과 함께 결정.

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

다음 일감과 관련됨: 이슈 #312: 17-3. 노드 추가 API (지갑 연동)해결James Lee2026/05/192026/05/21

Actions
다음 일감과 관련됨: 이슈 #313: 17-4. 노드 삭제 API해결James Lee2026/05/192026/05/22

Actions
다음 일감을 우선 진행: 이슈 #375: JWT 전역 인터셉터 구현 (alphea-ai-edge)해결James Lee2026/04/15

Actions
다음에 진행할 일감: 이슈 #432: Engine + ConnectNode 포팅 — Synth / Dispatcher / Scheduler / VNodeCreditor + bidi 스트림 완성해결James Lee2026/04/172026/04/17

Actions

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

  • 다음 일감을 우선 진행:에 이슈 #375: JWT 전역 인터셉터 구현 (alphea-ai-edge)이(가) 추가되었습니다.

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

  • 다음 일감과 관련됨:에 이슈 #312: 17-3. 노드 추가 API (지갑 연동)이(가) 추가되었습니다.

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

  • 다음 일감과 관련됨:에 이슈 #313: 17-4. 노드 삭제 API이(가) 추가되었습니다.

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

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

구현 완료 — 해결 처리

commit:server|539b32f

구현 내용

  • src/store/nodes.rs::touch_heartbeat — UPDATE nodes SET last_heartbeat_at = NOW(), updated_at = NOW() WHERE node_id = $1 AND archived_at IS NULL. rows affected 가 0 이면 false 반환 (핸들러가 NotFound 매핑).
  • src/handler/node.rs::heartbeat — 인증·소유권·DB 갱신·응답:
    1. caller_wallet(&request)? 로 호출자 지갑 확보 (JWT 검증은 글로벌 AuthLayer 가 처리)
    2. node_id 빈 문자열 → InvalidArgument
    3. get_by_id 로 active 노드 조회 → 없으면 NotFound
    4. owner case-insensitive 비교 → 불일치 PermissionDenied: caller does not own this node
    5. touch_heartbeat 실행 — race 로 archived 된 경우도 NotFound
    6. cpu/mem/active_jobs 는 debug! 로 기록만 (스키마에 컬럼 없음, legacy 와 동일 정책)
    7. HeartbeatResponse { acknowledged: true, server_time: Utc::now().timestamp() } 반환

Legacy 대비 강화

  • 레거시 Go 핸들러는 어떤 인증도 없이 node_id 만 받아 heartbeat 을 기록 — 누구나 남의 노드 라이브니스를 위조 가능
  • 이번 포팅은 JWT 필수 + 소유권 체크 로 위조 차단

검증

  • cargo clippy --all-targets -- -D warnings — 경고 0
  • cargo test — 36개 통과 (회귀 없음)
  • examples/node_heartbeat.rs 6개 시나리오 전부 통과:
    • 등록 → baseline last_heartbeat_at = 0
    • heartbeat 후 last_heartbeat_at 가 server_time 까지 진행 (✓ heartbeat: acknowledged=true)
    • stranger JWT → PermissionDenied: caller does not own this node
    • 토큰 없음 → Unauthenticated: missing authorization header (글로벌 미들웨어에서 차단)
    • 존재하지 않는 node_id → NotFound: node not found
  • auth_roundtrip, node_register, node_delete, wallet_earnings 회귀 없음 확인

DoD 체크리스트

  • nodes::touch_heartbeat 추가 (rows-affected 기반 boolean 반환으로 NotFound/race 구분)
  • NodeService::heartbeat 핸들러 (인증 + 소유권 + DB 갱신 + 응답)
  • examples/node_heartbeat.rs 6개 시나리오 통과 (DoD 의 4개 + baseline 비교 + race-safe NotFound)
  • cargo clippy 경고 0
  • cargo test 전체 통과 (회귀 없음)

후속 (이 이슈 범위 외)

  • 메트릭 컬럼 추가 (last_cpu_usage, last_memory_usage, last_active_jobs) — 모니터링 대시보드 포팅 시 별도 이슈
  • INACTIVE → ACTIVE 자동 status 전환 — Dispatcher 포팅과 함께 결정

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

  • 다음에 진행할 일감:에 이슈 #432: Engine + ConnectNode 포팅 — Synth / Dispatcher / Scheduler / VNodeCreditor + bidi 스트림 완성이(가) 추가되었습니다.
Actions

내보내기 PDF Atom