diff --git a/.gitignore b/.gitignore index 4f2bbf9f5..3d9f3f094 100644 --- a/.gitignore +++ b/.gitignore @@ -71,3 +71,4 @@ CODEBUDDY.md # go core binary swanlab/bin/ +core/bin/ diff --git a/core/cmd/swanlab-core/main.go b/core/cmd/swanlab-core/main.go index e5bc58adc..7b22aa862 100644 --- a/core/cmd/swanlab-core/main.go +++ b/core/cmd/swanlab-core/main.go @@ -1,16 +1,19 @@ // Command swanlab-core 是 SwanLab Go core 的进程入口。 // -// 当前为发布脚手架阶段:仅建立进程生命周期骨架——版本上报(--version)、 -// 端点监听、父进程退出监控与信号处理;gRPC 服务端(CoreService / -// CoreSyncService / ProbeService)在后续迭代中接入,届时替换 serve 循环。 +// 启动与端点约定: // -// 端点约定: +// --port-filename <路径> listen 成功后原子写入端点回报文件(SDK 发现端点依据) +// --parent-pid 监控指定的父进程 PID,父进程退出则 core 联动退出 +// --listen unix:///path/to/uds Linux/macOS 本地通信(手动调试入口) +// --listen tcp://127.0.0.1:port Windows 回环地址 // -// --listen unix:///path/to/uds Linux/macOS 进程内通信(默认路径由 Python SDK 分配) -// --listen tcp://127.0.0.1:port Windows 回环地址(uds 不可用,named pipe 支持后续提供) +// 未显式指定 --listen 时按平台自选端点: +// POSIX 优先使用 port-filename 同目录下的 core.sock(UDS),listen 失败时记录 warning 并回退至 127.0.0.1 随机回环端口; +// Windows 默认直接使用随机回环端口。 // -// 生命周期:父进程退出(process 包监控)或收到 SIGINT/SIGTERM 时优雅退出, -// 防止 Python SDK 崩溃后 core 沦为孤儿进程。 +// 生命周期管理: +// 汇集 Teardown RPC、系统信号、父进程退出监控或 Serve 异常,统一触发 Controller 的 +// 关闭序列(优先 GracefulStop,超时强制 Stop);退出时清理自己创建的 socket 与 port-file 文件。 package main import ( @@ -21,17 +24,25 @@ import ( "net" "os" "os/signal" + "path/filepath" "runtime" "strconv" "strings" "syscall" + "time" + "google.golang.org/grpc" + + "github.com/swanhubx/swanlab/core/internal/manager" "github.com/swanhubx/swanlab/core/internal/pkg/console" + "github.com/swanhubx/swanlab/core/internal/pkg/portinfo" "github.com/swanhubx/swanlab/core/internal/pkg/process" + "github.com/swanhubx/swanlab/core/internal/server" + "github.com/swanhubx/swanlab/core/internal/service" ) // version 与 commit 由构建管线通过 -ldflags -X 注入(见 core/hatch.py), -// 缺省值仅供本地 go run / go build 使用。 +// 缺省值供本地 go run / go build 使用。 var ( version = "dev" commit = "unknown" @@ -49,17 +60,30 @@ const ( exitRunError = 1 ) +// 自选端点与收尾参数。 +const ( + coreSocketName = "core.sock" + loopbackAddr = "127.0.0.1:0" + shutdownGrace = 10 * time.Second +) + func main() { os.Exit(run(os.Args[1:])) } func run(args []string) int { fs := flag.NewFlagSet("swanlab-core", flag.ContinueOnError) - printVersion := fs.Bool("version", false, "打印版本信息后退出") + printVersion := fs.Bool("version", false, "print version and exit") listenAddr := fs.String("listen", os.Getenv(envListenAddr), - "监听端点,格式 unix:// 或 tcp://<地址:端口>;Windows 仅支持 tcp:// 回环地址") + "listen endpoint, unix:// or tcp://; auto-selected per platform when unset") + portFilename := fs.String("port-filename", "", + "endpoint report file; atomically written once listen succeeds, for callers to poll") parentPID := fs.Int("parent-pid", envInt(envParentPID), - "预期父进程 PID,父进程退出时 core 随之退出;未指定时取启动瞬间的实际父进程") + "expected parent PID; core exits when the parent exits, defaults to the actual parent at startup") + detach := fs.Bool("detach", false, + "detach from parent process (reserved; accepted but ignored in this build)") + idleTimeout := fs.Duration("idle-timeout", 0, + "detached idle timeout (reserved; accepted but ignored in this build)") if err := fs.Parse(args); err != nil { if errors.Is(err, flag.ErrHelp) { return 0 @@ -71,87 +95,188 @@ func run(args []string) int { fmt.Printf("swanlab-core %s (commit %s)\n", version, commit) return 0 } - if *listenAddr == "" { - console.Error("未指定监听端点:通过 --listen 或环境变量 " + envListenAddr + " 传入") + if *detach || *idleTimeout != 0 { + console.Warning("--detach/--idle-timeout accepted but ignored: detached service is not implemented; core stays bound to the parent process") + } + if *listenAddr == "" && *portFilename == "" { + console.Error("no listen endpoint: pass --listen (manual debug) or --port-filename (SDK startup convention)") return exitUsageError } - ln, err := listen(*listenAddr) + // 自建资源记录,退出时清理自己创建的部分。 + var socketPath string + selfPID := os.Getpid() // 退出时凭它确认 port-file 属本实例 + wrotePortFile := false + defer func() { + cleanupSocket(socketPath) + if wrotePortFile { + cleanupPortFile(*portFilename, selfPID) + } + }() + + ln, err := openEndpoint(*listenAddr, *portFilename) if err != nil { - console.Error("监听失败:", err) + console.Error("listen failed:", err) return exitRunError } - defer ln.Close() + defer func() { _ = ln.Close() }() + if addr, ok := ln.Addr().(*net.UnixAddr); ok && !strings.HasPrefix(addr.Name, "@") { + socketPath = addr.Name + } - // 父进程监控:显式传入的 PID 优先(Python SDK 启动约定),未传时回退为 - // 监控启动瞬间的实际父进程(本地终端运行场景)。监控建立失败按约定终止启动。 + // 父进程监控:显式传入的 PID 生效(启动约定);未传时监控启动瞬间的 + // 实际父进程(本地终端运行场景)。监控建立失败终止启动。 pid := *parentPID if pid <= 0 { pid = os.Getppid() } parentExited, err := process.NotifyOnParentExit(pid) if err != nil { - console.Error("父进程监控建立失败,终止启动:", err) + console.Error("failed to watch parent process, aborting startup:", err) return exitRunError } + // 装配服务核心分层: + // - server.Controller: 负责 gRPC 进程宿主生命周期与优雅退出控制; + // - manager.Manager: 负责 Run 领域会话管理与路由注册; + // - service.CoreService: 负责 gRPC 请求接入与协议映射。 + grpcServer := grpc.NewServer() + ctrl := server.NewController(grpcServer, shutdownGrace) + service.NewCoreService(ctrl, manager.New()).Register(grpcServer) + + // listen 与 server 初始化成功后写 port-file。 + if *portFilename != "" { + info := portinfo.Info{Protocol: portinfo.ProtocolVersion, PID: selfPID} + switch addr := ln.Addr().(type) { + case *net.UnixAddr: + info.UnixPath = addr.Name + case *net.TCPAddr: + info.SockPort = addr.Port + default: + console.Error("unrecognized listener address type:", ln.Addr()) + return exitRunError + } + if err2 := portinfo.WriteFile(*portFilename, &info); err2 != nil { + console.Error("failed to write port-file:", err2) + return exitRunError + } + wrotePortFile = true + } + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() - console.Infof("swanlab-core %s listening on %s (parent pid %d)", version, *listenAddr, pid) + console.Infof("swanlab-core %s listening on %s (parent pid %d)", version, ln.Addr(), pid) serveErr := make(chan error, 1) go func() { - serveErr <- serve(ln) + serveErr <- grpcServer.Serve(ln) }() + exitCode := 0 + var cause string + var serveFailure error + served := false select { case <-ctx.Done(): - console.Info("收到退出信号,正在关闭") + console.Info("shutdown signal received, stopping") + cause = "signal" case <-parentExited: - console.Warning("父进程已退出,core 随之退出") - case err := <-serveErr: - if err != nil { - console.Error("监听异常退出:", err) - return exitRunError - } + console.Warning("parent process exited, stopping core") + cause = "parent-exit" + case serveFailure = <-serveErr: + served = true + cause = "serve-error" + } + ctrl.Shutdown(cause) + // Shutdown 的关闭序列会让 Serve 返回(GracefulStop 超时转 Stop); + // 上面的 select 未消费 serveErr 时,在此收取其结果。 + if !served { + serveFailure = <-serveErr + } + <-ctrl.Done() + if serveFailure != nil { + console.Error("gRPC Serve exited with error:", serveFailure) + exitCode = exitRunError } - return 0 + return exitCode } -// listen 按协议前缀创建监听器。uds 仅在非 Windows 平台可用;Windows 使用 -// TCP 回环地址兜底(named pipe 接入后在此分支扩展)。 -func listen(addr string) (net.Listener, error) { - scheme, rest, ok := strings.Cut(addr, "://") +// openEndpoint 创建监听器。显式 --listen 生效(手动调试,失败不回退, +// tcp:// 限定回环);未传时按平台自选:POSIX 尝试 port-filename 同目录 +// 下的 UDS,失败记录 warning 后回退随机回环 TCP;Windows 使用随机回环端口。 +func openEndpoint(listenAddr, portFilename string) (net.Listener, error) { + if listenAddr == "" { + if runtime.GOOS == "windows" { + return net.Listen("tcp", loopbackAddr) + } + sockPath := filepath.Join(filepath.Dir(portFilename), coreSocketName) + ln, err := net.Listen("unix", sockPath) + if err != nil { + console.Warningf("unix listen on %s failed: %v; falling back to loopback tcp %s", sockPath, err, loopbackAddr) + return net.Listen("tcp", loopbackAddr) + } + return ln, nil + } + scheme, rest, ok := strings.Cut(listenAddr, "://") if !ok { - return nil, fmt.Errorf("监听端点缺少协议前缀(unix:// 或 tcp://): %s", addr) + return nil, fmt.Errorf("listen endpoint missing scheme prefix (unix:// or tcp://): %s", listenAddr) } switch scheme { case "unix": if runtime.GOOS == "windows" { - return nil, errors.New("windows 平台不支持 unix:// 端点,请使用 tcp://127.0.0.1:<端口>") + return nil, errors.New("unix:// endpoints are not supported on Windows; use tcp://127.0.0.1:") } return net.Listen("unix", rest) case "tcp": + if err := requireLoopbackHost(rest); err != nil { + return nil, err + } return net.Listen("tcp", rest) default: - return nil, fmt.Errorf("不支持的监听协议 %q(仅 unix:// 或 tcp://)", scheme) + return nil, fmt.Errorf("unsupported listen scheme %q (only unix:// or tcp://)", scheme) } } -// serve 接受连接后立即关闭。脚手架阶段仅验证端点连通性, -// gRPC 服务端就绪后由此接入 serve 逻辑。 -func serve(ln net.Listener) error { - for { - conn, err := ln.Accept() - if err != nil { - if errors.Is(err, net.ErrClosed) { - return nil - } - return err - } - _ = conn.Close() +// cleanupSocket 删除自己创建的 UDS socket 文件;Linux 抽象 socket(@ 前缀) +// 不占文件系统,无需清理。 +func cleanupSocket(path string) { + if path == "" || strings.HasPrefix(path, "@") { + return + } + _ = os.Remove(path) +} + +// cleanupPortFile 在 pid 匹配时删除 port-file。同一路径被后启动实例 +// 覆盖后,本实例退出不得误删他者文件;文件缺失、损坏或 pid 不匹配时不删。 +func cleanupPortFile(path string, pid int) { + if path == "" || pid <= 0 { + return + } + info, err := portinfo.ParseFile(path) + if err != nil { + return + } + if info.PID != pid { + return + } + _ = os.Remove(path) +} + +// requireLoopbackHost 限制显式 tcp:// 监听地址为回环,避免服务暴露到 +// 网络;放行 127.0.0.1/::1/localhost。 +func requireLoopbackHost(addr string) error { + host, _, err := net.SplitHostPort(addr) + if err != nil { + return fmt.Errorf("parse tcp listen address %q: %w", addr, err) + } + if host == "localhost" { + return nil + } + if ip := net.ParseIP(host); ip != nil && ip.IsLoopback() { + return nil } + return fmt.Errorf("tcp listen address %q is not loopback; use 127.0.0.1 or ::1", addr) } // envInt 解析整型环境变量,缺失或非法时返回 0。 diff --git a/core/internal/manager/manager.go b/core/internal/manager/manager.go new file mode 100644 index 000000000..6a7b05bf8 --- /dev/null +++ b/core/internal/manager/manager.go @@ -0,0 +1,99 @@ +package manager + +import ( + "crypto/rand" + "encoding/base64" + "errors" + "fmt" + "sync" +) + +var ( + // ErrEmptyHandle 表示未提供有效的 run_handle。 + ErrEmptyHandle = errors.New("run_handle must not be empty") + // ErrRunNotFound 表示请求的 run_handle 不存在或已被确认释放。 + ErrRunNotFound = errors.New("unknown or released run_handle") + // ErrRunNotFinished 表示会话尚未标记结束,不能提前确认释放。 + ErrRunNotFinished = errors.New("run finish has not been delivered for this handle") +) + +// Manager 维护 run_handle 到会话的映射注册表,并统一编排 Run 级的生命周期。 +// +// 实现方式: +// 采用读写锁保护 sessions 映射表;所有针对会话的操作与释放均保证并发安全与原子性, +// 杜绝误释放未完成会话或产生竞态问题。 +type Manager struct { + mu sync.RWMutex + sessions map[string]*Session +} + +// New 创建并初始化会话管理器实例。 +func New() *Manager { + return &Manager{sessions: make(map[string]*Session)} +} + +// Start 创建新的 Run 会话,并返回随机生成的安全路由句柄(run_handle)。 +func (m *Manager) Start() (string, error) { + buf := make([]byte, 32) + if _, err := rand.Read(buf); err != nil { + return "", fmt.Errorf("generate run handle: %w", err) + } + handle := base64.RawURLEncoding.EncodeToString(buf) + + m.mu.Lock() + defer m.mu.Unlock() + if _, exists := m.sessions[handle]; exists { + return "", errors.New("run handle collision") + } + m.sessions[handle] = newSession() + return handle, nil +} + +// Finish 幂等标记指定会话为结束状态。 +// 获取注册表读锁定位会话并在会话锁内更新状态,保证状态变更期间会话不被并发 Confirm 移除。 +func (m *Manager) Finish(handle string) error { + if handle == "" { + return ErrEmptyHandle + } + m.mu.RLock() + defer m.mu.RUnlock() + s, ok := m.sessions[handle] + if !ok { + return ErrRunNotFound + } + s.finish() + return nil +} + +// State 获取指定会话的当前生命周期状态快照。 +func (m *Manager) State(handle string) (RunState, error) { + if handle == "" { + return 0, ErrEmptyHandle + } + m.mu.RLock() + defer m.mu.RUnlock() + s, ok := m.sessions[handle] + if !ok { + return 0, ErrRunNotFound + } + return s.stateSnapshot(), nil +} + +// Confirm 原子校验会话是否已完成并释放会话资源。 +// 若会话尚未完成,则拒绝释放并保留会话,供调用方后续重试。 +func (m *Manager) Confirm(handle string) error { + if handle == "" { + return ErrEmptyHandle + } + m.mu.Lock() + defer m.mu.Unlock() + s, ok := m.sessions[handle] + if !ok { + return ErrRunNotFound + } + if !s.finished() { + return ErrRunNotFinished + } + delete(m.sessions, handle) + return nil +} diff --git a/core/internal/manager/manager_test.go b/core/internal/manager/manager_test.go new file mode 100644 index 000000000..936b4258f --- /dev/null +++ b/core/internal/manager/manager_test.go @@ -0,0 +1,127 @@ +package manager + +import ( + "errors" + "fmt" + "sync" + "testing" +) + +func TestManagerLifecycle(t *testing.T) { + m := New() + handle, err := m.Start() + if err != nil || handle == "" { + t.Fatalf("Start: handle=%q err=%v", handle, err) + } + if state, err := m.State(handle); err != nil || state != RunStateRunning { + t.Fatalf("State after start: state=%v err=%v", state, err) + } + if err := m.Confirm(handle); !errors.Is(err, ErrRunNotFinished) { + t.Fatalf("early Confirm err=%v, want ErrRunNotFinished", err) + } + if err := m.Finish(handle); err != nil { + t.Fatalf("Finish: %v", err) + } + if state, err := m.State(handle); err != nil || state != RunStateFinished { + t.Fatalf("State after finish: state=%v err=%v", state, err) + } + if err := m.Confirm(handle); err != nil { + t.Fatalf("Confirm: %v", err) + } + if _, err := m.State(handle); !errors.Is(err, ErrRunNotFound) { + t.Fatalf("State after confirm err=%v, want ErrRunNotFound", err) + } +} + +func TestFinishConfirmRace(t *testing.T) { + m := New() + for i := 0; i < 20; i++ { + handle, err := m.Start() + if err != nil { + t.Fatal(err) + } + var wg sync.WaitGroup + var finishErr, confirmErr error + wg.Add(2) + go func() { + defer wg.Done() + finishErr = m.Finish(handle) + }() + go func() { + defer wg.Done() + confirmErr = m.Confirm(handle) + }() + wg.Wait() + if finishErr != nil { + t.Fatalf("iteration %d Finish: %v", i, finishErr) + } + if confirmErr != nil && !errors.Is(confirmErr, ErrRunNotFinished) { + t.Fatalf("iteration %d Confirm: %v", i, confirmErr) + } + if confirmErr != nil { + if err := m.Confirm(handle); err != nil { + t.Fatalf("iteration %d retry Confirm: %v", i, err) + } + } + } +} + +func TestRunIsolation(t *testing.T) { + m := New() + handleA, err := m.Start() + if err != nil { + t.Fatal(err) + } + handleB, err := m.Start() + if err != nil { + t.Fatal(err) + } + if handleA == handleB { + t.Fatal("two runs must receive distinct handles") + } + if err := m.Finish(handleA); err != nil { + t.Fatal(err) + } + if state, _ := m.State(handleA); state != RunStateFinished { + t.Fatalf("run A state=%v, want Finished", state) + } + if state, _ := m.State(handleB); state != RunStateRunning { + t.Fatalf("run B state=%v, want Running", state) + } +} + +func TestConcurrentRuns(t *testing.T) { + m := New() + const clients = 4 + var wg sync.WaitGroup + errs := make(chan error, clients) + for i := 0; i < clients; i++ { + wg.Add(1) + go func(id int) { + defer wg.Done() + handle, err := m.Start() + if err != nil { + errs <- fmt.Errorf("client %d start: %w", id, err) + return + } + for j := 0; j < 20; j++ { + if _, err = m.State(handle); err != nil { + errs <- fmt.Errorf("client %d state: %w", id, err) + return + } + } + if err = m.Finish(handle); err != nil { + errs <- fmt.Errorf("client %d finish: %w", id, err) + return + } + if err = m.Confirm(handle); err != nil { + errs <- fmt.Errorf("client %d confirm: %w", id, err) + } + }(i) + } + wg.Wait() + close(errs) + for err := range errs { + t.Error(err) + } +} diff --git a/core/internal/manager/session.go b/core/internal/manager/session.go new file mode 100644 index 000000000..00aec8fcf --- /dev/null +++ b/core/internal/manager/session.go @@ -0,0 +1,45 @@ +// Package manager 负责维护 swanlab-core 的 Run 会话与业务编排逻辑。 +// +// 该包与具体传输协议无关(不依赖 gRPC/Protobuf),专职管理 Run 级状态机与会话路由。 +package manager + +import "sync" + +// RunState 表示单个实验会话的内部生命周期状态。 +type RunState int32 + +const ( + // RunStateRunning 会话正在运行中。 + RunStateRunning RunState = iota + // RunStateFinished 会话已标记完成。 + RunStateFinished +) + +// Session 保存单个 run_handle 对应的会话状态与生命周期数据。 +type Session struct { + mu sync.Mutex + state RunState +} + +func newSession() *Session { + return &Session{state: RunStateRunning} +} + +// finish 幂等标记会话已结束。 +func (s *Session) finish() { + s.mu.Lock() + defer s.mu.Unlock() + s.state = RunStateFinished +} + +// stateSnapshot 返回当前会话状态的并发安全快照。 +func (s *Session) stateSnapshot() RunState { + s.mu.Lock() + defer s.mu.Unlock() + return s.state +} + +// finished 检查会话是否已完成。 +func (s *Session) finished() bool { + return s.stateSnapshot() == RunStateFinished +} diff --git a/core/internal/pkg/portinfo/portinfo.go b/core/internal/pkg/portinfo/portinfo.go new file mode 100644 index 000000000..29da155e4 --- /dev/null +++ b/core/internal/pkg/portinfo/portinfo.go @@ -0,0 +1,262 @@ +// Package portinfo 实现 swanlab-core 端口信息文件(port-file)的读写。 +// +// port-file 是 core 服务在 listen 成功后向调用方回报监听端点的约定文件, +// 承担临时 endpoint discovery。文件格式(v1)为若干行 key=value 文本, +// 以独立的 EOF 行结尾: +// +// protocol=1 +// pid= (诊断与 stale 检查辅助) +// unix=/short/private/runtime/core.sock (POSIX 平台) +// sock=12345 (Windows 或 UDS 回退,替代 unix 行) +// EOF +// +// 写入经同目录临时文件 + fsync + chmod(0600) + rename 原子提交;解析拒绝 +// 重复 key、未知 key、未知协议版本、缺失字段、非法端口或 pid、非 EOF 结尾 +// 和超长内容。 +package portinfo + +import ( + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strconv" + "strings" +) + +// 协议与格式约束。 +const ( + // ProtocolVersion 是当前 port-file 格式版本。 + ProtocolVersion = 1 + // MaxFileSize 是 port-file 内容长度上限,超出视为损坏或恶意构造。 + MaxFileSize = 4096 + // maxPID 是 pid 字段的取值上限。 + maxPID = 1<<31 - 1 + // maxUnixPathLen 限定 unix 端点路径长度,实际可用长度还受 sun_path 限制。 + maxUnixPathLen = 256 + // filePerm 是 port-file 的 owner-only 权限。 + filePerm = 0o600 + // eofMarker 是文件结尾标记,独占一行。 + eofMarker = "EOF" +) + +// 字段名约定,解析与序列化共用。 +const ( + keyProtocol = "protocol" + keyUnix = "unix" + keySock = "sock" + keyPID = "pid" +) + +// Info 是 port-file 的结构化内容,UnixPath 与 SockPort 二选一。 +type Info struct { + Protocol int + UnixPath string + SockPort int + PID int +} + +// Marshal 将 Info 序列化为 v1 格式字节串,序列化前完成校验。 +func Marshal(info *Info) ([]byte, error) { + if err := validate(info); err != nil { + return nil, err + } + var b strings.Builder + fmt.Fprintf(&b, "%s=%d\n", keyProtocol, info.Protocol) + fmt.Fprintf(&b, "%s=%d\n", keyPID, info.PID) + if info.UnixPath != "" { + fmt.Fprintf(&b, "%s=%s\n", keyUnix, info.UnixPath) + } else { + fmt.Fprintf(&b, "%s=%d\n", keySock, info.SockPort) + } + b.WriteString(eofMarker + "\n") + return []byte(b.String()), nil +} + +// WriteFile 原子写入 port-file:先写同目录临时文件,fsync、chmod 后 rename 覆盖目标。 +// 任一步失败不破坏既有目标文件,临时文件会被清理。 +func WriteFile(path string, info *Info) error { + data, err := Marshal(info) + if err != nil { + return err + } + dir := filepath.Dir(path) + tmp, err := os.CreateTemp(dir, ".portinfo-*") + if err != nil { + return fmt.Errorf("create temp port-file in %s: %w", dir, err) + } + tmpName := tmp.Name() + renamed := false + defer func() { + if !renamed { + _ = os.Remove(tmpName) + } + }() + if _, err := tmp.Write(data); err != nil { + _ = tmp.Close() + return fmt.Errorf("write temp port-file: %w", err) + } + _ = tmp.Sync() + if err := tmp.Close(); err != nil { + return fmt.Errorf("close temp port-file: %w", err) + } + if err := os.Chmod(tmpName, filePerm); err != nil { + return fmt.Errorf("chmod temp port-file: %w", err) + } + if err := os.Rename(tmpName, path); err != nil { + return fmt.Errorf("commit port-file: %w", err) + } + renamed = true + syncDir(dir) + return nil +} + +// ParseFile 读取并解析 port-file。文件超过 MaxFileSize 时按损坏处理。 +func ParseFile(path string) (Info, error) { + f, err := os.Open(path) + if err != nil { + return Info{}, fmt.Errorf("open port-file: %w", err) + } + defer func() { _ = f.Close() }() + data, err := io.ReadAll(io.LimitReader(f, MaxFileSize+1)) + if err != nil { + return Info{}, fmt.Errorf("read port-file: %w", err) + } + return Parse(data) +} + +// Parse 解析 v1 格式内容。键值行顺序不限,每个字段出现一次, +// 以独立 EOF 行结尾(末尾换行可选)。 +func Parse(data []byte) (Info, error) { + if len(data) > MaxFileSize { + return Info{}, fmt.Errorf("port-file exceeds %d bytes", MaxFileSize) + } + content := strings.TrimSuffix(string(data), "\n") + if !strings.HasSuffix(content, "\n"+eofMarker) && content != eofMarker { + return Info{}, errors.New("port-file must end with an EOF line") + } + body := strings.TrimSuffix(content, "\n"+eofMarker) + if content == eofMarker { + body = "" + } + + var info Info + seen := make(map[string]bool, 4) + for _, line := range strings.Split(body, "\n") { + if line == "" { + return Info{}, errors.New("port-file contains empty line") + } + key, value, ok := strings.Cut(line, "=") + if !ok { + return Info{}, fmt.Errorf("port-file line is not key=value: %q", line) + } + if seen[key] { + return Info{}, fmt.Errorf("port-file contains duplicate key %q", key) + } + switch key { + case keyProtocol: + if value != strconv.Itoa(ProtocolVersion) { + return Info{}, fmt.Errorf("unsupported port-file protocol %q", value) + } + info.Protocol = ProtocolVersion + case keyUnix: + if !strings.HasPrefix(value, "/") || len(value) > maxUnixPathLen { + return Info{}, errors.New("unix endpoint must be an absolute path within length limit") + } + info.UnixPath = value + case keySock: + port, err := parsePort(value) + if err != nil { + return Info{}, err + } + info.SockPort = port + case keyPID: + pid, err := parsePID(value) + if err != nil { + return Info{}, err + } + info.PID = pid + default: + return Info{}, fmt.Errorf("port-file contains unknown key %q", key) + } + seen[key] = true + } + + if !seen[keyProtocol] { + return Info{}, fmt.Errorf("port-file missing %q field", keyProtocol) + } + if !seen[keyPID] { + return Info{}, fmt.Errorf("port-file missing %q field", keyPID) + } + if seen[keyUnix] == seen[keySock] { + return Info{}, fmt.Errorf("port-file must contain exactly one of %q or %q", keyUnix, keySock) + } + return info, nil +} + +// validate 校验 Info 的字段约束。 +func validate(info *Info) error { + if info.Protocol != ProtocolVersion { + return fmt.Errorf("unsupported port-file protocol %d", info.Protocol) + } + if info.PID < 1 || info.PID > maxPID { + return fmt.Errorf("pid %d out of range", info.PID) + } + hasUnix := info.UnixPath != "" + hasSock := info.SockPort != 0 + if hasUnix == hasSock { + return fmt.Errorf("exactly one of unix path or sock port must be set") + } + if hasUnix { + if !strings.HasPrefix(info.UnixPath, "/") || len(info.UnixPath) > maxUnixPathLen { + return fmt.Errorf("unix socket path must be absolute and at most %d bytes", maxUnixPathLen) + } + } else if info.SockPort < 1 || info.SockPort > 65535 { + return fmt.Errorf("sock port %d out of range", info.SockPort) + } + return nil +} + +// parsePID 解析十进制 pid:数字、无前导零、范围 1-maxPID。 +func parsePID(value string) (int, error) { + if value == "" || strings.HasPrefix(value, "0") || !isDigits(value) { + return 0, fmt.Errorf("invalid pid %q", value) + } + pid, err := strconv.Atoi(value) + if err != nil || pid < 1 || pid > maxPID { + return 0, fmt.Errorf("invalid pid %q", value) + } + return pid, nil +} + +// parsePort 解析十进制端口号:数字、无前导零、范围 1-65535。 +func parsePort(value string) (int, error) { + if value == "" || strings.HasPrefix(value, "0") || !isDigits(value) { + return 0, fmt.Errorf("invalid sock port %q", value) + } + port, err := strconv.Atoi(value) + if err != nil || port < 1 || port > 65535 { + return 0, fmt.Errorf("invalid sock port %q", value) + } + return port, nil +} + +func isDigits(s string) bool { + for _, r := range s { + if r < '0' || r > '9' { + return false + } + } + return true +} + +// syncDir 持久化目录项,使 rename 结果落盘;失败不影响写入结果。 +func syncDir(dir string) { + d, err := os.Open(dir) + if err != nil { + return + } + defer func() { _ = d.Close() }() + _ = d.Sync() +} diff --git a/core/internal/pkg/portinfo/portinfo_test.go b/core/internal/pkg/portinfo/portinfo_test.go new file mode 100644 index 000000000..7688b77f2 --- /dev/null +++ b/core/internal/pkg/portinfo/portinfo_test.go @@ -0,0 +1,194 @@ +package portinfo + +import ( + "os" + "path/filepath" + "runtime" + "strings" + "testing" +) + +func validUnixContent() string { + return "protocol=1\npid=4242\nunix=/tmp/swanlab/core.sock\nEOF\n" +} + +func TestMarshalUnixFormat(t *testing.T) { + data, err := Marshal(&Info{Protocol: 1, PID: 4242, UnixPath: "/tmp/swanlab/core.sock"}) + if err != nil { + t.Fatalf("Marshal: %v", err) + } + if got := string(data); got != validUnixContent() { + t.Fatalf("Marshal output mismatch:\n got: %q\nwant: %q", got, validUnixContent()) + } +} + +func TestMarshalSockFormat(t *testing.T) { + data, err := Marshal(&Info{Protocol: 1, PID: 4242, SockPort: 12345}) + if err != nil { + t.Fatalf("Marshal: %v", err) + } + want := "protocol=1\npid=4242\nsock=12345\nEOF\n" + if got := string(data); got != want { + t.Fatalf("Marshal output mismatch:\n got: %q\nwant: %q", got, want) + } +} + +func TestWriteAndParseRoundTrip(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "core.port") + want := Info{Protocol: 1, PID: 4242, UnixPath: "/tmp/swanlab/core.sock"} + err := WriteFile(path, &want) + if err != nil { + t.Fatalf("WriteFile: %v", err) + } + got, err := ParseFile(path) + if err != nil { + t.Fatalf("ParseFile: %v", err) + } + if got != want { + t.Fatalf("round trip mismatch: got %+v, want %+v", got, want) + } + if runtime.GOOS != "windows" { + var info os.FileInfo + info, err = os.Stat(path) + if err != nil { + t.Fatalf("Stat: %v", err) + } + if perm := info.Mode().Perm(); perm != filePerm { + t.Fatalf("port-file perm = %o, want %o", perm, filePerm) + } + } + // 原子写入不应遗留临时文件 + var entries []os.DirEntry + entries, err = os.ReadDir(dir) + if err != nil { + t.Fatalf("ReadDir: %v", err) + } + if len(entries) != 1 { + t.Fatalf("runtime dir has %d entries after write, want 1", len(entries)) + } +} + +func TestWriteFileAtomicallyReplaces(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "core.port") + first := Info{Protocol: 1, PID: 4242, UnixPath: "/tmp/a.sock"} + if err := WriteFile(path, &first); err != nil { + t.Fatalf("first WriteFile: %v", err) + } + second := Info{Protocol: 1, PID: 4343, UnixPath: "/tmp/b.sock"} + if err := WriteFile(path, &second); err != nil { + t.Fatalf("second WriteFile: %v", err) + } + got, err := ParseFile(path) + if err != nil { + t.Fatalf("ParseFile: %v", err) + } + if got != second { + t.Fatalf("after replace got %+v, want %+v", got, second) + } + entries, err := os.ReadDir(dir) + if err != nil { + t.Fatalf("ReadDir: %v", err) + } + if len(entries) != 1 { + t.Fatalf("runtime dir has %d entries after replace, want 1", len(entries)) + } +} + +func TestWriteFileRejectsInvalidInfo(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "core.port") + cases := map[string]Info{ + "bad protocol": {Protocol: 2, PID: 4242, UnixPath: "/tmp/a.sock"}, + "both endings": {Protocol: 1, PID: 4242, UnixPath: "/tmp/a.sock", SockPort: 80}, + "no endpoint": {Protocol: 1, PID: 4242}, + "relative path": {Protocol: 1, PID: 4242, UnixPath: "tmp/a.sock"}, + "port range": {Protocol: 1, PID: 4242, SockPort: 65536}, + "pid zero": {Protocol: 1, PID: 0, UnixPath: "/tmp/a.sock"}, + "pid negative": {Protocol: 1, PID: -1, UnixPath: "/tmp/a.sock"}, + "pid over max": {Protocol: 1, PID: maxPID + 1, UnixPath: "/tmp/a.sock"}, + } + for name, info := range cases { + if err := WriteFile(path, &info); err == nil { + t.Fatalf("%s: WriteFile unexpectedly succeeded", name) + } + } + if _, err := os.Stat(path); !os.IsNotExist(err) { + t.Fatalf("invalid info must not create target file, stat err = %v", err) + } + entries, err := os.ReadDir(dir) + if err != nil { + t.Fatalf("ReadDir: %v", err) + } + if len(entries) != 0 { + t.Fatalf("invalid info left %d temp files, want 0", len(entries)) + } +} + +func TestParseAcceptsValidVariants(t *testing.T) { + // EOF 行末尾可无换行 + content := strings.TrimSuffix(validUnixContent(), "\n") + if _, err := Parse([]byte(content)); err != nil { + t.Fatalf("parse without trailing newline: %v", err) + } + // 键值行顺序不影响解析 + reordered := "unix=/tmp/swanlab/core.sock\npid=4242\nprotocol=1\nEOF\n" + if _, err := Parse([]byte(reordered)); err != nil { + t.Fatalf("parse reordered lines: %v", err) + } + // sock 端点(Windows 或 UDS 回退形态) + sockContent := "protocol=1\npid=4242\nsock=12345\nEOF\n" + info, err := Parse([]byte(sockContent)) + if err != nil { + t.Fatalf("parse sock content: %v", err) + } + if info.SockPort != 12345 || info.UnixPath != "" { + t.Fatalf("sock parse got %+v", info) + } +} + +func TestParseRejectsMalformed(t *testing.T) { + cases := map[string]string{ + "missing EOF": "protocol=1\npid=4242\nunix=/tmp/a.sock\n", + "EOF not own line": "protocol=1\npid=4242\nunix=/tmp/a.sockEOF\n", + "content after EOF": validUnixContent() + "extra\n", + "torn write": "protocol=1\npid=4242\n", + "empty body": "EOF\n", + "empty line": "protocol=1\n\npid=4242\nunix=/tmp/a.sock\nEOF\n", + "not key value": "protocol=1\npid\nunix=/tmp/a.sock\nEOF\n", + "unknown key": "protocol=1\npid=4242\nunix=/tmp/a.sock\nextra=1\nEOF\n", + "legacy mode key": "protocol=1\npid=4242\nmode=owner\nunix=/tmp/a.sock\nEOF\n", + "legacy auth key": "protocol=1\npid=4242\nunix=/tmp/a.sock\nauth_token=abc\nEOF\n", + "duplicate key": "protocol=1\nprotocol=1\npid=4242\nunix=/tmp/a.sock\nEOF\n", + "unknown protocol": "protocol=2\npid=4242\nunix=/tmp/a.sock\nEOF\n", + "non numeric protocol": "protocol=abc\npid=4242\nunix=/tmp/a.sock\nEOF\n", + "missing protocol": "pid=4242\nunix=/tmp/a.sock\nEOF\n", + "missing pid": "protocol=1\nunix=/tmp/a.sock\nEOF\n", + "missing endpoint": "protocol=1\npid=4242\nEOF\n", + "both endpoints": "protocol=1\npid=4242\nunix=/tmp/a.sock\nsock=12345\nEOF\n", + "relative unix path": "protocol=1\npid=4242\nunix=tmp/a.sock\nEOF\n", + "oversize unix path": "protocol=1\npid=4242\nunix=/" + strings.Repeat("a", maxUnixPathLen) + "\nEOF\n", + "port zero": "protocol=1\npid=4242\nsock=0\nEOF\n", + "port range": "protocol=1\npid=4242\nsock=65536\nEOF\n", + "port leading zero": "protocol=1\npid=4242\nsock=01234\nEOF\n", + "port not digits": "protocol=1\npid=4242\nsock=12a45\nEOF\n", + "pid zero": "protocol=1\npid=0\nunix=/tmp/a.sock\nEOF\n", + "pid negative": "protocol=1\npid=-1\nunix=/tmp/a.sock\nEOF\n", + "pid leading zero": "protocol=1\npid=04242\nunix=/tmp/a.sock\nEOF\n", + "pid not digits": "protocol=1\npid=42a42\nunix=/tmp/a.sock\nEOF\n", + "pid out of range": "protocol=1\npid=2147483648\nunix=/tmp/a.sock\nEOF\n", + } + for name, content := range cases { + if _, err := Parse([]byte(content)); err == nil { + t.Fatalf("%s: Parse unexpectedly succeeded", name) + } + } +} + +func TestParseRejectsOversizeContent(t *testing.T) { + content := "protocol=1\npid=4242\nunix=/tmp/" + strings.Repeat("a", MaxFileSize) + "\nEOF\n" + if _, err := Parse([]byte(content)); err == nil { + t.Fatal("Parse unexpectedly succeeded for oversize content") + } +} diff --git a/core/internal/pkg/process/parent_integration_test.go b/core/internal/pkg/process/parent_integration_test.go index 8cc862f20..c92df665b 100644 --- a/core/internal/pkg/process/parent_integration_test.go +++ b/core/internal/pkg/process/parent_integration_test.go @@ -6,8 +6,10 @@ import ( "os" "os/exec" "path/filepath" + "runtime" "strconv" "strings" + "syscall" "testing" "time" @@ -119,6 +121,173 @@ func runMonitoredChildHelper() { os.Exit(0) } +// TestNotifyOnParentExitSurvivesSpawnThreadExit 验证进程级父死兜底(G5): +// +// 场景:父进程从非主线程 spawn 被监控子进程,该线程先退出(触发一次 PDEATHSIG +// 伪通知,必须被 PPID 复查过滤),父进程本体随后才退出。PDEATHSIG 绑定的是 +// spawn 线程,伪通知之后不再有信号,子进程只能依赖 pidfd/PPID 轮询兜底感知 +// 父进程退出,且不能因伪通知提前退出。 +// +// 三级结构与 TestNotifyOnParentExitNotifiesCaller 一致: +// 测试进程 -> parent helper(从 LockOSThread 的 goroutine spawn child)-> monitored child。 +func TestNotifyOnParentExitSurvivesSpawnThreadExit(t *testing.T) { + if runtime.GOOS != "linux" { + t.Skip("PDEATHSIG spawn-thread semantics are Linux-specific") + } + + switch os.Getenv(parentExitHelperMode) { + case "thread-parent": + runThreadParentHelper() + case "thread-child": + runThreadMonitoredChildHelper() + } + + helperDir := t.TempDir() + cmd := exec.Command(os.Args[0], "-test.run=^TestNotifyOnParentExitSurvivesSpawnThreadExit$") + cmd.Env = append( + os.Environ(), + parentExitHelperMode+"=thread-parent", + "SWANLAB_PARENT_EXIT_DIR="+helperDir, + ) + cmd.Stderr = os.Stderr + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + + // 1. child 就绪且 spawn 线程已退出后,child 不得因 PDEATHSIG 伪通知提前退出。 + if err := waitForMarker(filepath.Join(helperDir, "thread-dead"), 5*time.Second); err != nil { + killHelperProcess(t, filepath.Join(helperDir, "child.pid")) + t.Fatalf("wait for spawn thread exit: %v", err) + } + childPID, err := readPIDFile(filepath.Join(helperDir, "child.pid")) + if err != nil { + t.Fatalf("read child pid: %v", err) + } + if !processAlive(childPID) { + killHelperProcess(t, filepath.Join(helperDir, "child.pid")) + t.Fatal("child exited prematurely after spawn thread death (spurious PDEATHSIG not filtered)") + } + + // 2. parent helper 自行退出(thread-parent 模式在 thread-dead 后 os.Exit(0)), + // child 必须在兜底监控预算内感知并退出。 + if err := cmd.Wait(); err != nil { + t.Fatalf("thread parent helper failed: %v", err) + } + // parent 已退出但可能尚未被内核重新托管,轮询等待 child 退出。 + deadline := time.Now().Add(10 * time.Second) + for time.Now().Before(deadline) { + if !processAlive(childPID) { + return // child 正确随父进程退出 + } + time.Sleep(50 * time.Millisecond) + } + t.Fatal("child did not exit after parent process death (process-level fallback failed)") +} + +// runThreadParentHelper 在专用 OS 线程上 spawn child,随后结束该 goroutine +// 使线程销毁(等价于"Python 从非主线程 spawn 且该线程先退出"),确认 child +// 存活后退出自身,验证 child 的进程级父死监控。 +func runThreadParentHelper() { + helperDir := os.Getenv("SWANLAB_PARENT_EXIT_DIR") + type result struct { + pid int + err error + } + ch := make(chan result, 1) + go func() { + // 锁定 OS 线程:本 goroutine 返回时线程被 runtime 终止, + // 内核随之向 child 发送一次 PDEATHSIG 伪通知。 + runtime.LockOSThread() + cmd := exec.Command(os.Args[0], "-test.run=^TestNotifyOnParentExitSurvivesSpawnThreadExit$") + cmd.Env = append( + os.Environ(), + parentExitHelperMode+"=thread-child", + "SWANLAB_PARENT_EXIT_DIR="+helperDir, + ) + cmd.Stderr = os.Stderr + if err := cmd.Start(); err != nil { + ch <- result{err: err} + return + } + ch <- result{pid: cmd.Process.Pid} + // goroutine 返回 -> 锁定线程销毁 -> PDEATHSIG 伪通知 + }() + res := <-ch + if res.err != nil { + fmt.Fprintf(os.Stderr, "start monitored child: %v\n", res.err) + os.Exit(2) + } + if err := os.WriteFile(filepath.Join(helperDir, "child.pid"), []byte(strconv.Itoa(res.pid)), 0o600); err != nil { + fmt.Fprintf(os.Stderr, "write child PID: %v\n", err) + os.Exit(2) + } + // 等 child 建立监控(ready 标记),再给线程销毁留出传播时间。 + if err := waitForMarkerFile(filepath.Join(helperDir, "ready"), 5*time.Second); err != nil { + fmt.Fprintf(os.Stderr, "monitored child did not become ready: %v\n", err) + os.Exit(2) + } + time.Sleep(300 * time.Millisecond) + if err := os.WriteFile(filepath.Join(helperDir, "thread-dead"), nil, 0o600); err != nil { + fmt.Fprintf(os.Stderr, "mark thread dead: %v\n", err) + os.Exit(2) + } + // 覆盖至少一个轮询周期,证明 child 在线程死亡 + 伪通知后仍然存活。 + time.Sleep(1500 * time.Millisecond) + os.Exit(0) +} + +// runThreadMonitoredChildHelper 与 runMonitoredChildHelper 类似, +// 但在父进程退出后写 exited 标记再退出,供测试区分"随父退出"与"提前退出"。 +func runThreadMonitoredChildHelper() { + helperDir := os.Getenv("SWANLAB_PARENT_EXIT_DIR") + parentExited, err := NotifyOnParentExit(os.Getppid()) + if err != nil { + fmt.Fprintf(os.Stderr, "monitor parent: %v\n", err) + os.Exit(2) + } + if err := os.WriteFile(filepath.Join(helperDir, "ready"), nil, 0o600); err != nil { + fmt.Fprintf(os.Stderr, "mark child ready: %v\n", err) + os.Exit(2) + } + // process 包只负责发出通知;由调用方决定收到通知后的退出方式。 + <-parentExited + _ = os.WriteFile(filepath.Join(helperDir, "exited"), nil, 0o600) + os.Exit(0) +} + +func readPIDFile(path string) (int, error) { + data, err := os.ReadFile(path) + if err != nil { + return 0, err + } + return strconv.Atoi(string(data)) +} + +// processAlive 通过 signal 0 探测进程是否存在(不含僵尸态判断, +// 测试场景中进程由测试进程树的子孙构成,不会被长期悬挂为僵尸)。 +func processAlive(pid int) bool { + proc, err := os.FindProcess(pid) + if err != nil { + return false + } + return proc.Signal(syscall.Signal(0)) == nil +} + +func waitForMarker(path string, timeout time.Duration) error { + return waitForMarkerFile(path, timeout) +} + +func waitForMarkerFile(path string, timeout time.Duration) error { + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + if _, err := os.Stat(path); err == nil { + return nil + } + time.Sleep(10 * time.Millisecond) + } + return fmt.Errorf("marker %s not found within %s", path, timeout) +} + func killHelperProcess(t *testing.T, pidPath string) { data, err := os.ReadFile(pidPath) if err != nil { diff --git a/core/internal/pkg/process/parent_linux.go b/core/internal/pkg/process/parent_linux.go index ba47e8365..b2c0a80a1 100644 --- a/core/internal/pkg/process/parent_linux.go +++ b/core/internal/pkg/process/parent_linux.go @@ -6,42 +6,101 @@ import ( "fmt" "os" "os/signal" + "sync" + "time" "golang.org/x/sys/unix" "github.com/swanhubx/swanlab/core/internal/pkg/console" ) +// parentPollInterval 是进程级兜底监控的轮询间隔:pidfd 不可用时退化为 +// 低频 PID/PPID 轮询,在退出响应速度和常驻唤醒开销之间取平衡。 +const parentPollInterval = time.Second + func notifyOnParentExit(parentPID int) (<-chan struct{}, error) { parentExited := make(chan struct{}) + var closeOnce sync.Once + notify := func() { + closeOnce.Do(func() { + console.Debugf("Parent process %d exited", parentPID) + close(parentExited) + }) + } + + // 快路径:PR_SET_PDEATHSIG 绑定的是调用 prctl 时的父进程线程。 + // 若 Python 从非主线程 spawn 且该线程先于进程退出,PDEATHSIG 只会触发一次 + // 伪通知,由下方的 PPID 复查过滤;此后不会再有信号,必须依赖进程级兜底。 + // SIGUSR1 保留给父死监控,signal.Notify 接管后不再触发默认终止行为, + // 其他组件不得复用该信号。 parentDeathSignals := make(chan os.Signal, 1) signal.Notify(parentDeathSignals, unix.SIGUSR1) - - // PR_SET_PDEATHSIG 绑定的是调用 prctl 时的父进程,而不是任意指定 PID。 - // 外层和下方的 PID 校验共同保证该父进程就是调用方预期的 parentPID。 if err := unix.Prctl(unix.PR_SET_PDEATHSIG, uintptr(unix.SIGUSR1), 0, 0, 0); err != nil { signal.Stop(parentDeathSignals) return nil, fmt.Errorf("set parent death signal: %w", err) } - // 父进程可能恰好在外层首次检查与 prctl 调用之间退出,必须再次确认。 if err := checkParent(parentPID); err != nil { signal.Stop(parentDeathSignals) return nil, err } - go func() { defer signal.Stop(parentDeathSignals) for range parentDeathSignals { - // SIGUSR1 也可能来自其他进程;只有父 PID 改变才视为父进程退出。 + // SIGUSR1 也可能来自其他进程或已退出的 spawn 线程; + // 只有父 PID 改变才视为父进程退出。 if checkParent(parentPID) == nil { continue } - console.Debugf("Parent process %d exited", parentPID) - close(parentExited) + notify() return } }() + // 进程级兜底:监控父进程本身而非创建本进程的线程。 + go func() { + watchParentProcess(parentPID) + notify() + }() + return parentExited, nil } + +// watchParentProcess 阻塞直到父进程退出。优先使用 pidfd(内核级通知), +// 不可用或中途出错时退化为低频 PPID 轮询。 +func watchParentProcess(parentPID int) { + if fd, err := unix.PidfdOpen(parentPID, 0); err == nil { + defer func() { _ = unix.Close(fd) }() + fds := []unix.PollFd{{Fd: int32(fd), Events: unix.POLLIN}} + for { + n, err := unix.Poll(fds, -1) + if err == unix.EINTR { + continue + } + if err != nil { + // pidfd 等待异常(如被信号打断外的错误),退化为轮询兜底。 + break + } + if n > 0 { + return // 进程退出,pidfd 可读 + } + } + } + pollParentExit(parentPID) +} + +// pollParentExit 以固定间隔检查 PPID 是否改变;父进程存活期间 PPID 保持不变, +// 整个进程退出后被重新托管(init/subreaper),PPID 随之改变。 +// goroutine 启动前后存在极短竞态窗口,进入等待循环前先立即检查一次。 +func pollParentExit(parentPID int) { + if checkParent(parentPID) != nil { + return + } + ticker := time.NewTicker(parentPollInterval) + defer ticker.Stop() + for range ticker.C { + if checkParent(parentPID) != nil { + return + } + } +} diff --git a/core/internal/server/controller.go b/core/internal/server/controller.go new file mode 100644 index 000000000..bafe98f72 --- /dev/null +++ b/core/internal/server/controller.go @@ -0,0 +1,72 @@ +// Package server 负责管理本地 gRPC 服务的宿主环境与生命周期。 +// +// 职责: +// - 管理服务级生命周期状态(NotReady、Ready、Stopping、Closed); +// - 统一仲裁服务退出流程,支持优雅退出与超时兜底强制关闭。 +package server + +import ( + "sync" + "time" + + "google.golang.org/grpc" + + "github.com/swanhubx/swanlab/core/internal/pkg/console" +) + +// Controller 负责协调服务的统一关闭路径,并管理服务生命周期。 +// +// 实现方式: +// 汇集 Teardown RPC、系统信号、父进程退出及 Serve 异常等所有退出来源; +// 通过 sync.Once 保证幂等执行:先触发 GracefulStop 尝试优雅退出, +// 超时后强制调用 Stop 兜底,确保进程可靠结束。 +type Controller struct { + server *grpc.Server + grace time.Duration + once sync.Once + done chan struct{} + lc *Lifecycle +} + +// NewController 创建服务控制器,grace 为优雅退出的等待上限。 +func NewController(g *grpc.Server, grace time.Duration) *Controller { + return &Controller{ + server: g, + grace: grace, + done: make(chan struct{}), + lc: newLifecycle(), + } +} + +// Lifecycle 返回由本控制器管理的服务生命周期状态机。 +func (c *Controller) Lifecycle() *Lifecycle { + return c.lc +} + +// Shutdown 触发服务关闭流程,操作具备幂等性;cause 仅用于日志标识退出来源。 +func (c *Controller) Shutdown(cause string) { + c.once.Do(func() { + c.lc.BeginStopping() + console.Infof("core service shutting down (%s)", cause) + go func() { + defer close(c.done) + graceful := make(chan struct{}) + go func() { + c.server.GracefulStop() + close(graceful) + }() + select { + case <-graceful: + case <-time.After(c.grace): + c.server.Stop() + <-graceful + } + c.lc.Close() + }() + }) +} + +// Done 在关闭序列完成后关闭,供调用方等待收尾结束。 +func (c *Controller) Done() <-chan struct{} { + return c.done +} diff --git a/core/internal/server/controller_test.go b/core/internal/server/controller_test.go new file mode 100644 index 000000000..8fc30914c --- /dev/null +++ b/core/internal/server/controller_test.go @@ -0,0 +1,48 @@ +package server + +import ( + "testing" + "time" + + "google.golang.org/grpc" +) + +const testGrace = 2 * time.Second + +func TestControllerShutdownIdempotent(t *testing.T) { + g := grpc.NewServer() + ctrl := NewController(g, testGrace) + ctrl.Shutdown("first") + ctrl.Shutdown("second") + select { + case <-ctrl.Done(): + case <-time.After(2 * time.Second): + t.Fatal("controller Done not closed after Shutdown") + } + <-ctrl.Done() + if st := ctrl.Lifecycle().Get(); st != StateClosed { + t.Fatalf("lifecycle state after shutdown = %v, want StateClosed", st) + } +} + +func TestLifecycleStateTransitions(t *testing.T) { + lc := newLifecycle() + if lc.Get() != StateNotReady { + t.Fatalf("initial state = %v, want StateNotReady", lc.Get()) + } + if !lc.Spinup() || lc.Get() != StateReady { + t.Fatalf("spinup state = %v, want StateReady", lc.Get()) + } + if !lc.Spinup() { + t.Fatal("repeated Spinup in READY must succeed") + } + lc.BeginStopping() + if lc.Get() != StateStopping || lc.Spinup() { + t.Fatalf("stopping state = %v, Spinup must be rejected", lc.Get()) + } + lc.Close() + lc.Close() + if lc.Get() != StateClosed { + t.Fatalf("closed state = %v, want StateClosed", lc.Get()) + } +} diff --git a/core/internal/server/lifecycle.go b/core/internal/server/lifecycle.go new file mode 100644 index 000000000..9451d8e1c --- /dev/null +++ b/core/internal/server/lifecycle.go @@ -0,0 +1,76 @@ +package server + +import "sync" + +// ServiceState 表示 gRPC 服务的进程级生命周期状态。 +type ServiceState int32 + +const ( + // StateNotReady 服务已监听但尚未完成握手(SpinupService),拒绝业务 RPC。 + StateNotReady ServiceState = iota + // StateReady 服务已就绪,可正常接受和处理业务请求。 + StateReady + // StateStopping 服务正在执行退出流程。 + StateStopping + // StateClosed 服务已完全关闭。 + StateClosed +) + +// Lifecycle 维护并发安全的服务生命周期状态机。 +// +// 状态流转: +// - StateNotReady -> StateReady:通过 Spinup 触发,Ready 状态下重复调用幂等成功; +// - StateNotReady / StateReady -> StateStopping:服务开始关闭(Shutdown)时进入; +// - StateStopping -> StateClosed:服务收尾完成后进入。 +type Lifecycle struct { + mu sync.RWMutex + state ServiceState +} + +func newLifecycle() *Lifecycle { + return &Lifecycle{state: StateNotReady} +} + +// Get 返回当前的服务生命周期状态。 +func (l *Lifecycle) Get() ServiceState { + l.mu.RLock() + defer l.mu.RUnlock() + return l.state +} + +// Ready 检查服务当前是否处于就绪(StateReady)状态。 +func (l *Lifecycle) Ready() bool { + return l.Get() == StateReady +} + +// Spinup 将服务状态置为 StateReady。 +// 在 StateReady 状态下重复调用幂等返回 true;若服务已处于退出或关闭中,则返回 false。 +func (l *Lifecycle) Spinup() bool { + l.mu.Lock() + defer l.mu.Unlock() + switch l.state { + case StateNotReady: + l.state = StateReady + return true + case StateReady: + return true + default: + return false + } +} + +// BeginStopping 将服务置为 StateStopping 退出状态,多次调用幂等。 +func (l *Lifecycle) BeginStopping() { + l.mu.Lock() + defer l.mu.Unlock() + if l.state == StateNotReady || l.state == StateReady { + l.state = StateStopping + } +} + +// Close 将服务置为 StateClosed 状态,标识服务已完全关闭。 +func (l *Lifecycle) Close() { + l.mu.Lock() + defer l.mu.Unlock() + l.state = StateClosed +} diff --git a/core/internal/service/core_service.go b/core/internal/service/core_service.go new file mode 100644 index 000000000..b2bafa8e8 --- /dev/null +++ b/core/internal/service/core_service.go @@ -0,0 +1,189 @@ +// Package service 提供 swanlab-core 的 gRPC 协议接入层实现。 +// +// 职责: +// - 实现 gRPC 契约(CoreServiceServer),校验并解析 Protobuf 请求; +// - 拦截未就绪服务的业务调用,前置统一做状态检查; +// - 将内部领域错误(manager)映射为标准的 gRPC 状态码(InvalidArgument、NotFound 等); +// - 将服务退出指令委派给 internal/server 控制器执行。 +package service + +import ( + "context" + "errors" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" + + "github.com/swanhubx/swanlab/core/internal/manager" + "github.com/swanhubx/swanlab/core/internal/server" + corev1 "github.com/swanhubx/swanlab/core/proto/swanlab/grpc/core/v1" + operationv1 "github.com/swanhubx/swanlab/core/proto/swanlab/operation/v1" +) + +// CoreService 实现生成的 CoreServiceServer 契约,作为 gRPC 请求的接入网关。 +type CoreService struct { + corev1.UnimplementedCoreServiceServer + + controller *server.Controller + lifecycle *server.Lifecycle + manager *manager.Manager +} + +// NewCoreService 创建 gRPC 接入服务实例,关联服务生命周期控制器与 Run 会话管理器。 +func NewCoreService(controller *server.Controller, runManager *manager.Manager) *CoreService { + return &CoreService{ + controller: controller, + lifecycle: controller.Lifecycle(), + manager: runManager, + } +} + +// Register 将当前服务注册到指定的 gRPC Server。 +func (s *CoreService) Register(g *grpc.Server) { + corev1.RegisterCoreServiceServer(g, s) +} + +// requireReady 检查服务是否处于就绪状态,未就绪时返回 FailedPrecondition 错误。 +func (s *CoreService) requireReady() error { + if s.lifecycle.Ready() { + return nil + } + return status.Error(codes.FailedPrecondition, "service is not ready; call SpinupService first") +} + +// rejectUpsert 统一拦截数据上传 RPC,在底层存储与传输通道接入前明确返回 Unimplemented。 +func (s *CoreService) rejectUpsert() error { + if err := s.requireReady(); err != nil { + return err + } + return status.Error(codes.Unimplemented, "upsert data path is not implemented in this core build") +} + +// SpinupService 执行服务启动握手,将服务状态置为就绪(Ready)。 +func (s *CoreService) SpinupService( + _ context.Context, + _ *corev1.SpinupServiceRequest, +) (*corev1.SpinupServiceResponse, error) { + if !s.lifecycle.Spinup() { + return nil, status.Error(codes.FailedPrecondition, "service is stopping or closed") + } + return &corev1.SpinupServiceResponse{}, nil +} + +// TeardownService 请求关闭整个 Core 服务,异步触发控制器的关闭流程。 +func (s *CoreService) TeardownService( + _ context.Context, + _ *corev1.TeardownServiceRequest, +) (*corev1.TeardownServiceResponse, error) { + s.controller.Shutdown("teardown") + return &corev1.TeardownServiceResponse{}, nil +} + +// DeliverRunStart 接收并校验启动记录,创建新的 Run 会话并返回对应的 run_handle。 +func (s *CoreService) DeliverRunStart( + _ context.Context, + req *corev1.DeliverRunStartRequest, +) (*corev1.DeliverRunStartResponse, error) { + if err := s.requireReady(); err != nil { + return nil, err + } + if req.GetStartRecord() == nil { + return nil, status.Error(codes.InvalidArgument, "start_record must not be nil") + } + handle, err := s.manager.Start() + if err != nil { + return nil, status.Errorf(codes.Internal, "start run session: %v", err) + } + return &corev1.DeliverRunStartResponse{ + Success: true, + Run: req.GetStartRecord(), + NewExperiment: true, + RunHandle: handle, + }, nil +} + +func (s *CoreService) UpsertColumns(context.Context, *corev1.UpsertColumnsRequest) (*emptypb.Empty, error) { + return nil, s.rejectUpsert() +} + +func (s *CoreService) UpsertScalars(context.Context, *corev1.UpsertScalarsRequest) (*emptypb.Empty, error) { + return nil, s.rejectUpsert() +} + +func (s *CoreService) UpsertMedia(context.Context, *corev1.UpsertMediaRequest) (*emptypb.Empty, error) { + return nil, s.rejectUpsert() +} + +func (s *CoreService) UpsertLogs(context.Context, *corev1.UpsertLogsRequest) (*emptypb.Empty, error) { + return nil, s.rejectUpsert() +} + +func (s *CoreService) UpsertSaves(context.Context, *corev1.UpsertSavesRequest) (*emptypb.Empty, error) { + return nil, s.rejectUpsert() +} + +// DeliverRunFinish 标记指定 run_handle 的会话结束。 +func (s *CoreService) DeliverRunFinish( + _ context.Context, + req *corev1.DeliverRunFinishRequest, +) (*corev1.DeliverRunFinishResponse, error) { + if err := s.requireReady(); err != nil { + return nil, err + } + if err := s.manager.Finish(req.GetRunHandle()); err != nil { + return nil, managerStatus(err) + } + return &corev1.DeliverRunFinishResponse{Success: true}, nil +} + +// GetOperationStats 查询指定会话的当前运行与排空状态。 +func (s *CoreService) GetOperationStats( + _ context.Context, + req *corev1.GetOperationStatsRequest, +) (*corev1.GetOperationStatsResponse, error) { + if err := s.requireReady(); err != nil { + return nil, err + } + runState, err := s.manager.State(req.GetRunHandle()) + if err != nil { + return nil, managerStatus(err) + } + state := operationv1.CoreState_CORE_STATE_RUNNING + if runState == manager.RunStateFinished { + state = operationv1.CoreState_CORE_STATE_FINISHED + } + return &corev1.GetOperationStatsResponse{ + Success: true, + Stats: &operationv1.OperationStats{State: state}, + }, nil +} + +// ConfirmRunFinish 确认会话已完成并释放相关资源。 +func (s *CoreService) ConfirmRunFinish( + _ context.Context, + req *corev1.ConfirmRunFinishRequest, +) (*corev1.ConfirmRunFinishResponse, error) { + if err := s.requireReady(); err != nil { + return nil, err + } + if err := s.manager.Confirm(req.GetRunHandle()); err != nil { + return nil, managerStatus(err) + } + return &corev1.ConfirmRunFinishResponse{Success: true}, nil +} + +// managerStatus 将 manager 层的领域错误转换为对应的 gRPC 状态码。 +func managerStatus(err error) error { + switch { + case errors.Is(err, manager.ErrEmptyHandle): + return status.Error(codes.InvalidArgument, err.Error()) + case errors.Is(err, manager.ErrRunNotFound): + return status.Error(codes.NotFound, err.Error()) + case errors.Is(err, manager.ErrRunNotFinished): + return status.Error(codes.FailedPrecondition, err.Error()) + default: + return status.Error(codes.Internal, err.Error()) + } +} diff --git a/core/internal/service/core_service_test.go b/core/internal/service/core_service_test.go new file mode 100644 index 000000000..ca0d3dee3 --- /dev/null +++ b/core/internal/service/core_service_test.go @@ -0,0 +1,203 @@ +package service + +import ( + "context" + "net" + "testing" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/status" + "google.golang.org/grpc/test/bufconn" + + "github.com/swanhubx/swanlab/core/internal/manager" + "github.com/swanhubx/swanlab/core/internal/server" + corev1 "github.com/swanhubx/swanlab/core/proto/swanlab/grpc/core/v1" + operationv1 "github.com/swanhubx/swanlab/core/proto/swanlab/operation/v1" + runv1 "github.com/swanhubx/swanlab/core/proto/swanlab/run/v1" +) + +const ( + bufconnSize = 1 << 20 + callTimeout = 2 * time.Second + noShutdownWindow = 200 * time.Millisecond +) + +type testEnv struct { + client corev1.CoreServiceClient + ctrl *server.Controller +} + +func newTestEnv(t *testing.T) *testEnv { + t.Helper() + g := grpc.NewServer() + ctrl := server.NewController(g, 2*time.Second) + NewCoreService(ctrl, manager.New()).Register(g) + lis := bufconn.Listen(bufconnSize) + go func() { _ = g.Serve(lis) }() + conn, err := grpc.NewClient( + "passthrough:///bufnet", + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { + return lis.DialContext(context.Background()) + }), + ) + if err != nil { + t.Fatalf("grpc.NewClient: %v", err) + } + t.Cleanup(func() { + _ = conn.Close() + ctrl.Shutdown("test-cleanup") + <-ctrl.Done() + }) + return &testEnv{client: corev1.NewCoreServiceClient(conn), ctrl: ctrl} +} + +func callCtx(t *testing.T) context.Context { + t.Helper() + ctx, cancel := context.WithTimeout(context.Background(), callTimeout) + t.Cleanup(cancel) + return ctx +} + +func spinup(t *testing.T, env *testEnv) { + t.Helper() + if _, err := env.client.SpinupService(callCtx(t), &corev1.SpinupServiceRequest{}); err != nil { + t.Fatalf("SpinupService: %v", err) + } +} + +func startRun(t *testing.T, env *testEnv) string { + t.Helper() + resp, err := env.client.DeliverRunStart(callCtx(t), &corev1.DeliverRunStartRequest{ + StartRecord: &runv1.StartRecord{}, + }) + if err != nil || !resp.GetSuccess() || resp.GetRunHandle() == "" { + t.Fatalf("DeliverRunStart: resp=%v err=%v", resp, err) + } + return resp.GetRunHandle() +} + +func TestSpinupAndTeardown(t *testing.T) { + env := newTestEnv(t) + spinup(t, env) + spinup(t, env) + if _, err := env.client.TeardownService(callCtx(t), &corev1.TeardownServiceRequest{}); err != nil { + t.Fatalf("TeardownService: %v", err) + } + select { + case <-env.ctrl.Done(): + case <-time.After(callTimeout): + t.Fatal("shutdown not completed after teardown") + } +} + +func TestRunRPCsRejectedBeforeReady(t *testing.T) { + env := newTestEnv(t) + ctx := callCtx(t) + for name, call := range map[string]func() error{ + "start": func() error { + _, err := env.client.DeliverRunStart(ctx, &corev1.DeliverRunStartRequest{StartRecord: &runv1.StartRecord{}}) + return err + }, + "upsert": func() error { + _, err := env.client.UpsertScalars(ctx, &corev1.UpsertScalarsRequest{RunHandle: "any"}) + return err + }, + "stats": func() error { + _, err := env.client.GetOperationStats(ctx, &corev1.GetOperationStatsRequest{RunHandle: "any"}) + return err + }, + "finish": func() error { + _, err := env.client.DeliverRunFinish(ctx, &corev1.DeliverRunFinishRequest{RunHandle: "any"}) + return err + }, + "confirm": func() error { + _, err := env.client.ConfirmRunFinish(ctx, &corev1.ConfirmRunFinishRequest{RunHandle: "any"}) + return err + }, + } { + if err := call(); status.Code(err) != codes.FailedPrecondition { + t.Fatalf("%s before ready: err=%v, want FailedPrecondition", name, err) + } + } +} + +func TestUpsertsRemainUnimplemented(t *testing.T) { + env := newTestEnv(t) + spinup(t, env) + handle := startRun(t, env) + ctx := callCtx(t) + calls := []func() error{ + func() error { + _, err := env.client.UpsertColumns(ctx, &corev1.UpsertColumnsRequest{RunHandle: handle}) + return err + }, + func() error { + _, err := env.client.UpsertScalars(ctx, &corev1.UpsertScalarsRequest{RunHandle: handle}) + return err + }, + func() error { + _, err := env.client.UpsertMedia(ctx, &corev1.UpsertMediaRequest{RunHandle: handle}) + return err + }, + func() error { + _, err := env.client.UpsertLogs(ctx, &corev1.UpsertLogsRequest{RunHandle: handle}) + return err + }, + func() error { + _, err := env.client.UpsertSaves(ctx, &corev1.UpsertSavesRequest{RunHandle: handle}) + return err + }, + } + for i, call := range calls { + if err := call(); status.Code(err) != codes.Unimplemented { + t.Fatalf("upsert %d err=%v, want Unimplemented", i, err) + } + } +} + +func TestRunLifecycleAndErrorMapping(t *testing.T) { + env := newTestEnv(t) + spinup(t, env) + ctx := callCtx(t) + + if _, err := env.client.DeliverRunStart(ctx, &corev1.DeliverRunStartRequest{}); status.Code(err) != codes.InvalidArgument { + t.Fatalf("missing start_record err=%v, want InvalidArgument", err) + } + if _, err := env.client.GetOperationStats(ctx, &corev1.GetOperationStatsRequest{}); status.Code(err) != codes.InvalidArgument { + t.Fatalf("empty handle err=%v, want InvalidArgument", err) + } + if _, err := env.client.GetOperationStats(ctx, &corev1.GetOperationStatsRequest{RunHandle: "missing"}); status.Code(err) != codes.NotFound { + t.Fatalf("unknown handle err=%v, want NotFound", err) + } + + handle := startRun(t, env) + resp, err := env.client.GetOperationStats(ctx, &corev1.GetOperationStatsRequest{RunHandle: handle}) + if err != nil || resp.GetStats().GetState() != operationv1.CoreState_CORE_STATE_RUNNING { + t.Fatalf("running stats: resp=%v err=%v", resp, err) + } + if _, err = env.client.ConfirmRunFinish(ctx, &corev1.ConfirmRunFinishRequest{RunHandle: handle}); status.Code(err) != codes.FailedPrecondition { + t.Fatalf("early confirm err=%v, want FailedPrecondition", err) + } + if _, err = env.client.DeliverRunFinish(ctx, &corev1.DeliverRunFinishRequest{RunHandle: handle}); err != nil { + t.Fatalf("DeliverRunFinish: %v", err) + } + resp, err = env.client.GetOperationStats(ctx, &corev1.GetOperationStatsRequest{RunHandle: handle}) + if err != nil || resp.GetStats().GetState() != operationv1.CoreState_CORE_STATE_FINISHED { + t.Fatalf("finished stats: resp=%v err=%v", resp, err) + } + if _, err := env.client.ConfirmRunFinish(ctx, &corev1.ConfirmRunFinishRequest{RunHandle: handle}); err != nil { + t.Fatalf("ConfirmRunFinish: %v", err) + } + select { + case <-env.ctrl.Done(): + t.Fatal("ConfirmRunFinish must not stop the server") + case <-time.After(noShutdownWindow): + } + if _, err := env.client.GetOperationStats(ctx, &corev1.GetOperationStatsRequest{RunHandle: handle}); status.Code(err) != codes.NotFound { + t.Fatalf("released handle err=%v, want NotFound", err) + } +} diff --git a/core/proto/swanlab/grpc/core/v1/core.pb.go b/core/proto/swanlab/grpc/core/v1/core.pb.go index 46d2a3dde..31457e422 100644 --- a/core/proto/swanlab/grpc/core/v1/core.pb.go +++ b/core/proto/swanlab/grpc/core/v1/core.pb.go @@ -29,6 +29,154 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +// SpinupService 请求。无身份字段:core 面向同一本地信任域,不做应用层鉴权。 +type SpinupServiceRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SpinupServiceRequest) Reset() { + *x = SpinupServiceRequest{} + mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SpinupServiceRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SpinupServiceRequest) ProtoMessage() {} + +func (x *SpinupServiceRequest) ProtoReflect() protoreflect.Message { + mi := &file_swanlab_grpc_core_v1_core_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 SpinupServiceRequest.ProtoReflect.Descriptor instead. +func (*SpinupServiceRequest) Descriptor() ([]byte, []int) { + return file_swanlab_grpc_core_v1_core_proto_rawDescGZIP(), []int{0} +} + +// SpinupService 响应。无字段,返回即表示服务已 READY。 +type SpinupServiceResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SpinupServiceResponse) Reset() { + *x = SpinupServiceResponse{} + mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SpinupServiceResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SpinupServiceResponse) ProtoMessage() {} + +func (x *SpinupServiceResponse) ProtoReflect() protoreflect.Message { + mi := &file_swanlab_grpc_core_v1_core_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 SpinupServiceResponse.ProtoReflect.Descriptor instead. +func (*SpinupServiceResponse) Descriptor() ([]byte, []int) { + return file_swanlab_grpc_core_v1_core_proto_rawDescGZIP(), []int{1} +} + +// TeardownService 请求。无身份字段,同 SpinupServiceRequest。 +type TeardownServiceRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TeardownServiceRequest) Reset() { + *x = TeardownServiceRequest{} + mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TeardownServiceRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TeardownServiceRequest) ProtoMessage() {} + +func (x *TeardownServiceRequest) ProtoReflect() protoreflect.Message { + mi := &file_swanlab_grpc_core_v1_core_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 TeardownServiceRequest.ProtoReflect.Descriptor instead. +func (*TeardownServiceRequest) Descriptor() ([]byte, []int) { + return file_swanlab_grpc_core_v1_core_proto_rawDescGZIP(), []int{2} +} + +// TeardownService 响应。 +type TeardownServiceResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TeardownServiceResponse) Reset() { + *x = TeardownServiceResponse{} + mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TeardownServiceResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TeardownServiceResponse) ProtoMessage() {} + +func (x *TeardownServiceResponse) ProtoReflect() protoreflect.Message { + mi := &file_swanlab_grpc_core_v1_core_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 TeardownServiceResponse.ProtoReflect.Descriptor instead. +func (*TeardownServiceResponse) Descriptor() ([]byte, []int) { + return file_swanlab_grpc_core_v1_core_proto_rawDescGZIP(), []int{3} +} + // DeliverRunStart 请求,生命周期函数,运行正式开始 type DeliverRunStartRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -40,7 +188,7 @@ type DeliverRunStartRequest struct { func (x *DeliverRunStartRequest) Reset() { *x = DeliverRunStartRequest{} - mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[0] + mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -52,7 +200,7 @@ func (x *DeliverRunStartRequest) String() string { func (*DeliverRunStartRequest) ProtoMessage() {} func (x *DeliverRunStartRequest) ProtoReflect() protoreflect.Message { - mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[0] + mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -65,7 +213,7 @@ func (x *DeliverRunStartRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeliverRunStartRequest.ProtoReflect.Descriptor instead. func (*DeliverRunStartRequest) Descriptor() ([]byte, []int) { - return file_swanlab_grpc_core_v1_core_proto_rawDescGZIP(), []int{0} + return file_swanlab_grpc_core_v1_core_proto_rawDescGZIP(), []int{4} } func (x *DeliverRunStartRequest) GetCoreSettings() *v1.CoreSettings { @@ -93,13 +241,14 @@ type DeliverRunStartResponse struct { GlobalStep int64 `protobuf:"varint,6,opt,name=global_step,json=globalStep,proto3" json:"global_step,omitempty"` // 起始全局步数 GlobalSystemStep int64 `protobuf:"varint,7,opt,name=global_system_step,json=globalSystemStep,proto3" json:"global_system_step,omitempty"` // 起始全局系统步数 NewExperiment bool `protobuf:"varint,8,opt,name=new_experiment,json=newExperiment,proto3" json:"new_experiment,omitempty"` // 是否是新实验 + RunHandle string `protobuf:"bytes,9,opt,name=run_handle,json=runHandle,proto3" json:"run_handle,omitempty"` // 服务端生成的 opaque 会话标识,后续所有 run 级 RPC 必须携带 unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *DeliverRunStartResponse) Reset() { *x = DeliverRunStartResponse{} - mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[1] + mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -111,7 +260,7 @@ func (x *DeliverRunStartResponse) String() string { func (*DeliverRunStartResponse) ProtoMessage() {} func (x *DeliverRunStartResponse) ProtoReflect() protoreflect.Message { - mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[1] + mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -124,7 +273,7 @@ func (x *DeliverRunStartResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeliverRunStartResponse.ProtoReflect.Descriptor instead. func (*DeliverRunStartResponse) Descriptor() ([]byte, []int) { - return file_swanlab_grpc_core_v1_core_proto_rawDescGZIP(), []int{1} + return file_swanlab_grpc_core_v1_core_proto_rawDescGZIP(), []int{5} } func (x *DeliverRunStartResponse) GetSuccess() bool { @@ -183,17 +332,25 @@ func (x *DeliverRunStartResponse) GetNewExperiment() bool { return false } +func (x *DeliverRunStartResponse) GetRunHandle() string { + if x != nil { + return x.RunHandle + } + return "" +} + // UpsertColumns 请求,异步插入一批列定义 type UpsertColumnsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - Columns []*v12.ColumnRecord `protobuf:"bytes,1,rep,name=columns,proto3" json:"columns,omitempty"` // 列定义列表 + Columns []*v12.ColumnRecord `protobuf:"bytes,1,rep,name=columns,proto3" json:"columns,omitempty"` // 列定义列表 + RunHandle string `protobuf:"bytes,2,opt,name=run_handle,json=runHandle,proto3" json:"run_handle,omitempty"` // 目标 run 会话标识 unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *UpsertColumnsRequest) Reset() { *x = UpsertColumnsRequest{} - mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[2] + mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -205,7 +362,7 @@ func (x *UpsertColumnsRequest) String() string { func (*UpsertColumnsRequest) ProtoMessage() {} func (x *UpsertColumnsRequest) ProtoReflect() protoreflect.Message { - mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[2] + mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -218,7 +375,7 @@ func (x *UpsertColumnsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpsertColumnsRequest.ProtoReflect.Descriptor instead. func (*UpsertColumnsRequest) Descriptor() ([]byte, []int) { - return file_swanlab_grpc_core_v1_core_proto_rawDescGZIP(), []int{2} + return file_swanlab_grpc_core_v1_core_proto_rawDescGZIP(), []int{6} } func (x *UpsertColumnsRequest) GetColumns() []*v12.ColumnRecord { @@ -228,17 +385,25 @@ func (x *UpsertColumnsRequest) GetColumns() []*v12.ColumnRecord { return nil } +func (x *UpsertColumnsRequest) GetRunHandle() string { + if x != nil { + return x.RunHandle + } + return "" +} + // UpsertScalars 请求,异步插入一批标量数据 type UpsertScalarsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - Data []*v13.ScalarRecord `protobuf:"bytes,1,rep,name=data,proto3" json:"data,omitempty"` // 标量记录列表 + Data []*v13.ScalarRecord `protobuf:"bytes,1,rep,name=data,proto3" json:"data,omitempty"` // 标量记录列表 + RunHandle string `protobuf:"bytes,2,opt,name=run_handle,json=runHandle,proto3" json:"run_handle,omitempty"` // 目标 run 会话标识 unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *UpsertScalarsRequest) Reset() { *x = UpsertScalarsRequest{} - mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[3] + mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -250,7 +415,7 @@ func (x *UpsertScalarsRequest) String() string { func (*UpsertScalarsRequest) ProtoMessage() {} func (x *UpsertScalarsRequest) ProtoReflect() protoreflect.Message { - mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[3] + mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -263,7 +428,7 @@ func (x *UpsertScalarsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpsertScalarsRequest.ProtoReflect.Descriptor instead. func (*UpsertScalarsRequest) Descriptor() ([]byte, []int) { - return file_swanlab_grpc_core_v1_core_proto_rawDescGZIP(), []int{3} + return file_swanlab_grpc_core_v1_core_proto_rawDescGZIP(), []int{7} } func (x *UpsertScalarsRequest) GetData() []*v13.ScalarRecord { @@ -273,17 +438,25 @@ func (x *UpsertScalarsRequest) GetData() []*v13.ScalarRecord { return nil } +func (x *UpsertScalarsRequest) GetRunHandle() string { + if x != nil { + return x.RunHandle + } + return "" +} + // UpsertMedia 请求,异步插入一批媒体数据 type UpsertMediaRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - Data []*v13.MediaRecord `protobuf:"bytes,1,rep,name=data,proto3" json:"data,omitempty"` // 媒体记录列表 + Data []*v13.MediaRecord `protobuf:"bytes,1,rep,name=data,proto3" json:"data,omitempty"` // 媒体记录列表 + RunHandle string `protobuf:"bytes,2,opt,name=run_handle,json=runHandle,proto3" json:"run_handle,omitempty"` // 目标 run 会话标识 unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *UpsertMediaRequest) Reset() { *x = UpsertMediaRequest{} - mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[4] + mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -295,7 +468,7 @@ func (x *UpsertMediaRequest) String() string { func (*UpsertMediaRequest) ProtoMessage() {} func (x *UpsertMediaRequest) ProtoReflect() protoreflect.Message { - mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[4] + mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -308,7 +481,7 @@ func (x *UpsertMediaRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpsertMediaRequest.ProtoReflect.Descriptor instead. func (*UpsertMediaRequest) Descriptor() ([]byte, []int) { - return file_swanlab_grpc_core_v1_core_proto_rawDescGZIP(), []int{4} + return file_swanlab_grpc_core_v1_core_proto_rawDescGZIP(), []int{8} } func (x *UpsertMediaRequest) GetData() []*v13.MediaRecord { @@ -318,17 +491,25 @@ func (x *UpsertMediaRequest) GetData() []*v13.MediaRecord { return nil } +func (x *UpsertMediaRequest) GetRunHandle() string { + if x != nil { + return x.RunHandle + } + return "" +} + // UpsertLogs 请求,异步插入一批终端输出 type UpsertLogsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - Logs []*v14.LogRecord `protobuf:"bytes,1,rep,name=logs,proto3" json:"logs,omitempty"` // 终端日志记录列表 + Logs []*v14.LogRecord `protobuf:"bytes,1,rep,name=logs,proto3" json:"logs,omitempty"` // 终端日志记录列表 + RunHandle string `protobuf:"bytes,2,opt,name=run_handle,json=runHandle,proto3" json:"run_handle,omitempty"` // 目标 run 会话标识 unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *UpsertLogsRequest) Reset() { *x = UpsertLogsRequest{} - mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[5] + mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -340,7 +521,7 @@ func (x *UpsertLogsRequest) String() string { func (*UpsertLogsRequest) ProtoMessage() {} func (x *UpsertLogsRequest) ProtoReflect() protoreflect.Message { - mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[5] + mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -353,7 +534,7 @@ func (x *UpsertLogsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpsertLogsRequest.ProtoReflect.Descriptor instead. func (*UpsertLogsRequest) Descriptor() ([]byte, []int) { - return file_swanlab_grpc_core_v1_core_proto_rawDescGZIP(), []int{5} + return file_swanlab_grpc_core_v1_core_proto_rawDescGZIP(), []int{9} } func (x *UpsertLogsRequest) GetLogs() []*v14.LogRecord { @@ -363,17 +544,25 @@ func (x *UpsertLogsRequest) GetLogs() []*v14.LogRecord { return nil } +func (x *UpsertLogsRequest) GetRunHandle() string { + if x != nil { + return x.RunHandle + } + return "" +} + // UpsertSaves 请求,异步插入一批文件保存记录 type UpsertSavesRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - Saves []*v15.SaveRecord `protobuf:"bytes,1,rep,name=saves,proto3" json:"saves,omitempty"` // 保存记录列表 + Saves []*v15.SaveRecord `protobuf:"bytes,1,rep,name=saves,proto3" json:"saves,omitempty"` // 保存记录列表 + RunHandle string `protobuf:"bytes,2,opt,name=run_handle,json=runHandle,proto3" json:"run_handle,omitempty"` // 目标 run 会话标识 unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *UpsertSavesRequest) Reset() { *x = UpsertSavesRequest{} - mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[6] + mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -385,7 +574,7 @@ func (x *UpsertSavesRequest) String() string { func (*UpsertSavesRequest) ProtoMessage() {} func (x *UpsertSavesRequest) ProtoReflect() protoreflect.Message { - mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[6] + mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -398,7 +587,7 @@ func (x *UpsertSavesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpsertSavesRequest.ProtoReflect.Descriptor instead. func (*UpsertSavesRequest) Descriptor() ([]byte, []int) { - return file_swanlab_grpc_core_v1_core_proto_rawDescGZIP(), []int{6} + return file_swanlab_grpc_core_v1_core_proto_rawDescGZIP(), []int{10} } func (x *UpsertSavesRequest) GetSaves() []*v15.SaveRecord { @@ -408,17 +597,25 @@ func (x *UpsertSavesRequest) GetSaves() []*v15.SaveRecord { return nil } +func (x *UpsertSavesRequest) GetRunHandle() string { + if x != nil { + return x.RunHandle + } + return "" +} + // DeliverRunFinish 请求,生命周期函数,运行结束 type DeliverRunFinishRequest struct { state protoimpl.MessageState `protogen:"open.v1"` FinishRecord *v11.FinishRecord `protobuf:"bytes,1,opt,name=finish_record,json=finishRecord,proto3" json:"finish_record,omitempty"` // Run 结束记录 + RunHandle string `protobuf:"bytes,2,opt,name=run_handle,json=runHandle,proto3" json:"run_handle,omitempty"` // 目标 run 会话标识 unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *DeliverRunFinishRequest) Reset() { *x = DeliverRunFinishRequest{} - mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[7] + mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -430,7 +627,7 @@ func (x *DeliverRunFinishRequest) String() string { func (*DeliverRunFinishRequest) ProtoMessage() {} func (x *DeliverRunFinishRequest) ProtoReflect() protoreflect.Message { - mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[7] + mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -443,7 +640,7 @@ func (x *DeliverRunFinishRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeliverRunFinishRequest.ProtoReflect.Descriptor instead. func (*DeliverRunFinishRequest) Descriptor() ([]byte, []int) { - return file_swanlab_grpc_core_v1_core_proto_rawDescGZIP(), []int{7} + return file_swanlab_grpc_core_v1_core_proto_rawDescGZIP(), []int{11} } func (x *DeliverRunFinishRequest) GetFinishRecord() *v11.FinishRecord { @@ -453,6 +650,13 @@ func (x *DeliverRunFinishRequest) GetFinishRecord() *v11.FinishRecord { return nil } +func (x *DeliverRunFinishRequest) GetRunHandle() string { + if x != nil { + return x.RunHandle + } + return "" +} + // DeliverRunFinish 响应 type DeliverRunFinishResponse struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -464,7 +668,7 @@ type DeliverRunFinishResponse struct { func (x *DeliverRunFinishResponse) Reset() { *x = DeliverRunFinishResponse{} - mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[8] + mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -476,7 +680,7 @@ func (x *DeliverRunFinishResponse) String() string { func (*DeliverRunFinishResponse) ProtoMessage() {} func (x *DeliverRunFinishResponse) ProtoReflect() protoreflect.Message { - mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[8] + mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -489,7 +693,7 @@ func (x *DeliverRunFinishResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeliverRunFinishResponse.ProtoReflect.Descriptor instead. func (*DeliverRunFinishResponse) Descriptor() ([]byte, []int) { - return file_swanlab_grpc_core_v1_core_proto_rawDescGZIP(), []int{8} + return file_swanlab_grpc_core_v1_core_proto_rawDescGZIP(), []int{12} } func (x *DeliverRunFinishResponse) GetSuccess() bool { @@ -506,6 +710,51 @@ func (x *DeliverRunFinishResponse) GetMessage() string { return "" } +// GetOperationStats 请求。 +type GetOperationStatsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + RunHandle string `protobuf:"bytes,1,opt,name=run_handle,json=runHandle,proto3" json:"run_handle,omitempty"` // 目标 run 会话标识 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetOperationStatsRequest) Reset() { + *x = GetOperationStatsRequest{} + mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetOperationStatsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetOperationStatsRequest) ProtoMessage() {} + +func (x *GetOperationStatsRequest) ProtoReflect() protoreflect.Message { + mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[13] + 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 GetOperationStatsRequest.ProtoReflect.Descriptor instead. +func (*GetOperationStatsRequest) Descriptor() ([]byte, []int) { + return file_swanlab_grpc_core_v1_core_proto_rawDescGZIP(), []int{13} +} + +func (x *GetOperationStatsRequest) GetRunHandle() string { + if x != nil { + return x.RunHandle + } + return "" +} + // GetOperationStats 相应 type GetOperationStatsResponse struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -518,7 +767,7 @@ type GetOperationStatsResponse struct { func (x *GetOperationStatsResponse) Reset() { *x = GetOperationStatsResponse{} - mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[9] + mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -530,7 +779,7 @@ func (x *GetOperationStatsResponse) String() string { func (*GetOperationStatsResponse) ProtoMessage() {} func (x *GetOperationStatsResponse) ProtoReflect() protoreflect.Message { - mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[9] + mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -543,7 +792,7 @@ func (x *GetOperationStatsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetOperationStatsResponse.ProtoReflect.Descriptor instead. func (*GetOperationStatsResponse) Descriptor() ([]byte, []int) { - return file_swanlab_grpc_core_v1_core_proto_rawDescGZIP(), []int{9} + return file_swanlab_grpc_core_v1_core_proto_rawDescGZIP(), []int{14} } func (x *GetOperationStatsResponse) GetSuccess() bool { @@ -567,6 +816,51 @@ func (x *GetOperationStatsResponse) GetStats() *v16.OperationStats { return nil } +// ConfirmRunFinish 请求。 +type ConfirmRunFinishRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + RunHandle string `protobuf:"bytes,1,opt,name=run_handle,json=runHandle,proto3" json:"run_handle,omitempty"` // 目标 run 会话标识 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ConfirmRunFinishRequest) Reset() { + *x = ConfirmRunFinishRequest{} + mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ConfirmRunFinishRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ConfirmRunFinishRequest) ProtoMessage() {} + +func (x *ConfirmRunFinishRequest) ProtoReflect() protoreflect.Message { + mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[15] + 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 ConfirmRunFinishRequest.ProtoReflect.Descriptor instead. +func (*ConfirmRunFinishRequest) Descriptor() ([]byte, []int) { + return file_swanlab_grpc_core_v1_core_proto_rawDescGZIP(), []int{15} +} + +func (x *ConfirmRunFinishRequest) GetRunHandle() string { + if x != nil { + return x.RunHandle + } + return "" +} + // ConfirmRunFinish 响应 type ConfirmRunFinishResponse struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -578,7 +872,7 @@ type ConfirmRunFinishResponse struct { func (x *ConfirmRunFinishResponse) Reset() { *x = ConfirmRunFinishResponse{} - mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[10] + mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -590,7 +884,7 @@ func (x *ConfirmRunFinishResponse) String() string { func (*ConfirmRunFinishResponse) ProtoMessage() {} func (x *ConfirmRunFinishResponse) ProtoReflect() protoreflect.Message { - mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[10] + mi := &file_swanlab_grpc_core_v1_core_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -603,7 +897,7 @@ func (x *ConfirmRunFinishResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ConfirmRunFinishResponse.ProtoReflect.Descriptor instead. func (*ConfirmRunFinishResponse) Descriptor() ([]byte, []int) { - return file_swanlab_grpc_core_v1_core_proto_rawDescGZIP(), []int{10} + return file_swanlab_grpc_core_v1_core_proto_rawDescGZIP(), []int{16} } func (x *ConfirmRunFinishResponse) GetSuccess() bool { @@ -624,10 +918,14 @@ var File_swanlab_grpc_core_v1_core_proto protoreflect.FileDescriptor const file_swanlab_grpc_core_v1_core_proto_rawDesc = "" + "\n" + - "\x1fswanlab/grpc/core/v1/core.proto\x12\x14swanlab.grpc.core.v1\x1a\x1bgoogle/protobuf/empty.proto\x1a\x18swanlab/run/v1/run.proto\x1a!swanlab/metric/data/v1/data.proto\x1a%swanlab/metric/column/v1/column.proto\x1a\x1dswanlab/terminal/v1/log.proto\x1a\x1aswanlab/save/v1/save.proto\x1a$swanlab/operation/v1/operation.proto\x1a#swanlab/settings/core/v1/core.proto\"\xa5\x01\n" + + "\x1fswanlab/grpc/core/v1/core.proto\x12\x14swanlab.grpc.core.v1\x1a\x1bgoogle/protobuf/empty.proto\x1a\x18swanlab/run/v1/run.proto\x1a!swanlab/metric/data/v1/data.proto\x1a%swanlab/metric/column/v1/column.proto\x1a\x1dswanlab/terminal/v1/log.proto\x1a\x1aswanlab/save/v1/save.proto\x1a$swanlab/operation/v1/operation.proto\x1a#swanlab/settings/core/v1/core.proto\"\x16\n" + + "\x14SpinupServiceRequest\"\x17\n" + + "\x15SpinupServiceResponse\"\x18\n" + + "\x16TeardownServiceRequest\"\x19\n" + + "\x17TeardownServiceResponse\"\xa5\x01\n" + "\x16DeliverRunStartRequest\x12K\n" + "\rcore_settings\x18\x01 \x01(\v2&.swanlab.settings.core.v1.CoreSettingsR\fcoreSettings\x12>\n" + - "\fstart_record\x18\x02 \x01(\v2\x1b.swanlab.run.v1.StartRecordR\vstartRecord\"\x9a\x02\n" + + "\fstart_record\x18\x02 \x01(\v2\x1b.swanlab.run.v1.StartRecordR\vstartRecord\"\xb9\x02\n" + "\x17DeliverRunStartResponse\x12\x18\n" + "\asuccess\x18\x01 \x01(\bR\asuccess\x12\x18\n" + "\amessage\x18\x02 \x01(\tR\amessage\x12-\n" + @@ -637,30 +935,52 @@ const file_swanlab_grpc_core_v1_core_proto_rawDesc = "" + "\vglobal_step\x18\x06 \x01(\x03R\n" + "globalStep\x12,\n" + "\x12global_system_step\x18\a \x01(\x03R\x10globalSystemStep\x12%\n" + - "\x0enew_experiment\x18\b \x01(\bR\rnewExperiment\"X\n" + + "\x0enew_experiment\x18\b \x01(\bR\rnewExperiment\x12\x1d\n" + + "\n" + + "run_handle\x18\t \x01(\tR\trunHandle\"w\n" + "\x14UpsertColumnsRequest\x12@\n" + - "\acolumns\x18\x01 \x03(\v2&.swanlab.metric.column.v1.ColumnRecordR\acolumns\"P\n" + + "\acolumns\x18\x01 \x03(\v2&.swanlab.metric.column.v1.ColumnRecordR\acolumns\x12\x1d\n" + + "\n" + + "run_handle\x18\x02 \x01(\tR\trunHandle\"o\n" + "\x14UpsertScalarsRequest\x128\n" + - "\x04data\x18\x01 \x03(\v2$.swanlab.metric.data.v1.ScalarRecordR\x04data\"M\n" + + "\x04data\x18\x01 \x03(\v2$.swanlab.metric.data.v1.ScalarRecordR\x04data\x12\x1d\n" + + "\n" + + "run_handle\x18\x02 \x01(\tR\trunHandle\"l\n" + "\x12UpsertMediaRequest\x127\n" + - "\x04data\x18\x01 \x03(\v2#.swanlab.metric.data.v1.MediaRecordR\x04data\"G\n" + + "\x04data\x18\x01 \x03(\v2#.swanlab.metric.data.v1.MediaRecordR\x04data\x12\x1d\n" + + "\n" + + "run_handle\x18\x02 \x01(\tR\trunHandle\"f\n" + "\x11UpsertLogsRequest\x122\n" + - "\x04logs\x18\x01 \x03(\v2\x1e.swanlab.terminal.v1.LogRecordR\x04logs\"G\n" + + "\x04logs\x18\x01 \x03(\v2\x1e.swanlab.terminal.v1.LogRecordR\x04logs\x12\x1d\n" + + "\n" + + "run_handle\x18\x02 \x01(\tR\trunHandle\"f\n" + "\x12UpsertSavesRequest\x121\n" + - "\x05saves\x18\x01 \x03(\v2\x1b.swanlab.save.v1.SaveRecordR\x05saves\"\\\n" + + "\x05saves\x18\x01 \x03(\v2\x1b.swanlab.save.v1.SaveRecordR\x05saves\x12\x1d\n" + + "\n" + + "run_handle\x18\x02 \x01(\tR\trunHandle\"{\n" + "\x17DeliverRunFinishRequest\x12A\n" + - "\rfinish_record\x18\x01 \x01(\v2\x1c.swanlab.run.v1.FinishRecordR\ffinishRecord\"N\n" + + "\rfinish_record\x18\x01 \x01(\v2\x1c.swanlab.run.v1.FinishRecordR\ffinishRecord\x12\x1d\n" + + "\n" + + "run_handle\x18\x02 \x01(\tR\trunHandle\"N\n" + "\x18DeliverRunFinishResponse\x12\x18\n" + "\asuccess\x18\x01 \x01(\bR\asuccess\x12\x18\n" + - "\amessage\x18\x02 \x01(\tR\amessage\"\x8b\x01\n" + + "\amessage\x18\x02 \x01(\tR\amessage\"9\n" + + "\x18GetOperationStatsRequest\x12\x1d\n" + + "\n" + + "run_handle\x18\x01 \x01(\tR\trunHandle\"\x8b\x01\n" + "\x19GetOperationStatsResponse\x12\x18\n" + "\asuccess\x18\x01 \x01(\bR\asuccess\x12\x18\n" + "\amessage\x18\x02 \x01(\tR\amessage\x12:\n" + - "\x05stats\x18\x03 \x01(\v2$.swanlab.operation.v1.OperationStatsR\x05stats\"N\n" + + "\x05stats\x18\x03 \x01(\v2$.swanlab.operation.v1.OperationStatsR\x05stats\"8\n" + + "\x17ConfirmRunFinishRequest\x12\x1d\n" + + "\n" + + "run_handle\x18\x01 \x01(\tR\trunHandle\"N\n" + "\x18ConfirmRunFinishResponse\x12\x18\n" + "\asuccess\x18\x01 \x01(\bR\asuccess\x12\x18\n" + - "\amessage\x18\x02 \x01(\tR\amessage2\xc5\x06\n" + - "\vCoreService\x12n\n" + + "\amessage\x18\x02 \x01(\tR\amessage2\xce\b\n" + + "\vCoreService\x12h\n" + + "\rSpinupService\x12*.swanlab.grpc.core.v1.SpinupServiceRequest\x1a+.swanlab.grpc.core.v1.SpinupServiceResponse\x12n\n" + + "\x0fTeardownService\x12,.swanlab.grpc.core.v1.TeardownServiceRequest\x1a-.swanlab.grpc.core.v1.TeardownServiceResponse\x12n\n" + "\x0fDeliverRunStart\x12,.swanlab.grpc.core.v1.DeliverRunStartRequest\x1a-.swanlab.grpc.core.v1.DeliverRunStartResponse\x12S\n" + "\rUpsertColumns\x12*.swanlab.grpc.core.v1.UpsertColumnsRequest\x1a\x16.google.protobuf.Empty\x12S\n" + "\rUpsertScalars\x12*.swanlab.grpc.core.v1.UpsertScalarsRequest\x1a\x16.google.protobuf.Empty\x12O\n" + @@ -668,9 +988,9 @@ const file_swanlab_grpc_core_v1_core_proto_rawDesc = "" + "\n" + "UpsertLogs\x12'.swanlab.grpc.core.v1.UpsertLogsRequest\x1a\x16.google.protobuf.Empty\x12O\n" + "\vUpsertSaves\x12(.swanlab.grpc.core.v1.UpsertSavesRequest\x1a\x16.google.protobuf.Empty\x12q\n" + - "\x10DeliverRunFinish\x12-.swanlab.grpc.core.v1.DeliverRunFinishRequest\x1a..swanlab.grpc.core.v1.DeliverRunFinishResponse\x12\\\n" + - "\x11GetOperationStats\x12\x16.google.protobuf.Empty\x1a/.swanlab.grpc.core.v1.GetOperationStatsResponse\x12Z\n" + - "\x10ConfirmRunFinish\x12\x16.google.protobuf.Empty\x1a..swanlab.grpc.core.v1.ConfirmRunFinishResponseBDZBgithub.com/swanhubx/swanlab/core/proto/swanlab/grpc/core/v1;corev1b\x06proto3" + "\x10DeliverRunFinish\x12-.swanlab.grpc.core.v1.DeliverRunFinishRequest\x1a..swanlab.grpc.core.v1.DeliverRunFinishResponse\x12t\n" + + "\x11GetOperationStats\x12..swanlab.grpc.core.v1.GetOperationStatsRequest\x1a/.swanlab.grpc.core.v1.GetOperationStatsResponse\x12q\n" + + "\x10ConfirmRunFinish\x12-.swanlab.grpc.core.v1.ConfirmRunFinishRequest\x1a..swanlab.grpc.core.v1.ConfirmRunFinishResponseBDZBgithub.com/swanhubx/swanlab/core/proto/swanlab/grpc/core/v1;corev1b\x06proto3" var ( file_swanlab_grpc_core_v1_core_proto_rawDescOnce sync.Once @@ -684,61 +1004,71 @@ func file_swanlab_grpc_core_v1_core_proto_rawDescGZIP() []byte { return file_swanlab_grpc_core_v1_core_proto_rawDescData } -var file_swanlab_grpc_core_v1_core_proto_msgTypes = make([]protoimpl.MessageInfo, 11) +var file_swanlab_grpc_core_v1_core_proto_msgTypes = make([]protoimpl.MessageInfo, 17) var file_swanlab_grpc_core_v1_core_proto_goTypes = []any{ - (*DeliverRunStartRequest)(nil), // 0: swanlab.grpc.core.v1.DeliverRunStartRequest - (*DeliverRunStartResponse)(nil), // 1: swanlab.grpc.core.v1.DeliverRunStartResponse - (*UpsertColumnsRequest)(nil), // 2: swanlab.grpc.core.v1.UpsertColumnsRequest - (*UpsertScalarsRequest)(nil), // 3: swanlab.grpc.core.v1.UpsertScalarsRequest - (*UpsertMediaRequest)(nil), // 4: swanlab.grpc.core.v1.UpsertMediaRequest - (*UpsertLogsRequest)(nil), // 5: swanlab.grpc.core.v1.UpsertLogsRequest - (*UpsertSavesRequest)(nil), // 6: swanlab.grpc.core.v1.UpsertSavesRequest - (*DeliverRunFinishRequest)(nil), // 7: swanlab.grpc.core.v1.DeliverRunFinishRequest - (*DeliverRunFinishResponse)(nil), // 8: swanlab.grpc.core.v1.DeliverRunFinishResponse - (*GetOperationStatsResponse)(nil), // 9: swanlab.grpc.core.v1.GetOperationStatsResponse - (*ConfirmRunFinishResponse)(nil), // 10: swanlab.grpc.core.v1.ConfirmRunFinishResponse - (*v1.CoreSettings)(nil), // 11: swanlab.settings.core.v1.CoreSettings - (*v11.StartRecord)(nil), // 12: swanlab.run.v1.StartRecord - (*v12.ColumnRecord)(nil), // 13: swanlab.metric.column.v1.ColumnRecord - (*v13.ScalarRecord)(nil), // 14: swanlab.metric.data.v1.ScalarRecord - (*v13.MediaRecord)(nil), // 15: swanlab.metric.data.v1.MediaRecord - (*v14.LogRecord)(nil), // 16: swanlab.terminal.v1.LogRecord - (*v15.SaveRecord)(nil), // 17: swanlab.save.v1.SaveRecord - (*v11.FinishRecord)(nil), // 18: swanlab.run.v1.FinishRecord - (*v16.OperationStats)(nil), // 19: swanlab.operation.v1.OperationStats - (*emptypb.Empty)(nil), // 20: google.protobuf.Empty + (*SpinupServiceRequest)(nil), // 0: swanlab.grpc.core.v1.SpinupServiceRequest + (*SpinupServiceResponse)(nil), // 1: swanlab.grpc.core.v1.SpinupServiceResponse + (*TeardownServiceRequest)(nil), // 2: swanlab.grpc.core.v1.TeardownServiceRequest + (*TeardownServiceResponse)(nil), // 3: swanlab.grpc.core.v1.TeardownServiceResponse + (*DeliverRunStartRequest)(nil), // 4: swanlab.grpc.core.v1.DeliverRunStartRequest + (*DeliverRunStartResponse)(nil), // 5: swanlab.grpc.core.v1.DeliverRunStartResponse + (*UpsertColumnsRequest)(nil), // 6: swanlab.grpc.core.v1.UpsertColumnsRequest + (*UpsertScalarsRequest)(nil), // 7: swanlab.grpc.core.v1.UpsertScalarsRequest + (*UpsertMediaRequest)(nil), // 8: swanlab.grpc.core.v1.UpsertMediaRequest + (*UpsertLogsRequest)(nil), // 9: swanlab.grpc.core.v1.UpsertLogsRequest + (*UpsertSavesRequest)(nil), // 10: swanlab.grpc.core.v1.UpsertSavesRequest + (*DeliverRunFinishRequest)(nil), // 11: swanlab.grpc.core.v1.DeliverRunFinishRequest + (*DeliverRunFinishResponse)(nil), // 12: swanlab.grpc.core.v1.DeliverRunFinishResponse + (*GetOperationStatsRequest)(nil), // 13: swanlab.grpc.core.v1.GetOperationStatsRequest + (*GetOperationStatsResponse)(nil), // 14: swanlab.grpc.core.v1.GetOperationStatsResponse + (*ConfirmRunFinishRequest)(nil), // 15: swanlab.grpc.core.v1.ConfirmRunFinishRequest + (*ConfirmRunFinishResponse)(nil), // 16: swanlab.grpc.core.v1.ConfirmRunFinishResponse + (*v1.CoreSettings)(nil), // 17: swanlab.settings.core.v1.CoreSettings + (*v11.StartRecord)(nil), // 18: swanlab.run.v1.StartRecord + (*v12.ColumnRecord)(nil), // 19: swanlab.metric.column.v1.ColumnRecord + (*v13.ScalarRecord)(nil), // 20: swanlab.metric.data.v1.ScalarRecord + (*v13.MediaRecord)(nil), // 21: swanlab.metric.data.v1.MediaRecord + (*v14.LogRecord)(nil), // 22: swanlab.terminal.v1.LogRecord + (*v15.SaveRecord)(nil), // 23: swanlab.save.v1.SaveRecord + (*v11.FinishRecord)(nil), // 24: swanlab.run.v1.FinishRecord + (*v16.OperationStats)(nil), // 25: swanlab.operation.v1.OperationStats + (*emptypb.Empty)(nil), // 26: google.protobuf.Empty } var file_swanlab_grpc_core_v1_core_proto_depIdxs = []int32{ - 11, // 0: swanlab.grpc.core.v1.DeliverRunStartRequest.core_settings:type_name -> swanlab.settings.core.v1.CoreSettings - 12, // 1: swanlab.grpc.core.v1.DeliverRunStartRequest.start_record:type_name -> swanlab.run.v1.StartRecord - 12, // 2: swanlab.grpc.core.v1.DeliverRunStartResponse.run:type_name -> swanlab.run.v1.StartRecord - 13, // 3: swanlab.grpc.core.v1.UpsertColumnsRequest.columns:type_name -> swanlab.metric.column.v1.ColumnRecord - 14, // 4: swanlab.grpc.core.v1.UpsertScalarsRequest.data:type_name -> swanlab.metric.data.v1.ScalarRecord - 15, // 5: swanlab.grpc.core.v1.UpsertMediaRequest.data:type_name -> swanlab.metric.data.v1.MediaRecord - 16, // 6: swanlab.grpc.core.v1.UpsertLogsRequest.logs:type_name -> swanlab.terminal.v1.LogRecord - 17, // 7: swanlab.grpc.core.v1.UpsertSavesRequest.saves:type_name -> swanlab.save.v1.SaveRecord - 18, // 8: swanlab.grpc.core.v1.DeliverRunFinishRequest.finish_record:type_name -> swanlab.run.v1.FinishRecord - 19, // 9: swanlab.grpc.core.v1.GetOperationStatsResponse.stats:type_name -> swanlab.operation.v1.OperationStats - 0, // 10: swanlab.grpc.core.v1.CoreService.DeliverRunStart:input_type -> swanlab.grpc.core.v1.DeliverRunStartRequest - 2, // 11: swanlab.grpc.core.v1.CoreService.UpsertColumns:input_type -> swanlab.grpc.core.v1.UpsertColumnsRequest - 3, // 12: swanlab.grpc.core.v1.CoreService.UpsertScalars:input_type -> swanlab.grpc.core.v1.UpsertScalarsRequest - 4, // 13: swanlab.grpc.core.v1.CoreService.UpsertMedia:input_type -> swanlab.grpc.core.v1.UpsertMediaRequest - 5, // 14: swanlab.grpc.core.v1.CoreService.UpsertLogs:input_type -> swanlab.grpc.core.v1.UpsertLogsRequest - 6, // 15: swanlab.grpc.core.v1.CoreService.UpsertSaves:input_type -> swanlab.grpc.core.v1.UpsertSavesRequest - 7, // 16: swanlab.grpc.core.v1.CoreService.DeliverRunFinish:input_type -> swanlab.grpc.core.v1.DeliverRunFinishRequest - 20, // 17: swanlab.grpc.core.v1.CoreService.GetOperationStats:input_type -> google.protobuf.Empty - 20, // 18: swanlab.grpc.core.v1.CoreService.ConfirmRunFinish:input_type -> google.protobuf.Empty - 1, // 19: swanlab.grpc.core.v1.CoreService.DeliverRunStart:output_type -> swanlab.grpc.core.v1.DeliverRunStartResponse - 20, // 20: swanlab.grpc.core.v1.CoreService.UpsertColumns:output_type -> google.protobuf.Empty - 20, // 21: swanlab.grpc.core.v1.CoreService.UpsertScalars:output_type -> google.protobuf.Empty - 20, // 22: swanlab.grpc.core.v1.CoreService.UpsertMedia:output_type -> google.protobuf.Empty - 20, // 23: swanlab.grpc.core.v1.CoreService.UpsertLogs:output_type -> google.protobuf.Empty - 20, // 24: swanlab.grpc.core.v1.CoreService.UpsertSaves:output_type -> google.protobuf.Empty - 8, // 25: swanlab.grpc.core.v1.CoreService.DeliverRunFinish:output_type -> swanlab.grpc.core.v1.DeliverRunFinishResponse - 9, // 26: swanlab.grpc.core.v1.CoreService.GetOperationStats:output_type -> swanlab.grpc.core.v1.GetOperationStatsResponse - 10, // 27: swanlab.grpc.core.v1.CoreService.ConfirmRunFinish:output_type -> swanlab.grpc.core.v1.ConfirmRunFinishResponse - 19, // [19:28] is the sub-list for method output_type - 10, // [10:19] is the sub-list for method input_type + 17, // 0: swanlab.grpc.core.v1.DeliverRunStartRequest.core_settings:type_name -> swanlab.settings.core.v1.CoreSettings + 18, // 1: swanlab.grpc.core.v1.DeliverRunStartRequest.start_record:type_name -> swanlab.run.v1.StartRecord + 18, // 2: swanlab.grpc.core.v1.DeliverRunStartResponse.run:type_name -> swanlab.run.v1.StartRecord + 19, // 3: swanlab.grpc.core.v1.UpsertColumnsRequest.columns:type_name -> swanlab.metric.column.v1.ColumnRecord + 20, // 4: swanlab.grpc.core.v1.UpsertScalarsRequest.data:type_name -> swanlab.metric.data.v1.ScalarRecord + 21, // 5: swanlab.grpc.core.v1.UpsertMediaRequest.data:type_name -> swanlab.metric.data.v1.MediaRecord + 22, // 6: swanlab.grpc.core.v1.UpsertLogsRequest.logs:type_name -> swanlab.terminal.v1.LogRecord + 23, // 7: swanlab.grpc.core.v1.UpsertSavesRequest.saves:type_name -> swanlab.save.v1.SaveRecord + 24, // 8: swanlab.grpc.core.v1.DeliverRunFinishRequest.finish_record:type_name -> swanlab.run.v1.FinishRecord + 25, // 9: swanlab.grpc.core.v1.GetOperationStatsResponse.stats:type_name -> swanlab.operation.v1.OperationStats + 0, // 10: swanlab.grpc.core.v1.CoreService.SpinupService:input_type -> swanlab.grpc.core.v1.SpinupServiceRequest + 2, // 11: swanlab.grpc.core.v1.CoreService.TeardownService:input_type -> swanlab.grpc.core.v1.TeardownServiceRequest + 4, // 12: swanlab.grpc.core.v1.CoreService.DeliverRunStart:input_type -> swanlab.grpc.core.v1.DeliverRunStartRequest + 6, // 13: swanlab.grpc.core.v1.CoreService.UpsertColumns:input_type -> swanlab.grpc.core.v1.UpsertColumnsRequest + 7, // 14: swanlab.grpc.core.v1.CoreService.UpsertScalars:input_type -> swanlab.grpc.core.v1.UpsertScalarsRequest + 8, // 15: swanlab.grpc.core.v1.CoreService.UpsertMedia:input_type -> swanlab.grpc.core.v1.UpsertMediaRequest + 9, // 16: swanlab.grpc.core.v1.CoreService.UpsertLogs:input_type -> swanlab.grpc.core.v1.UpsertLogsRequest + 10, // 17: swanlab.grpc.core.v1.CoreService.UpsertSaves:input_type -> swanlab.grpc.core.v1.UpsertSavesRequest + 11, // 18: swanlab.grpc.core.v1.CoreService.DeliverRunFinish:input_type -> swanlab.grpc.core.v1.DeliverRunFinishRequest + 13, // 19: swanlab.grpc.core.v1.CoreService.GetOperationStats:input_type -> swanlab.grpc.core.v1.GetOperationStatsRequest + 15, // 20: swanlab.grpc.core.v1.CoreService.ConfirmRunFinish:input_type -> swanlab.grpc.core.v1.ConfirmRunFinishRequest + 1, // 21: swanlab.grpc.core.v1.CoreService.SpinupService:output_type -> swanlab.grpc.core.v1.SpinupServiceResponse + 3, // 22: swanlab.grpc.core.v1.CoreService.TeardownService:output_type -> swanlab.grpc.core.v1.TeardownServiceResponse + 5, // 23: swanlab.grpc.core.v1.CoreService.DeliverRunStart:output_type -> swanlab.grpc.core.v1.DeliverRunStartResponse + 26, // 24: swanlab.grpc.core.v1.CoreService.UpsertColumns:output_type -> google.protobuf.Empty + 26, // 25: swanlab.grpc.core.v1.CoreService.UpsertScalars:output_type -> google.protobuf.Empty + 26, // 26: swanlab.grpc.core.v1.CoreService.UpsertMedia:output_type -> google.protobuf.Empty + 26, // 27: swanlab.grpc.core.v1.CoreService.UpsertLogs:output_type -> google.protobuf.Empty + 26, // 28: swanlab.grpc.core.v1.CoreService.UpsertSaves:output_type -> google.protobuf.Empty + 12, // 29: swanlab.grpc.core.v1.CoreService.DeliverRunFinish:output_type -> swanlab.grpc.core.v1.DeliverRunFinishResponse + 14, // 30: swanlab.grpc.core.v1.CoreService.GetOperationStats:output_type -> swanlab.grpc.core.v1.GetOperationStatsResponse + 16, // 31: swanlab.grpc.core.v1.CoreService.ConfirmRunFinish:output_type -> swanlab.grpc.core.v1.ConfirmRunFinishResponse + 21, // [21:32] is the sub-list for method output_type + 10, // [10:21] is the sub-list for method input_type 10, // [10:10] is the sub-list for extension type_name 10, // [10:10] is the sub-list for extension extendee 0, // [0:10] is the sub-list for field type_name @@ -755,7 +1085,7 @@ func file_swanlab_grpc_core_v1_core_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_swanlab_grpc_core_v1_core_proto_rawDesc), len(file_swanlab_grpc_core_v1_core_proto_rawDesc)), NumEnums: 0, - NumMessages: 11, + NumMessages: 17, NumExtensions: 0, NumServices: 1, }, diff --git a/core/proto/swanlab/grpc/core/v1/core_grpc.pb.go b/core/proto/swanlab/grpc/core/v1/core_grpc.pb.go index a002549a9..213732827 100644 --- a/core/proto/swanlab/grpc/core/v1/core_grpc.pb.go +++ b/core/proto/swanlab/grpc/core/v1/core_grpc.pb.go @@ -20,6 +20,8 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( + CoreService_SpinupService_FullMethodName = "/swanlab.grpc.core.v1.CoreService/SpinupService" + CoreService_TeardownService_FullMethodName = "/swanlab.grpc.core.v1.CoreService/TeardownService" CoreService_DeliverRunStart_FullMethodName = "/swanlab.grpc.core.v1.CoreService/DeliverRunStart" CoreService_UpsertColumns_FullMethodName = "/swanlab.grpc.core.v1.CoreService/UpsertColumns" CoreService_UpsertScalars_FullMethodName = "/swanlab.grpc.core.v1.CoreService/UpsertScalars" @@ -35,8 +37,21 @@ const ( // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. // -// CoreService 是核心业务接口,用于同步或异步地接收实验记录 +// CoreService 是核心业务接口,用于同步或异步地接收实验记录。 +// +// 生命周期约定: +// +// 1. Core 与 run 级生命周期严格分离:SpinupService / TeardownService 作用于整个 core 服务进程; +// 2. 所有 run 级 RPC 通过显式 run_handle 路由到具体会话,不依赖 channel 隐式绑定 run; +// 3. ConfirmRunFinish 确认单个 run 已排空并释放其资源,不关闭 gRPC Server; +// 须在对应 run 的 DeliverRunFinish 之后调用,提前调用返回 FAILED_PRECONDITION 且会话保留; +// 4. GetOperationStats / ConfirmRunFinish 输入使用专用 request,由 run_handle 指定目标 run。 type CoreServiceClient interface { + // SpinupService 完成服务级初始化并把服务置为 READY,不负责创建 run 级 datastore 和 transport + // 启动失败 RPC 返回 FAILED_PRECONDITION;重复 Spinup 在 READY 下幂等成功,共享一个 core 服务进程。 + SpinupService(ctx context.Context, in *SpinupServiceRequest, opts ...grpc.CallOption) (*SpinupServiceResponse, error) + // TeardownService 关闭整个服务进程。不同于 ConfirmRunFinish ,能连接 core 的 client 都可调用。 + TeardownService(ctx context.Context, in *TeardownServiceRequest, opts ...grpc.CallOption) (*TeardownServiceResponse, error) // DeliverRunStart 接收单条 StartRecord,用于实验开始,并返回必要的信息。 DeliverRunStart(ctx context.Context, in *DeliverRunStartRequest, opts ...grpc.CallOption) (*DeliverRunStartResponse, error) // UpsertColumns 接收一组 ColumnRecord 并写入,每一条记录用于定义某一个指标 @@ -51,10 +66,11 @@ type CoreServiceClient interface { UpsertSaves(ctx context.Context, in *UpsertSavesRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) // DeliverRunFinish 接收单条 FinishRecord,用于实验结束。 DeliverRunFinish(ctx context.Context, in *DeliverRunFinishRequest, opts ...grpc.CallOption) (*DeliverRunFinishResponse, error) - // GetOperationStats 返回 Core 当前运行状态和上传进度快照。 - GetOperationStats(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*GetOperationStatsResponse, error) - // ConfirmRunFinish 确认 Core 运行结束,可以安全退出 - ConfirmRunFinish(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ConfirmRunFinishResponse, error) + // GetOperationStats 返回指定 run 当前运行状态和上传进度快照。 + GetOperationStats(ctx context.Context, in *GetOperationStatsRequest, opts ...grpc.CallOption) (*GetOperationStatsResponse, error) + // ConfirmRunFinish 确认指定 run 已排空并释放其资源,不关闭 gRPC Server。 + // 前置条件:该 run 的 DeliverRunFinish 已上传完成,提前调用返回 FAILED_PRECONDITION。 + ConfirmRunFinish(ctx context.Context, in *ConfirmRunFinishRequest, opts ...grpc.CallOption) (*ConfirmRunFinishResponse, error) } type coreServiceClient struct { @@ -65,6 +81,26 @@ func NewCoreServiceClient(cc grpc.ClientConnInterface) CoreServiceClient { return &coreServiceClient{cc} } +func (c *coreServiceClient) SpinupService(ctx context.Context, in *SpinupServiceRequest, opts ...grpc.CallOption) (*SpinupServiceResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SpinupServiceResponse) + err := c.cc.Invoke(ctx, CoreService_SpinupService_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *coreServiceClient) TeardownService(ctx context.Context, in *TeardownServiceRequest, opts ...grpc.CallOption) (*TeardownServiceResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(TeardownServiceResponse) + err := c.cc.Invoke(ctx, CoreService_TeardownService_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *coreServiceClient) DeliverRunStart(ctx context.Context, in *DeliverRunStartRequest, opts ...grpc.CallOption) (*DeliverRunStartResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(DeliverRunStartResponse) @@ -135,7 +171,7 @@ func (c *coreServiceClient) DeliverRunFinish(ctx context.Context, in *DeliverRun return out, nil } -func (c *coreServiceClient) GetOperationStats(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*GetOperationStatsResponse, error) { +func (c *coreServiceClient) GetOperationStats(ctx context.Context, in *GetOperationStatsRequest, opts ...grpc.CallOption) (*GetOperationStatsResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(GetOperationStatsResponse) err := c.cc.Invoke(ctx, CoreService_GetOperationStats_FullMethodName, in, out, cOpts...) @@ -145,7 +181,7 @@ func (c *coreServiceClient) GetOperationStats(ctx context.Context, in *emptypb.E return out, nil } -func (c *coreServiceClient) ConfirmRunFinish(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ConfirmRunFinishResponse, error) { +func (c *coreServiceClient) ConfirmRunFinish(ctx context.Context, in *ConfirmRunFinishRequest, opts ...grpc.CallOption) (*ConfirmRunFinishResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(ConfirmRunFinishResponse) err := c.cc.Invoke(ctx, CoreService_ConfirmRunFinish_FullMethodName, in, out, cOpts...) @@ -159,8 +195,21 @@ func (c *coreServiceClient) ConfirmRunFinish(ctx context.Context, in *emptypb.Em // All implementations must embed UnimplementedCoreServiceServer // for forward compatibility. // -// CoreService 是核心业务接口,用于同步或异步地接收实验记录 +// CoreService 是核心业务接口,用于同步或异步地接收实验记录。 +// +// 生命周期约定: +// +// 1. Core 与 run 级生命周期严格分离:SpinupService / TeardownService 作用于整个 core 服务进程; +// 2. 所有 run 级 RPC 通过显式 run_handle 路由到具体会话,不依赖 channel 隐式绑定 run; +// 3. ConfirmRunFinish 确认单个 run 已排空并释放其资源,不关闭 gRPC Server; +// 须在对应 run 的 DeliverRunFinish 之后调用,提前调用返回 FAILED_PRECONDITION 且会话保留; +// 4. GetOperationStats / ConfirmRunFinish 输入使用专用 request,由 run_handle 指定目标 run。 type CoreServiceServer interface { + // SpinupService 完成服务级初始化并把服务置为 READY,不负责创建 run 级 datastore 和 transport + // 启动失败 RPC 返回 FAILED_PRECONDITION;重复 Spinup 在 READY 下幂等成功,共享一个 core 服务进程。 + SpinupService(context.Context, *SpinupServiceRequest) (*SpinupServiceResponse, error) + // TeardownService 关闭整个服务进程。不同于 ConfirmRunFinish ,能连接 core 的 client 都可调用。 + TeardownService(context.Context, *TeardownServiceRequest) (*TeardownServiceResponse, error) // DeliverRunStart 接收单条 StartRecord,用于实验开始,并返回必要的信息。 DeliverRunStart(context.Context, *DeliverRunStartRequest) (*DeliverRunStartResponse, error) // UpsertColumns 接收一组 ColumnRecord 并写入,每一条记录用于定义某一个指标 @@ -175,10 +224,11 @@ type CoreServiceServer interface { UpsertSaves(context.Context, *UpsertSavesRequest) (*emptypb.Empty, error) // DeliverRunFinish 接收单条 FinishRecord,用于实验结束。 DeliverRunFinish(context.Context, *DeliverRunFinishRequest) (*DeliverRunFinishResponse, error) - // GetOperationStats 返回 Core 当前运行状态和上传进度快照。 - GetOperationStats(context.Context, *emptypb.Empty) (*GetOperationStatsResponse, error) - // ConfirmRunFinish 确认 Core 运行结束,可以安全退出 - ConfirmRunFinish(context.Context, *emptypb.Empty) (*ConfirmRunFinishResponse, error) + // GetOperationStats 返回指定 run 当前运行状态和上传进度快照。 + GetOperationStats(context.Context, *GetOperationStatsRequest) (*GetOperationStatsResponse, error) + // ConfirmRunFinish 确认指定 run 已排空并释放其资源,不关闭 gRPC Server。 + // 前置条件:该 run 的 DeliverRunFinish 已上传完成,提前调用返回 FAILED_PRECONDITION。 + ConfirmRunFinish(context.Context, *ConfirmRunFinishRequest) (*ConfirmRunFinishResponse, error) mustEmbedUnimplementedCoreServiceServer() } @@ -189,6 +239,12 @@ type CoreServiceServer interface { // pointer dereference when methods are called. type UnimplementedCoreServiceServer struct{} +func (UnimplementedCoreServiceServer) SpinupService(context.Context, *SpinupServiceRequest) (*SpinupServiceResponse, error) { + return nil, status.Error(codes.Unimplemented, "method SpinupService not implemented") +} +func (UnimplementedCoreServiceServer) TeardownService(context.Context, *TeardownServiceRequest) (*TeardownServiceResponse, error) { + return nil, status.Error(codes.Unimplemented, "method TeardownService not implemented") +} func (UnimplementedCoreServiceServer) DeliverRunStart(context.Context, *DeliverRunStartRequest) (*DeliverRunStartResponse, error) { return nil, status.Error(codes.Unimplemented, "method DeliverRunStart not implemented") } @@ -210,10 +266,10 @@ func (UnimplementedCoreServiceServer) UpsertSaves(context.Context, *UpsertSavesR func (UnimplementedCoreServiceServer) DeliverRunFinish(context.Context, *DeliverRunFinishRequest) (*DeliverRunFinishResponse, error) { return nil, status.Error(codes.Unimplemented, "method DeliverRunFinish not implemented") } -func (UnimplementedCoreServiceServer) GetOperationStats(context.Context, *emptypb.Empty) (*GetOperationStatsResponse, error) { +func (UnimplementedCoreServiceServer) GetOperationStats(context.Context, *GetOperationStatsRequest) (*GetOperationStatsResponse, error) { return nil, status.Error(codes.Unimplemented, "method GetOperationStats not implemented") } -func (UnimplementedCoreServiceServer) ConfirmRunFinish(context.Context, *emptypb.Empty) (*ConfirmRunFinishResponse, error) { +func (UnimplementedCoreServiceServer) ConfirmRunFinish(context.Context, *ConfirmRunFinishRequest) (*ConfirmRunFinishResponse, error) { return nil, status.Error(codes.Unimplemented, "method ConfirmRunFinish not implemented") } func (UnimplementedCoreServiceServer) mustEmbedUnimplementedCoreServiceServer() {} @@ -237,6 +293,42 @@ func RegisterCoreServiceServer(s grpc.ServiceRegistrar, srv CoreServiceServer) { s.RegisterService(&CoreService_ServiceDesc, srv) } +func _CoreService_SpinupService_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SpinupServiceRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CoreServiceServer).SpinupService(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: CoreService_SpinupService_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CoreServiceServer).SpinupService(ctx, req.(*SpinupServiceRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _CoreService_TeardownService_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TeardownServiceRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CoreServiceServer).TeardownService(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: CoreService_TeardownService_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CoreServiceServer).TeardownService(ctx, req.(*TeardownServiceRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _CoreService_DeliverRunStart_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(DeliverRunStartRequest) if err := dec(in); err != nil { @@ -364,7 +456,7 @@ func _CoreService_DeliverRunFinish_Handler(srv interface{}, ctx context.Context, } func _CoreService_GetOperationStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(emptypb.Empty) + in := new(GetOperationStatsRequest) if err := dec(in); err != nil { return nil, err } @@ -376,13 +468,13 @@ func _CoreService_GetOperationStats_Handler(srv interface{}, ctx context.Context FullMethod: CoreService_GetOperationStats_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(CoreServiceServer).GetOperationStats(ctx, req.(*emptypb.Empty)) + return srv.(CoreServiceServer).GetOperationStats(ctx, req.(*GetOperationStatsRequest)) } return interceptor(ctx, in, info, handler) } func _CoreService_ConfirmRunFinish_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(emptypb.Empty) + in := new(ConfirmRunFinishRequest) if err := dec(in); err != nil { return nil, err } @@ -394,7 +486,7 @@ func _CoreService_ConfirmRunFinish_Handler(srv interface{}, ctx context.Context, FullMethod: CoreService_ConfirmRunFinish_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(CoreServiceServer).ConfirmRunFinish(ctx, req.(*emptypb.Empty)) + return srv.(CoreServiceServer).ConfirmRunFinish(ctx, req.(*ConfirmRunFinishRequest)) } return interceptor(ctx, in, info, handler) } @@ -406,6 +498,14 @@ var CoreService_ServiceDesc = grpc.ServiceDesc{ ServiceName: "swanlab.grpc.core.v1.CoreService", HandlerType: (*CoreServiceServer)(nil), Methods: []grpc.MethodDesc{ + { + MethodName: "SpinupService", + Handler: _CoreService_SpinupService_Handler, + }, + { + MethodName: "TeardownService", + Handler: _CoreService_TeardownService_Handler, + }, { MethodName: "DeliverRunStart", Handler: _CoreService_DeliverRunStart_Handler, diff --git a/protos/swanlab/grpc/core/v1/core.proto b/protos/swanlab/grpc/core/v1/core.proto index bb5ababb9..6e921937c 100644 --- a/protos/swanlab/grpc/core/v1/core.proto +++ b/protos/swanlab/grpc/core/v1/core.proto @@ -13,8 +13,23 @@ import "swanlab/settings/core/v1/core.proto"; option go_package = "github.com/swanhubx/swanlab/core/proto/swanlab/grpc/core/v1;corev1"; -// CoreService 是核心业务接口,用于同步或异步地接收实验记录 +// CoreService 是核心业务接口,用于同步或异步地接收实验记录。 +// +// 生命周期约定: +// +// 1. Core 与 run 级生命周期严格分离:SpinupService / TeardownService 作用于整个 core 服务进程; +// 2. 所有 run 级 RPC 通过显式 run_handle 路由到具体会话,不依赖 channel 隐式绑定 run; +// 3. ConfirmRunFinish 确认单个 run 已排空并释放其资源,不关闭 gRPC Server; +// 须在对应 run 的 DeliverRunFinish 之后调用,提前调用返回 FAILED_PRECONDITION 且会话保留; +// 4. GetOperationStats / ConfirmRunFinish 输入使用专用 request,由 run_handle 指定目标 run。 service CoreService { + // SpinupService 完成服务级初始化并把服务置为 READY,不负责创建 run 级 datastore 和 transport + // 启动失败 RPC 返回 FAILED_PRECONDITION;重复 Spinup 在 READY 下幂等成功,共享一个 core 服务进程。 + rpc SpinupService(SpinupServiceRequest) returns (SpinupServiceResponse); + + // TeardownService 关闭整个服务进程。不同于 ConfirmRunFinish ,能连接 core 的 client 都可调用。 + rpc TeardownService(TeardownServiceRequest) returns (TeardownServiceResponse); + // DeliverRunStart 接收单条 StartRecord,用于实验开始,并返回必要的信息。 rpc DeliverRunStart(DeliverRunStartRequest) returns (DeliverRunStartResponse); @@ -36,11 +51,28 @@ service CoreService { // DeliverRunFinish 接收单条 FinishRecord,用于实验结束。 rpc DeliverRunFinish(DeliverRunFinishRequest) returns (DeliverRunFinishResponse); - // GetOperationStats 返回 Core 当前运行状态和上传进度快照。 - rpc GetOperationStats(google.protobuf.Empty) returns (GetOperationStatsResponse); + // GetOperationStats 返回指定 run 当前运行状态和上传进度快照。 + rpc GetOperationStats(GetOperationStatsRequest) returns (GetOperationStatsResponse); + + // ConfirmRunFinish 确认指定 run 已排空并释放其资源,不关闭 gRPC Server。 + // 前置条件:该 run 的 DeliverRunFinish 已上传完成,提前调用返回 FAILED_PRECONDITION。 + rpc ConfirmRunFinish(ConfirmRunFinishRequest) returns (ConfirmRunFinishResponse); +} + +// SpinupService 请求。无身份字段:core 面向同一本地信任域,不做应用层鉴权。 +message SpinupServiceRequest { +} + +// SpinupService 响应。无字段,返回即表示服务已 READY。 +message SpinupServiceResponse { +} + +// TeardownService 请求。无身份字段,同 SpinupServiceRequest。 +message TeardownServiceRequest { +} - // ConfirmRunFinish 确认 Core 运行结束,可以安全退出 - rpc ConfirmRunFinish(google.protobuf.Empty) returns (ConfirmRunFinishResponse); +// TeardownService 响应。 +message TeardownServiceResponse { } // DeliverRunStart 请求,生命周期函数,运行正式开始 @@ -51,44 +83,51 @@ message DeliverRunStartRequest { // DeliverRunStart 响应 message DeliverRunStartResponse { - bool success = 1; // 请求是否成功 - string message = 2; // 请求失败的响应 - swanlab.run.v1.StartRecord run = 3; // 最终创建的 Run 记录 - string path = 4; // 对应的实验路径,格式为 /:username/:project_name/:slug(run_id) - string name = 5; // 对应的实验名称 - int64 global_step = 6; // 起始全局步数 - int64 global_system_step = 7; // 起始全局系统步数 - bool new_experiment = 8; // 是否是新实验 + bool success = 1; // 请求是否成功 + string message = 2; // 请求失败的响应 + swanlab.run.v1.StartRecord run = 3; // 最终创建的 Run 记录 + string path = 4; // 对应的实验路径,格式为 /:username/:project_name/:slug(run_id) + string name = 5; // 对应的实验名称 + int64 global_step = 6; // 起始全局步数 + int64 global_system_step = 7; // 起始全局系统步数 + bool new_experiment = 8; // 是否是新实验 + string run_handle = 9; // 服务端生成的 opaque 会话标识,后续所有 run 级 RPC 必须携带 } // UpsertColumns 请求,异步插入一批列定义 message UpsertColumnsRequest { repeated swanlab.metric.column.v1.ColumnRecord columns = 1; // 列定义列表 + string run_handle = 2; // 目标 run 会话标识 } // UpsertScalars 请求,异步插入一批标量数据 message UpsertScalarsRequest { repeated swanlab.metric.data.v1.ScalarRecord data = 1; // 标量记录列表 + string run_handle = 2; // 目标 run 会话标识 } // UpsertMedia 请求,异步插入一批媒体数据 message UpsertMediaRequest { repeated swanlab.metric.data.v1.MediaRecord data = 1; // 媒体记录列表 + string run_handle = 2; // 目标 run 会话标识 } // UpsertLogs 请求,异步插入一批终端输出 message UpsertLogsRequest { repeated swanlab.terminal.v1.LogRecord logs = 1; // 终端日志记录列表 + string run_handle = 2; // 目标 run 会话标识 } // UpsertSaves 请求,异步插入一批文件保存记录 message UpsertSavesRequest { repeated swanlab.save.v1.SaveRecord saves = 1; // 保存记录列表 + string run_handle = 2; // 目标 run 会话标识 } // DeliverRunFinish 请求,生命周期函数,运行结束 message DeliverRunFinishRequest { swanlab.run.v1.FinishRecord finish_record = 1; // Run 结束记录 + string run_handle = 2; // 目标 run 会话标识 } // DeliverRunFinish 响应 @@ -97,6 +136,11 @@ message DeliverRunFinishResponse { string message = 2; // 请求失败的响应 } +// GetOperationStats 请求。 +message GetOperationStatsRequest { + string run_handle = 1; // 目标 run 会话标识 +} + // GetOperationStats 相应 message GetOperationStatsResponse { bool success = 1; // 请求是否成功 @@ -104,6 +148,11 @@ message GetOperationStatsResponse { swanlab.operation.v1.OperationStats stats = 3; // Core 当前运行状态和上传进度快照 } +// ConfirmRunFinish 请求。 +message ConfirmRunFinishRequest { + string run_handle = 1; // 目标 run 会话标识 +} + // ConfirmRunFinish 响应 message ConfirmRunFinishResponse { bool success = 1; // 请求是否成功 diff --git a/swanlab/proto/swanlab/grpc/core/v1/core_pb2.py b/swanlab/proto/swanlab/grpc/core/v1/core_pb2.py index 008916c96..216bacbe0 100644 --- a/swanlab/proto/swanlab/grpc/core/v1/core_pb2.py +++ b/swanlab/proto/swanlab/grpc/core/v1/core_pb2.py @@ -32,7 +32,7 @@ from swanlab.proto.swanlab.settings.core.v1 import core_pb2 as swanlab_dot_settings_dot_core_dot_v1_dot_core__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1fswanlab/grpc/core/v1/core.proto\x12\x14swanlab.grpc.core.v1\x1a\x1bgoogle/protobuf/empty.proto\x1a\x18swanlab/run/v1/run.proto\x1a!swanlab/metric/data/v1/data.proto\x1a%swanlab/metric/column/v1/column.proto\x1a\x1dswanlab/terminal/v1/log.proto\x1a\x1aswanlab/save/v1/save.proto\x1a$swanlab/operation/v1/operation.proto\x1a#swanlab/settings/core/v1/core.proto\"\x8a\x01\n\x16\x44\x65liverRunStartRequest\x12=\n\rcore_settings\x18\x01 \x01(\x0b\x32&.swanlab.settings.core.v1.CoreSettings\x12\x31\n\x0cstart_record\x18\x02 \x01(\x0b\x32\x1b.swanlab.run.v1.StartRecord\"\xca\x01\n\x17\x44\x65liverRunStartResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x0f\n\x07message\x18\x02 \x01(\t\x12(\n\x03run\x18\x03 \x01(\x0b\x32\x1b.swanlab.run.v1.StartRecord\x12\x0c\n\x04path\x18\x04 \x01(\t\x12\x0c\n\x04name\x18\x05 \x01(\t\x12\x13\n\x0bglobal_step\x18\x06 \x01(\x03\x12\x1a\n\x12global_system_step\x18\x07 \x01(\x03\x12\x16\n\x0enew_experiment\x18\x08 \x01(\x08\"O\n\x14UpsertColumnsRequest\x12\x37\n\x07\x63olumns\x18\x01 \x03(\x0b\x32&.swanlab.metric.column.v1.ColumnRecord\"J\n\x14UpsertScalarsRequest\x12\x32\n\x04\x64\x61ta\x18\x01 \x03(\x0b\x32$.swanlab.metric.data.v1.ScalarRecord\"G\n\x12UpsertMediaRequest\x12\x31\n\x04\x64\x61ta\x18\x01 \x03(\x0b\x32#.swanlab.metric.data.v1.MediaRecord\"A\n\x11UpsertLogsRequest\x12,\n\x04logs\x18\x01 \x03(\x0b\x32\x1e.swanlab.terminal.v1.LogRecord\"@\n\x12UpsertSavesRequest\x12*\n\x05saves\x18\x01 \x03(\x0b\x32\x1b.swanlab.save.v1.SaveRecord\"N\n\x17\x44\x65liverRunFinishRequest\x12\x33\n\rfinish_record\x18\x01 \x01(\x0b\x32\x1c.swanlab.run.v1.FinishRecord\"<\n\x18\x44\x65liverRunFinishResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x0f\n\x07message\x18\x02 \x01(\t\"r\n\x19GetOperationStatsResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x33\n\x05stats\x18\x03 \x01(\x0b\x32$.swanlab.operation.v1.OperationStats\"<\n\x18\x43onfirmRunFinishResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x0f\n\x07message\x18\x02 \x01(\t2\xc5\x06\n\x0b\x43oreService\x12n\n\x0f\x44\x65liverRunStart\x12,.swanlab.grpc.core.v1.DeliverRunStartRequest\x1a-.swanlab.grpc.core.v1.DeliverRunStartResponse\x12S\n\rUpsertColumns\x12*.swanlab.grpc.core.v1.UpsertColumnsRequest\x1a\x16.google.protobuf.Empty\x12S\n\rUpsertScalars\x12*.swanlab.grpc.core.v1.UpsertScalarsRequest\x1a\x16.google.protobuf.Empty\x12O\n\x0bUpsertMedia\x12(.swanlab.grpc.core.v1.UpsertMediaRequest\x1a\x16.google.protobuf.Empty\x12M\n\nUpsertLogs\x12\'.swanlab.grpc.core.v1.UpsertLogsRequest\x1a\x16.google.protobuf.Empty\x12O\n\x0bUpsertSaves\x12(.swanlab.grpc.core.v1.UpsertSavesRequest\x1a\x16.google.protobuf.Empty\x12q\n\x10\x44\x65liverRunFinish\x12-.swanlab.grpc.core.v1.DeliverRunFinishRequest\x1a..swanlab.grpc.core.v1.DeliverRunFinishResponse\x12\\\n\x11GetOperationStats\x12\x16.google.protobuf.Empty\x1a/.swanlab.grpc.core.v1.GetOperationStatsResponse\x12Z\n\x10\x43onfirmRunFinish\x12\x16.google.protobuf.Empty\x1a..swanlab.grpc.core.v1.ConfirmRunFinishResponseBDZBgithub.com/swanhubx/swanlab/core/proto/swanlab/grpc/core/v1;corev1b\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1fswanlab/grpc/core/v1/core.proto\x12\x14swanlab.grpc.core.v1\x1a\x1bgoogle/protobuf/empty.proto\x1a\x18swanlab/run/v1/run.proto\x1a!swanlab/metric/data/v1/data.proto\x1a%swanlab/metric/column/v1/column.proto\x1a\x1dswanlab/terminal/v1/log.proto\x1a\x1aswanlab/save/v1/save.proto\x1a$swanlab/operation/v1/operation.proto\x1a#swanlab/settings/core/v1/core.proto\"\x16\n\x14SpinupServiceRequest\"\x17\n\x15SpinupServiceResponse\"\x18\n\x16TeardownServiceRequest\"\x19\n\x17TeardownServiceResponse\"\x8a\x01\n\x16\x44\x65liverRunStartRequest\x12=\n\rcore_settings\x18\x01 \x01(\x0b\x32&.swanlab.settings.core.v1.CoreSettings\x12\x31\n\x0cstart_record\x18\x02 \x01(\x0b\x32\x1b.swanlab.run.v1.StartRecord\"\xde\x01\n\x17\x44\x65liverRunStartResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x0f\n\x07message\x18\x02 \x01(\t\x12(\n\x03run\x18\x03 \x01(\x0b\x32\x1b.swanlab.run.v1.StartRecord\x12\x0c\n\x04path\x18\x04 \x01(\t\x12\x0c\n\x04name\x18\x05 \x01(\t\x12\x13\n\x0bglobal_step\x18\x06 \x01(\x03\x12\x1a\n\x12global_system_step\x18\x07 \x01(\x03\x12\x16\n\x0enew_experiment\x18\x08 \x01(\x08\x12\x12\n\nrun_handle\x18\t \x01(\t\"c\n\x14UpsertColumnsRequest\x12\x37\n\x07\x63olumns\x18\x01 \x03(\x0b\x32&.swanlab.metric.column.v1.ColumnRecord\x12\x12\n\nrun_handle\x18\x02 \x01(\t\"^\n\x14UpsertScalarsRequest\x12\x32\n\x04\x64\x61ta\x18\x01 \x03(\x0b\x32$.swanlab.metric.data.v1.ScalarRecord\x12\x12\n\nrun_handle\x18\x02 \x01(\t\"[\n\x12UpsertMediaRequest\x12\x31\n\x04\x64\x61ta\x18\x01 \x03(\x0b\x32#.swanlab.metric.data.v1.MediaRecord\x12\x12\n\nrun_handle\x18\x02 \x01(\t\"U\n\x11UpsertLogsRequest\x12,\n\x04logs\x18\x01 \x03(\x0b\x32\x1e.swanlab.terminal.v1.LogRecord\x12\x12\n\nrun_handle\x18\x02 \x01(\t\"T\n\x12UpsertSavesRequest\x12*\n\x05saves\x18\x01 \x03(\x0b\x32\x1b.swanlab.save.v1.SaveRecord\x12\x12\n\nrun_handle\x18\x02 \x01(\t\"b\n\x17\x44\x65liverRunFinishRequest\x12\x33\n\rfinish_record\x18\x01 \x01(\x0b\x32\x1c.swanlab.run.v1.FinishRecord\x12\x12\n\nrun_handle\x18\x02 \x01(\t\"<\n\x18\x44\x65liverRunFinishResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x0f\n\x07message\x18\x02 \x01(\t\".\n\x18GetOperationStatsRequest\x12\x12\n\nrun_handle\x18\x01 \x01(\t\"r\n\x19GetOperationStatsResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x33\n\x05stats\x18\x03 \x01(\x0b\x32$.swanlab.operation.v1.OperationStats\"-\n\x17\x43onfirmRunFinishRequest\x12\x12\n\nrun_handle\x18\x01 \x01(\t\"<\n\x18\x43onfirmRunFinishResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x0f\n\x07message\x18\x02 \x01(\t2\xce\x08\n\x0b\x43oreService\x12h\n\rSpinupService\x12*.swanlab.grpc.core.v1.SpinupServiceRequest\x1a+.swanlab.grpc.core.v1.SpinupServiceResponse\x12n\n\x0fTeardownService\x12,.swanlab.grpc.core.v1.TeardownServiceRequest\x1a-.swanlab.grpc.core.v1.TeardownServiceResponse\x12n\n\x0f\x44\x65liverRunStart\x12,.swanlab.grpc.core.v1.DeliverRunStartRequest\x1a-.swanlab.grpc.core.v1.DeliverRunStartResponse\x12S\n\rUpsertColumns\x12*.swanlab.grpc.core.v1.UpsertColumnsRequest\x1a\x16.google.protobuf.Empty\x12S\n\rUpsertScalars\x12*.swanlab.grpc.core.v1.UpsertScalarsRequest\x1a\x16.google.protobuf.Empty\x12O\n\x0bUpsertMedia\x12(.swanlab.grpc.core.v1.UpsertMediaRequest\x1a\x16.google.protobuf.Empty\x12M\n\nUpsertLogs\x12\'.swanlab.grpc.core.v1.UpsertLogsRequest\x1a\x16.google.protobuf.Empty\x12O\n\x0bUpsertSaves\x12(.swanlab.grpc.core.v1.UpsertSavesRequest\x1a\x16.google.protobuf.Empty\x12q\n\x10\x44\x65liverRunFinish\x12-.swanlab.grpc.core.v1.DeliverRunFinishRequest\x1a..swanlab.grpc.core.v1.DeliverRunFinishResponse\x12t\n\x11GetOperationStats\x12..swanlab.grpc.core.v1.GetOperationStatsRequest\x1a/.swanlab.grpc.core.v1.GetOperationStatsResponse\x12q\n\x10\x43onfirmRunFinish\x12-.swanlab.grpc.core.v1.ConfirmRunFinishRequest\x1a..swanlab.grpc.core.v1.ConfirmRunFinishResponseBDZBgithub.com/swanhubx/swanlab/core/proto/swanlab/grpc/core/v1;corev1b\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -40,28 +40,40 @@ if not _descriptor._USE_C_DESCRIPTORS: _globals['DESCRIPTOR']._loaded_options = None _globals['DESCRIPTOR']._serialized_options = b'ZBgithub.com/swanhubx/swanlab/core/proto/swanlab/grpc/core/v1;corev1' - _globals['_DELIVERRUNSTARTREQUEST']._serialized_start=321 - _globals['_DELIVERRUNSTARTREQUEST']._serialized_end=459 - _globals['_DELIVERRUNSTARTRESPONSE']._serialized_start=462 - _globals['_DELIVERRUNSTARTRESPONSE']._serialized_end=664 - _globals['_UPSERTCOLUMNSREQUEST']._serialized_start=666 - _globals['_UPSERTCOLUMNSREQUEST']._serialized_end=745 - _globals['_UPSERTSCALARSREQUEST']._serialized_start=747 - _globals['_UPSERTSCALARSREQUEST']._serialized_end=821 - _globals['_UPSERTMEDIAREQUEST']._serialized_start=823 - _globals['_UPSERTMEDIAREQUEST']._serialized_end=894 - _globals['_UPSERTLOGSREQUEST']._serialized_start=896 - _globals['_UPSERTLOGSREQUEST']._serialized_end=961 - _globals['_UPSERTSAVESREQUEST']._serialized_start=963 - _globals['_UPSERTSAVESREQUEST']._serialized_end=1027 - _globals['_DELIVERRUNFINISHREQUEST']._serialized_start=1029 - _globals['_DELIVERRUNFINISHREQUEST']._serialized_end=1107 - _globals['_DELIVERRUNFINISHRESPONSE']._serialized_start=1109 - _globals['_DELIVERRUNFINISHRESPONSE']._serialized_end=1169 - _globals['_GETOPERATIONSTATSRESPONSE']._serialized_start=1171 - _globals['_GETOPERATIONSTATSRESPONSE']._serialized_end=1285 - _globals['_CONFIRMRUNFINISHRESPONSE']._serialized_start=1287 - _globals['_CONFIRMRUNFINISHRESPONSE']._serialized_end=1347 - _globals['_CORESERVICE']._serialized_start=1350 - _globals['_CORESERVICE']._serialized_end=2187 + _globals['_SPINUPSERVICEREQUEST']._serialized_start=320 + _globals['_SPINUPSERVICEREQUEST']._serialized_end=342 + _globals['_SPINUPSERVICERESPONSE']._serialized_start=344 + _globals['_SPINUPSERVICERESPONSE']._serialized_end=367 + _globals['_TEARDOWNSERVICEREQUEST']._serialized_start=369 + _globals['_TEARDOWNSERVICEREQUEST']._serialized_end=393 + _globals['_TEARDOWNSERVICERESPONSE']._serialized_start=395 + _globals['_TEARDOWNSERVICERESPONSE']._serialized_end=420 + _globals['_DELIVERRUNSTARTREQUEST']._serialized_start=423 + _globals['_DELIVERRUNSTARTREQUEST']._serialized_end=561 + _globals['_DELIVERRUNSTARTRESPONSE']._serialized_start=564 + _globals['_DELIVERRUNSTARTRESPONSE']._serialized_end=786 + _globals['_UPSERTCOLUMNSREQUEST']._serialized_start=788 + _globals['_UPSERTCOLUMNSREQUEST']._serialized_end=887 + _globals['_UPSERTSCALARSREQUEST']._serialized_start=889 + _globals['_UPSERTSCALARSREQUEST']._serialized_end=983 + _globals['_UPSERTMEDIAREQUEST']._serialized_start=985 + _globals['_UPSERTMEDIAREQUEST']._serialized_end=1076 + _globals['_UPSERTLOGSREQUEST']._serialized_start=1078 + _globals['_UPSERTLOGSREQUEST']._serialized_end=1163 + _globals['_UPSERTSAVESREQUEST']._serialized_start=1165 + _globals['_UPSERTSAVESREQUEST']._serialized_end=1249 + _globals['_DELIVERRUNFINISHREQUEST']._serialized_start=1251 + _globals['_DELIVERRUNFINISHREQUEST']._serialized_end=1349 + _globals['_DELIVERRUNFINISHRESPONSE']._serialized_start=1351 + _globals['_DELIVERRUNFINISHRESPONSE']._serialized_end=1411 + _globals['_GETOPERATIONSTATSREQUEST']._serialized_start=1413 + _globals['_GETOPERATIONSTATSREQUEST']._serialized_end=1459 + _globals['_GETOPERATIONSTATSRESPONSE']._serialized_start=1461 + _globals['_GETOPERATIONSTATSRESPONSE']._serialized_end=1575 + _globals['_CONFIRMRUNFINISHREQUEST']._serialized_start=1577 + _globals['_CONFIRMRUNFINISHREQUEST']._serialized_end=1622 + _globals['_CONFIRMRUNFINISHRESPONSE']._serialized_start=1624 + _globals['_CONFIRMRUNFINISHRESPONSE']._serialized_end=1684 + _globals['_CORESERVICE']._serialized_start=1687 + _globals['_CORESERVICE']._serialized_end=2789 # @@protoc_insertion_point(module_scope) diff --git a/swanlab/proto/swanlab/grpc/core/v1/core_pb2.pyi b/swanlab/proto/swanlab/grpc/core/v1/core_pb2.pyi index 3a95628a4..2089c4493 100644 --- a/swanlab/proto/swanlab/grpc/core/v1/core_pb2.pyi +++ b/swanlab/proto/swanlab/grpc/core/v1/core_pb2.pyi @@ -14,6 +14,22 @@ from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union DESCRIPTOR: _descriptor.FileDescriptor +class SpinupServiceRequest(_message.Message): + __slots__ = () + def __init__(self) -> None: ... + +class SpinupServiceResponse(_message.Message): + __slots__ = () + def __init__(self) -> None: ... + +class TeardownServiceRequest(_message.Message): + __slots__ = () + def __init__(self) -> None: ... + +class TeardownServiceResponse(_message.Message): + __slots__ = () + def __init__(self) -> None: ... + class DeliverRunStartRequest(_message.Message): __slots__ = ("core_settings", "start_record") CORE_SETTINGS_FIELD_NUMBER: _ClassVar[int] @@ -23,7 +39,7 @@ class DeliverRunStartRequest(_message.Message): def __init__(self, core_settings: _Optional[_Union[_core_pb2.CoreSettings, _Mapping]] = ..., start_record: _Optional[_Union[_run_pb2.StartRecord, _Mapping]] = ...) -> None: ... class DeliverRunStartResponse(_message.Message): - __slots__ = ("success", "message", "run", "path", "name", "global_step", "global_system_step", "new_experiment") + __slots__ = ("success", "message", "run", "path", "name", "global_step", "global_system_step", "new_experiment", "run_handle") SUCCESS_FIELD_NUMBER: _ClassVar[int] MESSAGE_FIELD_NUMBER: _ClassVar[int] RUN_FIELD_NUMBER: _ClassVar[int] @@ -32,6 +48,7 @@ class DeliverRunStartResponse(_message.Message): GLOBAL_STEP_FIELD_NUMBER: _ClassVar[int] GLOBAL_SYSTEM_STEP_FIELD_NUMBER: _ClassVar[int] NEW_EXPERIMENT_FIELD_NUMBER: _ClassVar[int] + RUN_HANDLE_FIELD_NUMBER: _ClassVar[int] success: bool message: str run: _run_pb2.StartRecord @@ -40,43 +57,56 @@ class DeliverRunStartResponse(_message.Message): global_step: int global_system_step: int new_experiment: bool - def __init__(self, success: bool = ..., message: _Optional[str] = ..., run: _Optional[_Union[_run_pb2.StartRecord, _Mapping]] = ..., path: _Optional[str] = ..., name: _Optional[str] = ..., global_step: _Optional[int] = ..., global_system_step: _Optional[int] = ..., new_experiment: bool = ...) -> None: ... + run_handle: str + def __init__(self, success: bool = ..., message: _Optional[str] = ..., run: _Optional[_Union[_run_pb2.StartRecord, _Mapping]] = ..., path: _Optional[str] = ..., name: _Optional[str] = ..., global_step: _Optional[int] = ..., global_system_step: _Optional[int] = ..., new_experiment: bool = ..., run_handle: _Optional[str] = ...) -> None: ... class UpsertColumnsRequest(_message.Message): - __slots__ = ("columns",) + __slots__ = ("columns", "run_handle") COLUMNS_FIELD_NUMBER: _ClassVar[int] + RUN_HANDLE_FIELD_NUMBER: _ClassVar[int] columns: _containers.RepeatedCompositeFieldContainer[_column_pb2.ColumnRecord] - def __init__(self, columns: _Optional[_Iterable[_Union[_column_pb2.ColumnRecord, _Mapping]]] = ...) -> None: ... + run_handle: str + def __init__(self, columns: _Optional[_Iterable[_Union[_column_pb2.ColumnRecord, _Mapping]]] = ..., run_handle: _Optional[str] = ...) -> None: ... class UpsertScalarsRequest(_message.Message): - __slots__ = ("data",) + __slots__ = ("data", "run_handle") DATA_FIELD_NUMBER: _ClassVar[int] + RUN_HANDLE_FIELD_NUMBER: _ClassVar[int] data: _containers.RepeatedCompositeFieldContainer[_data_pb2.ScalarRecord] - def __init__(self, data: _Optional[_Iterable[_Union[_data_pb2.ScalarRecord, _Mapping]]] = ...) -> None: ... + run_handle: str + def __init__(self, data: _Optional[_Iterable[_Union[_data_pb2.ScalarRecord, _Mapping]]] = ..., run_handle: _Optional[str] = ...) -> None: ... class UpsertMediaRequest(_message.Message): - __slots__ = ("data",) + __slots__ = ("data", "run_handle") DATA_FIELD_NUMBER: _ClassVar[int] + RUN_HANDLE_FIELD_NUMBER: _ClassVar[int] data: _containers.RepeatedCompositeFieldContainer[_data_pb2.MediaRecord] - def __init__(self, data: _Optional[_Iterable[_Union[_data_pb2.MediaRecord, _Mapping]]] = ...) -> None: ... + run_handle: str + def __init__(self, data: _Optional[_Iterable[_Union[_data_pb2.MediaRecord, _Mapping]]] = ..., run_handle: _Optional[str] = ...) -> None: ... class UpsertLogsRequest(_message.Message): - __slots__ = ("logs",) + __slots__ = ("logs", "run_handle") LOGS_FIELD_NUMBER: _ClassVar[int] + RUN_HANDLE_FIELD_NUMBER: _ClassVar[int] logs: _containers.RepeatedCompositeFieldContainer[_log_pb2.LogRecord] - def __init__(self, logs: _Optional[_Iterable[_Union[_log_pb2.LogRecord, _Mapping]]] = ...) -> None: ... + run_handle: str + def __init__(self, logs: _Optional[_Iterable[_Union[_log_pb2.LogRecord, _Mapping]]] = ..., run_handle: _Optional[str] = ...) -> None: ... class UpsertSavesRequest(_message.Message): - __slots__ = ("saves",) + __slots__ = ("saves", "run_handle") SAVES_FIELD_NUMBER: _ClassVar[int] + RUN_HANDLE_FIELD_NUMBER: _ClassVar[int] saves: _containers.RepeatedCompositeFieldContainer[_save_pb2.SaveRecord] - def __init__(self, saves: _Optional[_Iterable[_Union[_save_pb2.SaveRecord, _Mapping]]] = ...) -> None: ... + run_handle: str + def __init__(self, saves: _Optional[_Iterable[_Union[_save_pb2.SaveRecord, _Mapping]]] = ..., run_handle: _Optional[str] = ...) -> None: ... class DeliverRunFinishRequest(_message.Message): - __slots__ = ("finish_record",) + __slots__ = ("finish_record", "run_handle") FINISH_RECORD_FIELD_NUMBER: _ClassVar[int] + RUN_HANDLE_FIELD_NUMBER: _ClassVar[int] finish_record: _run_pb2.FinishRecord - def __init__(self, finish_record: _Optional[_Union[_run_pb2.FinishRecord, _Mapping]] = ...) -> None: ... + run_handle: str + def __init__(self, finish_record: _Optional[_Union[_run_pb2.FinishRecord, _Mapping]] = ..., run_handle: _Optional[str] = ...) -> None: ... class DeliverRunFinishResponse(_message.Message): __slots__ = ("success", "message") @@ -86,6 +116,12 @@ class DeliverRunFinishResponse(_message.Message): message: str def __init__(self, success: bool = ..., message: _Optional[str] = ...) -> None: ... +class GetOperationStatsRequest(_message.Message): + __slots__ = ("run_handle",) + RUN_HANDLE_FIELD_NUMBER: _ClassVar[int] + run_handle: str + def __init__(self, run_handle: _Optional[str] = ...) -> None: ... + class GetOperationStatsResponse(_message.Message): __slots__ = ("success", "message", "stats") SUCCESS_FIELD_NUMBER: _ClassVar[int] @@ -96,6 +132,12 @@ class GetOperationStatsResponse(_message.Message): stats: _operation_pb2.OperationStats def __init__(self, success: bool = ..., message: _Optional[str] = ..., stats: _Optional[_Union[_operation_pb2.OperationStats, _Mapping]] = ...) -> None: ... +class ConfirmRunFinishRequest(_message.Message): + __slots__ = ("run_handle",) + RUN_HANDLE_FIELD_NUMBER: _ClassVar[int] + run_handle: str + def __init__(self, run_handle: _Optional[str] = ...) -> None: ... + class ConfirmRunFinishResponse(_message.Message): __slots__ = ("success", "message") SUCCESS_FIELD_NUMBER: _ClassVar[int] diff --git a/swanlab/proto/swanlab/grpc/core/v1/core_pb2_grpc.py b/swanlab/proto/swanlab/grpc/core/v1/core_pb2_grpc.py index 2e93153ac..7fe860778 100644 --- a/swanlab/proto/swanlab/grpc/core/v1/core_pb2_grpc.py +++ b/swanlab/proto/swanlab/grpc/core/v1/core_pb2_grpc.py @@ -27,7 +27,15 @@ class CoreServiceStub(object): - """CoreService 是核心业务接口,用于同步或异步地接收实验记录 + """CoreService 是核心业务接口,用于同步或异步地接收实验记录。 + + 生命周期约定: + + 1. Core 与 run 级生命周期严格分离:SpinupService / TeardownService 作用于整个 core 服务进程; + 2. 所有 run 级 RPC 通过显式 run_handle 路由到具体会话,不依赖 channel 隐式绑定 run; + 3. ConfirmRunFinish 确认单个 run 已排空并释放其资源,不关闭 gRPC Server; + 须在对应 run 的 DeliverRunFinish 之后调用,提前调用返回 FAILED_PRECONDITION 且会话保留; + 4. GetOperationStats / ConfirmRunFinish 输入使用专用 request,由 run_handle 指定目标 run。 """ def __init__(self, channel): @@ -36,6 +44,16 @@ def __init__(self, channel): Args: channel: A grpc.Channel. """ + self.SpinupService = channel.unary_unary( + '/swanlab.grpc.core.v1.CoreService/SpinupService', + request_serializer=swanlab_dot_grpc_dot_core_dot_v1_dot_core__pb2.SpinupServiceRequest.SerializeToString, + response_deserializer=swanlab_dot_grpc_dot_core_dot_v1_dot_core__pb2.SpinupServiceResponse.FromString, + _registered_method=True) + self.TeardownService = channel.unary_unary( + '/swanlab.grpc.core.v1.CoreService/TeardownService', + request_serializer=swanlab_dot_grpc_dot_core_dot_v1_dot_core__pb2.TeardownServiceRequest.SerializeToString, + response_deserializer=swanlab_dot_grpc_dot_core_dot_v1_dot_core__pb2.TeardownServiceResponse.FromString, + _registered_method=True) self.DeliverRunStart = channel.unary_unary( '/swanlab.grpc.core.v1.CoreService/DeliverRunStart', request_serializer=swanlab_dot_grpc_dot_core_dot_v1_dot_core__pb2.DeliverRunStartRequest.SerializeToString, @@ -73,20 +91,43 @@ def __init__(self, channel): _registered_method=True) self.GetOperationStats = channel.unary_unary( '/swanlab.grpc.core.v1.CoreService/GetOperationStats', - request_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, + request_serializer=swanlab_dot_grpc_dot_core_dot_v1_dot_core__pb2.GetOperationStatsRequest.SerializeToString, response_deserializer=swanlab_dot_grpc_dot_core_dot_v1_dot_core__pb2.GetOperationStatsResponse.FromString, _registered_method=True) self.ConfirmRunFinish = channel.unary_unary( '/swanlab.grpc.core.v1.CoreService/ConfirmRunFinish', - request_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, + request_serializer=swanlab_dot_grpc_dot_core_dot_v1_dot_core__pb2.ConfirmRunFinishRequest.SerializeToString, response_deserializer=swanlab_dot_grpc_dot_core_dot_v1_dot_core__pb2.ConfirmRunFinishResponse.FromString, _registered_method=True) class CoreServiceServicer(object): - """CoreService 是核心业务接口,用于同步或异步地接收实验记录 + """CoreService 是核心业务接口,用于同步或异步地接收实验记录。 + + 生命周期约定: + + 1. Core 与 run 级生命周期严格分离:SpinupService / TeardownService 作用于整个 core 服务进程; + 2. 所有 run 级 RPC 通过显式 run_handle 路由到具体会话,不依赖 channel 隐式绑定 run; + 3. ConfirmRunFinish 确认单个 run 已排空并释放其资源,不关闭 gRPC Server; + 须在对应 run 的 DeliverRunFinish 之后调用,提前调用返回 FAILED_PRECONDITION 且会话保留; + 4. GetOperationStats / ConfirmRunFinish 输入使用专用 request,由 run_handle 指定目标 run。 """ + def SpinupService(self, request, context): + """SpinupService 完成服务级初始化并把服务置为 READY,不负责创建 run 级 datastore 和 transport + 启动失败 RPC 返回 FAILED_PRECONDITION;重复 Spinup 在 READY 下幂等成功,共享一个 core 服务进程。 + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def TeardownService(self, request, context): + """TeardownService 关闭整个服务进程。不同于 ConfirmRunFinish ,能连接 core 的 client 都可调用。 + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + def DeliverRunStart(self, request, context): """DeliverRunStart 接收单条 StartRecord,用于实验开始,并返回必要的信息。 """ @@ -137,14 +178,15 @@ def DeliverRunFinish(self, request, context): raise NotImplementedError('Method not implemented!') def GetOperationStats(self, request, context): - """GetOperationStats 返回 Core 当前运行状态和上传进度快照。 + """GetOperationStats 返回指定 run 当前运行状态和上传进度快照。 """ context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') def ConfirmRunFinish(self, request, context): - """ConfirmRunFinish 确认 Core 运行结束,可以安全退出 + """ConfirmRunFinish 确认指定 run 已排空并释放其资源,不关闭 gRPC Server。 + 前置条件:该 run 的 DeliverRunFinish 已上传完成,提前调用返回 FAILED_PRECONDITION。 """ context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_details('Method not implemented!') @@ -153,6 +195,16 @@ def ConfirmRunFinish(self, request, context): def add_CoreServiceServicer_to_server(servicer, server): rpc_method_handlers = { + 'SpinupService': grpc.unary_unary_rpc_method_handler( + servicer.SpinupService, + request_deserializer=swanlab_dot_grpc_dot_core_dot_v1_dot_core__pb2.SpinupServiceRequest.FromString, + response_serializer=swanlab_dot_grpc_dot_core_dot_v1_dot_core__pb2.SpinupServiceResponse.SerializeToString, + ), + 'TeardownService': grpc.unary_unary_rpc_method_handler( + servicer.TeardownService, + request_deserializer=swanlab_dot_grpc_dot_core_dot_v1_dot_core__pb2.TeardownServiceRequest.FromString, + response_serializer=swanlab_dot_grpc_dot_core_dot_v1_dot_core__pb2.TeardownServiceResponse.SerializeToString, + ), 'DeliverRunStart': grpc.unary_unary_rpc_method_handler( servicer.DeliverRunStart, request_deserializer=swanlab_dot_grpc_dot_core_dot_v1_dot_core__pb2.DeliverRunStartRequest.FromString, @@ -190,12 +242,12 @@ def add_CoreServiceServicer_to_server(servicer, server): ), 'GetOperationStats': grpc.unary_unary_rpc_method_handler( servicer.GetOperationStats, - request_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, + request_deserializer=swanlab_dot_grpc_dot_core_dot_v1_dot_core__pb2.GetOperationStatsRequest.FromString, response_serializer=swanlab_dot_grpc_dot_core_dot_v1_dot_core__pb2.GetOperationStatsResponse.SerializeToString, ), 'ConfirmRunFinish': grpc.unary_unary_rpc_method_handler( servicer.ConfirmRunFinish, - request_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, + request_deserializer=swanlab_dot_grpc_dot_core_dot_v1_dot_core__pb2.ConfirmRunFinishRequest.FromString, response_serializer=swanlab_dot_grpc_dot_core_dot_v1_dot_core__pb2.ConfirmRunFinishResponse.SerializeToString, ), } @@ -207,9 +259,71 @@ def add_CoreServiceServicer_to_server(servicer, server): # This class is part of an EXPERIMENTAL API. class CoreService(object): - """CoreService 是核心业务接口,用于同步或异步地接收实验记录 + """CoreService 是核心业务接口,用于同步或异步地接收实验记录。 + + 生命周期约定: + + 1. Core 与 run 级生命周期严格分离:SpinupService / TeardownService 作用于整个 core 服务进程; + 2. 所有 run 级 RPC 通过显式 run_handle 路由到具体会话,不依赖 channel 隐式绑定 run; + 3. ConfirmRunFinish 确认单个 run 已排空并释放其资源,不关闭 gRPC Server; + 须在对应 run 的 DeliverRunFinish 之后调用,提前调用返回 FAILED_PRECONDITION 且会话保留; + 4. GetOperationStats / ConfirmRunFinish 输入使用专用 request,由 run_handle 指定目标 run。 """ + @staticmethod + def SpinupService(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/swanlab.grpc.core.v1.CoreService/SpinupService', + swanlab_dot_grpc_dot_core_dot_v1_dot_core__pb2.SpinupServiceRequest.SerializeToString, + swanlab_dot_grpc_dot_core_dot_v1_dot_core__pb2.SpinupServiceResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def TeardownService(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/swanlab.grpc.core.v1.CoreService/TeardownService', + swanlab_dot_grpc_dot_core_dot_v1_dot_core__pb2.TeardownServiceRequest.SerializeToString, + swanlab_dot_grpc_dot_core_dot_v1_dot_core__pb2.TeardownServiceResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + @staticmethod def DeliverRunStart(request, target, @@ -414,7 +528,7 @@ def GetOperationStats(request, request, target, '/swanlab.grpc.core.v1.CoreService/GetOperationStats', - google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, + swanlab_dot_grpc_dot_core_dot_v1_dot_core__pb2.GetOperationStatsRequest.SerializeToString, swanlab_dot_grpc_dot_core_dot_v1_dot_core__pb2.GetOperationStatsResponse.FromString, options, channel_credentials, @@ -441,7 +555,7 @@ def ConfirmRunFinish(request, request, target, '/swanlab.grpc.core.v1.CoreService/ConfirmRunFinish', - google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, + swanlab_dot_grpc_dot_core_dot_v1_dot_core__pb2.ConfirmRunFinishRequest.SerializeToString, swanlab_dot_grpc_dot_core_dot_v1_dot_core__pb2.ConfirmRunFinishResponse.FromString, options, channel_credentials, diff --git a/swanlab/sdk/internal/pkg/fork/__init__.py b/swanlab/sdk/internal/pkg/fork/__init__.py index da04307fd..b05149270 100644 --- a/swanlab/sdk/internal/pkg/fork/__init__.py +++ b/swanlab/sdk/internal/pkg/fork/__init__.py @@ -13,7 +13,8 @@ 设计原则: - 回调注册表:各组件通过 ``register()`` 注册自己的 fork 清理逻辑,而非内联 ``register_at_fork`` 调用 -- 显式优于隐式:fork 后的清理通过回调显式触发,而非依赖属性计算的副作用 +- 生命周期安全:bound method 回调以弱引用存储,注册不会延长实例生命周期; + 回调内禁止获取实例级锁(fork 时该锁可能由不会存在于子进程的线程持有) - 面向未来:当 ``swanlab-core`` 上线后,可注册重连回调替代简单的 ``clear_run()`` 使用示例:: @@ -34,16 +35,38 @@ fork.unregister(some_cleanup) """ +import inspect import os import threading -from typing import Callable, List +import weakref +from typing import Callable, List, Optional -# 回调列表:fork 后在子进程中执行的清理函数 +# 回调注册表:fork 后在子进程中执行的清理函数。 +# bound method 以 WeakMethod 存储:注册不得延长实例生命周期, +# 否则实例上的 weakref.finalize 等最后保险永远不会触发。 +# 模块级函数与 lambda 保持强引用(其生命周期本就不随实例结束)。 # 使用锁保护,因为 register() 可能在多线程环境中被调用 -_callbacks: List[Callable[[], None]] = [] +_callbacks: List[object] = [] _lock = threading.Lock() +def _make_entry(callback: Callable[[], None]) -> object: + if inspect.ismethod(callback): + return weakref.WeakMethod(callback) + return callback + + +def _resolve_entry(entry: object) -> Optional[Callable[[], None]]: + if isinstance(entry, weakref.WeakMethod): + return entry() + return entry # type: ignore[return-value] + + +def _prune_dead() -> None: + """清除已死亡的弱引用条目。需持锁调用。""" + _callbacks[:] = [e for e in _callbacks if _resolve_entry(e) is not None] + + def current_pid() -> int: """获取当前进程 PID。 @@ -72,11 +95,17 @@ def register(callback: Callable[[], None]) -> None: 回调在 ``os.register_at_fork(after_in_child=...)`` 触发时执行, 即 fork 后子进程的第一时间。回调应尽量轻量,避免阻塞或抛出异常。 + 特别注意:回调内不得获取可能被其他线程持有、而该线程不会存在于 + 子进程中的锁,否则子进程会在 fork 返回前永久阻塞。 + + bound method 以弱引用存储:实例被回收后回调自动失效;注册方仍应 + 在生命周期结束时调用 ``unregister`` 即时移除条目。 :param callback: 无参回调函数,在 fork 后的子进程中执行 """ with _lock: - _callbacks.append(callback) + _prune_dead() + _callbacks.append(_make_entry(callback)) def unregister(callback: Callable[[], None]) -> None: @@ -88,10 +117,11 @@ def unregister(callback: Callable[[], None]) -> None: :param callback: 之前通过 ``register()`` 注册的回调函数 """ with _lock: - try: - _callbacks.remove(callback) - except ValueError: - pass + _prune_dead() + for index, entry in enumerate(_callbacks): + if _resolve_entry(entry) == callback: + del _callbacks[index] + return def _before_fork() -> None: @@ -114,15 +144,18 @@ def _after_in_child() -> None: """``os.register_at_fork(after_in_child=...)`` 的处理器。 在 fork 后的子进程中执行所有已注册的回调,然后重置锁。 - 回调执行顺序与注册顺序一致(FIFO)。 + 回调执行顺序与注册顺序一致(FIFO);弱引用已死亡的条目被跳过 + (父进程侧的注册表不因 fork 而改变)。 因为 _before_fork 已获取锁,此处锁处于持有状态。 直接重置为新锁,避免继承父进程的锁状态。 """ # _before_fork 已获取锁,此时 _callbacks 一定处于一致状态 try: - for cb in list(_callbacks): - cb() + for entry in list(_callbacks): + cb = _resolve_entry(entry) + if cb is not None: + cb() finally: global _lock _lock = threading.Lock()