refactor: use nginx -s reload instead of systemd

- doc: add comment
This commit is contained in:
2026-08-17 10:10:14 +08:00
parent 79f18fcdea
commit f536987a7e
62 changed files with 2216 additions and 598 deletions
+108 -37
View File
@@ -1,5 +1,5 @@
// Package backendexecutor prepares and starts one explicitly named backend container.
// Gateway switching is deliberately outside this package.
// Package backendexecutor 负责准备并启动一个显式命名的后端容器。
// 网关流量切换被刻意排除在本包职责之外。
package backendexecutor
import (
@@ -25,56 +25,95 @@ import (
)
const (
healthPath = "/yms/actuator/health"
healthTimeout = 120 * time.Second
healthInterval = time.Second
// healthPath 后端 Actuator 健康检查端点的固定路径。
healthPath = "/yms/actuator/health"
// healthTimeout 等待后端容器健康的整体超时时间。
healthTimeout = 120 * time.Second
// healthInterval Actuator 健康检查的轮询间隔。
healthInterval = time.Second
// containerStopTimeoutSeconds 停止后端容器时允许的最长等待秒数。
containerStopTimeoutSeconds = 150 * 60
hostNetworkMode = "host"
bindMountType = "bind"
stepLoadImage = "backend.image.load"
stepPullImage = "backend.image.pull"
stepRemoveContainer = "backend.container.remove-inactive"
stepCreateContainer = "backend.container.create"
stepStartContainer = "backend.container.start"
stepCheckHealth = "backend.container.health"
// hostNetworkMode 表示后端容器使用宿主机网络命名空间。
hostNetworkMode = "host"
// bindMountType 表示后端容器的挂载类型为绑定挂载。
bindMountType = "bind"
// stepLoadImage 从归档加载后端镜像的持久化步骤键。
stepLoadImage = "backend.image.load"
// stepPullImage 从仓库拉取后端镜像的持久化步骤键。
stepPullImage = "backend.image.pull"
// stepRemoveContainer 移除非活动后端容器的持久化步骤键。
stepRemoveContainer = "backend.container.remove-inactive"
// stepCreateContainer 创建非活动后端容器的持久化步骤键。
stepCreateContainer = "backend.container.create"
// stepStartContainer 启动非活动后端容器的持久化步骤键。
stepStartContainer = "backend.container.start"
// stepCheckHealth 等待后端 Actuator 健康的持久化步骤键。
stepCheckHealth = "backend.container.health"
)
const (
// ImageAcquisitionLoad 表示通过本地归档加载方式获取后端镜像。
ImageAcquisitionLoad = "load"
// ImageAcquisitionPull 表示通过远程仓库拉取方式获取后端镜像。
ImageAcquisitionPull = "pull"
)
// Request contains exact values supplied by the update package and local deployment configuration.
// ImageReference is opaque: the executor never extracts meaning from its tag.
// Request 携带 update 包与本地部署配置提供的精确取值。
// ImageReference 不透明值:执行器从不解析其标签中的任何含义。
type Request struct {
ImageAcquisition string
ArchivePath string
ImageReference string
ExpectedImageDigest string
Platform containerengine.Platform
ContainerName string
Port int
PortEnvironmentKey string
ConfigSource string
ConfigTarget string
TmpSource string
TmpTarget string
// ImageAcquisition 指定镜像获取方式,取值为 ImageAcquisitionLoad 或 ImageAcquisitionPull。
ImageAcquisition string
// ArchivePath 当 ImageAcquisition 为 ImageAcquisitionLoad 时本地镜像归档的绝对路径。
ArchivePath string
// ImageReference 后端镜像的精确引用,执行器不解析其语义。
ImageReference string
// ExpectedImageDigest 后端镜像期望的清单摘要。
ExpectedImageDigest string
// Platform 后端镜像显式指定的操作系统与架构。
Platform containerengine.Platform
// ContainerName 后端容器的精确名称。
ContainerName string
// Port 后端容器的监听端口,仅允许 8080 或 8081。
Port int
// PortEnvironmentKey 注入端口值的环境变量键。
PortEnvironmentKey string
// ConfigSource 宿主机上后端配置文件的绝对路径。
ConfigSource string
// ConfigTarget 后端配置在容器内的绝对挂载路径。
ConfigTarget string
// TmpSource 宿主机上后端临时目录的绝对路径。
TmpSource string
// TmpTarget 后端临时目录在容器内的绝对挂载路径。
TmpTarget string
// ConfigEnvironmentKey 注入配置位置的环境变量键。
ConfigEnvironmentKey string
ConfigLocation string
RestartPolicy containerengine.RestartPolicy
HealthEndpoint string
StartLog bool
LogReporter func(string)
// ConfigLocation 后端配置在容器内的位置取值。
ConfigLocation string
// RestartPolicy 后端容器的重启策略。
RestartPolicy containerengine.RestartPolicy
// HealthEndpoint 后端 Actuator 健康检查的 HTTP 端点。
HealthEndpoint string
// StartLog 表示是否读取并回传容器启动日志。
StartLog bool
// LogReporter 用于回传容器启动日志的每一行。
LogReporter func(string)
}
// Executor drives the persisted transaction up to SWITCHING after the new container is healthy.
// Executor 在新容器恢复健康后,将持久化事务推进到 StateSwitching 状态。
type Executor struct {
store *transaction.Store
// store 持久化事务的存储。
store *transaction.Store
// coordinator 负责事务的独占执行与步骤执行。
coordinator *transaction.Coordinator
engine containerengine.Engine
checker *healthcheck.ActuatorChecker
// engine 底层容器引擎。
engine containerengine.Engine
// checker Actuator 健康检查器。
checker *healthcheck.ActuatorChecker
}
// New 构造一个 Executor,并校验所有必要依赖非空。
// store 为事务存储,coordinator 为事务协调器,engine 为容器引擎,httpClient 用于健康检查。
// 任一必要依赖为 nil 时返回错误;健康检查器构造失败时返回该错误。
func New(store *transaction.Store, coordinator *transaction.Coordinator, engine containerengine.Engine, httpClient *http.Client) (*Executor, error) {
if store == nil {
return nil, errors.New("transaction store is required")
@@ -92,7 +131,9 @@ func New(store *transaction.Store, coordinator *transaction.Coordinator, engine
return &Executor{store: store, coordinator: coordinator, engine: engine, checker: checker}, nil
}
// Run resumes from the transaction's persisted state. It does not switch gateway traffic.
// Run 从事务的持久化状态恢复执行。它不会切换网关流量。
// transactionID 待执行的事务标识,request 是本次执行携带的请求参数。
// transactionID 为空时返回错误;其余错误来自事务协调器的独占执行。
func (e *Executor) Run(ctx context.Context, transactionID string, request Request) error {
if strings.TrimSpace(transactionID) == "" {
return errors.New("transaction ID is required")
@@ -102,6 +143,8 @@ func (e *Executor) Run(ctx context.Context, transactionID string, request Reques
})
}
// run 在独占执行上下文内的实现,循环读取事务状态并按状态推进,
// 直到事务进入 StateSwitching 状态后返回。每个状态分支处理失败或推进错误时立即返回。
func (e *Executor) run(ctx context.Context, transactionID string, request Request) error {
for {
record, err := e.store.Transaction(ctx, transactionID)
@@ -147,6 +190,9 @@ func (e *Executor) run(ctx context.Context, transactionID string, request Reques
}
}
// failUnlessRecoverable 判断 prepare 阶段的错误是否可恢复。
// 若 cause 是 UncertainStepError 或 ErrStepConflict,则直接原样返回(保留不确定性以便重放恢复);
// 否则将事务标记为 StateFailed 并合并返回 cause 与状态迁移错误。
func (e *Executor) failUnlessRecoverable(ctx context.Context, transactionID string, cause error) error {
var uncertain *transaction.UncertainStepError
if errors.As(cause, &uncertain) || errors.Is(cause, transaction.ErrStepConflict) {
@@ -156,6 +202,8 @@ func (e *Executor) failUnlessRecoverable(ctx context.Context, transactionID stri
return errors.Join(cause, transitionErr)
}
// validate 对请求输入做运行时校验:先校验请求字段,再根据镜像获取方式校验镜像归档,
// 随后校验配置文件为普通文件、临时目录为直接目录,最后对容器引擎执行 Ping 探活。
func (e *Executor) validate(ctx context.Context, request Request) error {
if err := validateRequest(request); err != nil {
return err
@@ -177,6 +225,8 @@ func (e *Executor) validate(ctx context.Context, request Request) error {
return nil
}
// prepare 根据镜像获取方式执行对应的加载或拉取步骤,随后核对镜像、移除旧的非活动容器,
// 并创建新的非活动后端容器。所有步骤均通过事务协调器持久化执行以保证可恢复。
func (e *Executor) prepare(ctx context.Context, transactionID string, request Request) error {
switch request.ImageAcquisition {
case ImageAcquisitionLoad:
@@ -208,6 +258,8 @@ func (e *Executor) prepare(ctx context.Context, transactionID string, request Re
return err
}
// startAndCheck 先启动非活动后端容器,若请求要求读取启动日志则逐行回传,
// 最后执行健康检查步骤等待后端 Actuator 健康。
func (e *Executor) startAndCheck(ctx context.Context, transactionID string, request Request) error {
startOperation := &startContainerOperation{engine: e.engine, name: request.ContainerName}
if _, err := e.coordinator.ExecuteStep(ctx, transactionID, startIntent(request), startOperation); err != nil {
@@ -239,6 +291,8 @@ func (e *Executor) startAndCheck(ctx context.Context, transactionID string, requ
return err
}
// validateRequest 对请求字段做静态校验,确保所有取值精确且自洽。
// 任一字段不符合要求时返回描述性错误。
func validateRequest(request Request) error {
switch request.ImageAcquisition {
case ImageAcquisitionLoad:
@@ -295,6 +349,8 @@ func validateRequest(request Request) error {
return nil
}
// validateRestartPolicy 校验重启策略名称与其最大重试次数的组合是否合法。
// "no"、"always"、"unless-stopped" 不接受最大重试次数;"on-failure" 允许非负次数;其余名称不受支持。
func validateRestartPolicy(policy containerengine.RestartPolicy) error {
switch policy.Name {
case "no", "always", "unless-stopped":
@@ -311,6 +367,7 @@ func validateRestartPolicy(policy containerengine.RestartPolicy) error {
return nil
}
// regularFile 校验 path 指向一个普通文件,description 用于构造错误信息。
func regularFile(path, description string) error {
info, err := os.Stat(path)
if err != nil {
@@ -322,6 +379,7 @@ func regularFile(path, description string) error {
return nil
}
// directDirectory 校验 path 指向一个目录且不是符号链接(即“直接目录”)。
func directDirectory(path, description string) error {
info, err := os.Lstat(path)
if err != nil {
@@ -333,6 +391,8 @@ func directDirectory(path, description string) error {
return nil
}
// containerSpec 根据请求构造后端容器的完整规格,包括名称、镜像引用、平台、环境变量、
// 宿主机网络模式、重启策略、绑定挂载、用户与停止超时。
func containerSpec(request Request) containerengine.ContainerSpec {
return containerengine.ContainerSpec{
Name: request.ContainerName,
@@ -353,6 +413,7 @@ func containerSpec(request Request) containerengine.ContainerSpec {
}
}
// pullIntent 构造拉取并校验后端镜像步骤的持久化意图。
func pullIntent(request Request) transaction.StepIntent {
return intent(stepPullImage, "pull and verify backend image", struct {
ImageReference string `json:"imageReference"`
@@ -361,6 +422,7 @@ func pullIntent(request Request) transaction.StepIntent {
}{request.ImageReference, request.ExpectedImageDigest, request.Platform})
}
// loadIntent 构造从归档加载并校验后端镜像步骤的持久化意图。
func loadIntent(request Request) transaction.StepIntent {
return intent(stepLoadImage, "load and verify backend image", struct {
ArchivePath string `json:"archivePath"`
@@ -370,6 +432,7 @@ func loadIntent(request Request) transaction.StepIntent {
}{request.ArchivePath, request.ImageReference, request.ExpectedImageDigest, request.Platform})
}
// createIntent 构造创建非活动后端容器步骤的持久化意图,imageID 为已核对镜像的标识。
func createIntent(request Request, imageID string) transaction.StepIntent {
return intent(stepCreateContainer, "create inactive backend container", struct {
Spec containerengine.ContainerSpec `json:"spec"`
@@ -377,18 +440,21 @@ func createIntent(request Request, imageID string) transaction.StepIntent {
}{containerSpec(request), imageID})
}
// removeIntent 构造移除非活动后端容器步骤的持久化意图。
func removeIntent(request Request) transaction.StepIntent {
return intent(stepRemoveContainer, "remove inactive backend container", struct {
ContainerName string `json:"containerName"`
}{request.ContainerName})
}
// startIntent 构造启动非活动后端容器步骤的持久化意图。
func startIntent(request Request) transaction.StepIntent {
return intent(stepStartContainer, "start inactive backend container", struct {
ContainerName string `json:"containerName"`
}{request.ContainerName})
}
// healthIntent 构造等待后端 Actuator 健康步骤的持久化意图。
func healthIntent(request Request) transaction.StepIntent {
return intent(stepCheckHealth, "wait for backend Actuator health", struct {
ContainerName string `json:"containerName"`
@@ -397,6 +463,7 @@ func healthIntent(request Request) transaction.StepIntent {
}{request.ContainerName, request.HealthEndpoint, healthTimeout})
}
// intent 将任意值序列化为 JSON 后封装成事务步骤意图。序列化失败视为内部错误并直接 panic。
func intent(key, name string, value any) transaction.StepIntent {
payload, err := json.Marshal(value)
if err != nil {
@@ -405,6 +472,9 @@ func intent(key, name string, value any) transaction.StepIntent {
return transaction.StepIntent{Key: key, Name: name, Intent: payload}
}
// imageMatches 判断已核对镜像是否与期望摘要及期望平台完全匹配。
// 当镜像平台与期望平台不一致时直接返回 false;否则依据镜像的描述符摘要或仓库摘要
// 中是否存在与期望摘要相等的证据来判定匹配。
func imageMatches(image containerengine.Image, expectedDigest string, expectedPlatform containerengine.Platform) (bool, error) {
expected, err := opencontainersdigest.Parse(expectedDigest)
if err != nil {
@@ -438,6 +508,7 @@ func imageMatches(image containerengine.Image, expectedDigest string, expectedPl
return false, nil
}
// resultJSON 将任意值序列化为 JSON RawMessage。序列化失败视为内部错误并直接 panic。
func resultJSON(value any) json.RawMessage {
payload, err := json.Marshal(value)
if err != nil {
+50 -6
View File
@@ -19,11 +19,16 @@ import (
)
const (
testDigest = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
testRepository = "harbor.ymswell.asia/ymswell/glory-ymswell"
// testDigest 测试中使用的固定镜像清单摘要。
testDigest = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
// testRepository 测试中使用的镜像仓库前缀。
testRepository = "harbor.ymswell.asia/ymswell/glory-ymswell"
// healthyResponse 测试中健康检查端点返回的固定 UP 响应体。
healthyResponse = `{"status":"UP","components":{"db":{"status":"UP"},"diskSpace":{"status":"UP"},"ping":{"status":"UP"},"redis":{"status":"UP"}}}`
)
// TestExecutorPreservesOpaqueImageTagsAndReachesSwitching 验证执行器对不透明镜像标签保持原样传递,
// 并能使事务推进到 StateSwitching;同时验证在 StateSwitching 状态重复执行不会产生额外引擎调用。
func TestExecutorPreservesOpaqueImageTagsAndReachesSwitching(t *testing.T) {
tags := []string{
"20260814-093609-d7ed70f0-v1.1.8.1",
@@ -80,6 +85,8 @@ func TestExecutorPreservesOpaqueImageTagsAndReachesSwitching(t *testing.T) {
}
}
// TestExecutorRecoversRecordedCreateIntentWithoutRepeatingCreate 验证在 crash 窗口内已记录创建步骤意图后,
// 恢复执行不会重复创建容器,而是直接复用已完成的创建步骤并最终进入 StateSwitching。
func TestExecutorRecoversRecordedCreateIntentWithoutRepeatingCreate(t *testing.T) {
ctx := context.Background()
store, coordinator := testTransactionKernel(t)
@@ -125,6 +132,7 @@ func TestExecutorRecoversRecordedCreateIntentWithoutRepeatingCreate(t *testing.T
}
}
// TestExecutorMarksValidationFailureTerminal 验证请求校验失败时事务被置为 StateFailed 终态。
func TestExecutorMarksValidationFailureTerminal(t *testing.T) {
ctx := context.Background()
store, coordinator := testTransactionKernel(t)
@@ -143,6 +151,8 @@ func TestExecutorMarksValidationFailureTerminal(t *testing.T) {
}
}
// TestExecutorReplacesInactiveContainerWithConflictingImage 验证当存在镜像标识冲突的非活动容器时,
// 执行器会移除该容器并重新创建,最终进入 StateSwitching。
func TestExecutorReplacesInactiveContainerWithConflictingImage(t *testing.T) {
ctx := context.Background()
store, coordinator := testTransactionKernel(t)
@@ -170,6 +180,8 @@ func TestExecutorReplacesInactiveContainerWithConflictingImage(t *testing.T) {
}
}
// TestExecutorRejectsChangedRecoveryRequestWithoutChangingState 验证恢复执行时若请求参数被改动,
// 执行器会因持久化意图冲突而拒绝,且不改变事务的 StateStarting 状态。
func TestExecutorRejectsChangedRecoveryRequestWithoutChangingState(t *testing.T) {
ctx := context.Background()
store, coordinator := testTransactionKernel(t)
@@ -198,6 +210,8 @@ func TestExecutorRejectsChangedRecoveryRequestWithoutChangingState(t *testing.T)
}
}
// TestImageMatchesRequiresDigestEvidenceAndExactPlatform 验证镜像匹配逻辑要求存在摘要证据且平台完全一致,
// 镜像 ID 单独存在不足以满足清单摘要匹配,平台不一致时不得判定匹配。
func TestImageMatchesRequiresDigestEvidenceAndExactPlatform(t *testing.T) {
t.Parallel()
platform := containerengine.Platform{OS: "linux", Architecture: "arm64"}
@@ -222,6 +236,7 @@ func TestImageMatchesRequiresDigestEvidenceAndExactPlatform(t *testing.T) {
}
}
// testRequest 构造一个合法的测试请求,并在临时目录中写入镜像归档与后端配置文件。
func testRequest(t *testing.T, imageReference string) Request {
t.Helper()
directory := t.TempDir()
@@ -253,6 +268,7 @@ func testRequest(t *testing.T, imageReference string) Request {
}
}
// testTransactionKernel 创建测试用的事务存储与协调器,并在测试结束时自动关闭存储。
func testTransactionKernel(t *testing.T) (*transaction.Store, *transaction.Coordinator) {
t.Helper()
store, err := transaction.OpenStore(context.Background(), filepath.Join(t.TempDir(), "transactions.db"))
@@ -267,6 +283,7 @@ func testTransactionKernel(t *testing.T) (*transaction.Store, *transaction.Coord
return store, coordinator
}
// testExecutor 使用固定响应体构造健康检查 HTTP 客户端并创建后端执行器。
func testExecutor(t *testing.T, store *transaction.Store, coordinator *transaction.Coordinator, engine containerengine.Engine, body string) *Executor {
t.Helper()
client := &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
@@ -279,6 +296,7 @@ func testExecutor(t *testing.T, store *transaction.Store, coordinator *transacti
return executor
}
// createTransaction 以给定后缀创建一个新的后端测试事务并返回其记录。
func createTransaction(t *testing.T, store *transaction.Store, suffix string) transaction.Transaction {
t.Helper()
record, _, err := store.CreateTransaction(context.Background(), transaction.CreateRequest{
@@ -293,6 +311,7 @@ func createTransaction(t *testing.T, store *transaction.Store, suffix string) tr
return record
}
// transitionToPrepared 将指定事务依次推进到 StateValidating 与 StatePrepared 状态。
func transitionToPrepared(t *testing.T, store *transaction.Store, transactionID string) {
t.Helper()
ctx := context.Background()
@@ -304,12 +323,15 @@ func transitionToPrepared(t *testing.T, store *transaction.Store, transactionID
}
}
// roundTripFunc http.RoundTripper 的函数式适配器,用于在测试中固定 HTTP 响应。
type roundTripFunc func(*http.Request) (*http.Response, error)
// RoundTrip 实现 http.RoundTripper 接口,直接调用底层函数。
func (f roundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) {
return f(request)
}
// engineCalls 记录 fakeEngine 各方法被调用的次数,用于断言执行路径。
type engineCalls struct {
ping int
load int
@@ -319,12 +341,19 @@ type engineCalls struct {
remove int
}
// fakeEngine containerengine.Engine 的内存实现,用于测试中模拟容器引擎行为。
type fakeEngine struct {
mu sync.Mutex
request Request
loadedImage containerengine.Image
// mu 保护下方字段的并发访问。
mu sync.Mutex
// request 构造该引擎时使用的请求参数。
request Request
// loadedImage 引擎加载后提供的镜像。
loadedImage containerengine.Image
// imageAvailable 表示镜像当前是否可用。
imageAvailable bool
containers map[string]containerengine.Container
// containers 引擎维护的容器表,键为容器名称。
containers map[string]containerengine.Container
// lastCreateSpec 记录最近一次创建容器所用的规格。
lastCreateSpec containerengine.ContainerSpec
pingCalls int
loadCalls int
@@ -334,6 +363,7 @@ type fakeEngine struct {
removeCalls int
}
// newFakeEngine 构造一个已按请求平台预置镜像的 fakeEngine。
func newFakeEngine(request Request) *fakeEngine {
return &fakeEngine{
request: request,
@@ -346,6 +376,7 @@ func newFakeEngine(request Request) *fakeEngine {
}
}
// Ping 实现容器引擎的 Ping,记录调用次数并始终返回成功。
func (e *fakeEngine) Ping(context.Context) error {
e.mu.Lock()
defer e.mu.Unlock()
@@ -353,6 +384,7 @@ func (e *fakeEngine) Ping(context.Context) error {
return nil
}
// LoadImage 实现容器引擎的 LoadImage,读取输入流后标记镜像可用并记录调用次数。
func (e *fakeEngine) LoadImage(_ context.Context, input io.Reader) error {
e.mu.Lock()
defer e.mu.Unlock()
@@ -364,6 +396,7 @@ func (e *fakeEngine) LoadImage(_ context.Context, input io.Reader) error {
return nil
}
// PullImage 实现容器引擎的 PullImage,标记镜像可用并记录调用次数。
func (e *fakeEngine) PullImage(context.Context, string) error {
e.mu.Lock()
defer e.mu.Unlock()
@@ -372,6 +405,7 @@ func (e *fakeEngine) PullImage(context.Context, string) error {
return nil
}
// InspectImage 实现容器引擎的 InspectImage,镜像不可用时返回 ErrNotFound,否则返回已加载镜像。
func (e *fakeEngine) InspectImage(context.Context, string) (containerengine.Image, error) {
e.mu.Lock()
defer e.mu.Unlock()
@@ -382,6 +416,7 @@ func (e *fakeEngine) InspectImage(context.Context, string) (containerengine.Imag
return e.loadedImage, nil
}
// CreateContainer 实现容器引擎的 CreateContainer,记录规格并创建容器,同名时返回错误。
func (e *fakeEngine) CreateContainer(_ context.Context, spec containerengine.ContainerSpec) (containerengine.Container, error) {
e.mu.Lock()
defer e.mu.Unlock()
@@ -395,6 +430,7 @@ func (e *fakeEngine) CreateContainer(_ context.Context, spec containerengine.Con
return record, nil
}
// StartContainer 实现容器引擎的 StartContainer,将指定容器标记为运行状态,不存在时返回 ErrNotFound。
func (e *fakeEngine) StartContainer(_ context.Context, name string) error {
e.mu.Lock()
defer e.mu.Unlock()
@@ -409,10 +445,12 @@ func (e *fakeEngine) StartContainer(_ context.Context, name string) error {
return nil
}
// ContainerLogs 实现容器引擎的 ContainerLogs,返回空的日志读取器。
func (e *fakeEngine) ContainerLogs(context.Context, string) (io.ReadCloser, error) {
return io.NopCloser(strings.NewReader("")), nil
}
// StopContainer 实现容器引擎的 StopContainer,将指定容器标记为已退出状态,不存在时返回 ErrNotFound。
func (e *fakeEngine) StopContainer(_ context.Context, name string) error {
e.mu.Lock()
defer e.mu.Unlock()
@@ -426,6 +464,7 @@ func (e *fakeEngine) StopContainer(_ context.Context, name string) error {
return nil
}
// InspectContainer 实现容器引擎的 InspectContainer,返回容器记录,不存在时返回 ErrNotFound。
func (e *fakeEngine) InspectContainer(_ context.Context, name string) (containerengine.Container, error) {
e.mu.Lock()
defer e.mu.Unlock()
@@ -436,6 +475,7 @@ func (e *fakeEngine) InspectContainer(_ context.Context, name string) (container
return record, nil
}
// RemoveContainer 实现容器引擎的 RemoveContainer,删除指定容器并记录调用次数,不存在时返回 ErrNotFound。
func (e *fakeEngine) RemoveContainer(_ context.Context, name string, _ bool) error {
e.mu.Lock()
defer e.mu.Unlock()
@@ -447,8 +487,10 @@ func (e *fakeEngine) RemoveContainer(_ context.Context, name string, _ bool) err
return nil
}
// Close 实现容器引擎的 Close,不做任何清理并返回 nil。
func (e *fakeEngine) Close() error { return nil }
// containerFromSpec 根据容器规格构造一条容器记录,running 指定其初始运行状态。
func (e *fakeEngine) containerFromSpec(spec containerengine.ContainerSpec, running bool) containerengine.Container {
return containerengine.Container{
ID: "container-id-" + spec.Name,
@@ -467,6 +509,7 @@ func (e *fakeEngine) containerFromSpec(spec containerengine.ContainerSpec, runni
}
}
// callCounts 返回 fakeEngine 各方法当前的调用次数快照。
func (e *fakeEngine) callCounts() engineCalls {
return engineCalls{
ping: e.pingCalls,
@@ -478,4 +521,5 @@ func (e *fakeEngine) callCounts() engineCalls {
}
}
// 编译期断言确保 fakeEngine 实现 containerengine.Engine 接口。
var _ containerengine.Engine = (*fakeEngine)(nil)
+73 -19
View File
@@ -15,25 +15,39 @@ import (
"yms-daemon/internal/transaction"
)
// loadImageOperation 表示“从本地归档加载并校验后端镜像”的事务操作。
type loadImageOperation struct {
engine containerengine.Engine
archivePath string
// engine 执行加载操作的容器引擎。
engine containerengine.Engine
// archivePath 本地镜像归档的绝对路径。
archivePath string
// imageReference 镜像加载后的目标精确引用。
imageReference string
// expectedDigest 加载镜像后期望的清单摘要。
expectedDigest string
platform containerengine.Platform
// platform 镜像的期望平台。
platform containerengine.Platform
}
// pullImageOperation 表示“从远程仓库拉取并校验后端镜像”的事务操作。
type pullImageOperation struct {
engine containerengine.Engine
// engine 执行拉取操作的容器引擎。
engine containerengine.Engine
// imageReference 待拉取的镜像精确引用。
imageReference string
// expectedDigest 拉取镜像后期望的清单摘要。
expectedDigest string
platform containerengine.Platform
// platform 镜像的期望平台。
platform containerengine.Platform
}
// Apply 调用容器引擎按 imageReference 拉取后端镜像。
func (o *pullImageOperation) Apply(ctx context.Context) error {
return o.engine.PullImage(ctx, o.imageReference)
}
// Inspect 检查拉取操作是否已生效:镜像不存在时为未应用;存在时依据摘要与平台判定,
// 匹配则返回已应用,否则返回未应用。无法判定或解析失败时返回错误。
func (o *pullImageOperation) Inspect(ctx context.Context) (transaction.Inspection, error) {
image, err := o.engine.InspectImage(ctx, o.imageReference)
if errors.Is(err, containerengine.ErrNotFound) {
@@ -53,6 +67,7 @@ func (o *pullImageOperation) Inspect(ctx context.Context) (transaction.Inspectio
return transaction.Inspection{Status: transaction.InspectionApplied, Result: result}, nil
}
// Apply 打开本地镜像归档并调用容器引擎加载后端镜像。
func (o *loadImageOperation) Apply(ctx context.Context) error {
archive, err := os.Open(o.archivePath)
if err != nil {
@@ -62,6 +77,8 @@ func (o *loadImageOperation) Apply(ctx context.Context) error {
return o.engine.LoadImage(ctx, archive)
}
// Inspect 检查加载操作是否已生效:镜像不存在时为未应用;存在时依据摘要与平台判定,
// 匹配则返回已应用,否则返回未应用。结果携带镜像标识、仓库摘要、描述符摘要与平台信息。
func (o *loadImageOperation) Inspect(ctx context.Context) (transaction.Inspection, error) {
image, err := o.engine.InspectImage(ctx, o.imageReference)
if errors.Is(err, containerengine.ErrNotFound) {
@@ -86,17 +103,25 @@ func (o *loadImageOperation) Inspect(ctx context.Context) (transaction.Inspectio
return transaction.Inspection{Status: transaction.InspectionApplied, Result: result}, nil
}
// createContainerOperation 表示“创建非活动后端容器”的事务操作。
type createContainerOperation struct {
engine containerengine.Engine
expectedImage containerengine.Image
spec containerengine.ContainerSpec
}
type removeContainerOperation struct {
// engine 执行创建操作的容器引擎。
engine containerengine.Engine
name string
// expectedImage 创建容器时期望使用的镜像。
expectedImage containerengine.Image
// spec 待创建容器的完整规格。
spec containerengine.ContainerSpec
}
// removeContainerOperation 表示“移除非活动后端容器”的事务操作。
type removeContainerOperation struct {
// engine 执行移除操作的容器引擎。
engine containerengine.Engine
// name 待移除容器的名称。
name string
}
// Apply 强制移除指定名称的非活动容器,容器不存在时视为成功。
func (o *removeContainerOperation) Apply(ctx context.Context) error {
err := o.engine.RemoveContainer(ctx, o.name, true)
if errors.Is(err, containerengine.ErrNotFound) {
@@ -105,6 +130,7 @@ func (o *removeContainerOperation) Apply(ctx context.Context) error {
return err
}
// Inspect 检查移除操作是否已生效:容器不存在即为已应用,存在则为未应用并返回容器信息。
func (o *removeContainerOperation) Inspect(ctx context.Context) (transaction.Inspection, error) {
record, err := o.engine.InspectContainer(ctx, o.name)
if errors.Is(err, containerengine.ErrNotFound) {
@@ -116,11 +142,14 @@ func (o *removeContainerOperation) Inspect(ctx context.Context) (transaction.Ins
return transaction.Inspection{Status: transaction.InspectionNotApplied, Result: containerResult(record)}, nil
}
// Apply 调用容器引擎按规格创建非活动后端容器。
func (o *createContainerOperation) Apply(ctx context.Context) error {
_, err := o.engine.CreateContainer(ctx, o.spec)
return err
}
// Inspect 检查创建操作是否已生效:容器不存在时为未应用;存在时若与期望镜像及规格完全匹配则为已应用,
// 否则为未知状态(存在冲突),以便上层据此重放或报错。
func (o *createContainerOperation) Inspect(ctx context.Context) (transaction.Inspection, error) {
record, err := o.engine.InspectContainer(ctx, o.spec.Name)
if errors.Is(err, containerengine.ErrNotFound) {
@@ -136,15 +165,21 @@ func (o *createContainerOperation) Inspect(ctx context.Context) (transaction.Ins
return transaction.Inspection{Status: transaction.InspectionApplied, Result: result}, nil
}
// startContainerOperation 表示“启动非活动后端容器”的事务操作。
type startContainerOperation struct {
// engine 执行启动操作的容器引擎。
engine containerengine.Engine
name string
// name 待启动容器的名称。
name string
}
// Apply 调用容器引擎启动指定名称的后端容器。
func (o *startContainerOperation) Apply(ctx context.Context) error {
return o.engine.StartContainer(ctx, o.name)
}
// Inspect 检查启动操作是否已生效:容器不存在时为未应用;存在且处于运行且非 Dead 状态时为已应用,
// 否则为未应用。结果携带容器运行状态信息。
func (o *startContainerOperation) Inspect(ctx context.Context) (transaction.Inspection, error) {
record, err := o.engine.InspectContainer(ctx, o.name)
if errors.Is(err, containerengine.ErrNotFound) {
@@ -160,18 +195,28 @@ func (o *startContainerOperation) Inspect(ctx context.Context) (transaction.Insp
return transaction.Inspection{Status: transaction.InspectionNotApplied, Result: result}, nil
}
// healthOperation 表示“等待后端 Actuator 健康”的事务操作。
type healthOperation struct {
engine containerengine.Engine
checker *healthcheck.ActuatorChecker
name string
// engine 用于查询容器运行状态。
engine containerengine.Engine
// checker Actuator 健康检查器。
checker *healthcheck.ActuatorChecker
// name 待检查容器的名称。
name string
// endpoint 健康检查的 HTTP 端点。
endpoint string
timeout time.Duration
// timeout 等待健康的整体超时时间。
timeout time.Duration
mu sync.Mutex
// mu 保护 confirmed 与 confirmedReport 的并发访问。
mu sync.Mutex
// confirmedReport 已确认的健康报告。
confirmedReport healthcheck.ActuatorReport
confirmed bool
// confirmed 表示健康检查结果是否已经确认。
confirmed bool
}
// Apply 调用健康检查器等待后端健康,成功后加锁保存已确认的健康报告并标记为已确认。
func (o *healthOperation) Apply(ctx context.Context) error {
report, err := o.checker.Wait(ctx, o.endpoint, o.timeout, o.running)
if err != nil {
@@ -184,6 +229,8 @@ func (o *healthOperation) Apply(ctx context.Context) error {
return nil
}
// Inspect 检查健康操作是否已生效:若先前已确认则直接返回已确认结果,
// 否则执行一次即时健康检查并返回检查结论。
func (o *healthOperation) Inspect(ctx context.Context) (transaction.Inspection, error) {
o.mu.Lock()
if o.confirmed {
@@ -196,6 +243,7 @@ func (o *healthOperation) Inspect(ctx context.Context) (transaction.Inspection,
return healthInspection(report, ready, err)
}
// running 查询后端容器是否处于运行且非 Dead 状态,容器不存在时返回 false 且无错误。
func (o *healthOperation) running(ctx context.Context) (bool, error) {
record, err := o.engine.InspectContainer(ctx, o.name)
if errors.Is(err, containerengine.ErrNotFound) {
@@ -207,6 +255,8 @@ func (o *healthOperation) running(ctx context.Context) (bool, error) {
return record.Running && !record.Dead, nil
}
// healthInspection 将健康检查结果转换为事务检查结论:工作负载停止、检查出错或未就绪时均为未应用,
// 仅当 ready 为 true 且无错误时才判定为已应用。结果始终携带健康报告的 JSON 序列化。
func healthInspection(report healthcheck.ActuatorReport, ready bool, err error) (transaction.Inspection, error) {
result := resultJSON(report)
if errors.Is(err, healthcheck.ErrWorkloadStopped) {
@@ -221,6 +271,8 @@ func healthInspection(report healthcheck.ActuatorReport, ready bool, err error)
return transaction.Inspection{Status: transaction.InspectionApplied, Result: result}, nil
}
// containerMatches 判断实际容器记录是否与期望镜像标识及期望规格完全匹配,
// 包括镜像、网络模式、重启策略、用户、停止超时以及环境变量与挂载集合的逐项包含关系。
func containerMatches(record containerengine.Container, expectedImageID string, spec containerengine.ContainerSpec) bool {
if record.ImageID != expectedImageID || record.NetworkMode != spec.NetworkMode || record.RestartPolicy != spec.RestartPolicy || record.User != spec.User || record.StopTimeoutSeconds != spec.StopTimeoutSeconds {
return false
@@ -238,6 +290,7 @@ func containerMatches(record containerengine.Container, expectedImageID string,
return true
}
// containerResult 将容器记录的标识、镜像、运行、Dead 与状态字段序列化为 JSON RawMessage。
func containerResult(record containerengine.Container) json.RawMessage {
return resultJSON(struct {
ID string `json:"id"`
@@ -248,6 +301,7 @@ func containerResult(record containerengine.Container) json.RawMessage {
}{record.ID, record.ImageID, record.Running, record.Dead, record.Status})
}
// 以下编译期断言确保各操作类型均实现 transaction.Operation 接口。
var _ transaction.Operation = (*loadImageOperation)(nil)
var _ transaction.Operation = (*pullImageOperation)(nil)
var _ transaction.Operation = (*removeContainerOperation)(nil)