diff --git a/Makefile b/Makefile index be4b285..372fb78 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,7 @@ test: proto: protoc \ --go_out=. \ - --go_opt=paths=source_relative \ + --go_opt=module=iop \ --proto_path=. \ proto/iop/runtime.proto \ proto/iop/node.proto \ diff --git a/agent-task/edge_node_arch/code_review_0.log b/agent-task/edge_node_arch/code_review_0.log new file mode 100644 index 0000000..005d70d --- /dev/null +++ b/agent-task/edge_node_arch/code_review_0.log @@ -0,0 +1,200 @@ + + +# Code Review Reference - REFACTOR + +## 개요 + +date=2026-05-02 +task=edge_node_arch, plan=1, tag=REFACTOR + +## 이 파일을 읽는 리뷰 에이전트에게 + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료 후 반드시 아래 순서로 아카이브하세요. + +1. `CODE_REVIEW.md` → `code_review_1.log` +2. `PLAN.md` → `plan_1.log` +3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 `PLAN.md` + `CODE_REVIEW.md` 스텁 작성. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [REFACTOR-1] proto 컴파일 및 go.mod 업데이트 | [x] | +| [REFACTOR-2] node transport 클라이언트 계층 구현 | [x] | +| [REFACTOR-3] node.go proto 타입으로 교체 | [x] | +| [REFACTOR-4] node bootstrap DialEdge 연결 추가 | [x] | +| [REFACTOR-5] edge TCP 서버 + NodeRegistry 구현 | [x] | + +## 계획 대비 변경 사항 + +- `make`가 환경에 설치되어 있지 않아 `make proto`는 실패했다. 동일한 Makefile 명령을 `protoc --go_out=. --go_opt=module=iop --proto_path=.`로 직접 실행해 `proto/gen/iop/*.pb.go`를 생성했다. +- 기존 Makefile의 `paths=source_relative` 옵션은 `proto/iop/*.pb.go`를 생성하므로, PLAN의 기대 경로(`proto/gen/iop`)와 import path(`iop/proto/gen/iop`)를 맞추기 위해 `--go_opt=module=iop`로 변경했다. +- CODE_REVIEW의 체크포인트 중 `RunRequest.Input`을 bytes/json으로 확인하라는 항목은 현재 `proto/iop/runtime.proto` 정의와 PLAN의 본문이 `google.protobuf.Struct`를 사용하므로 `AsMap()` 변환 기준으로 구현했다. +- `edgeParserMap`에는 `SendRequestTyped` 응답 파싱을 위해 `CapabilityResponse` 파서를 추가했다. + +## 주요 설계 결정 + +- node는 proto-socket `TcpClient`를 `transport.Session`으로 감싸고, `RunRequest`, `CapabilityRequest`, `CancelRequest` 리스너를 세션 생성 시 등록한다. +- edge는 proto-socket `TcpServer`로 node 연결을 받고, 연결 직후 `CapabilityRequest`를 보내 `NodeRegistry`에 nodeID와 adapter 목록을 등록한다. +- `RunEvent`는 삭제된 JSON Envelope 없이 protobuf 메시지를 proto-socket으로 직접 전송한다. +- `RunRequest.Policy`와 `RunRequest.Input`은 nil-safe helper로 `google.protobuf.Struct.AsMap()` 변환한다. + +## 리뷰어를 위한 체크포인트 + +- `go.mod`에 `replace git.toki-labs.com/toki/common-proto-socket/go => ../proto-socket/go` 존재 여부 +- `proto/gen/iop/` 하위 4개 파일 생성 여부 (runtime, node, control, job) +- `transport/session.go`의 `Handler` 인터페이스가 `*iop.RunRequest`, `*iop.CapabilityResponse`, `*iop.CancelRequest` 사용 여부 +- `transport/session.go`의 `newSession`에서 `AddListenerTyped`(RunRequest, CancelRequest), `AddRequestListenerTyped`(CapabilityRequest) 세 리스너 모두 등록 여부 +- `transport/parser.go`의 `nodeParserMap`에 RunRequest, CancelRequest, CapabilityRequest 세 타입 등록 여부 +- `node.go`의 `sessionSink.Emit`이 `*iop.RunEvent`를 `sess.Send`로 직접 전송하는지 (Envelope 없음) +- `node.go`의 `OnRunRequest`에서 `req.GetPolicy()` / `req.GetInput()` Struct를 `AsMap()`으로 `map[string]any` 변환 여부 +- `bootstrap/module.go`에 `transport.DialEdge` 호출이 fx OnStart에 있고 OnStop에서 `sess.Close()` 호출 여부 +- `apps/edge/internal/node/registry.go`의 NodeRegistry가 Register/Unregister/Pick/Count/All 메서드 모두 구현 여부 +- edge `onNodeConnected`에서 CapabilityRequest 전송 후 nodeID를 받아 Registry에 등록하는 흐름 확인 +- edge `onNodeConnected`에서 disconnect 시 Registry.Unregister 호출 여부 +- `registry_test.go` 테스트 3개 통과 여부 +- 최종 검증: edge 실행 후 node 연결 시 edge 로그에 `node registered` 출력 확인 + +## 검증 결과 + +_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._ + +### REFACTOR-1 중간 검증 +``` +$ PATH=/config/go-sdk/bin:$PATH make proto +/usr/bin/bash: line 1: make: command not found + +$ PATH=/config/go-sdk/bin:$PATH protoc --go_out=. --go_opt=module=iop --proto_path=. proto/iop/runtime.proto proto/iop/node.proto proto/iop/control.proto proto/iop/job.proto +(no output) + +$ ls proto/gen/iop/ +control.pb.go +job.pb.go +node.pb.go +runtime.pb.go + +$ PATH=/config/go-sdk/go/bin:/config/go-sdk/bin:$PATH go mod tidy +(no output) +``` + +### REFACTOR-2 중간 검증 +``` +$ PATH=/config/go-sdk/go/bin:/config/go-sdk/bin:$PATH go build ./apps/node/internal/transport/... +(no output) +``` + +### REFACTOR-3 중간 검증 +``` +$ PATH=/config/go-sdk/go/bin:/config/go-sdk/bin:$PATH go build ./apps/node/... +(no output) +``` + +### REFACTOR-4 중간 검증 +``` +$ PATH=/config/go-sdk/go/bin:/config/go-sdk/bin:$PATH go build ./apps/node/... +(no output) +``` + +### REFACTOR-5 중간 검증 +``` +$ PATH=/config/go-sdk/go/bin:/config/go-sdk/bin:$PATH go build ./apps/edge/... +(no output) + +$ PATH=/config/go-sdk/go/bin:/config/go-sdk/bin:$PATH go test ./apps/edge/internal/node/... +ok iop/apps/edge/internal/node 0.002s +``` + +### 최종 검증 +``` +# 터미널 A +$ ./bin/iop-edge serve --config configs/edge.yaml +{"level":"info","caller":"transport/server.go:66","msg":"edge listening for nodes","addr":"0.0.0.0:9090"} +{"level":"info","caller":"transport/server.go:75","msg":"node connection established"} +{"level":"info","caller":"transport/server.go:102","msg":"node registered","node_id":"node-001","adapters":1} + +# 터미널 B +$ ./bin/iop-node serve --config configs/node.yaml +{"level":"info","caller":"store/store.go:53","msg":"store ready","dsn":"file:iop.db?cache=shared&mode=rwc"} +{"level":"info","caller":"transport/client.go:35","msg":"connected to edge","addr":"localhost:9090","node_id":"node-001"} + +$ PATH=/config/go-sdk/go/bin:/config/go-sdk/bin:$PATH go build ./... +(no output) + +$ PATH=/config/go-sdk/go/bin:/config/go-sdk/bin:$PATH go test ./... +? iop/apps/control-plane/cmd/iop-control-plane [no test files] +? iop/apps/edge/cmd/iop-edge [no test files] +? iop/apps/edge/internal/bootstrap [no test files] +ok iop/apps/edge/internal/node (cached) +? iop/apps/edge/internal/transport [no test files] +? iop/apps/node/cmd/iop-node [no test files] +? iop/apps/node/internal/adapters [no test files] +? iop/apps/node/internal/adapters/cli [no test files] +? iop/apps/node/internal/adapters/mock [no test files] +? iop/apps/node/internal/adapters/ollama [no test files] +? iop/apps/node/internal/adapters/vllm [no test files] +? iop/apps/node/internal/bootstrap [no test files] +? iop/apps/node/internal/node [no test files] +? iop/apps/node/internal/router [no test files] +? iop/apps/node/internal/runtime [no test files] +? iop/apps/node/internal/store [no test files] +? iop/apps/node/internal/transport [no test files] +? iop/apps/worker/cmd/iop-worker [no test files] +? iop/packages/auth [no test files] +? iop/packages/config [no test files] +? iop/packages/jobs [no test files] +? iop/packages/metadata [no test files] +? iop/packages/observability [no test files] +? iop/packages/policy [no test files] +? iop/packages/version [no test files] +? iop/proto/gen/iop [no test files] +``` + +--- + +## 코드리뷰 결과 + +### 종합 판정: WARN + +### 차원별 평가 + +| 차원 | 판정 | +|------|------| +| Correctness | Warn | +| Completeness | Pass | +| Test coverage | Pass | +| API contract | Pass | +| Code quality | Pass | +| Plan deviation | Pass | +| Verification trust | Pass | + +### 발견된 문제 + +- **Suggested** `apps/edge/internal/transport/server.go:101-107` + `client.AddDisconnectListener` 등록이 `s.registry.Register(entry)` 이후에 위치함. + CapabilityResponse 수신 후 node가 즉시 disconnect되면 disconnect 이벤트가 listener 등록 전에 발생해 registry stale entry가 남을 수 있음. + Fix: `AddDisconnectListener` 블록을 `s.registry.Register(entry)` 이전으로 이동. + ```go + // before: + s.registry.Register(entry) + s.logger.Info("node registered", ...) + client.AddDisconnectListener(func(_ *toki.TcpClient) { ... }) + + // after: + client.AddDisconnectListener(func(_ *toki.TcpClient) { + s.registry.Unregister(entry.NodeID) + s.logger.Info("node unregistered", zap.String("node_id", entry.NodeID)) + }) + s.registry.Register(entry) + s.logger.Info("node registered", ...) + ``` + +- **Nit** `apps/node/internal/node/node.go:111` + `a.Capabilities(context.Background())`가 `OnCapabilityRequest`에 전달된 `ctx`를 무시함. + `ctx`를 직접 전달하면 호출 체인에서 cancellation이 일관성 있게 전파됨. + +### 다음 단계 + +WARN: Suggested 이슈 수정을 위한 새 PLAN.md와 CODE_REVIEW.md 스텁을 작성한 후 구현 루프를 계속한다. diff --git a/agent-task/edge_node_arch/code_review_1.log b/agent-task/edge_node_arch/code_review_1.log new file mode 100644 index 0000000..8b724ec --- /dev/null +++ b/agent-task/edge_node_arch/code_review_1.log @@ -0,0 +1,110 @@ + + +# Code Review Reference - REVIEW_REFACTOR + +## 개요 + +date=2026-05-02 +task=edge_node_arch, plan=1, tag=REVIEW_REFACTOR + +## 이 파일을 읽는 리뷰 에이전트에게 + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료 후 반드시 아래 순서로 아카이브하세요. + +1. `CODE_REVIEW.md` → `code_review_1.log` (N = 기존 code_review_*.log 수) +2. `PLAN.md` → `plan_1.log` (M = 기존 plan_*.log 수) +3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 `PLAN.md` + `CODE_REVIEW.md` 스텁 작성. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [REVIEW_REFACTOR-1] onNodeConnected disconnect 리스너 순서 수정 | [x] | + +## 계획 대비 변경 사항 + +없음. + +## 주요 설계 결정 + +- `CapabilityResponse`를 받아 `NodeEntry`를 만든 직후 disconnect listener를 먼저 등록하고, 그 다음 registry에 등록한다. +- listener는 기존과 동일하게 해당 nodeID를 registry에서 제거하고 `node unregistered` 로그를 남긴다. + +## 리뷰어를 위한 체크포인트 + +- `apps/edge/internal/transport/server.go`의 `onNodeConnected` 고루틴에서 `client.AddDisconnectListener(...)` 블록이 `s.registry.Register(entry)` 이전에 위치하는지 확인 +- `go build ./apps/edge/...`가 오류 없이 통과하는지 확인 +- `go build ./...`와 `go test ./...`가 모두 통과하는지 확인 + +## 검증 결과 + +_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._ + +### REVIEW_REFACTOR-1 중간 검증 +``` +$ PATH=/config/go-sdk/go/bin:/config/go-sdk/bin:$PATH go build ./apps/edge/... +(no output) +``` + +### 최종 검증 +``` +$ PATH=/config/go-sdk/go/bin:/config/go-sdk/bin:$PATH go build ./... +(no output) + +$ PATH=/config/go-sdk/go/bin:/config/go-sdk/bin:$PATH go test ./... +? iop/apps/control-plane/cmd/iop-control-plane [no test files] +? iop/apps/edge/cmd/iop-edge [no test files] +? iop/apps/edge/internal/bootstrap [no test files] +ok iop/apps/edge/internal/node (cached) +? iop/apps/edge/internal/transport [no test files] +? iop/apps/node/cmd/iop-node [no test files] +? iop/apps/node/internal/adapters [no test files] +? iop/apps/node/internal/adapters/cli [no test files] +? iop/apps/node/internal/adapters/mock [no test files] +? iop/apps/node/internal/adapters/ollama [no test files] +? iop/apps/node/internal/adapters/vllm [no test files] +? iop/apps/node/internal/bootstrap [no test files] +? iop/apps/node/internal/node [no test files] +? iop/apps/node/internal/router [no test files] +? iop/apps/node/internal/runtime [no test files] +? iop/apps/node/internal/store [no test files] +? iop/apps/node/internal/transport [no test files] +? iop/apps/worker/cmd/iop-worker [no test files] +? iop/packages/auth [no test files] +? iop/packages/config [no test files] +? iop/packages/jobs [no test files] +? iop/packages/metadata [no test files] +? iop/packages/observability [no test files] +? iop/packages/policy [no test files] +? iop/packages/version [no test files] +? iop/proto/gen/iop [no test files] +``` + +--- + +## 코드리뷰 결과 + +### 종합 판정: PASS + +### 차원별 평가 + +| 차원 | 판정 | +|------|------| +| Correctness | Pass | +| Completeness | Pass | +| Test coverage | Pass | +| API contract | Pass | +| Code quality | Pass | +| Plan deviation | Pass | +| Verification trust | Pass | + +### 발견된 문제 + +없음. `server.go:101-106` 기준으로 `AddDisconnectListener`가 `registry.Register` 이전에 위치함이 확인됨. race window 제거. + +### 다음 단계 + +PASS: `complete.log` 작성 후 종료. diff --git a/agent-task/edge_node_arch/complete.log b/agent-task/edge_node_arch/complete.log new file mode 100644 index 0000000..26bb847 --- /dev/null +++ b/agent-task/edge_node_arch/complete.log @@ -0,0 +1,33 @@ +# Task Complete — edge_node_arch + +## 완료 일시 + +2026-05-02 + +## 요약 + +edge를 TCP 서버로, node를 TCP 클라이언트로 재설계. proto-socket + protobuf 기반 통신 구현. 2 루프(REFACTOR → REVIEW_REFACTOR). + +## 루프 이력 + +| Plan | Code Review | 판정 | +|------|-------------|------| +| plan_0.log (REFACTOR) | code_review_0.log | WARN | +| plan_1.log (REVIEW_REFACTOR) | code_review_1.log | PASS | + +## 최종 리뷰 요약 + +- `proto/gen/iop/` 4개 파일 생성 (runtime, node, control, job) +- `apps/node/internal/transport/`: `client.go`, `parser.go`, `session.go` — proto-socket `TcpClient` 기반 node→edge 클라이언트 +- `apps/node/internal/node/node.go`: proto 타입 사용, `structAsMap` helper, `sessionSink.Emit`이 `*iop.RunEvent` 직접 전송 +- `apps/node/internal/bootstrap/module.go`: `transport.DialEdge` OnStart, `sess.Close()` OnStop +- `apps/edge/internal/node/registry.go`: `NodeRegistry` — Register/Unregister/Pick/Count/All +- `apps/edge/internal/transport/server.go`: proto-socket `TcpServer`, `onNodeConnected` → CapabilityRequest → Register +- `apps/edge/internal/bootstrap/module.go`: fx 모듈 wiring +- `packages/config/config.go`: `EdgeConfig`, `LoadEdge`, `TransportConf.EdgeAddr` +- `go.mod`: proto-socket require + replace +- `configs/edge.yaml` 추가 + +## 잔여 Nit + +- `apps/node/internal/node/node.go:111` `a.Capabilities(context.Background())` — 전달받은 `ctx` 대신 `context.Background()` 사용. 기능 영향 없음. diff --git a/agent-task/edge_node_arch/plan_0.log b/agent-task/edge_node_arch/plan_0.log new file mode 100644 index 0000000..46db087 --- /dev/null +++ b/agent-task/edge_node_arch/plan_0.log @@ -0,0 +1,1096 @@ + + +# edge TCP 서버 + node TCP 클라이언트 구현 (proto-socket / protobuf) + +## 이 파일을 읽는 구현 에이전트에게 + +각 체크리스트 항목을 완료한 뒤 `[x]`로 표시하세요. +중간 검증 명령을 실제로 실행하고 출력을 `CODE_REVIEW.md` 검증 결과 섹션에 붙여 넣으세요. +계획과 다르게 구현한 부분은 반드시 `계획 대비 변경 사항`에 기록하세요. + +## 배경 + +이전 작업에서 node의 TCP 서버 코드(`transport/server.go`, `session.go`, `frame.go`, +`codec.go`, `tls.go`)와 JSON 기반 `packages/protocol/protocol.go`를 모두 삭제했다. +`apps/node/internal/node/node.go`는 삭제된 `protocol.*` 타입을 참조해 현재 컴파일 +불가 상태다. 이 플랜은 올바른 아키텍처를 처음부터 구현한다: **edge가 TCP 서버로 +여러 노드를 수용하고, node가 시작 시 edge에 클라이언트로 연결**한다. wire format은 +`../proto-socket` 패키지 기반 protobuf이며 JSON을 사용하지 않는다. + +## 의존 관계 및 구현 순서 + +``` +REFACTOR-1 (proto gen + go.mod) + └─> REFACTOR-2 (node transport: client.go, session.go, parser.go) + └─> REFACTOR-3 (node.go — proto 타입으로 교체) + └─> REFACTOR-4 (node bootstrap — DialEdge 연결) + └─> REFACTOR-5 (edge server — 다중 노드 관리) +``` + +--- + +### [REFACTOR-1] proto 컴파일 및 go.mod에 proto-socket 추가 + +**문제** + +`proto/gen/iop/` 디렉터리가 없어 proto 생성 타입을 사용할 수 없다. +`go.mod`에 `git.toki-labs.com/toki/common-proto-socket/go` 의존성이 없다. + +**해결 방법** + +`go.mod`에 require + replace를 추가하고 `make proto`로 Go 소스를 생성한다. + +``` +# Before — go.mod require 블록 (go.mod:5-14) +require ( + github.com/prometheus/client_golang v1.20.5 + github.com/spf13/cobra v1.8.1 + github.com/spf13/viper v1.19.0 + go.uber.org/fx v1.22.2 + go.uber.org/zap v1.27.0 + google.golang.org/protobuf v1.36.5 + gopkg.in/yaml.v3 v3.0.1 + modernc.org/sqlite v1.33.1 +) + +# After +require ( + git.toki-labs.com/toki/common-proto-socket/go v0.0.0-00010101000000-000000000000 + github.com/prometheus/client_golang v1.20.5 + github.com/spf13/cobra v1.8.1 + github.com/spf13/viper v1.19.0 + go.uber.org/fx v1.22.2 + go.uber.org/zap v1.27.0 + google.golang.org/protobuf v1.36.5 + gopkg.in/yaml.v3 v3.0.1 + modernc.org/sqlite v1.33.1 +) + +replace git.toki-labs.com/toki/common-proto-socket/go => ../proto-socket/go +``` + +protoc 실행: + +```bash +PATH=/config/go-sdk/bin:$PATH make proto +PATH=/config/go-sdk/bin:$PATH go mod tidy +``` + +`Makefile:20-28`의 `proto` 타겟이 이미 올바르게 정의되어 있으므로 그대로 사용한다. + +**수정 파일 및 체크리스트** + +- [x] `go.mod` — require + replace 추가 +- [x] `proto/gen/iop/runtime.pb.go` — protoc 생성 (신규) +- [x] `proto/gen/iop/node.pb.go` — protoc 생성 (신규) +- [x] `proto/gen/iop/control.pb.go` — protoc 생성 (신규) +- [x] `proto/gen/iop/job.pb.go` — protoc 생성 (신규) + +**테스트 작성** + +생성 파일이므로 별도 테스트 불필요. + +**중간 검증** + +```bash +PATH=/config/go-sdk/bin:$PATH make proto +ls proto/gen/iop/ +PATH=/config/go-sdk/bin:$PATH go mod tidy +``` + +기대 결과: `runtime.pb.go node.pb.go control.pb.go job.pb.go` 4개 파일 생성 + +--- + +### [REFACTOR-2] node transport 클라이언트 계층 구현 + +**문제** + +`apps/node/internal/transport/` 디렉터리가 비어 있다. +proto-socket 기반의 edge 연결 클라이언트와 메시지 핸들러가 없다. + +**해결 방법** + +세 파일을 신규 생성한다. + +**`apps/node/internal/transport/parser.go`** + +node가 edge로부터 수신하는 메시지 타입의 파서를 등록한다. + +```go +package transport + +import ( + "google.golang.org/protobuf/proto" + + toki "git.toki-labs.com/toki/common-proto-socket/go" + iop "iop/proto/gen/iop" +) + +func nodeParserMap() toki.ParserMap { + return toki.ParserMap{ + toki.TypeNameOf(&iop.RunRequest{}): func(b []byte) (proto.Message, error) { + m := &iop.RunRequest{} + return m, proto.Unmarshal(b, m) + }, + toki.TypeNameOf(&iop.CancelRequest{}): func(b []byte) (proto.Message, error) { + m := &iop.CancelRequest{} + return m, proto.Unmarshal(b, m) + }, + toki.TypeNameOf(&iop.CapabilityRequest{}): func(b []byte) (proto.Message, error) { + m := &iop.CapabilityRequest{} + return m, proto.Unmarshal(b, m) + }, + } +} +``` + +**`apps/node/internal/transport/session.go`** + +proto-socket `TcpClient`를 래핑하고, edge로부터 오는 메시지 리스너를 등록한다. + +```go +package transport + +import ( + "context" + "sync" + + toki "git.toki-labs.com/toki/common-proto-socket/go" + "go.uber.org/zap" + "google.golang.org/protobuf/proto" + + iop "iop/proto/gen/iop" +) + +// Handler processes IOP messages received from edge. +type Handler interface { + OnRunRequest(ctx context.Context, sess *Session, req *iop.RunRequest) error + OnCapabilityRequest(ctx context.Context, sess *Session) (*iop.CapabilityResponse, error) + OnCancel(ctx context.Context, sess *Session, req *iop.CancelRequest) error +} + +// Session represents the node's persistent connection to edge. +type Session struct { + client *toki.TcpClient + nodeID string + logger *zap.Logger + cancelFns sync.Map // run_id → context.CancelFunc +} + +func newSession(client *toki.TcpClient, handler Handler, nodeID string, logger *zap.Logger) *Session { + s := &Session{client: client, nodeID: nodeID, logger: logger} + + toki.AddListenerTyped[*iop.RunRequest](&client.Communicator, func(req *iop.RunRequest) { + go func() { + if err := handler.OnRunRequest(context.Background(), s, req); err != nil { + logger.Warn("run request error", + zap.String("run_id", req.GetRunId()), + zap.Error(err), + ) + } + }() + }) + + toki.AddRequestListenerTyped[*iop.CapabilityRequest, *iop.CapabilityResponse]( + &client.Communicator, + func(_ *iop.CapabilityRequest) (*iop.CapabilityResponse, error) { + return handler.OnCapabilityRequest(context.Background(), s) + }, + ) + + toki.AddListenerTyped[*iop.CancelRequest](&client.Communicator, func(req *iop.CancelRequest) { + if err := handler.OnCancel(context.Background(), s, req); err != nil { + logger.Warn("cancel error", + zap.String("run_id", req.GetRunId()), + zap.Error(err), + ) + } + }) + + client.AddDisconnectListener(func(_ *toki.TcpClient) { + logger.Info("disconnected from edge", zap.String("node_id", nodeID)) + }) + + return s +} + +// Send transmits a proto message to edge. +func (s *Session) Send(m proto.Message) error { + return s.client.Send(m) +} + +// IsAlive reports whether the connection is active. +func (s *Session) IsAlive() bool { return s.client.IsAlive() } + +// Close terminates the connection to edge. +func (s *Session) Close() error { return s.client.Close() } + +// RegisterCancel stores a cancel function for a running execution. +func (s *Session) RegisterCancel(runID string, cancel context.CancelFunc) { + s.cancelFns.Store(runID, cancel) +} + +// DeregisterCancel removes a run's cancel function after completion. +func (s *Session) DeregisterCancel(runID string) { + s.cancelFns.Delete(runID) +} + +// CancelRun invokes the cancel function for a run if one is registered. +func (s *Session) CancelRun(runID string) { + if v, ok := s.cancelFns.Load(runID); ok { + v.(context.CancelFunc)() + } +} +``` + +**`apps/node/internal/transport/client.go`** + +edge 주소로 TCP 연결을 맺고 Session을 반환한다. + +```go +package transport + +import ( + "context" + "fmt" + "net" + + toki "git.toki-labs.com/toki/common-proto-socket/go" + "go.uber.org/zap" +) + +const ( + heartbeatIntervalSec = 30 + heartbeatWaitSec = 10 +) + +// DialEdge connects to the edge TCP server and returns a Session. +func DialEdge(ctx context.Context, addr string, handler Handler, nodeID string, logger *zap.Logger) (*Session, error) { + host, portStr, err := net.SplitHostPort(addr) + if err != nil { + return nil, fmt.Errorf("transport: invalid addr %q: %w", addr, err) + } + port := 0 + if _, err := fmt.Sscanf(portStr, "%d", &port); err != nil { + return nil, fmt.Errorf("transport: invalid port %q: %w", portStr, err) + } + + client, err := toki.DialTcp(ctx, host, port, heartbeatIntervalSec, heartbeatWaitSec, nodeParserMap()) + if err != nil { + return nil, fmt.Errorf("transport: dial edge %s: %w", addr, err) + } + + sess := newSession(client, handler, nodeID, logger) + logger.Info("connected to edge", zap.String("addr", addr), zap.String("node_id", nodeID)) + return sess, nil +} +``` + +**수정 파일 및 체크리스트** + +- [x] `apps/node/internal/transport/parser.go` — 신규 생성 +- [x] `apps/node/internal/transport/session.go` — 신규 생성 +- [x] `apps/node/internal/transport/client.go` — 신규 생성 + +**테스트 작성** + +proto-socket의 TCP/heartbeat/disconnect 동작은 `../proto-socket/go/test/`에서 검증됨. +Session 연결 흐름은 REFACTOR-5 최종 통합 검증으로 커버. 단위 테스트 생략. + +**중간 검증** + +```bash +PATH=/config/go-sdk/bin:$PATH go build ./apps/node/internal/transport/... +``` + +기대 결과: 빌드 에러 없음 + +--- + +### [REFACTOR-3] node.go — proto 타입으로 교체 + +**문제** + +`apps/node/internal/node/node.go`가 삭제된 `iop/packages/protocol` 패키지를 참조해 +컴파일 불가 상태다. (node.go:18, :48, :108, :116, :126, :139-168) + +**해결 방법** + +`protocol.*` 참조를 `iop/proto/gen/iop` 생성 타입으로 교체한다. + +```go +// Before (node.go:5-19 imports) +import ( + "context" + "encoding/json" + "fmt" + "time" + + "go.uber.org/zap" + + "iop/apps/node/internal/adapters" + "iop/apps/node/internal/runtime" + "iop/apps/node/internal/store" + "iop/apps/node/internal/transport" + "iop/packages/config" + "iop/packages/protocol" +) + +// After +import ( + "context" + "fmt" + "time" + + "go.uber.org/zap" + + "iop/apps/node/internal/adapters" + "iop/apps/node/internal/runtime" + "iop/apps/node/internal/store" + "iop/apps/node/internal/transport" + "iop/packages/config" + iop "iop/proto/gen/iop" +) +``` + +```go +// Before (node.go:48) +func (n *Node) OnRunRequest(ctx context.Context, sess *transport.Session, req *protocol.RunRequest) error { + n.logger.Info("run request received", + zap.String("run_id", req.RunID), + zap.String("adapter", req.Adapter), + zap.String("model", req.Model), + ) + + rr := runtime.RunRequest{ + RunID: req.RunID, + Adapter: req.Adapter, + Model: req.Model, + Workspace: req.Workspace, + Policy: req.Policy, + Input: req.Input, + TimeoutSec: req.TimeoutSec, + Metadata: req.Metadata, + } + +// After +// iop.RunRequest.Input / Policy 는 google.protobuf.Struct — AsMap()으로 변환. +func (n *Node) OnRunRequest(ctx context.Context, sess *transport.Session, req *iop.RunRequest) error { + n.logger.Info("run request received", + zap.String("run_id", req.GetRunId()), + zap.String("adapter", req.GetAdapter()), + zap.String("model", req.GetModel()), + ) + + rr := runtime.RunRequest{ + RunID: req.GetRunId(), + Adapter: req.GetAdapter(), + Model: req.GetModel(), + Workspace: req.GetWorkspace(), + Policy: req.GetPolicy().AsMap(), + Input: req.GetInput().AsMap(), + TimeoutSec: int(req.GetTimeoutSec()), + Metadata: req.GetMetadata(), + } +``` + +```go +// Before (node.go:108-122) +func (n *Node) OnCapabilityRequest(_ context.Context, _ *transport.Session) (*protocol.CapabilityResponse, error) { + resp := &protocol.CapabilityResponse{NodeID: n.nodeID} + for _, a := range n.registry.All() { + caps, err := a.Capabilities(context.Background()) + if err != nil { + n.logger.Warn("capabilities query failed", zap.String("adapter", a.Name()), zap.Error(err)) + continue + } + resp.Adapters = append(resp.Adapters, protocol.AdapterInfo{ + Name: caps.AdapterName, + Models: caps.Models, + MaxConcurrency: caps.MaxConcurrency, + }) + } + return resp, nil +} + +// After +func (n *Node) OnCapabilityRequest(_ context.Context, _ *transport.Session) (*iop.CapabilityResponse, error) { + resp := &iop.CapabilityResponse{NodeId: n.nodeID} + for _, a := range n.registry.All() { + caps, err := a.Capabilities(context.Background()) + if err != nil { + n.logger.Warn("capabilities query failed", zap.String("adapter", a.Name()), zap.Error(err)) + continue + } + resp.Adapters = append(resp.Adapters, &iop.AdapterInfo{ + Name: caps.AdapterName, + Models: caps.Models, + MaxConcurrency: int32(caps.MaxConcurrency), + }) + } + return resp, nil +} +``` + +```go +// Before (node.go:126-129) +func (n *Node) OnCancel(_ context.Context, sess *transport.Session, req *protocol.CancelRequest) error { + n.logger.Info("cancel request", zap.String("run_id", req.RunID)) + sess.CancelRun(req.RunID) + return nil +} + +// After +func (n *Node) OnCancel(_ context.Context, sess *transport.Session, req *iop.CancelRequest) error { + n.logger.Info("cancel request", zap.String("run_id", req.GetRunId())) + sess.CancelRun(req.GetRunId()) + return nil +} +``` + +```go +// Before (node.go:138-169) sessionSink.Emit +func (s *sessionSink) Emit(_ context.Context, event runtime.RuntimeEvent) error { + var usageInfo *protocol.UsageInfo + if event.Usage != nil { + usageInfo = &protocol.UsageInfo{ + InputTokens: event.Usage.InputTokens, + OutputTokens: event.Usage.OutputTokens, + } + } + + re := protocol.RunEvent{...} + payload, err := json.Marshal(re) + if err != nil { + return err + } + return s.sess.Send(&protocol.Envelope{...}) +} + +// After — Envelope 없이 RunEvent 직접 전송 (proto-socket이 래핑) +func (s *sessionSink) Emit(_ context.Context, event runtime.RuntimeEvent) error { + re := &iop.RunEvent{ + RunId: event.RunID, + Type: string(event.Type), + Delta: event.Delta, + Message: event.Message, + Error: event.Error, + Timestamp: event.Timestamp.UnixNano(), + } + if event.Usage != nil { + re.Usage = &iop.Usage{ + InputTokens: int32(event.Usage.InputTokens), + OutputTokens: int32(event.Usage.OutputTokens), + } + } + return s.sess.Send(re) +} +``` + +**수정 파일 및 체크리스트** + +- [x] `apps/node/internal/node/node.go` — imports 교체, 메서드 시그니처 및 본문 전체 교체 + +**테스트 작성** + +타입 교체이며 로직은 동일하므로 기존 테스트 없음. 최종 통합으로 검증. + +**중간 검증** + +```bash +PATH=/config/go-sdk/bin:$PATH go build ./apps/node/... +``` + +기대 결과: 빌드 에러 없음 + +--- + +### [REFACTOR-4] node bootstrap — DialEdge 연결 추가 + +**문제** + +`apps/node/internal/bootstrap/module.go:70-84`의 fx.Invoke가 metrics 서버만 +기동하고 edge 연결을 하지 않는다. + +**해결 방법** + +fx OnStart에서 `transport.DialEdge`를 호출해 edge에 연결하고, OnStop에서 닫는다. + +```go +// Before (bootstrap/module.go:70-84) +fx.Invoke(func(lc fx.Lifecycle, cfg *config.NodeConfig, logger *zap.Logger) { + lc.Append(fx.Hook{ + OnStart: func(ctx context.Context) error { + go func() { + if err := observability.ServeMetrics(cfg.Metrics.Port); err != nil { + logger.Warn("metrics server exited", zap.Error(err)) + } + }() + return nil + }, + OnStop: func(_ context.Context) error { return nil }, + }) +}), + +// After +fx.Invoke(func(lc fx.Lifecycle, n *node.Node, cfg *config.NodeConfig, logger *zap.Logger) { + var sess *transport.Session + lc.Append(fx.Hook{ + OnStart: func(ctx context.Context) error { + s, err := transport.DialEdge(ctx, cfg.Transport.EdgeAddr, n, cfg.Node.ID, logger) + if err != nil { + return err + } + sess = s + go func() { + if err := observability.ServeMetrics(cfg.Metrics.Port); err != nil { + logger.Warn("metrics server exited", zap.Error(err)) + } + }() + return nil + }, + OnStop: func(_ context.Context) error { + if sess != nil { + return sess.Close() + } + return nil + }, + }) +}), +``` + +import에 `"iop/apps/node/internal/transport"` 추가. + +**수정 파일 및 체크리스트** + +- [x] `apps/node/internal/bootstrap/module.go` — import 추가, fx.Invoke 교체 + +**테스트 작성** + +bootstrap 와이어링이므로 별도 테스트 불필요. + +**중간 검증** + +```bash +PATH=/config/go-sdk/bin:$PATH go build ./apps/node/... +``` + +기대 결과: 빌드 에러 없음 + +--- + +### [REFACTOR-5] edge TCP 서버 — 다중 노드 관리 + +**문제** + +- `apps/edge/cmd/iop-edge/main.go:27-29`의 serve 커맨드가 placeholder다. +- edge config 구조체가 없다. +- 여러 node가 연결될 수 있는 NodeRegistry가 없다. + +**해결 방법** + +proto-socket TcpServer로 여러 노드 연결을 수락하고, +각 노드의 nodeID·capabilities를 `NodeRegistry`에 등록·관리한다. + +**`packages/config/config.go` — EdgeConfig 추가 (파일 끝에 append)** + +```go +type EdgeConfig struct { + Server EdgeServerConf `mapstructure:"server" yaml:"server"` + TLS TLSConf `mapstructure:"tls" yaml:"tls"` + Logging LoggingConf `mapstructure:"logging" yaml:"logging"` + Metrics MetricsConf `mapstructure:"metrics" yaml:"metrics"` +} + +type EdgeServerConf struct { + Listen string `mapstructure:"listen" yaml:"listen"` +} + +func LoadEdge(cfgFile string) (*EdgeConfig, error) { + v := viper.New() + v.SetConfigFile(cfgFile) + v.SetDefault("server.listen", "0.0.0.0:9090") + v.SetDefault("logging.level", "info") + v.SetDefault("metrics.port", 9092) + if err := v.ReadInConfig(); err != nil { + return nil, err + } + var cfg EdgeConfig + return &cfg, v.Unmarshal(&cfg) +} +``` + +**`apps/edge/internal/node/registry.go` (신규)** + +연결된 노드를 nodeID로 관리한다. + +```go +package node + +import ( + "fmt" + "sync" + + toki "git.toki-labs.com/toki/common-proto-socket/go" + iop "iop/proto/gen/iop" +) + +// NodeEntry는 연결된 노드 하나를 나타낸다. +type NodeEntry struct { + NodeID string + Client *toki.TcpClient + Adapters []*iop.AdapterInfo +} + +// Registry는 edge에 연결된 모든 node를 관리한다. +type Registry struct { + mu sync.RWMutex + nodes map[string]*NodeEntry // nodeID → entry +} + +func NewRegistry() *Registry { + return &Registry{nodes: make(map[string]*NodeEntry)} +} + +func (r *Registry) Register(entry *NodeEntry) { + r.mu.Lock() + defer r.mu.Unlock() + r.nodes[entry.NodeID] = entry +} + +func (r *Registry) Unregister(nodeID string) { + r.mu.Lock() + defer r.mu.Unlock() + delete(r.nodes, nodeID) +} + +func (r *Registry) Get(nodeID string) (*NodeEntry, bool) { + r.mu.RLock() + defer r.mu.RUnlock() + e, ok := r.nodes[nodeID] + return e, ok +} + +func (r *Registry) All() []*NodeEntry { + r.mu.RLock() + defer r.mu.RUnlock() + out := make([]*NodeEntry, 0, len(r.nodes)) + for _, e := range r.nodes { + out = append(out, e) + } + return out +} + +func (r *Registry) Count() int { + r.mu.RLock() + defer r.mu.RUnlock() + return len(r.nodes) +} + +// Pick returns any one connected node (round-robin은 추후 구현). +func (r *Registry) Pick() (*NodeEntry, error) { + r.mu.RLock() + defer r.mu.RUnlock() + for _, e := range r.nodes { + return e, nil + } + return nil, fmt.Errorf("no nodes connected") +} +``` + +**`apps/edge/internal/transport/server.go` (신규)** + +proto-socket TcpServer로 노드 연결을 수락하고 Registry에 등록한다. +연결 시 CapabilityRequest를 전송해 노드 정보를 확인한다. + +```go +package transport + +import ( + "net" + "strconv" + "time" + + toki "git.toki-labs.com/toki/common-proto-socket/go" + "go.uber.org/zap" + "google.golang.org/protobuf/proto" + + edgenode "iop/apps/edge/internal/node" + iop "iop/proto/gen/iop" +) + +const ( + heartbeatIntervalSec = 30 + heartbeatWaitSec = 10 + capabilityTimeout = 5 * time.Second +) + +func edgeParserMap() toki.ParserMap { + return toki.ParserMap{ + toki.TypeNameOf(&iop.RunEvent{}): func(b []byte) (proto.Message, error) { + m := &iop.RunEvent{} + return m, proto.Unmarshal(b, m) + }, + } +} + +// Server wraps proto-socket TcpServer and manages node connections. +type Server struct { + tcp *toki.TcpServer + registry *edgenode.Registry + logger *zap.Logger +} + +func NewServer(listen string, registry *edgenode.Registry, logger *zap.Logger) (*Server, error) { + host, portStr, err := net.SplitHostPort(listen) + if err != nil { + return nil, err + } + port, err := strconv.Atoi(portStr) + if err != nil { + return nil, err + } + + s := &Server{registry: registry, logger: logger} + s.tcp = toki.NewTcpServer(host, port, func(conn net.Conn) *toki.TcpClient { + return toki.NewTcpClient(conn, heartbeatIntervalSec, heartbeatWaitSec, edgeParserMap()) + }) + s.tcp.OnClientConnected = s.onNodeConnected + return s, nil +} + +func (s *Server) Start(ctx context.Context) error { + if err := s.tcp.Start(ctx); err != nil { + return err + } + s.logger.Info("edge listening for nodes", zap.String("addr", s.tcp.Addr())) + return nil +} + +func (s *Server) Stop() error { + return s.tcp.Stop() +} + +func (s *Server) onNodeConnected(client *toki.TcpClient) { + s.logger.Info("node connection established") + + toki.AddListenerTyped[*iop.RunEvent](&client.Communicator, func(e *iop.RunEvent) { + s.logger.Debug("run event received", + zap.String("run_id", e.GetRunId()), + zap.String("type", e.GetType()), + ) + }) + + go func() { + resp, err := toki.SendRequestTyped[*iop.CapabilityRequest, *iop.CapabilityResponse]( + &client.Communicator, + &iop.CapabilityRequest{}, + capabilityTimeout, + ) + if err != nil { + s.logger.Warn("capability request failed", zap.Error(err)) + _ = client.Close() + return + } + + entry := &edgenode.NodeEntry{ + NodeID: resp.GetNodeId(), + Client: client, + Adapters: resp.GetAdapters(), + } + s.registry.Register(entry) + s.logger.Info("node registered", + zap.String("node_id", entry.NodeID), + zap.Int("adapters", len(entry.Adapters)), + ) + + client.AddDisconnectListener(func(_ *toki.TcpClient) { + s.registry.Unregister(entry.NodeID) + s.logger.Info("node unregistered", zap.String("node_id", entry.NodeID)) + }) + }() +} +``` + +> **주의**: proto-socket `TcpServer`에 `Addr()` 메서드가 없으면 Start 로그에서 +> 주소 출력을 생략하고 `logger.Info("edge listening for nodes")`만 사용한다. + +**`apps/edge/internal/bootstrap/module.go` (신규)** + +```go +package bootstrap + +import ( + "context" + + "go.uber.org/fx" + "go.uber.org/zap" + + edgenode "iop/apps/edge/internal/node" + "iop/apps/edge/internal/transport" + "iop/packages/config" + "iop/packages/observability" +) + +func Module(cfg *config.EdgeConfig) fx.Option { + return fx.Options( + fx.Provide( + func() *config.EdgeConfig { return cfg }, + + func(cfg *config.EdgeConfig) (*zap.Logger, error) { + return observability.NewLogger(cfg.Logging.Level) + }, + + func() *edgenode.Registry { + return edgenode.NewRegistry() + }, + + func(cfg *config.EdgeConfig, reg *edgenode.Registry, logger *zap.Logger) (*transport.Server, error) { + return transport.NewServer(cfg.Server.Listen, reg, logger) + }, + ), + + fx.Invoke(func(lc fx.Lifecycle, srv *transport.Server, cfg *config.EdgeConfig, logger *zap.Logger) { + lc.Append(fx.Hook{ + OnStart: func(ctx context.Context) error { + if err := srv.Start(ctx); err != nil { + return err + } + go func() { + if err := observability.ServeMetrics(cfg.Metrics.Port); err != nil { + logger.Warn("metrics server exited", zap.Error(err)) + } + }() + return nil + }, + OnStop: func(_ context.Context) error { + return srv.Stop() + }, + }) + }), + ) +} +``` + +**`apps/edge/cmd/iop-edge/main.go` — serve 커맨드 구현** + +```go +// Before (main.go:18-34) 전체 +package main + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" +) + +func main() { ... } + +func rootCmd() *cobra.Command { + root := &cobra.Command{...} + root.AddCommand(&cobra.Command{ + Use: "serve", + Run: func(...) { fmt.Println("iop-edge serve: not yet implemented") }, + }) + return root +} + +// After +package main + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + "go.uber.org/fx" + + "iop/apps/edge/internal/bootstrap" + "iop/packages/config" +) + +var cfgFile string + +func main() { + if err := rootCmd().Execute(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +func rootCmd() *cobra.Command { + root := &cobra.Command{ + Use: "iop-edge", + Short: "IOP Edge — node gateway server", + } + root.PersistentFlags().StringVarP(&cfgFile, "config", "c", "configs/edge.yaml", "config file path") + root.AddCommand(serveCmd()) + return root +} + +func serveCmd() *cobra.Command { + return &cobra.Command{ + Use: "serve", + Short: "Start the IOP edge server", + RunE: func(_ *cobra.Command, _ []string) error { + cfg, err := config.LoadEdge(cfgFile) + if err != nil { + return fmt.Errorf("load config: %w", err) + } + fx.New(bootstrap.Module(cfg)).Run() + return nil + }, + } +} +``` + +**`configs/edge.yaml` 업데이트** + +```yaml +# Before +server: + listen: "0.0.0.0:8080" + +# After +server: + listen: "0.0.0.0:9090" + +tls: + enabled: false + +logging: + level: "info" + +metrics: + port: 9092 +``` + +**수정 파일 및 체크리스트** + +- [x] `packages/config/config.go` — EdgeConfig, EdgeServerConf, LoadEdge 추가 +- [x] `apps/edge/internal/node/registry.go` — 신규 생성 +- [x] `apps/edge/internal/transport/server.go` — 신규 생성 +- [x] `apps/edge/internal/bootstrap/module.go` — 신규 생성 +- [x] `apps/edge/cmd/iop-edge/main.go` — serve 커맨드 구현으로 교체 +- [x] `configs/edge.yaml` — listen 포트 9090으로 수정 + +**테스트 작성** + +NodeRegistry의 Register/Unregister/Pick 로직은 단위 테스트로 검증한다. + +파일: `apps/edge/internal/node/registry_test.go` + +```go +package node_test + +import ( + "testing" + + toki "git.toki-labs.com/toki/common-proto-socket/go" + edgenode "iop/apps/edge/internal/node" + iop "iop/proto/gen/iop" +) + +func TestRegistry_RegisterAndCount(t *testing.T) { + reg := edgenode.NewRegistry() + entry := &edgenode.NodeEntry{ + NodeID: "node-001", + Client: &toki.TcpClient{}, + Adapters: []*iop.AdapterInfo{{Name: "mock"}}, + } + reg.Register(entry) + if reg.Count() != 1 { + t.Fatalf("expected 1 node, got %d", reg.Count()) + } +} + +func TestRegistry_Pick_Empty(t *testing.T) { + reg := edgenode.NewRegistry() + if _, err := reg.Pick(); err == nil { + t.Fatal("expected error on empty registry") + } +} + +func TestRegistry_Unregister(t *testing.T) { + reg := edgenode.NewRegistry() + reg.Register(&edgenode.NodeEntry{NodeID: "node-001"}) + reg.Unregister("node-001") + if reg.Count() != 0 { + t.Fatalf("expected 0 nodes after unregister, got %d", reg.Count()) + } +} +``` + +**중간 검증** + +```bash +PATH=/config/go-sdk/bin:$PATH go build ./apps/edge/... +PATH=/config/go-sdk/bin:$PATH go test ./apps/edge/internal/node/... +``` + +기대 결과: 빌드 및 테스트 통과 + +--- + +## 수정 파일 요약 + +| 파일 | 항목 | 변경 유형 | +|------|------|-----------| +| `go.mod` | REFACTOR-1 | 수정 | +| `proto/gen/iop/*.pb.go` | REFACTOR-1 | 신규(생성) | +| `apps/node/internal/transport/parser.go` | REFACTOR-2 | 신규 | +| `apps/node/internal/transport/session.go` | REFACTOR-2 | 신규 | +| `apps/node/internal/transport/client.go` | REFACTOR-2 | 신규 | +| `apps/node/internal/node/node.go` | REFACTOR-3 | 수정 | +| `apps/node/internal/bootstrap/module.go` | REFACTOR-4 | 수정 | +| `packages/config/config.go` | REFACTOR-5 | 수정 | +| `apps/edge/internal/node/registry.go` | REFACTOR-5 | 신규 | +| `apps/edge/internal/transport/server.go` | REFACTOR-5 | 신규 | +| `apps/edge/internal/bootstrap/module.go` | REFACTOR-5 | 신규 | +| `apps/edge/cmd/iop-edge/main.go` | REFACTOR-5 | 수정 | +| `configs/edge.yaml` | REFACTOR-5 | 수정 | + +--- + +## 최종 검증 + +터미널 두 개가 필요하다. + +**터미널 A — edge 서버 실행:** + +```bash +PATH=/config/go-sdk/bin:$PATH go build -o bin/iop-edge ./apps/edge/cmd/iop-edge +./bin/iop-edge serve --config configs/edge.yaml +``` + +기대 결과: +``` +{"level":"info",...,"msg":"edge listening for nodes"} +``` + +**터미널 B — node 실행 (edge에 연결):** + +```bash +PATH=/config/go-sdk/bin:$PATH go build -o bin/iop-node ./apps/node/cmd/iop-node +./bin/iop-node serve --config configs/node.yaml +``` + +기대 결과 — node 로그: +``` +{"level":"info",...,"msg":"connected to edge","addr":"localhost:9090","node_id":"node-001"} +``` + +기대 결과 — edge 로그: +``` +{"level":"info",...,"msg":"node connection established"} +{"level":"info",...,"msg":"node registered","node_id":"node-001","adapters":1} +``` + +**전체 빌드 및 테스트:** + +```bash +PATH=/config/go-sdk/bin:$PATH go build ./... +PATH=/config/go-sdk/bin:$PATH go test ./apps/edge/internal/node/... +``` diff --git a/agent-task/edge_node_arch/plan_1.log b/agent-task/edge_node_arch/plan_1.log new file mode 100644 index 0000000..f62048f --- /dev/null +++ b/agent-task/edge_node_arch/plan_1.log @@ -0,0 +1,79 @@ + + +# Plan - REVIEW_REFACTOR + +## 개요 + +date=2026-05-02 +task=edge_node_arch, plan=1, tag=REVIEW_REFACTOR +parent=REFACTOR (code_review_0.log) + +## 배경 + +REFACTOR 코드리뷰에서 발견된 Suggested 이슈 수정. + +--- + +## [REVIEW_REFACTOR-1] onNodeConnected disconnect 리스너 순서 수정 + +### 문제 + +`apps/edge/internal/transport/server.go`의 `onNodeConnected` 고루틴에서 +`s.registry.Register(entry)` 이후에 `client.AddDisconnectListener`가 등록됨. +두 호출 사이에 node가 disconnect되면 이벤트가 리스너 등록 전에 발생해 +registry에 stale entry가 영구 잔존할 수 있음. + +### 해결 + +`AddDisconnectListener` 블록을 `registry.Register` 이전으로 이동. + +Before (`server.go:101-110`): +```go +s.registry.Register(entry) +s.logger.Info("node registered", + zap.String("node_id", entry.NodeID), + zap.Int("adapters", len(entry.Adapters)), +) + +client.AddDisconnectListener(func(_ *toki.TcpClient) { + s.registry.Unregister(entry.NodeID) + s.logger.Info("node unregistered", zap.String("node_id", entry.NodeID)) +}) +``` + +After: +```go +client.AddDisconnectListener(func(_ *toki.TcpClient) { + s.registry.Unregister(entry.NodeID) + s.logger.Info("node unregistered", zap.String("node_id", entry.NodeID)) +}) + +s.registry.Register(entry) +s.logger.Info("node registered", + zap.String("node_id", entry.NodeID), + zap.Int("adapters", len(entry.Adapters)), +) +``` + +### 체크리스트 + +- [x] `server.go`에서 `AddDisconnectListener` 블록이 `registry.Register` 이전에 위치 + +### 테스트 결정 + +기존 registry_test.go는 Register/Unregister 순서를 직접 테스트하지 않으므로 코드 변경만으로 충분. + +### 중간 검증 + +``` +$ PATH=/config/go-sdk/go/bin:/config/go-sdk/bin:$PATH go build ./apps/edge/... +``` + +--- + +## 최종 검증 + +``` +$ PATH=/config/go-sdk/go/bin:/config/go-sdk/bin:$PATH go build ./... +$ PATH=/config/go-sdk/go/bin:/config/go-sdk/bin:$PATH go test ./... +``` diff --git a/agent-task/edge_node_unit_tests/CODE_REVIEW.md b/agent-task/edge_node_unit_tests/CODE_REVIEW.md new file mode 100644 index 0000000..9cb52d2 --- /dev/null +++ b/agent-task/edge_node_unit_tests/CODE_REVIEW.md @@ -0,0 +1,122 @@ + + +# Code Review Reference - TEST + +## 개요 + +date=2026-05-02 +task=edge_node_unit_tests, plan=0, tag=TEST + +## 이 파일을 읽는 리뷰 에이전트에게 + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료 후 반드시 아래 순서로 아카이브하세요. + +1. `CODE_REVIEW.md` → `code_review_0.log` (N = 기존 code_review_*.log 수) +2. `PLAN.md` → `plan_0.log` (M = 기존 plan_*.log 수) +3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 `PLAN.md` + `CODE_REVIEW.md` 스텁 작성. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [TEST-1] apps/node/internal/transport/parser_test.go | [x] | +| [TEST-2] apps/node/internal/transport/session_test.go | [x] | +| [TEST-3] apps/node/internal/node/node_test.go | [x] | + +## 계획 대비 변경 사항 + +- 계획대로 `apps/node/internal/transport/parser_test.go`, `apps/node/internal/transport/session_test.go`, `apps/node/internal/node/node_test.go`를 신규 작성했다. +- `parser_test.go`의 RunRequest 검증은 계획의 RunId·Adapter에 더해 Model도 함께 확인했다. +- `node_test.go`의 `fixedRouter`는 요청의 Model·Workspace·Policy·Input·TimeoutSec·Metadata를 `ExecutionSpec`으로 전달하도록 작성했다. + +## 주요 설계 결정 + +- `parser_test.go`는 `package transport`를 사용해 unexported `nodeParserMap()`을 직접 검증했다. +- `session_test.go`는 `&transport.Session{}` 제로값을 사용해 TcpClient 없이 cancel 등록·호출·해제 흐름만 검증했다. +- `node_test.go`는 `sink.Emit`을 호출하지 않는 `noEmitAdapter`와 in-memory SQLite store를 사용해 외부 연결 없이 Node 핵심 경로를 검증했다. + +## 리뷰어를 위한 체크포인트 + +- `parser_test.go`가 `package transport`로 선언되어 `nodeParserMap()` 접근 가능한지 +- 3개 메시지 타입(RunRequest, CancelRequest, CapabilityRequest) 파서 round-trip 검증 여부 +- `session_test.go`에서 `&transport.Session{}` 제로값 사용 (TcpClient nil) 확인 +- `TestSession_RegisterCancel_CancelRun`: cancel 호출 여부 확인 +- `TestSession_DeregisterCancel`: 해제 후 미호출 확인 +- `node_test.go`의 `noEmitAdapter`가 `sink.Emit`을 호출하지 않는지 확인 (TcpClient nil 안전) +- `makeNode`에서 `:memory:` SQLite 사용 여부 +- `TestOnCapabilityRequest`: NodeId·AdapterName·Models 필드 검증 +- `TestOnRunRequest_RouterError`: 에러 메시지에 `"node: resolve:"` 접두사 포함 +- `TestOnRunRequest_AdapterNotFound`: 에러 메시지에 `"not found after routing"` 포함 +- `TestOnRunRequest_Success`: nil 반환 확인 +- `TestOnCancel_CallsCancelFn`: cancel 함수 호출 확인 +- `go test ./apps/node/...` 전체 pass + +## 검증 결과 + +_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._ + +### TEST-1 중간 검증 +``` +$ PATH=/config/go-sdk/go/bin:/config/go-sdk/bin:$PATH go test ./apps/node/internal/transport/... +ok iop/apps/node/internal/transport 0.002s +``` + +### TEST-2 중간 검증 +``` +$ PATH=/config/go-sdk/go/bin:/config/go-sdk/bin:$PATH go test ./apps/node/internal/transport/... +ok iop/apps/node/internal/transport 0.002s +``` + +### TEST-3 중간 검증 +``` +$ PATH=/config/go-sdk/go/bin:/config/go-sdk/bin:$PATH go test ./apps/node/internal/node/... +ok iop/apps/node/internal/node 0.004s +``` + +### 최종 검증 +``` +$ PATH=/config/go-sdk/go/bin:/config/go-sdk/bin:$PATH go test ./apps/node/... +? iop/apps/node/cmd/iop-node [no test files] +? iop/apps/node/internal/adapters [no test files] +? iop/apps/node/internal/adapters/cli [no test files] +? iop/apps/node/internal/adapters/mock [no test files] +? iop/apps/node/internal/adapters/ollama [no test files] +? iop/apps/node/internal/adapters/vllm [no test files] +? iop/apps/node/internal/bootstrap [no test files] +ok iop/apps/node/internal/node (cached) +? iop/apps/node/internal/router [no test files] +? iop/apps/node/internal/runtime [no test files] +? iop/apps/node/internal/store [no test files] +ok iop/apps/node/internal/transport (cached) + +$ PATH=/config/go-sdk/go/bin:/config/go-sdk/bin:$PATH go test ./... +? iop/apps/control-plane/cmd/iop-control-plane [no test files] +? iop/apps/edge/cmd/iop-edge [no test files] +? iop/apps/edge/internal/bootstrap [no test files] +ok iop/apps/edge/internal/node (cached) +? iop/apps/edge/internal/transport [no test files] +? iop/apps/node/cmd/iop-node [no test files] +? iop/apps/node/internal/adapters [no test files] +? iop/apps/node/internal/adapters/cli [no test files] +? iop/apps/node/internal/adapters/mock [no test files] +? iop/apps/node/internal/adapters/ollama [no test files] +? iop/apps/node/internal/adapters/vllm [no test files] +? iop/apps/node/internal/bootstrap [no test files] +ok iop/apps/node/internal/node (cached) +? iop/apps/node/internal/router [no test files] +? iop/apps/node/internal/runtime [no test files] +? iop/apps/node/internal/store [no test files] +ok iop/apps/node/internal/transport (cached) +? iop/apps/worker/cmd/iop-worker [no test files] +? iop/packages/auth [no test files] +? iop/packages/config [no test files] +? iop/packages/jobs [no test files] +? iop/packages/metadata [no test files] +? iop/packages/observability [no test files] +? iop/packages/policy [no test files] +? iop/packages/version [no test files] +? iop/proto/gen/iop [no test files] +``` diff --git a/agent-task/edge_node_unit_tests/PLAN.md b/agent-task/edge_node_unit_tests/PLAN.md new file mode 100644 index 0000000..9b59e6e --- /dev/null +++ b/agent-task/edge_node_unit_tests/PLAN.md @@ -0,0 +1,268 @@ + + +# Plan - TEST + +## 이 파일을 읽는 구현 에이전트에게 + +각 항목의 체크리스트를 완료하고, 중간 검증과 최종 검증 명령을 실행한 뒤 출력을 `CODE_REVIEW.md`의 검증 결과 섹션에 붙여 넣는다. `CODE_REVIEW.md`의 모든 섹션(계획 대비 변경 사항, 주요 설계 결정, 검증 결과)을 실제 내용으로 채운다. + +## 배경 + +`edge_node_arch` 작업으로 구현된 transport 계층과 node 핵심 로직에 유닛 테스트가 없다. `apps/edge/internal/node/registry_test.go` 3개만 존재한다. parser의 역직렬화 정확성, Session의 cancel 관리, Node의 핵심 메서드(OnCapabilityRequest·OnRunRequest·OnCancel) 경로가 검증되지 않은 상태다. + +## 의존 관계 및 구현 순서 + +TEST-1 → TEST-2 → TEST-3 순으로 작성한다. TEST-3은 `*transport.Session` 제로값을 사용하므로 transport 패키지 변경이 없다면 순서 무관하게 컴파일된다. + +--- + +## [TEST-1] apps/node/internal/transport/parser_test.go + +### 문제 + +`parser.go`의 `nodeParserMap()`(line 10)이 3개의 메시지 타입(RunRequest, CancelRequest, CapabilityRequest)을 파싱하는데 역직렬화 정확성을 검증하는 테스트가 없다. + +### 해결 방법 + +각 파서 함수를 직접 호출해 proto.Marshal → parser → 필드 비교로 round-trip을 검증한다. `nodeParserMap()`은 unexported이므로 `package transport`(동일 패키지) 테스트로 작성한다. + +```go +// apps/node/internal/transport/parser_test.go +package transport + +import ( + "testing" + + "google.golang.org/protobuf/proto" + + iop "iop/proto/gen/iop" +) + +func TestNodeParserMap_RunRequest(t *testing.T) { + m := nodeParserMap() + original := &iop.RunRequest{RunId: "r1", Adapter: "mock", Model: "v1"} + b, _ := proto.Marshal(original) + + key := toki.TypeNameOf(original) + parsed, err := m[key](b) + if err != nil { + t.Fatalf("parse error: %v", err) + } + got := parsed.(*iop.RunRequest) + if got.GetRunId() != "r1" || got.GetAdapter() != "mock" { + t.Errorf("unexpected: %+v", got) + } +} +``` + +동일 패턴으로 CancelRequest, CapabilityRequest도 작성. + +### 수정 파일 및 체크리스트 + +- [ ] `apps/node/internal/transport/parser_test.go` 신규 생성 + - [ ] `package transport` 선언 + - [ ] `TestNodeParserMap_RunRequest`: RunId·Adapter 필드 검증 + - [ ] `TestNodeParserMap_CancelRequest`: RunId 필드 검증 + - [ ] `TestNodeParserMap_CapabilityRequest`: 역직렬화 오류 없음 검증 + +### 테스트 작성 + +신규 작성. parser는 새 공개 API가 아니지만 proto 역직렬화 로직의 정확성을 보장하는 회귀 방지 테스트로 필요하다. + +### 중간 검증 + +``` +$ PATH=/config/go-sdk/go/bin:/config/go-sdk/bin:$PATH go test ./apps/node/internal/transport/... +``` + +--- + +## [TEST-2] apps/node/internal/transport/session_test.go + +### 문제 + +`session.go`의 cancel 관리 메서드 `RegisterCancel`(line 78), `CancelRun`(line 88), `DeregisterCancel`(line 83)에 테스트가 없다. `*transport.Session` 제로값(`&Session{}`)은 `cancelFns sync.Map`이 유효한 제로값이므로 실제 TcpClient 없이 테스트 가능하다. + +### 해결 방법 + +`&transport.Session{}`을 직접 생성해 cancel 함수 등록 → 호출 → 해제 흐름을 검증한다. `package transport_test`로 작성한다. + +```go +// apps/node/internal/transport/session_test.go +package transport_test + +import ( + "testing" + + "iop/apps/node/internal/transport" +) + +func TestSession_RegisterCancel_CancelRun(t *testing.T) { + sess := &transport.Session{} + called := false + sess.RegisterCancel("run-1", func() { called = true }) + sess.CancelRun("run-1") + if !called { + t.Fatal("cancel function was not called") + } +} + +func TestSession_DeregisterCancel(t *testing.T) { + sess := &transport.Session{} + called := false + sess.RegisterCancel("run-1", func() { called = true }) + sess.DeregisterCancel("run-1") + sess.CancelRun("run-1") + if called { + t.Fatal("cancel function should not have been called after deregister") + } +} + +func TestSession_CancelRun_UnknownID(t *testing.T) { + sess := &transport.Session{} + // must not panic + sess.CancelRun("nonexistent") +} +``` + +### 수정 파일 및 체크리스트 + +- [ ] `apps/node/internal/transport/session_test.go` 신규 생성 + - [ ] `package transport_test` 선언 + - [ ] `TestSession_RegisterCancel_CancelRun`: cancel 함수 호출 확인 + - [ ] `TestSession_DeregisterCancel`: 해제 후 호출 안됨 확인 + - [ ] `TestSession_CancelRun_UnknownID`: panic 없음 확인 + +### 테스트 작성 + +신규 작성. cancel 관리 로직은 `OnRunRequest`의 timeout 경로에서 사용되며, race condition 수정(edge_node_arch REVIEW_REFACTOR)과 유사한 동시성 로직이다. 회귀 방지를 위해 필요하다. + +### 중간 검증 + +``` +$ PATH=/config/go-sdk/go/bin:/config/go-sdk/bin:$PATH go test ./apps/node/internal/transport/... +``` + +--- + +## [TEST-3] apps/node/internal/node/node_test.go + +### 문제 + +`node.go`의 `OnCapabilityRequest`(line 108), `OnRunRequest`(line 48), `OnCancel`(line 126)에 테스트가 없다. + +**의존성 처리 전략:** + +| 의존성 | 타입 | 처리 방법 | +|--------|------|-----------| +| `runtime.Router` | interface | 테스트 파일 내 인라인 mock | +| `*adapters.Registry` | struct | 직접 생성 후 no-emit 어댑터 등록 | +| `*store.Store` | struct (SQLite) | `store.New(":memory:", zap.NewNop())` | +| `*transport.Session` | struct | `&transport.Session{}` 제로값 | + +`OnRunRequest`에서 `sink.Emit` → `sess.Send` → `client.Send`로 이어지는 경로를 피하기 위해 `no-emit` 테스트 어댑터를 사용한다. 이 어댑터는 `sink.Emit`을 호출하지 않으므로 TcpClient가 nil이어도 안전하다. + +### 해결 방법 + +```go +// apps/node/internal/node/node_test.go +package node_test + +import ( + "context" + "errors" + "testing" + + "go.uber.org/zap" + + "iop/apps/node/internal/adapters" + "iop/apps/node/internal/node" + "iop/apps/node/internal/runtime" + "iop/apps/node/internal/store" + "iop/apps/node/internal/transport" + "iop/packages/config" + iop "iop/proto/gen/iop" +) + +// noEmitAdapter는 sink.Emit을 호출하지 않아 *transport.Session 제로값과 함께 사용 가능하다. +type noEmitAdapter struct{} + +func (a *noEmitAdapter) Name() string { return "test" } +func (a *noEmitAdapter) Capabilities(_ context.Context) (runtime.Capabilities, error) { + return runtime.Capabilities{AdapterName: "test", Models: []string{"v1"}, MaxConcurrency: 1}, nil +} +func (a *noEmitAdapter) Execute(_ context.Context, _ runtime.ExecutionSpec, _ runtime.EventSink) error { + return nil +} + +type fixedRouter struct{ adapterName string } + +func (r *fixedRouter) Resolve(_ context.Context, req runtime.RunRequest) (runtime.ExecutionSpec, error) { + return runtime.ExecutionSpec{RunID: req.RunID, Adapter: r.adapterName}, nil +} + +type errorRouter struct{ err error } + +func (r *errorRouter) Resolve(_ context.Context, _ runtime.RunRequest) (runtime.ExecutionSpec, error) { + return runtime.ExecutionSpec{}, r.err +} + +func makeNode(t *testing.T, rtr runtime.Router, reg *adapters.Registry) *node.Node { + t.Helper() + st, err := store.New(":memory:", zap.NewNop()) + if err != nil { + t.Fatalf("store: %v", err) + } + t.Cleanup(func() { st.Close() }) + cfg := &config.NodeConfig{Node: config.NodeInfo{ID: "test-node"}} + return node.New(cfg, rtr, reg, st, zap.NewNop()) +} +``` + +**TestOnCapabilityRequest**: mock adapter 1개 등록 후 응답의 nodeID·어댑터 이름·모델 검증 +**TestOnRunRequest_RouterError**: `errorRouter` 사용, 반환 에러가 "node: resolve:" 접두사를 포함하는지 검증 +**TestOnRunRequest_AdapterNotFound**: `fixedRouter`가 "missing"을 반환하고 registry에 없는 경우 에러 검증 +**TestOnRunRequest_Success**: `fixedRouter` + `noEmitAdapter` + `&transport.Session{}`, nil 반환 검증 +**TestOnCancel_CallsCancelFn**: `sess.RegisterCancel` 등록 후 `OnCancel` 호출 → cancel 함수 실행 확인 + +### 수정 파일 및 체크리스트 + +- [ ] `apps/node/internal/node/node_test.go` 신규 생성 + - [ ] `package node_test` 선언 + - [ ] `noEmitAdapter` 인라인 정의 (Emit 미호출) + - [ ] `fixedRouter`, `errorRouter` 인라인 정의 + - [ ] `makeNode` 헬퍼 (in-memory store 사용) + - [ ] `TestOnCapabilityRequest`: NodeId·AdapterName·Models 검증 + - [ ] `TestOnRunRequest_RouterError`: 에러 접두사 `"node: resolve:"` 검증 + - [ ] `TestOnRunRequest_AdapterNotFound`: 에러 포함 `"not found after routing"` 검증 + - [ ] `TestOnRunRequest_Success`: nil 반환 검증 + - [ ] `TestOnCancel_CallsCancelFn`: cancel 함수 호출 확인 + +### 테스트 작성 + +신규 작성. `OnRunRequest`의 error path는 bug fix 성격의 회귀 테스트이며, `OnCapabilityRequest`·`OnCancel`은 새 공개 API의 기본 경로 테스트다. in-memory SQLite를 사용해 외부 의존성 없이 실행 가능하다. + +### 중간 검증 + +``` +$ PATH=/config/go-sdk/go/bin:/config/go-sdk/bin:$PATH go test ./apps/node/internal/node/... +``` + +--- + +## 수정 파일 요약 + +| 파일 | 항목 | +|------|------| +| `apps/node/internal/transport/parser_test.go` (신규) | TEST-1 | +| `apps/node/internal/transport/session_test.go` (신규) | TEST-2 | +| `apps/node/internal/node/node_test.go` (신규) | TEST-3 | + +## 최종 검증 + +``` +$ PATH=/config/go-sdk/go/bin:/config/go-sdk/bin:$PATH go test ./apps/node/... +$ PATH=/config/go-sdk/go/bin:/config/go-sdk/bin:$PATH go test ./... +``` + +모든 테스트 pass, 새로 추가된 3개 파일 외 변경 없음. diff --git a/agent-task/node_testclient/CODE_REVIEW.md b/agent-task/node_testclient/CODE_REVIEW.md deleted file mode 100644 index a40cbac..0000000 --- a/agent-task/node_testclient/CODE_REVIEW.md +++ /dev/null @@ -1,75 +0,0 @@ - - -# Code Review Reference - TEST - -## 개요 - -date=2026-05-02 -task=node_testclient, plan=0, tag=TEST - -## 이 파일을 읽는 리뷰 에이전트에게 - -각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. -리뷰 완료 후 반드시 아래 순서로 아카이브하세요. - -1. `CODE_REVIEW.md` → `code_review_0.log` -2. `PLAN.md` → `plan_0.log` -3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 `PLAN.md` + `CODE_REVIEW.md` 스텁 작성. - ---- - -## 구현 항목별 완료 여부 - -| 항목 | 완료 여부 | -|------|---------| -| [TEST-1] iop-node-testclient 커맨드 생성 | [ ] | -| [TEST-2] Makefile에 testclient 빌드 타겟 추가 | [ ] | - -## 계획 대비 변경 사항 - -_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ - -## 주요 설계 결정 - -_구현 에이전트가 주요 설계 결정 사항을 기록한다._ - -## 리뷰어를 위한 체크포인트 - -- `sendMsg` / `recvEnvelope` 의 프레임 포맷이 서버 `transport/frame.go` 와 동일한 `4-byte big-endian length | JSON(Envelope)` 인지 확인 -- `internal` 경로 패키지를 import하지 않고 `iop/packages/protocol` 만 사용하는지 확인 -- `-caps` 플래그 없이 실행해도 RunRequest → RunEvent 흐름이 정상 동작하는지 확인 -- 60s 타임아웃 이전에 complete/error 이벤트 수신 시 정상 종료하는지 확인 -- Makefile `build` 타겟에 `iop-node-testclient` 라인이 추가되었는지 확인 - -## 검증 결과 - -_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._ - -### TEST-1 중간 검증 -``` -$ go build -o bin/iop-node-testclient ./apps/node/cmd/iop-node-testclient -(output) -``` - -### TEST-2 중간 검증 -``` -$ make build -$ ls -la bin/ -(output) -``` - -### 최종 검증 -``` -# 터미널 A -$ ./bin/iop-node serve --config configs/node.yaml -(output) - -# 터미널 B -$ ./bin/iop-node-testclient -$ ./bin/iop-node-testclient -caps -$ ./bin/iop-node-testclient -prompt "안녕하세요 IOP!" -(output) - -$ go build ./... -(output) -``` diff --git a/agent-task/node_testclient/PLAN.md b/agent-task/node_testclient/PLAN.md deleted file mode 100644 index a136772..0000000 --- a/agent-task/node_testclient/PLAN.md +++ /dev/null @@ -1,345 +0,0 @@ - - -# iop-node-testclient — 통합 테스트 클라이언트 - -## 이 파일을 읽는 구현 에이전트에게 - -각 체크리스트 항목을 완료한 뒤 `[x]`로 표시하세요. -중간 검증 명령을 실제로 실행하고 출력을 `CODE_REVIEW.md` 검증 결과 섹션에 붙여 넣으세요. -계획과 다르게 구현한 부분은 반드시 `계획 대비 변경 사항`에 기록하세요. - -## 배경 - -`iop-node serve` 가 실행 중일 때 실제 TCP 연결로 RunRequest를 전송하고 -스트리밍 RunEvent를 수신하는 클라이언트가 없다. -unit test 만으로는 transport 계층의 end-to-end 동작을 확인할 수 없으므로, -수동 및 자동 통합 검증에 쓸 수 있는 `iop-node-testclient` 커맨드를 추가한다. - ---- - -### [TEST-1] iop-node-testclient 커맨드 생성 - -**문제** - -`apps/node/cmd/` 아래에 테스트 클라이언트가 없다. -현재 transport 계층 -([apps/node/internal/transport/frame.go](apps/node/internal/transport/frame.go), -[apps/node/internal/transport/codec.go](apps/node/internal/transport/codec.go), -[apps/node/internal/transport/session.go:128-161](apps/node/internal/transport/session.go)) -의 wire format은 `4-byte big-endian length | JSON(Envelope)` 이다. -클라이언트도 동일 프레이밍을 구현해야 한다. - -**해결 방법** - -`apps/node/cmd/iop-node-testclient/main.go` 를 신규 생성한다. -이미 `iop/packages/protocol` 에 모든 wire 타입이 정의되어 있으므로 -프레임 읽기/쓰기는 `encoding/binary` + `encoding/json` 으로 직접 구현한다. -(transport 내부 패키지 unexported 함수 재사용 불가 — `internal` 경로) - -커맨드 흐름: - -``` -1. -addr 플래그로 node 주소 수신 (기본 localhost:9090) -2. TCP Dial -3. (선택) -caps 플래그 시 CapabilityRequest 전송 → 응답 출력 -4. RunRequest 전송 (adapter=mock, model=mock-echo, prompt="hello iop!") -5. RunEvent 스트림 수신 → 각 이벤트 출력 -6. complete 또는 error 이벤트 수신 시 종료 -``` - -전체 구현 코드: - -```go -// apps/node/cmd/iop-node-testclient/main.go -package main - -import ( - "context" - "encoding/binary" - "encoding/json" - "errors" - "flag" - "fmt" - "io" - "net" - "os" - "time" - - "iop/packages/protocol" -) - -func main() { - addr := flag.String("addr", "localhost:9090", "iop-node address") - caps := flag.Bool("caps", false, "send CapabilityRequest before RunRequest") - prompt := flag.String("prompt", "hello iop!", "prompt to send") - flag.Parse() - - if err := run(*addr, *caps, *prompt); err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n", err) - os.Exit(1) - } -} - -func run(addr string, sendCaps bool, prompt string) error { - conn, err := net.DialTimeout("tcp", addr, 5*time.Second) - if err != nil { - return fmt.Errorf("dial %s: %w", addr, err) - } - defer conn.Close() - fmt.Printf("connected → %s\n", addr) - - ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) - defer cancel() - - if sendCaps { - if err := sendMsg(conn, protocol.TypeCapabilityRequest, "", protocol.CapabilityRequest{}); err != nil { - return fmt.Errorf("send capability request: %w", err) - } - fmt.Println("→ CapabilityRequest sent") - env, err := recvEnvelope(conn) - if err != nil { - return fmt.Errorf("recv capability response: %w", err) - } - if env.Type == protocol.TypeCapabilityResponse { - var resp protocol.CapabilityResponse - _ = json.Unmarshal(env.Payload, &resp) - fmt.Printf("← CapabilityResponse node_id=%q adapters=%d\n", resp.NodeID, len(resp.Adapters)) - for _, a := range resp.Adapters { - fmt.Printf(" adapter=%q models=%v max_concurrency=%d\n", a.Name, a.Models, a.MaxConcurrency) - } - } - } - - runID := fmt.Sprintf("testclient-%d", time.Now().UnixNano()) - req := protocol.RunRequest{ - RunID: runID, - Adapter: "mock", - Model: "mock-echo", - Input: map[string]any{"prompt": prompt}, - } - if err := sendMsg(conn, protocol.TypeRunRequest, runID, req); err != nil { - return fmt.Errorf("send run request: %w", err) - } - fmt.Printf("→ RunRequest sent (run_id=%s)\n", runID) - - return receiveEvents(ctx, conn) -} - -func receiveEvents(ctx context.Context, conn net.Conn) error { - for { - if err := ctx.Err(); err != nil { - return fmt.Errorf("context: %w", err) - } - - env, err := recvEnvelope(conn) - if err != nil { - if errors.Is(err, io.EOF) { - fmt.Println("connection closed by server") - return nil - } - return fmt.Errorf("recv: %w", err) - } - - switch env.Type { - case protocol.TypeRunEvent: - var e protocol.RunEvent - _ = json.Unmarshal(env.Payload, &e) - switch e.Type { - case "start": - fmt.Printf("← [start]\n") - case "delta": - fmt.Printf("%s", e.Delta) - case "complete": - fmt.Printf("\n← [complete] message=%q input_tokens=%d output_tokens=%d\n", - e.Message, - func() int { - if e.Usage != nil { - return e.Usage.InputTokens - } - return 0 - }(), - func() int { - if e.Usage != nil { - return e.Usage.OutputTokens - } - return 0 - }(), - ) - return nil - case "error": - return fmt.Errorf("run error: %s", e.Error) - } - - case protocol.TypeHeartbeat: - // 무시 - - case protocol.TypeError: - var e protocol.ErrorMsg - _ = json.Unmarshal(env.Payload, &e) - return fmt.Errorf("server error %s: %s", e.Code, e.Message) - } - } -} - -// sendMsg wraps payload in an Envelope and writes a length-prefixed frame. -func sendMsg(conn net.Conn, msgType protocol.MessageType, runID string, payload any) error { - data, err := json.Marshal(payload) - if err != nil { - return err - } - env := protocol.Envelope{ - ProtocolVersion: protocol.ProtocolVersion, - RequestID: fmt.Sprintf("req-%d", time.Now().UnixNano()), - RunID: runID, - Type: msgType, - Payload: data, - } - envBytes, err := json.Marshal(env) - if err != nil { - return err - } - hdr := make([]byte, 4) - binary.BigEndian.PutUint32(hdr, uint32(len(envBytes))) - if _, err := conn.Write(hdr); err != nil { - return err - } - _, err = conn.Write(envBytes) - return err -} - -// recvEnvelope reads one length-prefixed frame and decodes the Envelope. -func recvEnvelope(conn net.Conn) (*protocol.Envelope, error) { - hdr := make([]byte, 4) - if _, err := io.ReadFull(conn, hdr); err != nil { - return nil, err - } - length := binary.BigEndian.Uint32(hdr) - if length == 0 { - return nil, fmt.Errorf("received empty frame") - } - buf := make([]byte, length) - if _, err := io.ReadFull(conn, buf); err != nil { - return nil, err - } - var env protocol.Envelope - return &env, json.Unmarshal(buf, &env) -} -``` - -**수정 파일 및 체크리스트** - -- [ ] `apps/node/cmd/iop-node-testclient/main.go` 신규 생성 - - [ ] `-addr` 플래그 (기본 `localhost:9090`) - - [ ] `-caps` 플래그 (CapabilityRequest 선택 전송) - - [ ] `-prompt` 플래그 (기본 `"hello iop!"`) - - [ ] TCP Dial + 타임아웃 (5s) - - [ ] CapabilityRequest 전송 및 응답 출력 (`-caps` 시) - - [ ] RunRequest 전송 (mock adapter) - - [ ] RunEvent 스트림 수신 루프 - - [ ] complete / error 시 정상 종료 - - [ ] 60s 전체 타임아웃 - -**테스트 작성** - -통합 테스트 클라이언트 자체는 수동/E2E 검증 도구이므로 별도 `_test.go` 파일은 작성하지 않는다. -단, `sendMsg` / `recvEnvelope` 함수는 `frame.go` + `codec.go` 유닛 테스트(node_unit_tests 태스크)로 간접 검증된다. - -**중간 검증** - -빌드 가능 여부 확인: - -```bash -go build -o bin/iop-node-testclient ./apps/node/cmd/iop-node-testclient -``` - -기대 결과: 빌드 에러 없음 - ---- - -### [TEST-2] Makefile에 testclient 빌드 타겟 추가 - -**문제** - -[Makefile:6-9](Makefile)의 `build` 타겟에 testclient가 없다. - -**해결 방법** - -```makefile -# Before (Makefile:7) - go build $(GOFLAGS) -o bin/iop-node ./apps/node/cmd/iop-node - -# After - go build $(GOFLAGS) -o bin/iop-node ./apps/node/cmd/iop-node - go build $(GOFLAGS) -o bin/iop-node-testclient ./apps/node/cmd/iop-node-testclient -``` - -**수정 파일 및 체크리스트** - -- [ ] `Makefile` — `build` 타겟에 `iop-node-testclient` 라인 추가 - -**테스트 작성** - -Makefile 변경이므로 테스트 파일 불필요. - -**중간 검증** - -```bash -make build -ls -la bin/ -``` - -기대 결과: `bin/iop-node-testclient` 파일 생성됨 - ---- - -## 수정 파일 요약 - -| 파일 | 항목 | -|------|------| -| `apps/node/cmd/iop-node-testclient/main.go` | TEST-1 | -| `Makefile` | TEST-2 | - -## 최종 검증 - -두 개의 터미널이 필요하다. - -**터미널 A — node 서버 실행:** - -```bash -go mod tidy -go build -o bin/iop-node ./apps/node/cmd/iop-node -./bin/iop-node serve --config configs/node.yaml -``` - -기대 결과: `transport listening addr=0.0.0.0:9090` 로그 출력 - -**터미널 B — testclient 실행:** - -```bash -go build -o bin/iop-node-testclient ./apps/node/cmd/iop-node-testclient - -# 기본 RunRequest -./bin/iop-node-testclient - -# CapabilityRequest 포함 -./bin/iop-node-testclient -caps - -# 커스텀 프롬프트 -./bin/iop-node-testclient -prompt "안녕하세요 IOP!" -``` - -기대 결과: - -``` -connected → localhost:9090 -→ RunRequest sent (run_id=testclient-...) -← [start] -echo: hello iop! -← [complete] message="mock execution complete" input_tokens=2 output_tokens=4 -``` - -**빌드 전체 확인:** - -```bash -go build ./... -``` diff --git a/agent-task/node_unit_tests/CODE_REVIEW.md b/agent-task/node_unit_tests/CODE_REVIEW.md deleted file mode 100644 index 0cb254e..0000000 --- a/agent-task/node_unit_tests/CODE_REVIEW.md +++ /dev/null @@ -1,82 +0,0 @@ - - -# Code Review Reference - TEST - -## 개요 - -date=2026-05-02 -task=node_unit_tests, plan=0, tag=TEST - -## 이 파일을 읽는 리뷰 에이전트에게 - -각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. -리뷰 완료 후 반드시 아래 순서로 아카이브하세요. - -1. `CODE_REVIEW.md` → `code_review_0.log` -2. `PLAN.md` → `plan_0.log` -3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 `PLAN.md` + `CODE_REVIEW.md` 스텁 작성. - ---- - -## 구현 항목별 완료 여부 - -| 항목 | 완료 여부 | -|------|---------| -| [TEST-1] transport/frame 유닛 테스트 | [ ] | -| [TEST-2] transport/codec 유닛 테스트 | [ ] | -| [TEST-3] mock adapter 유닛 테스트 | [ ] | -| [TEST-4] router 유닛 테스트 | [ ] | - -## 계획 대비 변경 사항 - -_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ - -## 주요 설계 결정 - -_구현 에이전트가 주요 설계 결정 사항을 기록한다._ - -## 리뷰어를 위한 체크포인트 - -- frame_test.go: `readFrame` / `writeFrame` 모두 `package transport` 화이트박스로 접근하는지 확인 -- codec_test.go: 잘못된 JSON 입력 시 nil 이 아닌 error 반환 여부 -- mock_test.go: 이벤트 순서가 start → delta+ → complete 이고 RunID가 모든 이벤트에 일관되게 전파되는지 확인 -- mock_test.go: cancel 테스트가 `context.Canceled` / `context.DeadlineExceeded` 외 에러는 실패로 처리하는지 확인 -- router_test.go: 빈 registry 케이스와 미등록 어댑터 케이스 모두 error 반환하는지 확인 - -## 검증 결과 - -_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._ - -### TEST-1 중간 검증 -``` -$ go test ./apps/node/internal/transport/... -run TestWriteReadFrame -v -$ go test ./apps/node/internal/transport/... -run TestReadFrame -v -(output) -``` - -### TEST-2 중간 검증 -``` -$ go test ./apps/node/internal/transport/... -run TestEncodeDecodeEnvelope -v -$ go test ./apps/node/internal/transport/... -run TestDecodeEnvelope -v -$ go test ./apps/node/internal/transport/... -run TestEncodeDecodePayload -v -(output) -``` - -### TEST-3 중간 검증 -``` -$ go test ./apps/node/internal/adapters/mock/... -v -(output) -``` - -### TEST-4 중간 검증 -``` -$ go test ./apps/node/internal/router/... -v -(output) -``` - -### 최종 검증 -``` -$ go test ./apps/node/internal/transport/... ./apps/node/internal/adapters/mock/... ./apps/node/internal/router/... -v -count=1 -$ go build ./... -(output) -``` diff --git a/agent-task/node_unit_tests/PLAN.md b/agent-task/node_unit_tests/PLAN.md deleted file mode 100644 index e052358..0000000 --- a/agent-task/node_unit_tests/PLAN.md +++ /dev/null @@ -1,481 +0,0 @@ - - -# Node 유닛 테스트 추가 - -## 이 파일을 읽는 구현 에이전트에게 - -각 체크리스트 항목을 완료한 뒤 `[x]`로 표시하세요. -중간 검증 명령을 실제로 실행하고 출력을 `CODE_REVIEW.md` 검증 결과 섹션에 붙여 넣으세요. -계획과 다르게 구현한 부분은 반드시 `계획 대비 변경 사항`에 기록하세요. - -## 배경 - -IOP 노드 스캐폴드가 완성되었으나 테스트 파일이 없다. -transport 레이어(frame/codec)와 핵심 컴포넌트(mock adapter, router)는 -외부 의존성 없이 순수하게 테스트 가능한 구조이므로 유닛 테스트를 추가해 -회귀 방지 기반을 만든다. - ---- - -### [TEST-1] transport/frame 유닛 테스트 - -**문제** - -[apps/node/internal/transport/frame.go:16-46](apps/node/internal/transport/frame.go)의 -`readFrame` / `writeFrame` 함수가 4-byte length-prefix 프레이밍을 올바르게 수행하는지 -검증하는 테스트가 없다. maxFrameSize 초과 케이스도 미검증. - -**해결 방법** - -`package transport` (화이트박스) 테스트 파일을 생성한다. -unexported 함수를 같은 패키지에서 직접 호출한다. - -```go -// apps/node/internal/transport/frame_test.go (신규) -package transport - -import ( - "bytes" - "testing" -) - -func TestWriteReadFrame_roundtrip(t *testing.T) { - payload := []byte(`{"type":"heartbeat","timestamp":1234}`) - var buf bytes.Buffer - if err := writeFrame(&buf, payload); err != nil { - t.Fatal(err) - } - got, err := readFrame(&buf) - if err != nil { - t.Fatal(err) - } - if !bytes.Equal(got, payload) { - t.Errorf("round-trip mismatch: got %q, want %q", got, payload) - } -} - -func TestWriteReadFrame_empty(t *testing.T) { - var buf bytes.Buffer - if err := writeFrame(&buf, []byte{}); err != nil { - t.Fatal(err) - } - got, err := readFrame(&buf) - if err != nil { - t.Fatal(err) - } - if len(got) != 0 { - t.Errorf("expected empty payload, got %d bytes", len(got)) - } -} - -func TestReadFrame_tooLarge(t *testing.T) { - // 헤더에 maxFrameSize+1 을 직접 기록 - var buf bytes.Buffer - hdr := make([]byte, 4) - // encoding/binary 없이 직접 big-endian 기록 - sz := uint32(maxFrameSize + 1) - hdr[0] = byte(sz >> 24) - hdr[1] = byte(sz >> 16) - hdr[2] = byte(sz >> 8) - hdr[3] = byte(sz) - buf.Write(hdr) - _, err := readFrame(&buf) - if err == nil { - t.Error("expected error for oversized frame, got nil") - } -} -``` - -**수정 파일 및 체크리스트** - -- [ ] `apps/node/internal/transport/frame_test.go` 신규 생성 - - [ ] `TestWriteReadFrame_roundtrip` 작성 - - [ ] `TestWriteReadFrame_empty` 작성 - - [ ] `TestReadFrame_tooLarge` 작성 - -**테스트 작성** - -이 항목 자체가 테스트 작성 작업이므로 별도 추가 테스트 없음. - -**중간 검증** - -```bash -go test ./apps/node/internal/transport/... -run TestWriteReadFrame -v -go test ./apps/node/internal/transport/... -run TestReadFrame -v -``` - -기대 결과: 3개 테스트 PASS - ---- - -### [TEST-2] transport/codec 유닛 테스트 - -**문제** - -[apps/node/internal/transport/codec.go:10-29](apps/node/internal/transport/codec.go)의 -`encodeEnvelope` / `decodeEnvelope` / `encodePayload` / `decodePayload` 함수에 테스트 없음. -JSON 인코딩 라운드트립과 잘못된 JSON 입력 시 에러 반환 여부가 미검증. - -**해결 방법** - -```go -// apps/node/internal/transport/codec_test.go (신규) -package transport - -import ( - "testing" - - "iop/packages/protocol" -) - -func TestEncodeDecodeEnvelope_roundtrip(t *testing.T) { - orig := &protocol.Envelope{ - ProtocolVersion: protocol.ProtocolVersion, - RequestID: "req-001", - RunID: "run-abc", - Type: protocol.TypeHeartbeat, - Payload: []byte(`{"timestamp":999}`), - } - data, err := encodeEnvelope(orig) - if err != nil { - t.Fatal(err) - } - got, err := decodeEnvelope(data) - if err != nil { - t.Fatal(err) - } - if got.RequestID != orig.RequestID { - t.Errorf("RequestID: got %q, want %q", got.RequestID, orig.RequestID) - } - if got.RunID != orig.RunID { - t.Errorf("RunID: got %q, want %q", got.RunID, orig.RunID) - } - if got.Type != orig.Type { - t.Errorf("Type: got %q, want %q", got.Type, orig.Type) - } -} - -func TestDecodeEnvelope_invalidJSON(t *testing.T) { - _, err := decodeEnvelope([]byte(`not-json`)) - if err == nil { - t.Error("expected error for invalid JSON, got nil") - } -} - -func TestEncodeDecodePayload_roundtrip(t *testing.T) { - type inner struct { - Foo string `json:"foo"` - Bar int `json:"bar"` - } - orig := inner{Foo: "hello", Bar: 42} - data, err := encodePayload(orig) - if err != nil { - t.Fatal(err) - } - var got inner - if err := decodePayload(data, &got); err != nil { - t.Fatal(err) - } - if got.Foo != orig.Foo || got.Bar != orig.Bar { - t.Errorf("payload mismatch: got %+v, want %+v", got, orig) - } -} -``` - -**수정 파일 및 체크리스트** - -- [ ] `apps/node/internal/transport/codec_test.go` 신규 생성 - - [ ] `TestEncodeDecodeEnvelope_roundtrip` 작성 - - [ ] `TestDecodeEnvelope_invalidJSON` 작성 - - [ ] `TestEncodeDecodePayload_roundtrip` 작성 - -**테스트 작성** - -이 항목 자체가 테스트 작성 작업. - -**중간 검증** - -```bash -go test ./apps/node/internal/transport/... -run TestEncodeDecodeEnvelope -v -go test ./apps/node/internal/transport/... -run TestDecodeEnvelope -v -go test ./apps/node/internal/transport/... -run TestEncodeDecodePayload -v -``` - -기대 결과: 3개 테스트 PASS - ---- - -### [TEST-3] mock adapter 유닛 테스트 - -**문제** - -[apps/node/internal/adapters/mock/mock.go:38-72](apps/node/internal/adapters/mock/mock.go)의 -`Execute` 함수가 start → delta+ → complete 순서로 이벤트를 방출하는지, -context cancel 시 조기 종료하는지 미검증. - -**해결 방법** - -`collectSink` 헬퍼 타입으로 이벤트를 수집한 뒤 순서와 타입을 검증한다. - -```go -// apps/node/internal/adapters/mock/mock_test.go (신규) -package mock - -import ( - "context" - "testing" - "time" - - "go.uber.org/zap" - - "iop/apps/node/internal/runtime" -) - -type collectSink struct { - events []runtime.RuntimeEvent -} - -func (c *collectSink) Emit(_ context.Context, e runtime.RuntimeEvent) error { - c.events = append(c.events, e) - return nil -} - -func TestMockExecute_sequence(t *testing.T) { - m := New(zap.NewNop()) - sink := &collectSink{} - spec := runtime.ExecutionSpec{ - RunID: "test-run-001", - Adapter: Name, - Input: map[string]any{"prompt": "hello world"}, - } - - if err := m.Execute(context.Background(), spec, sink); err != nil { - t.Fatalf("Execute: %v", err) - } - if len(sink.events) < 2 { - t.Fatalf("expected at least 2 events, got %d", len(sink.events)) - } - - // 첫 이벤트는 반드시 start - if sink.events[0].Type != runtime.EventTypeStart { - t.Errorf("first event: got %q, want %q", sink.events[0].Type, runtime.EventTypeStart) - } - // 마지막 이벤트는 반드시 complete - last := sink.events[len(sink.events)-1] - if last.Type != runtime.EventTypeComplete { - t.Errorf("last event: got %q, want %q", last.Type, runtime.EventTypeComplete) - } - // complete 이벤트에 Usage 포함 여부 - if last.Usage == nil { - t.Error("complete event: expected Usage, got nil") - } - // RunID 일관성 - for i, e := range sink.events { - if e.RunID != spec.RunID { - t.Errorf("event[%d].RunID: got %q, want %q", i, e.RunID, spec.RunID) - } - } -} - -func TestMockExecute_cancel(t *testing.T) { - m := New(zap.NewNop()) - sink := &collectSink{} - spec := runtime.ExecutionSpec{ - RunID: "test-cancel-001", - Adapter: Name, - Input: map[string]any{"prompt": "long long long long long long long long prompt"}, - } - - ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) - defer cancel() - - err := m.Execute(ctx, spec, sink) - // context cancel or deadline exceeded 는 정상 종료 케이스 - if err != nil && err != context.Canceled && err != context.DeadlineExceeded { - t.Errorf("unexpected error: %v", err) - } -} - -func TestMockCapabilities(t *testing.T) { - m := New(zap.NewNop()) - caps, err := m.Capabilities(context.Background()) - if err != nil { - t.Fatal(err) - } - if caps.AdapterName != Name { - t.Errorf("AdapterName: got %q, want %q", caps.AdapterName, Name) - } - if len(caps.Models) == 0 { - t.Error("expected at least one model") - } - if caps.MaxConcurrency <= 0 { - t.Error("expected positive MaxConcurrency") - } -} -``` - -**수정 파일 및 체크리스트** - -- [ ] `apps/node/internal/adapters/mock/mock_test.go` 신규 생성 - - [ ] `collectSink` 헬퍼 타입 작성 - - [ ] `TestMockExecute_sequence` 작성 - - [ ] `TestMockExecute_cancel` 작성 - - [ ] `TestMockCapabilities` 작성 - -**테스트 작성** - -이 항목 자체가 테스트 작성 작업. - -**중간 검증** - -```bash -go test ./apps/node/internal/adapters/mock/... -v -``` - -기대 결과: 3개 테스트 PASS (cancel 테스트는 50ms 내 종료) - ---- - -### [TEST-4] router 유닛 테스트 - -**문제** - -[apps/node/internal/router/router.go:24-47](apps/node/internal/router/router.go)의 -`Resolve` 함수가 adapter 이름 미지정 시 default 선택, 명시 시 해당 어댑터 선택, -미등록 어댑터 요청 시 에러 반환하는지 미검증. - -**해결 방법** - -```go -// apps/node/internal/router/router_test.go (신규) -package router - -import ( - "context" - "testing" - - "go.uber.org/zap" - - "iop/apps/node/internal/adapters" - "iop/apps/node/internal/adapters/mock" - "iop/apps/node/internal/runtime" -) - -func newTestRegistry() *adapters.Registry { - reg := adapters.NewRegistry() - reg.Register(mock.New(zap.NewNop())) - return reg -} - -func TestRouter_resolveDefault(t *testing.T) { - r := New(newTestRegistry(), zap.NewNop()) - spec, err := r.Resolve(context.Background(), runtime.RunRequest{ - RunID: "r-001", - // Adapter 비워두면 default 선택 - }) - if err != nil { - t.Fatalf("Resolve: %v", err) - } - if spec.Adapter != mock.Name { - t.Errorf("Adapter: got %q, want %q", spec.Adapter, mock.Name) - } - if spec.RunID != "r-001" { - t.Errorf("RunID: got %q, want %q", spec.RunID, "r-001") - } -} - -func TestRouter_resolveExplicit(t *testing.T) { - r := New(newTestRegistry(), zap.NewNop()) - spec, err := r.Resolve(context.Background(), runtime.RunRequest{ - RunID: "r-002", - Adapter: mock.Name, - Model: "mock-echo", - }) - if err != nil { - t.Fatalf("Resolve: %v", err) - } - if spec.Adapter != mock.Name { - t.Errorf("Adapter: got %q, want %q", spec.Adapter, mock.Name) - } - if spec.Model != "mock-echo" { - t.Errorf("Model: got %q, want %q", spec.Model, "mock-echo") - } -} - -func TestRouter_resolveUnknown(t *testing.T) { - r := New(newTestRegistry(), zap.NewNop()) - _, err := r.Resolve(context.Background(), runtime.RunRequest{ - RunID: "r-003", - Adapter: "nonexistent-adapter", - }) - if err == nil { - t.Error("expected error for unknown adapter, got nil") - } -} - -func TestRouter_emptyRegistry(t *testing.T) { - r := New(adapters.NewRegistry(), zap.NewNop()) - _, err := r.Resolve(context.Background(), runtime.RunRequest{RunID: "r-004"}) - if err == nil { - t.Error("expected error for empty registry, got nil") - } -} -``` - -**수정 파일 및 체크리스트** - -- [ ] `apps/node/internal/router/router_test.go` 신규 생성 - - [ ] `newTestRegistry` 헬퍼 작성 - - [ ] `TestRouter_resolveDefault` 작성 - - [ ] `TestRouter_resolveExplicit` 작성 - - [ ] `TestRouter_resolveUnknown` 작성 - - [ ] `TestRouter_emptyRegistry` 작성 - -**테스트 작성** - -이 항목 자체가 테스트 작성 작업. - -**중간 검증** - -```bash -go test ./apps/node/internal/router/... -v -``` - -기대 결과: 4개 테스트 PASS - ---- - -## 의존 관계 및 구현 순서 - -TEST-1, TEST-2 는 외부 iop 패키지 의존성이 없어 독립 실행 가능. -TEST-3 은 `go.uber.org/zap` 의존 → `go mod tidy` 완료 후 실행. -TEST-4 는 mock 패키지 의존 → TEST-3 파일 존재 후 실행. - -권장 순서: TEST-1 → TEST-2 → TEST-3 → TEST-4 - -## 수정 파일 요약 - -| 파일 | 항목 | -|------|------| -| `apps/node/internal/transport/frame_test.go` | TEST-1 | -| `apps/node/internal/transport/codec_test.go` | TEST-2 | -| `apps/node/internal/adapters/mock/mock_test.go` | TEST-3 | -| `apps/node/internal/router/router_test.go` | TEST-4 | - -## 최종 검증 - -```bash -# 의존성 먼저 해결 -go mod tidy - -# 전체 테스트 -go test ./apps/node/internal/transport/... \ - ./apps/node/internal/adapters/mock/... \ - ./apps/node/internal/router/... \ - -v -count=1 - -# 전체 빌드 깨짐 없음 확인 -go build ./... -``` - -기대 결과: 전체 테스트 PASS, 빌드 에러 없음 diff --git a/apps/edge/cmd/iop-edge/main.go b/apps/edge/cmd/iop-edge/main.go index 3d09e48..23453f0 100644 --- a/apps/edge/cmd/iop-edge/main.go +++ b/apps/edge/cmd/iop-edge/main.go @@ -5,8 +5,14 @@ import ( "os" "github.com/spf13/cobra" + "go.uber.org/fx" + + "iop/apps/edge/internal/bootstrap" + "iop/packages/config" ) +var cfgFile string + func main() { if err := rootCmd().Execute(); err != nil { fmt.Fprintln(os.Stderr, err) @@ -17,18 +23,24 @@ func main() { func rootCmd() *cobra.Command { root := &cobra.Command{ Use: "iop-edge", - Short: "IOP Edge — OpenAI-compatible API gateway (placeholder)", - Long: `iop-edge exposes an OpenAI-compatible HTTP API and routes requests -to IOP nodes via the TCP + Protobuf internal transport. - -This component is a placeholder. Implementation is planned after the node is stable.`, + Short: "IOP Edge — node gateway server", } - root.AddCommand(&cobra.Command{ - Use: "serve", - Short: "Start the edge server", - Run: func(_ *cobra.Command, _ []string) { - fmt.Println("iop-edge serve: not yet implemented") - }, - }) + root.PersistentFlags().StringVarP(&cfgFile, "config", "c", "configs/edge.yaml", "config file path") + root.AddCommand(serveCmd()) return root } + +func serveCmd() *cobra.Command { + return &cobra.Command{ + Use: "serve", + Short: "Start the edge server", + RunE: func(_ *cobra.Command, _ []string) error { + cfg, err := config.LoadEdge(cfgFile) + if err != nil { + return fmt.Errorf("load config: %w", err) + } + fx.New(bootstrap.Module(cfg)).Run() + return nil + }, + } +} diff --git a/apps/edge/internal/bootstrap/module.go b/apps/edge/internal/bootstrap/module.go new file mode 100644 index 0000000..6557d32 --- /dev/null +++ b/apps/edge/internal/bootstrap/module.go @@ -0,0 +1,52 @@ +package bootstrap + +import ( + "context" + + "go.uber.org/fx" + "go.uber.org/zap" + + edgenode "iop/apps/edge/internal/node" + "iop/apps/edge/internal/transport" + "iop/packages/config" + "iop/packages/observability" +) + +func Module(cfg *config.EdgeConfig) fx.Option { + return fx.Options( + fx.Provide( + func() *config.EdgeConfig { return cfg }, + + func(cfg *config.EdgeConfig) (*zap.Logger, error) { + return observability.NewLogger(cfg.Logging.Level) + }, + + func() *edgenode.Registry { + return edgenode.NewRegistry() + }, + + func(cfg *config.EdgeConfig, reg *edgenode.Registry, logger *zap.Logger) (*transport.Server, error) { + return transport.NewServer(cfg.Server.Listen, reg, logger) + }, + ), + + fx.Invoke(func(lc fx.Lifecycle, srv *transport.Server, cfg *config.EdgeConfig, logger *zap.Logger) { + lc.Append(fx.Hook{ + OnStart: func(ctx context.Context) error { + if err := srv.Start(ctx); err != nil { + return err + } + go func() { + if err := observability.ServeMetrics(cfg.Metrics.Port); err != nil { + logger.Warn("metrics server exited", zap.Error(err)) + } + }() + return nil + }, + OnStop: func(_ context.Context) error { + return srv.Stop() + }, + }) + }), + ) +} diff --git a/apps/edge/internal/node/registry.go b/apps/edge/internal/node/registry.go new file mode 100644 index 0000000..83659a7 --- /dev/null +++ b/apps/edge/internal/node/registry.go @@ -0,0 +1,71 @@ +package node + +import ( + "fmt" + "sync" + + toki "git.toki-labs.com/toki/common-proto-socket/go" + + iop "iop/proto/gen/iop" +) + +// NodeEntry represents one connected node. +type NodeEntry struct { + NodeID string + Client *toki.TcpClient + Adapters []*iop.AdapterInfo +} + +// Registry manages all nodes connected to edge. +type Registry struct { + mu sync.RWMutex + nodes map[string]*NodeEntry +} + +func NewRegistry() *Registry { + return &Registry{nodes: make(map[string]*NodeEntry)} +} + +func (r *Registry) Register(entry *NodeEntry) { + r.mu.Lock() + defer r.mu.Unlock() + r.nodes[entry.NodeID] = entry +} + +func (r *Registry) Unregister(nodeID string) { + r.mu.Lock() + defer r.mu.Unlock() + delete(r.nodes, nodeID) +} + +func (r *Registry) Get(nodeID string) (*NodeEntry, bool) { + r.mu.RLock() + defer r.mu.RUnlock() + e, ok := r.nodes[nodeID] + return e, ok +} + +func (r *Registry) All() []*NodeEntry { + r.mu.RLock() + defer r.mu.RUnlock() + out := make([]*NodeEntry, 0, len(r.nodes)) + for _, e := range r.nodes { + out = append(out, e) + } + return out +} + +func (r *Registry) Count() int { + r.mu.RLock() + defer r.mu.RUnlock() + return len(r.nodes) +} + +func (r *Registry) Pick() (*NodeEntry, error) { + r.mu.RLock() + defer r.mu.RUnlock() + for _, e := range r.nodes { + return e, nil + } + return nil, fmt.Errorf("no nodes connected") +} diff --git a/apps/edge/internal/node/registry_test.go b/apps/edge/internal/node/registry_test.go new file mode 100644 index 0000000..24e7630 --- /dev/null +++ b/apps/edge/internal/node/registry_test.go @@ -0,0 +1,39 @@ +package node_test + +import ( + "testing" + + toki "git.toki-labs.com/toki/common-proto-socket/go" + + edgenode "iop/apps/edge/internal/node" + iop "iop/proto/gen/iop" +) + +func TestRegistry_RegisterAndCount(t *testing.T) { + reg := edgenode.NewRegistry() + entry := &edgenode.NodeEntry{ + NodeID: "node-001", + Client: &toki.TcpClient{}, + Adapters: []*iop.AdapterInfo{{Name: "mock"}}, + } + reg.Register(entry) + if reg.Count() != 1 { + t.Fatalf("expected 1 node, got %d", reg.Count()) + } +} + +func TestRegistry_Pick_Empty(t *testing.T) { + reg := edgenode.NewRegistry() + if _, err := reg.Pick(); err == nil { + t.Fatal("expected error on empty registry") + } +} + +func TestRegistry_Unregister(t *testing.T) { + reg := edgenode.NewRegistry() + reg.Register(&edgenode.NodeEntry{NodeID: "node-001"}) + reg.Unregister("node-001") + if reg.Count() != 0 { + t.Fatalf("expected 0 nodes after unregister, got %d", reg.Count()) + } +} diff --git a/apps/edge/internal/transport/server.go b/apps/edge/internal/transport/server.go new file mode 100644 index 0000000..bb62f1b --- /dev/null +++ b/apps/edge/internal/transport/server.go @@ -0,0 +1,112 @@ +package transport + +import ( + "context" + "net" + "strconv" + "time" + + toki "git.toki-labs.com/toki/common-proto-socket/go" + "go.uber.org/zap" + "google.golang.org/protobuf/proto" + + edgenode "iop/apps/edge/internal/node" + iop "iop/proto/gen/iop" +) + +const ( + heartbeatIntervalSec = 30 + heartbeatWaitSec = 10 + capabilityTimeout = 5 * time.Second +) + +func edgeParserMap() toki.ParserMap { + return toki.ParserMap{ + toki.TypeNameOf(&iop.RunEvent{}): func(b []byte) (proto.Message, error) { + m := &iop.RunEvent{} + return m, proto.Unmarshal(b, m) + }, + toki.TypeNameOf(&iop.CapabilityResponse{}): func(b []byte) (proto.Message, error) { + m := &iop.CapabilityResponse{} + return m, proto.Unmarshal(b, m) + }, + } +} + +// Server wraps proto-socket TcpServer and manages node connections. +type Server struct { + tcp *toki.TcpServer + listen string + registry *edgenode.Registry + logger *zap.Logger +} + +func NewServer(listen string, registry *edgenode.Registry, logger *zap.Logger) (*Server, error) { + host, portStr, err := net.SplitHostPort(listen) + if err != nil { + return nil, err + } + port, err := strconv.Atoi(portStr) + if err != nil { + return nil, err + } + + s := &Server{listen: listen, registry: registry, logger: logger} + s.tcp = toki.NewTcpServer(host, port, func(conn net.Conn) *toki.TcpClient { + return toki.NewTcpClient(conn, heartbeatIntervalSec, heartbeatWaitSec, edgeParserMap()) + }) + s.tcp.OnClientConnected = s.onNodeConnected + return s, nil +} + +func (s *Server) Start(ctx context.Context) error { + if err := s.tcp.Start(ctx); err != nil { + return err + } + s.logger.Info("edge listening for nodes", zap.String("addr", s.listen)) + return nil +} + +func (s *Server) Stop() error { + return s.tcp.Stop() +} + +func (s *Server) onNodeConnected(client *toki.TcpClient) { + s.logger.Info("node connection established") + + toki.AddListenerTyped[*iop.RunEvent](&client.Communicator, func(e *iop.RunEvent) { + s.logger.Debug("run event received", + zap.String("run_id", e.GetRunId()), + zap.String("type", e.GetType()), + ) + }) + + go func() { + resp, err := toki.SendRequestTyped[*iop.CapabilityRequest, *iop.CapabilityResponse]( + &client.Communicator, + &iop.CapabilityRequest{}, + capabilityTimeout, + ) + if err != nil { + s.logger.Warn("capability request failed", zap.Error(err)) + _ = client.Close() + return + } + + entry := &edgenode.NodeEntry{ + NodeID: resp.GetNodeId(), + Client: client, + Adapters: resp.GetAdapters(), + } + client.AddDisconnectListener(func(_ *toki.TcpClient) { + s.registry.Unregister(entry.NodeID) + s.logger.Info("node unregistered", zap.String("node_id", entry.NodeID)) + }) + + s.registry.Register(entry) + s.logger.Info("node registered", + zap.String("node_id", entry.NodeID), + zap.Int("adapters", len(entry.Adapters)), + ) + }() +} diff --git a/apps/node/internal/bootstrap/module.go b/apps/node/internal/bootstrap/module.go index 229f355..ba93799 100644 --- a/apps/node/internal/bootstrap/module.go +++ b/apps/node/internal/bootstrap/module.go @@ -16,6 +16,7 @@ import ( "iop/apps/node/internal/router" "iop/apps/node/internal/runtime" "iop/apps/node/internal/store" + "iop/apps/node/internal/transport" "iop/packages/config" "iop/packages/observability" ) @@ -67,9 +68,15 @@ func Module(cfg *config.NodeConfig) fx.Option { }, ), - fx.Invoke(func(lc fx.Lifecycle, cfg *config.NodeConfig, logger *zap.Logger) { + fx.Invoke(func(lc fx.Lifecycle, n *node.Node, cfg *config.NodeConfig, logger *zap.Logger) { + var sess *transport.Session lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { + s, err := transport.DialEdge(ctx, cfg.Transport.EdgeAddr, n, cfg.Node.ID, logger) + if err != nil { + return err + } + sess = s go func() { if err := observability.ServeMetrics(cfg.Metrics.Port); err != nil { logger.Warn("metrics server exited", zap.Error(err)) @@ -77,7 +84,12 @@ func Module(cfg *config.NodeConfig) fx.Option { }() return nil }, - OnStop: func(_ context.Context) error { return nil }, + OnStop: func(_ context.Context) error { + if sess != nil { + return sess.Close() + } + return nil + }, }) }), ) diff --git a/apps/node/internal/node/node.go b/apps/node/internal/node/node.go index 8684ff1..2dea265 100644 --- a/apps/node/internal/node/node.go +++ b/apps/node/internal/node/node.go @@ -4,18 +4,18 @@ package node import ( "context" - "encoding/json" "fmt" "time" "go.uber.org/zap" + "google.golang.org/protobuf/types/known/structpb" "iop/apps/node/internal/adapters" "iop/apps/node/internal/runtime" "iop/apps/node/internal/store" "iop/apps/node/internal/transport" "iop/packages/config" - "iop/packages/protocol" + iop "iop/proto/gen/iop" ) // Node implements transport.Handler and coordinates the full execution pipeline. @@ -45,22 +45,22 @@ func New( } // OnRunRequest handles an incoming RunRequest from a transport Session. -func (n *Node) OnRunRequest(ctx context.Context, sess *transport.Session, req *protocol.RunRequest) error { +func (n *Node) OnRunRequest(ctx context.Context, sess *transport.Session, req *iop.RunRequest) error { n.logger.Info("run request received", - zap.String("run_id", req.RunID), - zap.String("adapter", req.Adapter), - zap.String("model", req.Model), + zap.String("run_id", req.GetRunId()), + zap.String("adapter", req.GetAdapter()), + zap.String("model", req.GetModel()), ) rr := runtime.RunRequest{ - RunID: req.RunID, - Adapter: req.Adapter, - Model: req.Model, - Workspace: req.Workspace, - Policy: req.Policy, - Input: req.Input, - TimeoutSec: req.TimeoutSec, - Metadata: req.Metadata, + RunID: req.GetRunId(), + Adapter: req.GetAdapter(), + Model: req.GetModel(), + Workspace: req.GetWorkspace(), + Policy: structAsMap(req.GetPolicy()), + Input: structAsMap(req.GetInput()), + TimeoutSec: int(req.GetTimeoutSec()), + Metadata: req.GetMetadata(), } spec, err := n.router.Resolve(ctx, rr) @@ -90,7 +90,7 @@ func (n *Node) OnRunRequest(ctx context.Context, sess *transport.Session, req *p defer sess.DeregisterCancel(spec.RunID) } - sink := &sessionSink{sess: sess, runID: spec.RunID} + sink := &sessionSink{sess: sess} execErr := adapter.Execute(execCtx, spec, sink) status := "completed" @@ -105,65 +105,57 @@ func (n *Node) OnRunRequest(ctx context.Context, sess *transport.Session, req *p } // OnCapabilityRequest returns the node's available adapters. -func (n *Node) OnCapabilityRequest(_ context.Context, _ *transport.Session) (*protocol.CapabilityResponse, error) { - resp := &protocol.CapabilityResponse{NodeID: n.nodeID} +func (n *Node) OnCapabilityRequest(_ context.Context, _ *transport.Session) (*iop.CapabilityResponse, error) { + resp := &iop.CapabilityResponse{NodeId: n.nodeID} for _, a := range n.registry.All() { caps, err := a.Capabilities(context.Background()) if err != nil { n.logger.Warn("capabilities query failed", zap.String("adapter", a.Name()), zap.Error(err)) continue } - resp.Adapters = append(resp.Adapters, protocol.AdapterInfo{ + resp.Adapters = append(resp.Adapters, &iop.AdapterInfo{ Name: caps.AdapterName, Models: caps.Models, - MaxConcurrency: caps.MaxConcurrency, + MaxConcurrency: int32(caps.MaxConcurrency), }) } return resp, nil } // OnCancel cancels a running execution. -func (n *Node) OnCancel(_ context.Context, sess *transport.Session, req *protocol.CancelRequest) error { - n.logger.Info("cancel request", zap.String("run_id", req.RunID)) - sess.CancelRun(req.RunID) +func (n *Node) OnCancel(_ context.Context, sess *transport.Session, req *iop.CancelRequest) error { + n.logger.Info("cancel request", zap.String("run_id", req.GetRunId())) + sess.CancelRun(req.GetRunId()) return nil } // sessionSink wraps a transport.Session to implement runtime.EventSink. type sessionSink struct { - sess *transport.Session - runID string + sess *transport.Session } func (s *sessionSink) Emit(_ context.Context, event runtime.RuntimeEvent) error { - var usageInfo *protocol.UsageInfo - if event.Usage != nil { - usageInfo = &protocol.UsageInfo{ - InputTokens: event.Usage.InputTokens, - OutputTokens: event.Usage.OutputTokens, - } - } - - re := protocol.RunEvent{ - RunID: event.RunID, + re := &iop.RunEvent{ + RunId: event.RunID, Type: string(event.Type), Delta: event.Delta, Message: event.Message, Error: event.Error, - Usage: usageInfo, Metadata: event.Metadata, Timestamp: event.Timestamp.UnixNano(), } - - payload, err := json.Marshal(re) - if err != nil { - return err + if event.Usage != nil { + re.Usage = &iop.Usage{ + InputTokens: int32(event.Usage.InputTokens), + OutputTokens: int32(event.Usage.OutputTokens), + } } - - return s.sess.Send(&protocol.Envelope{ - ProtocolVersion: protocol.ProtocolVersion, - RunID: s.runID, - Type: protocol.TypeRunEvent, - Payload: payload, - }) + return s.sess.Send(re) +} + +func structAsMap(s *structpb.Struct) map[string]any { + if s == nil { + return nil + } + return s.AsMap() } diff --git a/apps/node/internal/node/node_test.go b/apps/node/internal/node/node_test.go new file mode 100644 index 0000000..b150626 --- /dev/null +++ b/apps/node/internal/node/node_test.go @@ -0,0 +1,159 @@ +package node_test + +import ( + "context" + "errors" + "strings" + "testing" + + "go.uber.org/zap" + + "iop/apps/node/internal/adapters" + "iop/apps/node/internal/node" + "iop/apps/node/internal/runtime" + "iop/apps/node/internal/store" + "iop/apps/node/internal/transport" + "iop/packages/config" + iop "iop/proto/gen/iop" +) + +type noEmitAdapter struct{} + +func (a *noEmitAdapter) Name() string { return "test" } + +func (a *noEmitAdapter) Capabilities(_ context.Context) (runtime.Capabilities, error) { + return runtime.Capabilities{ + AdapterName: "test", + Models: []string{"v1"}, + MaxConcurrency: 1, + }, nil +} + +func (a *noEmitAdapter) Execute(_ context.Context, _ runtime.ExecutionSpec, _ runtime.EventSink) error { + return nil +} + +type fixedRouter struct { + adapterName string +} + +func (r *fixedRouter) Resolve(_ context.Context, req runtime.RunRequest) (runtime.ExecutionSpec, error) { + return runtime.ExecutionSpec{ + RunID: req.RunID, + Adapter: r.adapterName, + Model: req.Model, + Workspace: req.Workspace, + Policy: req.Policy, + Input: req.Input, + TimeoutSec: req.TimeoutSec, + Metadata: req.Metadata, + }, nil +} + +type errorRouter struct { + err error +} + +func (r *errorRouter) Resolve(_ context.Context, _ runtime.RunRequest) (runtime.ExecutionSpec, error) { + return runtime.ExecutionSpec{}, r.err +} + +func makeNode(t *testing.T, rtr runtime.Router, reg *adapters.Registry) *node.Node { + t.Helper() + + st, err := store.New(":memory:", zap.NewNop()) + if err != nil { + t.Fatalf("store: %v", err) + } + t.Cleanup(func() { + if err := st.Close(); err != nil { + t.Fatalf("close store: %v", err) + } + }) + + cfg := &config.NodeConfig{Node: config.NodeInfo{ID: "test-node"}} + return node.New(cfg, rtr, reg, st, zap.NewNop()) +} + +func TestOnCapabilityRequest(t *testing.T) { + reg := adapters.NewRegistry() + reg.Register(&noEmitAdapter{}) + n := makeNode(t, &fixedRouter{adapterName: "test"}, reg) + + resp, err := n.OnCapabilityRequest(context.Background(), &transport.Session{}) + if err != nil { + t.Fatalf("capability request: %v", err) + } + if resp.GetNodeId() != "test-node" { + t.Fatalf("unexpected node ID: %q", resp.GetNodeId()) + } + if len(resp.GetAdapters()) != 1 { + t.Fatalf("expected 1 adapter, got %d", len(resp.GetAdapters())) + } + adapter := resp.GetAdapters()[0] + if adapter.GetName() != "test" { + t.Fatalf("unexpected adapter name: %q", adapter.GetName()) + } + if len(adapter.GetModels()) != 1 || adapter.GetModels()[0] != "v1" { + t.Fatalf("unexpected models: %v", adapter.GetModels()) + } + if adapter.GetMaxConcurrency() != 1 { + t.Fatalf("unexpected max concurrency: %d", adapter.GetMaxConcurrency()) + } +} + +func TestOnRunRequest_RouterError(t *testing.T) { + reg := adapters.NewRegistry() + n := makeNode(t, &errorRouter{err: errors.New("boom")}, reg) + + err := n.OnRunRequest(context.Background(), &transport.Session{}, &iop.RunRequest{RunId: "run-1"}) + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "node: resolve:") { + t.Fatalf("expected resolve prefix, got %v", err) + } +} + +func TestOnRunRequest_AdapterNotFound(t *testing.T) { + reg := adapters.NewRegistry() + n := makeNode(t, &fixedRouter{adapterName: "missing"}, reg) + + err := n.OnRunRequest(context.Background(), &transport.Session{}, &iop.RunRequest{RunId: "run-1"}) + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "not found after routing") { + t.Fatalf("expected adapter lookup error, got %v", err) + } +} + +func TestOnRunRequest_Success(t *testing.T) { + reg := adapters.NewRegistry() + reg.Register(&noEmitAdapter{}) + n := makeNode(t, &fixedRouter{adapterName: "test"}, reg) + + err := n.OnRunRequest(context.Background(), &transport.Session{}, &iop.RunRequest{ + RunId: "run-1", + Adapter: "test", + Model: "v1", + }) + if err != nil { + t.Fatalf("run request: %v", err) + } +} + +func TestOnCancel_CallsCancelFn(t *testing.T) { + reg := adapters.NewRegistry() + n := makeNode(t, &fixedRouter{adapterName: "test"}, reg) + sess := &transport.Session{} + called := false + sess.RegisterCancel("run-1", func() { called = true }) + + if err := n.OnCancel(context.Background(), sess, &iop.CancelRequest{RunId: "run-1"}); err != nil { + t.Fatalf("cancel: %v", err) + } + if !called { + t.Fatal("cancel function was not called") + } +} diff --git a/apps/node/internal/transport/client.go b/apps/node/internal/transport/client.go new file mode 100644 index 0000000..811f80b --- /dev/null +++ b/apps/node/internal/transport/client.go @@ -0,0 +1,37 @@ +package transport + +import ( + "context" + "fmt" + "net" + "strconv" + + toki "git.toki-labs.com/toki/common-proto-socket/go" + "go.uber.org/zap" +) + +const ( + heartbeatIntervalSec = 30 + heartbeatWaitSec = 10 +) + +// DialEdge connects to the edge TCP server and returns a Session. +func DialEdge(ctx context.Context, addr string, handler Handler, nodeID string, logger *zap.Logger) (*Session, error) { + host, portStr, err := net.SplitHostPort(addr) + if err != nil { + return nil, fmt.Errorf("transport: invalid addr %q: %w", addr, err) + } + port, err := strconv.Atoi(portStr) + if err != nil { + return nil, fmt.Errorf("transport: invalid port %q: %w", portStr, err) + } + + client, err := toki.DialTcp(ctx, host, port, heartbeatIntervalSec, heartbeatWaitSec, nodeParserMap()) + if err != nil { + return nil, fmt.Errorf("transport: dial edge %s: %w", addr, err) + } + + sess := newSession(client, handler, nodeID, logger) + logger.Info("connected to edge", zap.String("addr", addr), zap.String("node_id", nodeID)) + return sess, nil +} diff --git a/apps/node/internal/transport/parser.go b/apps/node/internal/transport/parser.go new file mode 100644 index 0000000..8aaa02b --- /dev/null +++ b/apps/node/internal/transport/parser.go @@ -0,0 +1,25 @@ +package transport + +import ( + toki "git.toki-labs.com/toki/common-proto-socket/go" + "google.golang.org/protobuf/proto" + + iop "iop/proto/gen/iop" +) + +func nodeParserMap() toki.ParserMap { + return toki.ParserMap{ + toki.TypeNameOf(&iop.RunRequest{}): func(b []byte) (proto.Message, error) { + m := &iop.RunRequest{} + return m, proto.Unmarshal(b, m) + }, + toki.TypeNameOf(&iop.CancelRequest{}): func(b []byte) (proto.Message, error) { + m := &iop.CancelRequest{} + return m, proto.Unmarshal(b, m) + }, + toki.TypeNameOf(&iop.CapabilityRequest{}): func(b []byte) (proto.Message, error) { + m := &iop.CapabilityRequest{} + return m, proto.Unmarshal(b, m) + }, + } +} diff --git a/apps/node/internal/transport/parser_test.go b/apps/node/internal/transport/parser_test.go new file mode 100644 index 0000000..a06e989 --- /dev/null +++ b/apps/node/internal/transport/parser_test.go @@ -0,0 +1,65 @@ +package transport + +import ( + "testing" + + toki "git.toki-labs.com/toki/common-proto-socket/go" + "google.golang.org/protobuf/proto" + + iop "iop/proto/gen/iop" +) + +func TestNodeParserMap_RunRequest(t *testing.T) { + parsers := nodeParserMap() + original := &iop.RunRequest{RunId: "run-1", Adapter: "mock", Model: "v1"} + payload, err := proto.Marshal(original) + if err != nil { + t.Fatalf("marshal: %v", err) + } + + parsed, err := parsers[toki.TypeNameOf(original)](payload) + if err != nil { + t.Fatalf("parse: %v", err) + } + got := parsed.(*iop.RunRequest) + if got.GetRunId() != original.GetRunId() || + got.GetAdapter() != original.GetAdapter() || + got.GetModel() != original.GetModel() { + t.Fatalf("unexpected run request: %+v", got) + } +} + +func TestNodeParserMap_CancelRequest(t *testing.T) { + parsers := nodeParserMap() + original := &iop.CancelRequest{RunId: "run-1"} + payload, err := proto.Marshal(original) + if err != nil { + t.Fatalf("marshal: %v", err) + } + + parsed, err := parsers[toki.TypeNameOf(original)](payload) + if err != nil { + t.Fatalf("parse: %v", err) + } + got := parsed.(*iop.CancelRequest) + if got.GetRunId() != original.GetRunId() { + t.Fatalf("unexpected cancel request: %+v", got) + } +} + +func TestNodeParserMap_CapabilityRequest(t *testing.T) { + parsers := nodeParserMap() + original := &iop.CapabilityRequest{} + payload, err := proto.Marshal(original) + if err != nil { + t.Fatalf("marshal: %v", err) + } + + parsed, err := parsers[toki.TypeNameOf(original)](payload) + if err != nil { + t.Fatalf("parse: %v", err) + } + if _, ok := parsed.(*iop.CapabilityRequest); !ok { + t.Fatalf("unexpected capability request type: %T", parsed) + } +} diff --git a/apps/node/internal/transport/session.go b/apps/node/internal/transport/session.go new file mode 100644 index 0000000..0ffc3f2 --- /dev/null +++ b/apps/node/internal/transport/session.go @@ -0,0 +1,92 @@ +package transport + +import ( + "context" + "sync" + + toki "git.toki-labs.com/toki/common-proto-socket/go" + "go.uber.org/zap" + "google.golang.org/protobuf/proto" + + iop "iop/proto/gen/iop" +) + +// Handler processes IOP messages received from edge. +type Handler interface { + OnRunRequest(ctx context.Context, sess *Session, req *iop.RunRequest) error + OnCapabilityRequest(ctx context.Context, sess *Session) (*iop.CapabilityResponse, error) + OnCancel(ctx context.Context, sess *Session, req *iop.CancelRequest) error +} + +// Session represents the node's persistent connection to edge. +type Session struct { + client *toki.TcpClient + nodeID string + logger *zap.Logger + cancelFns sync.Map +} + +func newSession(client *toki.TcpClient, handler Handler, nodeID string, logger *zap.Logger) *Session { + s := &Session{client: client, nodeID: nodeID, logger: logger} + + toki.AddListenerTyped[*iop.RunRequest](&client.Communicator, func(req *iop.RunRequest) { + go func() { + if err := handler.OnRunRequest(context.Background(), s, req); err != nil { + logger.Warn("run request error", + zap.String("run_id", req.GetRunId()), + zap.Error(err), + ) + } + }() + }) + + toki.AddRequestListenerTyped[*iop.CapabilityRequest, *iop.CapabilityResponse]( + &client.Communicator, + func(_ *iop.CapabilityRequest) (*iop.CapabilityResponse, error) { + return handler.OnCapabilityRequest(context.Background(), s) + }, + ) + + toki.AddListenerTyped[*iop.CancelRequest](&client.Communicator, func(req *iop.CancelRequest) { + if err := handler.OnCancel(context.Background(), s, req); err != nil { + logger.Warn("cancel error", + zap.String("run_id", req.GetRunId()), + zap.Error(err), + ) + } + }) + + client.AddDisconnectListener(func(_ *toki.TcpClient) { + logger.Info("disconnected from edge", zap.String("node_id", nodeID)) + }) + + return s +} + +// Send transmits a proto message to edge. +func (s *Session) Send(m proto.Message) error { + return s.client.Send(m) +} + +// IsAlive reports whether the connection is active. +func (s *Session) IsAlive() bool { return s.client.IsAlive() } + +// Close terminates the connection to edge. +func (s *Session) Close() error { return s.client.Close() } + +// RegisterCancel stores a cancel function for a running execution. +func (s *Session) RegisterCancel(runID string, cancel context.CancelFunc) { + s.cancelFns.Store(runID, cancel) +} + +// DeregisterCancel removes a run's cancel function after completion. +func (s *Session) DeregisterCancel(runID string) { + s.cancelFns.Delete(runID) +} + +// CancelRun invokes the cancel function for a run if one is registered. +func (s *Session) CancelRun(runID string) { + if v, ok := s.cancelFns.Load(runID); ok { + v.(context.CancelFunc)() + } +} diff --git a/apps/node/internal/transport/session_test.go b/apps/node/internal/transport/session_test.go new file mode 100644 index 0000000..c5a05e0 --- /dev/null +++ b/apps/node/internal/transport/session_test.go @@ -0,0 +1,38 @@ +package transport_test + +import ( + "testing" + + "iop/apps/node/internal/transport" +) + +func TestSession_RegisterCancel_CancelRun(t *testing.T) { + sess := &transport.Session{} + called := false + + sess.RegisterCancel("run-1", func() { called = true }) + sess.CancelRun("run-1") + + if !called { + t.Fatal("cancel function was not called") + } +} + +func TestSession_DeregisterCancel(t *testing.T) { + sess := &transport.Session{} + called := false + + sess.RegisterCancel("run-1", func() { called = true }) + sess.DeregisterCancel("run-1") + sess.CancelRun("run-1") + + if called { + t.Fatal("cancel function should not have been called after deregister") + } +} + +func TestSession_CancelRun_UnknownID(t *testing.T) { + sess := &transport.Session{} + + sess.CancelRun("missing-run") +} diff --git a/configs/edge.yaml b/configs/edge.yaml index caad597..d05958f 100644 --- a/configs/edge.yaml +++ b/configs/edge.yaml @@ -1,5 +1,5 @@ server: - listen: "0.0.0.0:8080" + listen: "0.0.0.0:9090" tls: enabled: false diff --git a/go.mod b/go.mod index 0a8ce9f..0045dc3 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module iop go 1.24 require ( + git.toki-labs.com/toki/common-proto-socket/go v0.0.0-00010101000000-000000000000 github.com/prometheus/client_golang v1.20.5 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 @@ -12,3 +13,47 @@ require ( gopkg.in/yaml.v3 v3.0.1 modernc.org/sqlite v1.33.1 ) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.uber.org/dig v1.18.0 // indirect + go.uber.org/multierr v1.10.0 // indirect + golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 // indirect + golang.org/x/sys v0.22.0 // indirect + golang.org/x/text v0.16.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect + modernc.org/libc v1.55.3 // indirect + modernc.org/mathutil v1.6.0 // indirect + modernc.org/memory v1.8.0 // indirect + modernc.org/strutil v1.2.0 // indirect + modernc.org/token v1.1.0 // indirect + nhooyr.io/websocket v1.8.17 // indirect +) + +replace git.toki-labs.com/toki/common-proto-socket/go => ../proto-socket/go diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..779f40b --- /dev/null +++ b/go.sum @@ -0,0 +1,151 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +go.uber.org/dig v1.18.0 h1:imUL1UiY0Mg4bqbFfsRQO5G4CGRBec/ZujWTvSVp3pw= +go.uber.org/dig v1.18.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= +go.uber.org/fx v1.22.2 h1:iPW+OPxv0G8w75OemJ1RAnTUrF55zOJlXlo1TbJ0Buw= +go.uber.org/fx v1.22.2/go.mod h1:o/D9n+2mLP6v1EG+qsdT1O8wKopYAsqZasju97SDFCU= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 h1:mchzmB1XO2pMaKFRqk/+MV3mgGG96aqaPXaMifQU47w= +golang.org/x/exp v0.0.0-20231108232855-2478ac86f678/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= +modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= +modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y= +modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s= +modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= +modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw= +modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= +modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U= +modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= +modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= +modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= +modernc.org/sqlite v1.33.1 h1:trb6Z3YYoeM9eDL1O8do81kP+0ejv+YzgyFo+Gwy0nM= +modernc.org/sqlite v1.33.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k= +modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +nhooyr.io/websocket v1.8.17 h1:KEVeLJkUywCKVsnLIDlD/5gtayKp8VoCkksHCGGfT9Y= +nhooyr.io/websocket v1.8.17/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= diff --git a/packages/config/config.go b/packages/config/config.go index e902db9..54fb5ca 100644 --- a/packages/config/config.go +++ b/packages/config/config.go @@ -5,14 +5,21 @@ import ( ) type NodeConfig struct { - Node NodeInfo `mapstructure:"node" yaml:"node"` + Node NodeInfo `mapstructure:"node" yaml:"node"` Transport TransportConf `mapstructure:"transport" yaml:"transport"` - TLS TLSConf `mapstructure:"tls" yaml:"tls"` - Runtime RuntimeConf `mapstructure:"runtime" yaml:"runtime"` - SQLite SQLiteConf `mapstructure:"sqlite" yaml:"sqlite"` - Logging LoggingConf `mapstructure:"logging" yaml:"logging"` - Metrics MetricsConf `mapstructure:"metrics" yaml:"metrics"` - Adapters AdaptersConf `mapstructure:"adapters" yaml:"adapters"` + TLS TLSConf `mapstructure:"tls" yaml:"tls"` + Runtime RuntimeConf `mapstructure:"runtime" yaml:"runtime"` + SQLite SQLiteConf `mapstructure:"sqlite" yaml:"sqlite"` + Logging LoggingConf `mapstructure:"logging" yaml:"logging"` + Metrics MetricsConf `mapstructure:"metrics" yaml:"metrics"` + Adapters AdaptersConf `mapstructure:"adapters" yaml:"adapters"` +} + +type EdgeConfig struct { + Server EdgeServerConf `mapstructure:"server" yaml:"server"` + TLS TLSConf `mapstructure:"tls" yaml:"tls"` + Logging LoggingConf `mapstructure:"logging" yaml:"logging"` + Metrics MetricsConf `mapstructure:"metrics" yaml:"metrics"` } type NodeInfo struct { @@ -20,6 +27,10 @@ type NodeInfo struct { Name string `mapstructure:"name" yaml:"name"` } +type EdgeServerConf struct { + Listen string `mapstructure:"listen" yaml:"listen"` +} + type TransportConf struct { EdgeAddr string `mapstructure:"edge_addr" yaml:"edge_addr"` } @@ -89,6 +100,20 @@ func Load(cfgFile string) (*NodeConfig, error) { return &cfg, nil } +func LoadEdge(cfgFile string) (*EdgeConfig, error) { + v := viper.New() + v.SetConfigFile(cfgFile) + setEdgeDefaults(v) + if err := v.ReadInConfig(); err != nil { + return nil, err + } + var cfg EdgeConfig + if err := v.Unmarshal(&cfg); err != nil { + return nil, err + } + return &cfg, nil +} + func setDefaults(v *viper.Viper) { v.SetDefault("transport.edge_addr", "localhost:9090") v.SetDefault("runtime.concurrency", 4) @@ -98,3 +123,10 @@ func setDefaults(v *viper.Viper) { v.SetDefault("metrics.port", 9091) v.SetDefault("tls.enabled", false) } + +func setEdgeDefaults(v *viper.Viper) { + v.SetDefault("server.listen", "0.0.0.0:9090") + v.SetDefault("logging.level", "info") + v.SetDefault("metrics.port", 9092) + v.SetDefault("tls.enabled", false) +} diff --git a/proto/gen/iop/control.pb.go b/proto/gen/iop/control.pb.go new file mode 100644 index 0000000..24d8f12 --- /dev/null +++ b/proto/gen/iop/control.pb.go @@ -0,0 +1,296 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v5.29.3 +// source: proto/iop/control.proto + +package iop + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// PolicyRule defines a routing/resource constraint. +type PolicyRule struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Expression string `protobuf:"bytes,2,opt,name=expression,proto3" json:"expression,omitempty"` + Params map[string]string `protobuf:"bytes,3,rep,name=params,proto3" json:"params,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PolicyRule) Reset() { + *x = PolicyRule{} + mi := &file_proto_iop_control_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PolicyRule) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PolicyRule) ProtoMessage() {} + +func (x *PolicyRule) ProtoReflect() protoreflect.Message { + mi := &file_proto_iop_control_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PolicyRule.ProtoReflect.Descriptor instead. +func (*PolicyRule) Descriptor() ([]byte, []int) { + return file_proto_iop_control_proto_rawDescGZIP(), []int{0} +} + +func (x *PolicyRule) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *PolicyRule) GetExpression() string { + if x != nil { + return x.Expression + } + return "" +} + +func (x *PolicyRule) GetParams() map[string]string { + if x != nil { + return x.Params + } + return nil +} + +// ScheduleRequest asks the control-plane to route a job to a node. +type ScheduleRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + JobId string `protobuf:"bytes,1,opt,name=job_id,json=jobId,proto3" json:"job_id,omitempty"` + Model string `protobuf:"bytes,2,opt,name=model,proto3" json:"model,omitempty"` + Policies []*PolicyRule `protobuf:"bytes,3,rep,name=policies,proto3" json:"policies,omitempty"` + Metadata map[string]string `protobuf:"bytes,4,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ScheduleRequest) Reset() { + *x = ScheduleRequest{} + mi := &file_proto_iop_control_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ScheduleRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ScheduleRequest) ProtoMessage() {} + +func (x *ScheduleRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_iop_control_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ScheduleRequest.ProtoReflect.Descriptor instead. +func (*ScheduleRequest) Descriptor() ([]byte, []int) { + return file_proto_iop_control_proto_rawDescGZIP(), []int{1} +} + +func (x *ScheduleRequest) GetJobId() string { + if x != nil { + return x.JobId + } + return "" +} + +func (x *ScheduleRequest) GetModel() string { + if x != nil { + return x.Model + } + return "" +} + +func (x *ScheduleRequest) GetPolicies() []*PolicyRule { + if x != nil { + return x.Policies + } + return nil +} + +func (x *ScheduleRequest) GetMetadata() map[string]string { + if x != nil { + return x.Metadata + } + return nil +} + +// ScheduleResponse contains the selected node address. +type ScheduleResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + NodeId string `protobuf:"bytes,1,opt,name=node_id,json=nodeId,proto3" json:"node_id,omitempty"` + Address string `protobuf:"bytes,2,opt,name=address,proto3" json:"address,omitempty"` + Token string `protobuf:"bytes,3,opt,name=token,proto3" json:"token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ScheduleResponse) Reset() { + *x = ScheduleResponse{} + mi := &file_proto_iop_control_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ScheduleResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ScheduleResponse) ProtoMessage() {} + +func (x *ScheduleResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_iop_control_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ScheduleResponse.ProtoReflect.Descriptor instead. +func (*ScheduleResponse) Descriptor() ([]byte, []int) { + return file_proto_iop_control_proto_rawDescGZIP(), []int{2} +} + +func (x *ScheduleResponse) GetNodeId() string { + if x != nil { + return x.NodeId + } + return "" +} + +func (x *ScheduleResponse) GetAddress() string { + if x != nil { + return x.Address + } + return "" +} + +func (x *ScheduleResponse) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + +var File_proto_iop_control_proto protoreflect.FileDescriptor + +const file_proto_iop_control_proto_rawDesc = "" + + "\n" + + "\x17proto/iop/control.proto\x12\x03iop\"\xb0\x01\n" + + "\n" + + "PolicyRule\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1e\n" + + "\n" + + "expression\x18\x02 \x01(\tR\n" + + "expression\x123\n" + + "\x06params\x18\x03 \x03(\v2\x1b.iop.PolicyRule.ParamsEntryR\x06params\x1a9\n" + + "\vParamsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xe8\x01\n" + + "\x0fScheduleRequest\x12\x15\n" + + "\x06job_id\x18\x01 \x01(\tR\x05jobId\x12\x14\n" + + "\x05model\x18\x02 \x01(\tR\x05model\x12+\n" + + "\bpolicies\x18\x03 \x03(\v2\x0f.iop.PolicyRuleR\bpolicies\x12>\n" + + "\bmetadata\x18\x04 \x03(\v2\".iop.ScheduleRequest.MetadataEntryR\bmetadata\x1a;\n" + + "\rMetadataEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"[\n" + + "\x10ScheduleResponse\x12\x17\n" + + "\anode_id\x18\x01 \x01(\tR\x06nodeId\x12\x18\n" + + "\aaddress\x18\x02 \x01(\tR\aaddress\x12\x14\n" + + "\x05token\x18\x03 \x01(\tR\x05tokenB\x13Z\x11iop/proto/gen/iopb\x06proto3" + +var ( + file_proto_iop_control_proto_rawDescOnce sync.Once + file_proto_iop_control_proto_rawDescData []byte +) + +func file_proto_iop_control_proto_rawDescGZIP() []byte { + file_proto_iop_control_proto_rawDescOnce.Do(func() { + file_proto_iop_control_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_iop_control_proto_rawDesc), len(file_proto_iop_control_proto_rawDesc))) + }) + return file_proto_iop_control_proto_rawDescData +} + +var file_proto_iop_control_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_proto_iop_control_proto_goTypes = []any{ + (*PolicyRule)(nil), // 0: iop.PolicyRule + (*ScheduleRequest)(nil), // 1: iop.ScheduleRequest + (*ScheduleResponse)(nil), // 2: iop.ScheduleResponse + nil, // 3: iop.PolicyRule.ParamsEntry + nil, // 4: iop.ScheduleRequest.MetadataEntry +} +var file_proto_iop_control_proto_depIdxs = []int32{ + 3, // 0: iop.PolicyRule.params:type_name -> iop.PolicyRule.ParamsEntry + 0, // 1: iop.ScheduleRequest.policies:type_name -> iop.PolicyRule + 4, // 2: iop.ScheduleRequest.metadata:type_name -> iop.ScheduleRequest.MetadataEntry + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_proto_iop_control_proto_init() } +func file_proto_iop_control_proto_init() { + if File_proto_iop_control_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_iop_control_proto_rawDesc), len(file_proto_iop_control_proto_rawDesc)), + NumEnums: 0, + NumMessages: 5, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proto_iop_control_proto_goTypes, + DependencyIndexes: file_proto_iop_control_proto_depIdxs, + MessageInfos: file_proto_iop_control_proto_msgTypes, + }.Build() + File_proto_iop_control_proto = out.File + file_proto_iop_control_proto_goTypes = nil + file_proto_iop_control_proto_depIdxs = nil +} diff --git a/proto/gen/iop/job.pb.go b/proto/gen/iop/job.pb.go new file mode 100644 index 0000000..db4cb32 --- /dev/null +++ b/proto/gen/iop/job.pb.go @@ -0,0 +1,413 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v5.29.3 +// source: proto/iop/job.proto + +package iop + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type JobStatus int32 + +const ( + JobStatus_JOB_STATUS_UNSPECIFIED JobStatus = 0 + JobStatus_JOB_STATUS_PENDING JobStatus = 1 + JobStatus_JOB_STATUS_RUNNING JobStatus = 2 + JobStatus_JOB_STATUS_COMPLETED JobStatus = 3 + JobStatus_JOB_STATUS_FAILED JobStatus = 4 + JobStatus_JOB_STATUS_CANCELLED JobStatus = 5 +) + +// Enum value maps for JobStatus. +var ( + JobStatus_name = map[int32]string{ + 0: "JOB_STATUS_UNSPECIFIED", + 1: "JOB_STATUS_PENDING", + 2: "JOB_STATUS_RUNNING", + 3: "JOB_STATUS_COMPLETED", + 4: "JOB_STATUS_FAILED", + 5: "JOB_STATUS_CANCELLED", + } + JobStatus_value = map[string]int32{ + "JOB_STATUS_UNSPECIFIED": 0, + "JOB_STATUS_PENDING": 1, + "JOB_STATUS_RUNNING": 2, + "JOB_STATUS_COMPLETED": 3, + "JOB_STATUS_FAILED": 4, + "JOB_STATUS_CANCELLED": 5, + } +) + +func (x JobStatus) Enum() *JobStatus { + p := new(JobStatus) + *p = x + return p +} + +func (x JobStatus) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (JobStatus) Descriptor() protoreflect.EnumDescriptor { + return file_proto_iop_job_proto_enumTypes[0].Descriptor() +} + +func (JobStatus) Type() protoreflect.EnumType { + return &file_proto_iop_job_proto_enumTypes[0] +} + +func (x JobStatus) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use JobStatus.Descriptor instead. +func (JobStatus) EnumDescriptor() ([]byte, []int) { + return file_proto_iop_job_proto_rawDescGZIP(), []int{0} +} + +type Job struct { + state protoimpl.MessageState `protogen:"open.v1"` + JobId string `protobuf:"bytes,1,opt,name=job_id,json=jobId,proto3" json:"job_id,omitempty"` + RunId string `protobuf:"bytes,2,opt,name=run_id,json=runId,proto3" json:"run_id,omitempty"` + NodeId string `protobuf:"bytes,3,opt,name=node_id,json=nodeId,proto3" json:"node_id,omitempty"` + Adapter string `protobuf:"bytes,4,opt,name=adapter,proto3" json:"adapter,omitempty"` + Model string `protobuf:"bytes,5,opt,name=model,proto3" json:"model,omitempty"` + Status JobStatus `protobuf:"varint,6,opt,name=status,proto3,enum=iop.JobStatus" json:"status,omitempty"` + CreatedAt int64 `protobuf:"varint,7,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + StartedAt int64 `protobuf:"varint,8,opt,name=started_at,json=startedAt,proto3" json:"started_at,omitempty"` + FinishedAt int64 `protobuf:"varint,9,opt,name=finished_at,json=finishedAt,proto3" json:"finished_at,omitempty"` + Error string `protobuf:"bytes,10,opt,name=error,proto3" json:"error,omitempty"` + Metadata map[string]string `protobuf:"bytes,11,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Job) Reset() { + *x = Job{} + mi := &file_proto_iop_job_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Job) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Job) ProtoMessage() {} + +func (x *Job) ProtoReflect() protoreflect.Message { + mi := &file_proto_iop_job_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Job.ProtoReflect.Descriptor instead. +func (*Job) Descriptor() ([]byte, []int) { + return file_proto_iop_job_proto_rawDescGZIP(), []int{0} +} + +func (x *Job) GetJobId() string { + if x != nil { + return x.JobId + } + return "" +} + +func (x *Job) GetRunId() string { + if x != nil { + return x.RunId + } + return "" +} + +func (x *Job) GetNodeId() string { + if x != nil { + return x.NodeId + } + return "" +} + +func (x *Job) GetAdapter() string { + if x != nil { + return x.Adapter + } + return "" +} + +func (x *Job) GetModel() string { + if x != nil { + return x.Model + } + return "" +} + +func (x *Job) GetStatus() JobStatus { + if x != nil { + return x.Status + } + return JobStatus_JOB_STATUS_UNSPECIFIED +} + +func (x *Job) GetCreatedAt() int64 { + if x != nil { + return x.CreatedAt + } + return 0 +} + +func (x *Job) GetStartedAt() int64 { + if x != nil { + return x.StartedAt + } + return 0 +} + +func (x *Job) GetFinishedAt() int64 { + if x != nil { + return x.FinishedAt + } + return 0 +} + +func (x *Job) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +func (x *Job) GetMetadata() map[string]string { + if x != nil { + return x.Metadata + } + return nil +} + +type JobListRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + StatusFilter JobStatus `protobuf:"varint,1,opt,name=status_filter,json=statusFilter,proto3,enum=iop.JobStatus" json:"status_filter,omitempty"` + Limit int32 `protobuf:"varint,2,opt,name=limit,proto3" json:"limit,omitempty"` + Cursor string `protobuf:"bytes,3,opt,name=cursor,proto3" json:"cursor,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *JobListRequest) Reset() { + *x = JobListRequest{} + mi := &file_proto_iop_job_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *JobListRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*JobListRequest) ProtoMessage() {} + +func (x *JobListRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_iop_job_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use JobListRequest.ProtoReflect.Descriptor instead. +func (*JobListRequest) Descriptor() ([]byte, []int) { + return file_proto_iop_job_proto_rawDescGZIP(), []int{1} +} + +func (x *JobListRequest) GetStatusFilter() JobStatus { + if x != nil { + return x.StatusFilter + } + return JobStatus_JOB_STATUS_UNSPECIFIED +} + +func (x *JobListRequest) GetLimit() int32 { + if x != nil { + return x.Limit + } + return 0 +} + +func (x *JobListRequest) GetCursor() string { + if x != nil { + return x.Cursor + } + return "" +} + +type JobListResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Jobs []*Job `protobuf:"bytes,1,rep,name=jobs,proto3" json:"jobs,omitempty"` + Next string `protobuf:"bytes,2,opt,name=next,proto3" json:"next,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *JobListResponse) Reset() { + *x = JobListResponse{} + mi := &file_proto_iop_job_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *JobListResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*JobListResponse) ProtoMessage() {} + +func (x *JobListResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_iop_job_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use JobListResponse.ProtoReflect.Descriptor instead. +func (*JobListResponse) Descriptor() ([]byte, []int) { + return file_proto_iop_job_proto_rawDescGZIP(), []int{2} +} + +func (x *JobListResponse) GetJobs() []*Job { + if x != nil { + return x.Jobs + } + return nil +} + +func (x *JobListResponse) GetNext() string { + if x != nil { + return x.Next + } + return "" +} + +var File_proto_iop_job_proto protoreflect.FileDescriptor + +const file_proto_iop_job_proto_rawDesc = "" + + "\n" + + "\x13proto/iop/job.proto\x12\x03iop\"\x8a\x03\n" + + "\x03Job\x12\x15\n" + + "\x06job_id\x18\x01 \x01(\tR\x05jobId\x12\x15\n" + + "\x06run_id\x18\x02 \x01(\tR\x05runId\x12\x17\n" + + "\anode_id\x18\x03 \x01(\tR\x06nodeId\x12\x18\n" + + "\aadapter\x18\x04 \x01(\tR\aadapter\x12\x14\n" + + "\x05model\x18\x05 \x01(\tR\x05model\x12&\n" + + "\x06status\x18\x06 \x01(\x0e2\x0e.iop.JobStatusR\x06status\x12\x1d\n" + + "\n" + + "created_at\x18\a \x01(\x03R\tcreatedAt\x12\x1d\n" + + "\n" + + "started_at\x18\b \x01(\x03R\tstartedAt\x12\x1f\n" + + "\vfinished_at\x18\t \x01(\x03R\n" + + "finishedAt\x12\x14\n" + + "\x05error\x18\n" + + " \x01(\tR\x05error\x122\n" + + "\bmetadata\x18\v \x03(\v2\x16.iop.Job.MetadataEntryR\bmetadata\x1a;\n" + + "\rMetadataEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"s\n" + + "\x0eJobListRequest\x123\n" + + "\rstatus_filter\x18\x01 \x01(\x0e2\x0e.iop.JobStatusR\fstatusFilter\x12\x14\n" + + "\x05limit\x18\x02 \x01(\x05R\x05limit\x12\x16\n" + + "\x06cursor\x18\x03 \x01(\tR\x06cursor\"C\n" + + "\x0fJobListResponse\x12\x1c\n" + + "\x04jobs\x18\x01 \x03(\v2\b.iop.JobR\x04jobs\x12\x12\n" + + "\x04next\x18\x02 \x01(\tR\x04next*\xa2\x01\n" + + "\tJobStatus\x12\x1a\n" + + "\x16JOB_STATUS_UNSPECIFIED\x10\x00\x12\x16\n" + + "\x12JOB_STATUS_PENDING\x10\x01\x12\x16\n" + + "\x12JOB_STATUS_RUNNING\x10\x02\x12\x18\n" + + "\x14JOB_STATUS_COMPLETED\x10\x03\x12\x15\n" + + "\x11JOB_STATUS_FAILED\x10\x04\x12\x18\n" + + "\x14JOB_STATUS_CANCELLED\x10\x05B\x13Z\x11iop/proto/gen/iopb\x06proto3" + +var ( + file_proto_iop_job_proto_rawDescOnce sync.Once + file_proto_iop_job_proto_rawDescData []byte +) + +func file_proto_iop_job_proto_rawDescGZIP() []byte { + file_proto_iop_job_proto_rawDescOnce.Do(func() { + file_proto_iop_job_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_iop_job_proto_rawDesc), len(file_proto_iop_job_proto_rawDesc))) + }) + return file_proto_iop_job_proto_rawDescData +} + +var file_proto_iop_job_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_proto_iop_job_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_proto_iop_job_proto_goTypes = []any{ + (JobStatus)(0), // 0: iop.JobStatus + (*Job)(nil), // 1: iop.Job + (*JobListRequest)(nil), // 2: iop.JobListRequest + (*JobListResponse)(nil), // 3: iop.JobListResponse + nil, // 4: iop.Job.MetadataEntry +} +var file_proto_iop_job_proto_depIdxs = []int32{ + 0, // 0: iop.Job.status:type_name -> iop.JobStatus + 4, // 1: iop.Job.metadata:type_name -> iop.Job.MetadataEntry + 0, // 2: iop.JobListRequest.status_filter:type_name -> iop.JobStatus + 1, // 3: iop.JobListResponse.jobs:type_name -> iop.Job + 4, // [4:4] is the sub-list for method output_type + 4, // [4:4] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name +} + +func init() { file_proto_iop_job_proto_init() } +func file_proto_iop_job_proto_init() { + if File_proto_iop_job_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_iop_job_proto_rawDesc), len(file_proto_iop_job_proto_rawDesc)), + NumEnums: 1, + NumMessages: 4, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proto_iop_job_proto_goTypes, + DependencyIndexes: file_proto_iop_job_proto_depIdxs, + EnumInfos: file_proto_iop_job_proto_enumTypes, + MessageInfos: file_proto_iop_job_proto_msgTypes, + }.Build() + File_proto_iop_job_proto = out.File + file_proto_iop_job_proto_goTypes = nil + file_proto_iop_job_proto_depIdxs = nil +} diff --git a/proto/gen/iop/node.pb.go b/proto/gen/iop/node.pb.go new file mode 100644 index 0000000..c61b023 --- /dev/null +++ b/proto/gen/iop/node.pb.go @@ -0,0 +1,350 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v5.29.3 +// source: proto/iop/node.proto + +package iop + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type NodeStatus int32 + +const ( + NodeStatus_NODE_STATUS_UNSPECIFIED NodeStatus = 0 + NodeStatus_NODE_STATUS_ONLINE NodeStatus = 1 + NodeStatus_NODE_STATUS_OFFLINE NodeStatus = 2 + NodeStatus_NODE_STATUS_DRAINING NodeStatus = 3 +) + +// Enum value maps for NodeStatus. +var ( + NodeStatus_name = map[int32]string{ + 0: "NODE_STATUS_UNSPECIFIED", + 1: "NODE_STATUS_ONLINE", + 2: "NODE_STATUS_OFFLINE", + 3: "NODE_STATUS_DRAINING", + } + NodeStatus_value = map[string]int32{ + "NODE_STATUS_UNSPECIFIED": 0, + "NODE_STATUS_ONLINE": 1, + "NODE_STATUS_OFFLINE": 2, + "NODE_STATUS_DRAINING": 3, + } +) + +func (x NodeStatus) Enum() *NodeStatus { + p := new(NodeStatus) + *p = x + return p +} + +func (x NodeStatus) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (NodeStatus) Descriptor() protoreflect.EnumDescriptor { + return file_proto_iop_node_proto_enumTypes[0].Descriptor() +} + +func (NodeStatus) Type() protoreflect.EnumType { + return &file_proto_iop_node_proto_enumTypes[0] +} + +func (x NodeStatus) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use NodeStatus.Descriptor instead. +func (NodeStatus) EnumDescriptor() ([]byte, []int) { + return file_proto_iop_node_proto_rawDescGZIP(), []int{0} +} + +// NodeInfo describes a registered node. +type NodeInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + NodeId string `protobuf:"bytes,1,opt,name=node_id,json=nodeId,proto3" json:"node_id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Address string `protobuf:"bytes,3,opt,name=address,proto3" json:"address,omitempty"` + Version string `protobuf:"bytes,4,opt,name=version,proto3" json:"version,omitempty"` + Status NodeStatus `protobuf:"varint,5,opt,name=status,proto3,enum=iop.NodeStatus" json:"status,omitempty"` + Labels map[string]string `protobuf:"bytes,6,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NodeInfo) Reset() { + *x = NodeInfo{} + mi := &file_proto_iop_node_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NodeInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NodeInfo) ProtoMessage() {} + +func (x *NodeInfo) ProtoReflect() protoreflect.Message { + mi := &file_proto_iop_node_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NodeInfo.ProtoReflect.Descriptor instead. +func (*NodeInfo) Descriptor() ([]byte, []int) { + return file_proto_iop_node_proto_rawDescGZIP(), []int{0} +} + +func (x *NodeInfo) GetNodeId() string { + if x != nil { + return x.NodeId + } + return "" +} + +func (x *NodeInfo) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *NodeInfo) GetAddress() string { + if x != nil { + return x.Address + } + return "" +} + +func (x *NodeInfo) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *NodeInfo) GetStatus() NodeStatus { + if x != nil { + return x.Status + } + return NodeStatus_NODE_STATUS_UNSPECIFIED +} + +func (x *NodeInfo) GetLabels() map[string]string { + if x != nil { + return x.Labels + } + return nil +} + +// NodeRegisterRequest is sent to the control-plane on startup. +type NodeRegisterRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Info *NodeInfo `protobuf:"bytes,1,opt,name=info,proto3" json:"info,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NodeRegisterRequest) Reset() { + *x = NodeRegisterRequest{} + mi := &file_proto_iop_node_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NodeRegisterRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NodeRegisterRequest) ProtoMessage() {} + +func (x *NodeRegisterRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_iop_node_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NodeRegisterRequest.ProtoReflect.Descriptor instead. +func (*NodeRegisterRequest) Descriptor() ([]byte, []int) { + return file_proto_iop_node_proto_rawDescGZIP(), []int{1} +} + +func (x *NodeRegisterRequest) GetInfo() *NodeInfo { + if x != nil { + return x.Info + } + return nil +} + +// NodeRegisterResponse is returned by the control-plane. +type NodeRegisterResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Accepted bool `protobuf:"varint,1,opt,name=accepted,proto3" json:"accepted,omitempty"` + Token string `protobuf:"bytes,2,opt,name=token,proto3" json:"token,omitempty"` + Reason string `protobuf:"bytes,3,opt,name=reason,proto3" json:"reason,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NodeRegisterResponse) Reset() { + *x = NodeRegisterResponse{} + mi := &file_proto_iop_node_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NodeRegisterResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NodeRegisterResponse) ProtoMessage() {} + +func (x *NodeRegisterResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_iop_node_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NodeRegisterResponse.ProtoReflect.Descriptor instead. +func (*NodeRegisterResponse) Descriptor() ([]byte, []int) { + return file_proto_iop_node_proto_rawDescGZIP(), []int{2} +} + +func (x *NodeRegisterResponse) GetAccepted() bool { + if x != nil { + return x.Accepted + } + return false +} + +func (x *NodeRegisterResponse) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + +func (x *NodeRegisterResponse) GetReason() string { + if x != nil { + return x.Reason + } + return "" +} + +var File_proto_iop_node_proto protoreflect.FileDescriptor + +const file_proto_iop_node_proto_rawDesc = "" + + "\n" + + "\x14proto/iop/node.proto\x12\x03iop\"\x82\x02\n" + + "\bNodeInfo\x12\x17\n" + + "\anode_id\x18\x01 \x01(\tR\x06nodeId\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12\x18\n" + + "\aaddress\x18\x03 \x01(\tR\aaddress\x12\x18\n" + + "\aversion\x18\x04 \x01(\tR\aversion\x12'\n" + + "\x06status\x18\x05 \x01(\x0e2\x0f.iop.NodeStatusR\x06status\x121\n" + + "\x06labels\x18\x06 \x03(\v2\x19.iop.NodeInfo.LabelsEntryR\x06labels\x1a9\n" + + "\vLabelsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"8\n" + + "\x13NodeRegisterRequest\x12!\n" + + "\x04info\x18\x01 \x01(\v2\r.iop.NodeInfoR\x04info\"`\n" + + "\x14NodeRegisterResponse\x12\x1a\n" + + "\baccepted\x18\x01 \x01(\bR\baccepted\x12\x14\n" + + "\x05token\x18\x02 \x01(\tR\x05token\x12\x16\n" + + "\x06reason\x18\x03 \x01(\tR\x06reason*t\n" + + "\n" + + "NodeStatus\x12\x1b\n" + + "\x17NODE_STATUS_UNSPECIFIED\x10\x00\x12\x16\n" + + "\x12NODE_STATUS_ONLINE\x10\x01\x12\x17\n" + + "\x13NODE_STATUS_OFFLINE\x10\x02\x12\x18\n" + + "\x14NODE_STATUS_DRAINING\x10\x03B\x13Z\x11iop/proto/gen/iopb\x06proto3" + +var ( + file_proto_iop_node_proto_rawDescOnce sync.Once + file_proto_iop_node_proto_rawDescData []byte +) + +func file_proto_iop_node_proto_rawDescGZIP() []byte { + file_proto_iop_node_proto_rawDescOnce.Do(func() { + file_proto_iop_node_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_iop_node_proto_rawDesc), len(file_proto_iop_node_proto_rawDesc))) + }) + return file_proto_iop_node_proto_rawDescData +} + +var file_proto_iop_node_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_proto_iop_node_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_proto_iop_node_proto_goTypes = []any{ + (NodeStatus)(0), // 0: iop.NodeStatus + (*NodeInfo)(nil), // 1: iop.NodeInfo + (*NodeRegisterRequest)(nil), // 2: iop.NodeRegisterRequest + (*NodeRegisterResponse)(nil), // 3: iop.NodeRegisterResponse + nil, // 4: iop.NodeInfo.LabelsEntry +} +var file_proto_iop_node_proto_depIdxs = []int32{ + 0, // 0: iop.NodeInfo.status:type_name -> iop.NodeStatus + 4, // 1: iop.NodeInfo.labels:type_name -> iop.NodeInfo.LabelsEntry + 1, // 2: iop.NodeRegisterRequest.info:type_name -> iop.NodeInfo + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_proto_iop_node_proto_init() } +func file_proto_iop_node_proto_init() { + if File_proto_iop_node_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_iop_node_proto_rawDesc), len(file_proto_iop_node_proto_rawDesc)), + NumEnums: 1, + NumMessages: 4, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proto_iop_node_proto_goTypes, + DependencyIndexes: file_proto_iop_node_proto_depIdxs, + EnumInfos: file_proto_iop_node_proto_enumTypes, + MessageInfos: file_proto_iop_node_proto_msgTypes, + }.Build() + File_proto_iop_node_proto = out.File + file_proto_iop_node_proto_goTypes = nil + file_proto_iop_node_proto_depIdxs = nil +} diff --git a/proto/gen/iop/runtime.pb.go b/proto/gen/iop/runtime.pb.go new file mode 100644 index 0000000..d839ef2 --- /dev/null +++ b/proto/gen/iop/runtime.pb.go @@ -0,0 +1,686 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v5.29.3 +// source: proto/iop/runtime.proto + +package iop + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + structpb "google.golang.org/protobuf/types/known/structpb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// RunRequest initiates a model execution. +type RunRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + RunId string `protobuf:"bytes,1,opt,name=run_id,json=runId,proto3" json:"run_id,omitempty"` + Adapter string `protobuf:"bytes,2,opt,name=adapter,proto3" json:"adapter,omitempty"` + Model string `protobuf:"bytes,3,opt,name=model,proto3" json:"model,omitempty"` + Workspace string `protobuf:"bytes,4,opt,name=workspace,proto3" json:"workspace,omitempty"` + Policy *structpb.Struct `protobuf:"bytes,5,opt,name=policy,proto3" json:"policy,omitempty"` + Input *structpb.Struct `protobuf:"bytes,6,opt,name=input,proto3" json:"input,omitempty"` + TimeoutSec int32 `protobuf:"varint,7,opt,name=timeout_sec,json=timeoutSec,proto3" json:"timeout_sec,omitempty"` + Metadata map[string]string `protobuf:"bytes,8,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RunRequest) Reset() { + *x = RunRequest{} + mi := &file_proto_iop_runtime_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RunRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RunRequest) ProtoMessage() {} + +func (x *RunRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_iop_runtime_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RunRequest.ProtoReflect.Descriptor instead. +func (*RunRequest) Descriptor() ([]byte, []int) { + return file_proto_iop_runtime_proto_rawDescGZIP(), []int{0} +} + +func (x *RunRequest) GetRunId() string { + if x != nil { + return x.RunId + } + return "" +} + +func (x *RunRequest) GetAdapter() string { + if x != nil { + return x.Adapter + } + return "" +} + +func (x *RunRequest) GetModel() string { + if x != nil { + return x.Model + } + return "" +} + +func (x *RunRequest) GetWorkspace() string { + if x != nil { + return x.Workspace + } + return "" +} + +func (x *RunRequest) GetPolicy() *structpb.Struct { + if x != nil { + return x.Policy + } + return nil +} + +func (x *RunRequest) GetInput() *structpb.Struct { + if x != nil { + return x.Input + } + return nil +} + +func (x *RunRequest) GetTimeoutSec() int32 { + if x != nil { + return x.TimeoutSec + } + return 0 +} + +func (x *RunRequest) GetMetadata() map[string]string { + if x != nil { + return x.Metadata + } + return nil +} + +// RunEvent is a streaming execution event. +type RunEvent struct { + state protoimpl.MessageState `protogen:"open.v1"` + RunId string `protobuf:"bytes,1,opt,name=run_id,json=runId,proto3" json:"run_id,omitempty"` + Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` // start | delta | complete | error + Delta string `protobuf:"bytes,3,opt,name=delta,proto3" json:"delta,omitempty"` + Message string `protobuf:"bytes,4,opt,name=message,proto3" json:"message,omitempty"` + Error string `protobuf:"bytes,5,opt,name=error,proto3" json:"error,omitempty"` + Usage *Usage `protobuf:"bytes,6,opt,name=usage,proto3" json:"usage,omitempty"` + Metadata map[string]string `protobuf:"bytes,7,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Timestamp int64 `protobuf:"varint,8,opt,name=timestamp,proto3" json:"timestamp,omitempty"` // unix nano + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RunEvent) Reset() { + *x = RunEvent{} + mi := &file_proto_iop_runtime_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RunEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RunEvent) ProtoMessage() {} + +func (x *RunEvent) ProtoReflect() protoreflect.Message { + mi := &file_proto_iop_runtime_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RunEvent.ProtoReflect.Descriptor instead. +func (*RunEvent) Descriptor() ([]byte, []int) { + return file_proto_iop_runtime_proto_rawDescGZIP(), []int{1} +} + +func (x *RunEvent) GetRunId() string { + if x != nil { + return x.RunId + } + return "" +} + +func (x *RunEvent) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *RunEvent) GetDelta() string { + if x != nil { + return x.Delta + } + return "" +} + +func (x *RunEvent) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *RunEvent) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +func (x *RunEvent) GetUsage() *Usage { + if x != nil { + return x.Usage + } + return nil +} + +func (x *RunEvent) GetMetadata() map[string]string { + if x != nil { + return x.Metadata + } + return nil +} + +func (x *RunEvent) GetTimestamp() int64 { + if x != nil { + return x.Timestamp + } + return 0 +} + +type Usage struct { + state protoimpl.MessageState `protogen:"open.v1"` + InputTokens int32 `protobuf:"varint,1,opt,name=input_tokens,json=inputTokens,proto3" json:"input_tokens,omitempty"` + OutputTokens int32 `protobuf:"varint,2,opt,name=output_tokens,json=outputTokens,proto3" json:"output_tokens,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Usage) Reset() { + *x = Usage{} + mi := &file_proto_iop_runtime_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Usage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Usage) ProtoMessage() {} + +func (x *Usage) ProtoReflect() protoreflect.Message { + mi := &file_proto_iop_runtime_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Usage.ProtoReflect.Descriptor instead. +func (*Usage) Descriptor() ([]byte, []int) { + return file_proto_iop_runtime_proto_rawDescGZIP(), []int{2} +} + +func (x *Usage) GetInputTokens() int32 { + if x != nil { + return x.InputTokens + } + return 0 +} + +func (x *Usage) GetOutputTokens() int32 { + if x != nil { + return x.OutputTokens + } + return 0 +} + +// Heartbeat is sent by both sides to keep the connection alive. +type Heartbeat struct { + state protoimpl.MessageState `protogen:"open.v1"` + Timestamp int64 `protobuf:"varint,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Heartbeat) Reset() { + *x = Heartbeat{} + mi := &file_proto_iop_runtime_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Heartbeat) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Heartbeat) ProtoMessage() {} + +func (x *Heartbeat) ProtoReflect() protoreflect.Message { + mi := &file_proto_iop_runtime_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Heartbeat.ProtoReflect.Descriptor instead. +func (*Heartbeat) Descriptor() ([]byte, []int) { + return file_proto_iop_runtime_proto_rawDescGZIP(), []int{3} +} + +func (x *Heartbeat) GetTimestamp() int64 { + if x != nil { + return x.Timestamp + } + return 0 +} + +// CancelRequest asks the node to cancel a running execution. +type CancelRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + RunId string `protobuf:"bytes,1,opt,name=run_id,json=runId,proto3" json:"run_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CancelRequest) Reset() { + *x = CancelRequest{} + mi := &file_proto_iop_runtime_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CancelRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CancelRequest) ProtoMessage() {} + +func (x *CancelRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_iop_runtime_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CancelRequest.ProtoReflect.Descriptor instead. +func (*CancelRequest) Descriptor() ([]byte, []int) { + return file_proto_iop_runtime_proto_rawDescGZIP(), []int{4} +} + +func (x *CancelRequest) GetRunId() string { + if x != nil { + return x.RunId + } + return "" +} + +// Error is returned when a request fails at the transport layer. +type Error struct { + state protoimpl.MessageState `protogen:"open.v1"` + Code string `protobuf:"bytes,1,opt,name=code,proto3" json:"code,omitempty"` + Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Error) Reset() { + *x = Error{} + mi := &file_proto_iop_runtime_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Error) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Error) ProtoMessage() {} + +func (x *Error) ProtoReflect() protoreflect.Message { + mi := &file_proto_iop_runtime_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Error.ProtoReflect.Descriptor instead. +func (*Error) Descriptor() ([]byte, []int) { + return file_proto_iop_runtime_proto_rawDescGZIP(), []int{5} +} + +func (x *Error) GetCode() string { + if x != nil { + return x.Code + } + return "" +} + +func (x *Error) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +// CapabilityRequest asks the node what adapters/models it supports. +type CapabilityRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CapabilityRequest) Reset() { + *x = CapabilityRequest{} + mi := &file_proto_iop_runtime_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CapabilityRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CapabilityRequest) ProtoMessage() {} + +func (x *CapabilityRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_iop_runtime_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CapabilityRequest.ProtoReflect.Descriptor instead. +func (*CapabilityRequest) Descriptor() ([]byte, []int) { + return file_proto_iop_runtime_proto_rawDescGZIP(), []int{6} +} + +// CapabilityResponse lists the node's available adapters. +type CapabilityResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + NodeId string `protobuf:"bytes,1,opt,name=node_id,json=nodeId,proto3" json:"node_id,omitempty"` + Adapters []*AdapterInfo `protobuf:"bytes,2,rep,name=adapters,proto3" json:"adapters,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CapabilityResponse) Reset() { + *x = CapabilityResponse{} + mi := &file_proto_iop_runtime_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CapabilityResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CapabilityResponse) ProtoMessage() {} + +func (x *CapabilityResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_iop_runtime_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CapabilityResponse.ProtoReflect.Descriptor instead. +func (*CapabilityResponse) Descriptor() ([]byte, []int) { + return file_proto_iop_runtime_proto_rawDescGZIP(), []int{7} +} + +func (x *CapabilityResponse) GetNodeId() string { + if x != nil { + return x.NodeId + } + return "" +} + +func (x *CapabilityResponse) GetAdapters() []*AdapterInfo { + if x != nil { + return x.Adapters + } + return nil +} + +type AdapterInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Models []string `protobuf:"bytes,2,rep,name=models,proto3" json:"models,omitempty"` + MaxConcurrency int32 `protobuf:"varint,3,opt,name=max_concurrency,json=maxConcurrency,proto3" json:"max_concurrency,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AdapterInfo) Reset() { + *x = AdapterInfo{} + mi := &file_proto_iop_runtime_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AdapterInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AdapterInfo) ProtoMessage() {} + +func (x *AdapterInfo) ProtoReflect() protoreflect.Message { + mi := &file_proto_iop_runtime_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AdapterInfo.ProtoReflect.Descriptor instead. +func (*AdapterInfo) Descriptor() ([]byte, []int) { + return file_proto_iop_runtime_proto_rawDescGZIP(), []int{8} +} + +func (x *AdapterInfo) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *AdapterInfo) GetModels() []string { + if x != nil { + return x.Models + } + return nil +} + +func (x *AdapterInfo) GetMaxConcurrency() int32 { + if x != nil { + return x.MaxConcurrency + } + return 0 +} + +var File_proto_iop_runtime_proto protoreflect.FileDescriptor + +const file_proto_iop_runtime_proto_rawDesc = "" + + "\n" + + "\x17proto/iop/runtime.proto\x12\x03iop\x1a\x1cgoogle/protobuf/struct.proto\"\xea\x02\n" + + "\n" + + "RunRequest\x12\x15\n" + + "\x06run_id\x18\x01 \x01(\tR\x05runId\x12\x18\n" + + "\aadapter\x18\x02 \x01(\tR\aadapter\x12\x14\n" + + "\x05model\x18\x03 \x01(\tR\x05model\x12\x1c\n" + + "\tworkspace\x18\x04 \x01(\tR\tworkspace\x12/\n" + + "\x06policy\x18\x05 \x01(\v2\x17.google.protobuf.StructR\x06policy\x12-\n" + + "\x05input\x18\x06 \x01(\v2\x17.google.protobuf.StructR\x05input\x12\x1f\n" + + "\vtimeout_sec\x18\a \x01(\x05R\n" + + "timeoutSec\x129\n" + + "\bmetadata\x18\b \x03(\v2\x1d.iop.RunRequest.MetadataEntryR\bmetadata\x1a;\n" + + "\rMetadataEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xb1\x02\n" + + "\bRunEvent\x12\x15\n" + + "\x06run_id\x18\x01 \x01(\tR\x05runId\x12\x12\n" + + "\x04type\x18\x02 \x01(\tR\x04type\x12\x14\n" + + "\x05delta\x18\x03 \x01(\tR\x05delta\x12\x18\n" + + "\amessage\x18\x04 \x01(\tR\amessage\x12\x14\n" + + "\x05error\x18\x05 \x01(\tR\x05error\x12 \n" + + "\x05usage\x18\x06 \x01(\v2\n" + + ".iop.UsageR\x05usage\x127\n" + + "\bmetadata\x18\a \x03(\v2\x1b.iop.RunEvent.MetadataEntryR\bmetadata\x12\x1c\n" + + "\ttimestamp\x18\b \x01(\x03R\ttimestamp\x1a;\n" + + "\rMetadataEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"O\n" + + "\x05Usage\x12!\n" + + "\finput_tokens\x18\x01 \x01(\x05R\vinputTokens\x12#\n" + + "\routput_tokens\x18\x02 \x01(\x05R\foutputTokens\")\n" + + "\tHeartbeat\x12\x1c\n" + + "\ttimestamp\x18\x01 \x01(\x03R\ttimestamp\"&\n" + + "\rCancelRequest\x12\x15\n" + + "\x06run_id\x18\x01 \x01(\tR\x05runId\"5\n" + + "\x05Error\x12\x12\n" + + "\x04code\x18\x01 \x01(\tR\x04code\x12\x18\n" + + "\amessage\x18\x02 \x01(\tR\amessage\"\x13\n" + + "\x11CapabilityRequest\"[\n" + + "\x12CapabilityResponse\x12\x17\n" + + "\anode_id\x18\x01 \x01(\tR\x06nodeId\x12,\n" + + "\badapters\x18\x02 \x03(\v2\x10.iop.AdapterInfoR\badapters\"b\n" + + "\vAdapterInfo\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x16\n" + + "\x06models\x18\x02 \x03(\tR\x06models\x12'\n" + + "\x0fmax_concurrency\x18\x03 \x01(\x05R\x0emaxConcurrencyB\x13Z\x11iop/proto/gen/iopb\x06proto3" + +var ( + file_proto_iop_runtime_proto_rawDescOnce sync.Once + file_proto_iop_runtime_proto_rawDescData []byte +) + +func file_proto_iop_runtime_proto_rawDescGZIP() []byte { + file_proto_iop_runtime_proto_rawDescOnce.Do(func() { + file_proto_iop_runtime_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_iop_runtime_proto_rawDesc), len(file_proto_iop_runtime_proto_rawDesc))) + }) + return file_proto_iop_runtime_proto_rawDescData +} + +var file_proto_iop_runtime_proto_msgTypes = make([]protoimpl.MessageInfo, 11) +var file_proto_iop_runtime_proto_goTypes = []any{ + (*RunRequest)(nil), // 0: iop.RunRequest + (*RunEvent)(nil), // 1: iop.RunEvent + (*Usage)(nil), // 2: iop.Usage + (*Heartbeat)(nil), // 3: iop.Heartbeat + (*CancelRequest)(nil), // 4: iop.CancelRequest + (*Error)(nil), // 5: iop.Error + (*CapabilityRequest)(nil), // 6: iop.CapabilityRequest + (*CapabilityResponse)(nil), // 7: iop.CapabilityResponse + (*AdapterInfo)(nil), // 8: iop.AdapterInfo + nil, // 9: iop.RunRequest.MetadataEntry + nil, // 10: iop.RunEvent.MetadataEntry + (*structpb.Struct)(nil), // 11: google.protobuf.Struct +} +var file_proto_iop_runtime_proto_depIdxs = []int32{ + 11, // 0: iop.RunRequest.policy:type_name -> google.protobuf.Struct + 11, // 1: iop.RunRequest.input:type_name -> google.protobuf.Struct + 9, // 2: iop.RunRequest.metadata:type_name -> iop.RunRequest.MetadataEntry + 2, // 3: iop.RunEvent.usage:type_name -> iop.Usage + 10, // 4: iop.RunEvent.metadata:type_name -> iop.RunEvent.MetadataEntry + 8, // 5: iop.CapabilityResponse.adapters:type_name -> iop.AdapterInfo + 6, // [6:6] is the sub-list for method output_type + 6, // [6:6] is the sub-list for method input_type + 6, // [6:6] is the sub-list for extension type_name + 6, // [6:6] is the sub-list for extension extendee + 0, // [0:6] is the sub-list for field type_name +} + +func init() { file_proto_iop_runtime_proto_init() } +func file_proto_iop_runtime_proto_init() { + if File_proto_iop_runtime_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_iop_runtime_proto_rawDesc), len(file_proto_iop_runtime_proto_rawDesc)), + NumEnums: 0, + NumMessages: 11, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proto_iop_runtime_proto_goTypes, + DependencyIndexes: file_proto_iop_runtime_proto_depIdxs, + MessageInfos: file_proto_iop_runtime_proto_msgTypes, + }.Build() + File_proto_iop_runtime_proto = out.File + file_proto_iop_runtime_proto_goTypes = nil + file_proto_iop_runtime_proto_depIdxs = nil +} diff --git a/proto/iop/runtime.proto b/proto/iop/runtime.proto index d1c6fd4..dbfe8d9 100644 --- a/proto/iop/runtime.proto +++ b/proto/iop/runtime.proto @@ -2,39 +2,20 @@ syntax = "proto3"; package iop; +import "google/protobuf/struct.proto"; + option go_package = "iop/proto/gen/iop"; -// Envelope wraps every IOP TCP message. -message Envelope { - uint32 protocol_version = 1; - string request_id = 2; - string run_id = 3; - string stream_id = 4; - MessageType type = 5; - bytes payload = 6; // serialized inner message -} - -enum MessageType { - MESSAGE_TYPE_UNSPECIFIED = 0; - MESSAGE_TYPE_RUN_REQUEST = 1; - MESSAGE_TYPE_RUN_EVENT = 2; - MESSAGE_TYPE_HEARTBEAT = 3; - MESSAGE_TYPE_CANCEL_REQUEST = 4; - MESSAGE_TYPE_ERROR = 5; - MESSAGE_TYPE_CAPABILITY_REQUEST = 6; - MESSAGE_TYPE_CAPABILITY_RESPONSE = 7; -} - // RunRequest initiates a model execution. message RunRequest { - string run_id = 1; - string adapter = 2; - string model = 3; - string workspace = 4; - bytes policy = 5; // JSON-encoded policy map - bytes input = 6; // JSON-encoded input map - int32 timeout_sec = 7; - map metadata = 8; + string run_id = 1; + string adapter = 2; + string model = 3; + string workspace = 4; + google.protobuf.Struct policy = 5; + google.protobuf.Struct input = 6; + int32 timeout_sec = 7; + map metadata = 8; } // RunEvent is a streaming execution event.