Skip to content

[2/7] feat(core-api): talk to the swanlab server over http - #1806

Merged
Nexisato merged 11 commits into
feat/core/io-modelfrom
feat/core/io-2-client
Oct 29, 2026
Merged

Nexisato merged 11 commits into
feat/core/io-modelfrom
feat/core/io-2-client

Conversation

@Nexisato

@Nexisato Nexisato commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

概要

新增 SwanLab 云端通信层:REST 语义、鉴权与重试对齐 Python;House 写入严格只认 200(比 Python 更严格)。

变更内容

拆成两层,依赖方向固定为 api → client:

包 文件 职责
internal/client client.go 共享 *http.Transport、按 (api_host, sha256(api_key)) 复用 Client、Do(Request) (*Response, error) 原语
auth.go Authorization: ApiKey + /api/auth/verify 探活;旧后端退化为 sid Cookie(提前 7 天主动换发;401 直接按鉴权错误返回,不在请求层重放)
retry.go 5 次重试、0.5 指数退避、429/5xx、Retry-After、按请求关闭重试
errors.go APIError{Retryable, Code, Message, TraceID} 与错误分类
internal/api api.go REST 端点封装与业务结果判定
model.go 请求/响应体结构与协议枚举
internal/clienttest clienttest.go 测试共用的假后端与假时钟

关键行为:

  • /house/metrics 与心跳仅以 200 为成功,202 视为不可重试失败,并保留后端原文与 traceid。Python 当前把 202 当成功,此处有意收紧,让被静默丢弃的写入显式失败。
  • 实验创建区分 201(新建)与 200(已存在);resume="must" 时实验不存在直接报错。
  • createdAt / finishedAt 取发起请求时的 UTC 时间。
  • 上传类接口关闭底层重试,重试交由上层调度;空批次由上层 sender 过滤。
  • 预签名与 Save prepare 响应做结构校验(URL 逐项非空、uploadId 非空、分片数匹配且编号连续为 1..N),不符合契约直接判为不可重试协议错误,不进入上传阶段。
  • 共享连接池按上传并发提高空闲连接上限,Factory.Close 释放空闲连接;鉴权探活不占用全局锁,单身份慢探活不阻塞其它会话启动。

同时为 CoreSettings 增加 api_key / api_host / web_host(11/12/13),并重新生成 Go / Python 代码。

验证

  • go build ./...(linux / darwin / windows)、go vet ./...、go test -race ./... 通过;internal/client / internal/api 在 -count=10 -cpu=1 下重复运行无偶发失败。
  • golangci-lint run 0 issues;make proto 无漂移。
  • 本地 Mock 冒烟(仅 127.0.0.1):6 个场景 61 项断言通过。

Base: feat/core/io-model。

Keep `core/.smoke-tmp/` out of git: the playground smoke runner copies
`smoke/go/sN` there and runs it inside the module so it can import
`core/internal/**` without shipping temporary packages.
Add api_key (11), api_host (12) and web_host (13) so the Go core can
authenticate against the cloud with the same field numbers as the
ApiKey auth refactor, and regenerate both the Go and Python stubs.

api_key is a secret: it is only transported over DeliverRunStart, never
via command line arguments, and must be masked in any log output.
Keep business endpoints out of this package: it exposes a single
`Do(Request) (*Response, error)` round trip and owns everything about
how a request is sent.

- shared *http.Transport pool, per (api_host, sha256(api_key)) Client
  reuse, 60 s timeout, 5 retries, 0.5 backoff on 429/5xx and the
  X-SwanLab-SDK-Version header
- auth: stateless `Authorization: ApiKey` with a one-shot
  /api/auth/verify probe; legacy deployments fall back to sid cookies
  with a 7-day refresh window and locked relogin on 401
- APIError{Retryable, Code, Message, TraceID} with IsRetryable /
  IsAuthenticationError / IsInvalidResponse / IsHouseRejected, plus
  Response.HouseRejected for "202 instead of 200" writes
- clienttest: shared httptest fake cloud and injectable clock, reused by
  the client and api test suites
Layer the REST protocol on top of internal/client so transport policy
and protocol semantics can evolve separately: `api` depends on `client`
and never the other way around.

- model.go: every request/response body and protocol enum in one place
  (Experiment, Project, Summary, ColumnUpload, PrepareSaveFile,
  MultipartFile, ResumeMode, RunState, MetricKind, ...), no more
  endpoint-local anonymous structs
- api.go: project / experiment / metrics / series / presigned / save /
  profile / heartbeat
- result rules: House accepts only 200 (202 is a non-retryable
  rejection), experiment create distinguishes 200/201, createdAt and
  finishedAt are taken at request time, prepare responses are checked
  for element count and empty entries
…deflake the cancel test

- raise MaxIdleConnsPerHost from 2 to 16 (64 overall) on the cloned
  transport: the upload pipeline drives several tracks and file uploads
  concurrently, and the default value keeps dropping keep-alives
- move Factory.Client's auth probe outside the mutex so one slow identity
  cannot block other session startups; concurrent callers for the same
  identity still converge on the first cached instance
- drive TestContextCancellationIsNotRetried from a handler signal instead
  of a 20 ms sleep, which races with request dispatch on loaded CI runners
- cover both changes with pool-parameter and concurrent-factory tests
…rule

Both notes exist to stop a future reader from "fixing" the current
behaviour by accident:

- empty batches are the sender's responsibility: Python short-circuits in
  its api layer, but Go takes `metrics any`, so empty-checking here would
  need reflection; the pipeline knows the concrete slice
- House 202 is rejected on purpose even though Python's `response.ok`
  accepts it, so silently dropped writes surface as errors
…nections

- forward CloseIdleConnections through retryTransport: Factory.Close was a
  no-op because the wrapper never exposed the method, so idle connections
  were never released
- drop the 401 relogin and replay path: Python treats 401 as a plain auth
  error, and the loop could trigger one login per concurrent stale request
- classify auth response read failures as transport errors instead of
  protocol errors, so a truncated body keeps its retry semantics
- normalize api_host via parsed.Host so IPv6 literals keep their brackets
Validate presigned and Save prepare responses up front instead of letting
empty URLs, a missing uploadId or a broken part list reach the upload stage,
where they would surface as retryable network errors:

- urls must match the request size and every entry must be non-empty
- uploadId must be non-empty
- part count must match the requested count, part numbers must be 1..N with
  a URL each
@Nexisato
Nexisato marked this pull request as ready for review October 28, 2026 09:10
@Nexisato
Nexisato merged commit 8b8dc1c into feat/core/io-model Oct 29, 2026
26 checks passed
@Nexisato
Nexisato deleted the feat/core/io-2-client branch October 29, 2026 03:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant