Actions
이슈 #379
진행중
JL
JL
NodeService.Heartbeat 구현 (alphea-ai-edge)
이슈 #379:
NodeService.Heartbeat 구현 (alphea-ai-edge)
설명
배경¶
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메서드 본체:caller = caller_wallet(&request)?-
node_id빈문자열 →InvalidArgument - 노드 조회 (
get_by_id) → 없으면NotFound - owner 비교 → 불일치면
PermissionDenied -
touch_heartbeat실행 - cpu/mem/active_jobs 를
debug!로 기록 -
HeartbeatResponse { acknowledged: true, server_time: now_unix }반환
-
examples/node_heartbeat.rs— 노드 등록 → heartbeat 호출 →GetNode로last_heartbeat_at시간이 진행되었는지 확인. 또한:- stranger JWT →
PermissionDenied - 토큰 없음 →
Unauthenticated - 존재하지 않는 node_id →
NotFound
- stranger JWT →
완료 기준 (DoD)¶
-
nodes::touch_heartbeat추가 + 단위 테스트 (DB 없이 SQL 문법만 컴파일 단계에서 확인 가능한 수준) -
NodeService::heartbeat핸들러 (인증 + 소유권 + DB 갱신 + 응답) -
examples/node_heartbeat.rs4개 시나리오 통과 -
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 포팅과 함께 결정.
JL James Lee이(가) 7달 전에 변경
- 상태을(를) 신규에서 해결(으)로 변경되었습니다.
구현 완료 — 해결 처리¶
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 갱신·응답:-
caller_wallet(&request)?로 호출자 지갑 확보 (JWT 검증은 글로벌AuthLayer가 처리) -
node_id빈 문자열 →InvalidArgument -
get_by_id로 active 노드 조회 → 없으면NotFound - owner case-insensitive 비교 → 불일치
PermissionDenied: caller does not own this node -
touch_heartbeat실행 — race 로 archived 된 경우도NotFound - cpu/mem/active_jobs 는
debug!로 기록만 (스키마에 컬럼 없음, legacy 와 동일 정책) -
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.rs6개 시나리오 전부 통과:- 등록 → 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
- 등록 → baseline
-
auth_roundtrip,node_register,node_delete,wallet_earnings회귀 없음 확인
DoD 체크리스트¶
-
nodes::touch_heartbeat추가 (rows-affected 기반 boolean 반환으로 NotFound/race 구분) -
NodeService::heartbeat핸들러 (인증 + 소유권 + DB 갱신 + 응답) -
examples/node_heartbeat.rs6개 시나리오 통과 (DoD 의 4개 + baseline 비교 + race-safe NotFound) -
cargo clippy경고 0 -
cargo test전체 통과 (회귀 없음)
후속 (이 이슈 범위 외)¶
- 메트릭 컬럼 추가 (
last_cpu_usage,last_memory_usage,last_active_jobs) — 모니터링 대시보드 포팅 시 별도 이슈 - INACTIVE → ACTIVE 자동 status 전환 — Dispatcher 포팅과 함께 결정
Actions