fix: 账号列表与环境解耦;网关清理收敛已消失的旧代

- 社媒账号列表页不再拉取运行环境数据:账号是资源池,环境故障
  不应阻塞账号展示,409 横幅随之消失;「固定资源」「可调度」
  两列移除,绑定状态去运行环境页查看。详情页保持原行为。
- docker-gateway DELETE 清理:容器与登记网络均已不存在时返回
  204 而非 409,控制面 runtime_cleanup_pending 可收敛,修复
  外部清理过的旧代永久卡死;容器存活而网络消失仍 fail-closed。
This commit is contained in:
2026-09-07 09:44:22 +08:00
parent 1ce0375414
commit 83397927c2
4 changed files with 89 additions and 67 deletions
+10 -1
View File
@@ -714,7 +714,16 @@ func (api gateway) remove(c fiber.Ctx) error {
if networkExists {
err = api.removeTenantNetwork(id, input.BindingVersion, input.RuntimeID, networkGeneration, &input, containerID, exists)
} else if expectedNetworkID != "" {
return writeError(c, http.StatusConflict, errGenerationConflict)
if exists {
// 容器仍在但登记的隔离网络已消失:代状态异常,fail-closed 交由人工对账。
return writeError(c, http.StatusConflict, errGenerationConflict)
}
// 容器与登记网络均已不存在:请求指向的代在 Docker 侧已无残留,清理视为完成。
// 否则被外部清理过的旧代会永远 409,控制面的 runtime_cleanup_pending 无法收敛。
if !api.proxies.remove(id, input.BindingVersion, input.RuntimeID, input.NetworkID) {
return writeError(c, http.StatusConflict, errGenerationConflict)
}
return c.SendStatus(http.StatusNoContent)
}
if err != nil {
if errors.Is(err, errGenerationConflict) {
+54
View File
@@ -1263,6 +1263,60 @@ func TestGatewayDeleteWithoutContainerOrNetworkGenerationFailsClosed(t *testing.
}
}
func TestGatewayDeleteConvergesWhenGenerationAlreadyGone(t *testing.T) {
networkRequests := 0
server := httptest.NewServer(withAliasReservations("gateway-self", func(response http.ResponseWriter, request *http.Request) {
switch {
case request.Method == http.MethodGet && request.URL.Path == "/containers/"+namePrefix+"account-a/json":
response.WriteHeader(http.StatusNotFound)
case request.Method == http.MethodGet && request.URL.Path == "/networks/network-old":
networkRequests++
response.WriteHeader(http.StatusNotFound)
case strings.HasPrefix(request.URL.Path, "/networks/"):
t.Fatalf("network lookup leaked beyond the requested generation: %s %s", request.Method, request.URL.Path)
default:
t.Fatalf("unexpected Docker request %s %s", request.Method, request.URL.String())
}
}))
defer server.Close()
handler := newGatewayWithSelf(dockerClient{baseURL: server.URL, client: server.Client(), slow: server.Client()},
"creatorhub_browser", testToken, "gateway-self")
response := httptest.NewRecorder()
body := `{"binding_version":1,"runtime_id":"container-old","network_id":"network-old"}`
adaptor.FiberApp(handler).ServeHTTP(response, authed(http.MethodDelete, "/v1/browsers/account-a", strings.NewReader(body)))
if response.Code != http.StatusNoContent || networkRequests != 1 {
t.Fatalf("already-gone generation did not converge: status=%d networkRequests=%d body=%s",
response.Code, networkRequests, response.Body.String())
}
}
func TestGatewayDeleteFailsClosedWhenContainerOutlivesNetwork(t *testing.T) {
containerDeleted := false
server := httptest.NewServer(withAliasReservations("gateway-self", func(response http.ResponseWriter, request *http.Request) {
switch {
case request.Method == http.MethodGet && request.URL.Path == "/containers/"+namePrefix+"account-a/json":
_, _ = response.Write([]byte(`{"Id":"container-old","Config":{"Labels":{"` + managedLabel + `":"true","` + idLabel + `":"account-a","` + bindingVersionLabel + `":"1","` + networkIDLabel + `":"network-old"}}}`))
case request.Method == http.MethodGet && request.URL.Path == "/networks/network-old":
response.WriteHeader(http.StatusNotFound)
case request.Method == http.MethodDelete && strings.HasPrefix(request.URL.Path, "/containers/"):
containerDeleted = true
response.WriteHeader(http.StatusNoContent)
default:
t.Fatalf("unexpected Docker request %s %s", request.Method, request.URL.String())
}
}))
defer server.Close()
handler := newGatewayWithSelf(dockerClient{baseURL: server.URL, client: server.Client(), slow: server.Client()},
"creatorhub_browser", testToken, "gateway-self")
response := httptest.NewRecorder()
body := `{"binding_version":1,"runtime_id":"container-old","network_id":"network-old"}`
adaptor.FiberApp(handler).ServeHTTP(response, authed(http.MethodDelete, "/v1/browsers/account-a", strings.NewReader(body)))
if response.Code != http.StatusConflict || containerDeleted {
t.Fatalf("container outliving its network must fail closed: status=%d containerDeleted=%v body=%s",
response.Code, containerDeleted, response.Body.String())
}
}
func TestGatewayRejectsStaleProxyRestoreAfterReplacementGeneration(t *testing.T) {
type dockerState struct {
sync.Mutex