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
+76 -24
View File
@@ -20,27 +20,48 @@ import (
"yms-daemon/internal/transaction"
)
// inputTypeContainerImage 标识以容器镜像方式执行后端更新的输入类型。
const inputTypeContainerImage = "container-image"
// persistedContainerRequest 容器后端更新请求的持久化形态,以 JSON 存入事务
// 记录。事务在提交、重试或补偿时依赖这些字段重建执行上下文。
type persistedContainerRequest struct {
InputType string `json:"inputType"`
ImageReference string `json:"imageReference"`
ImmutableReference string `json:"immutableReference"`
ImageDigest string `json:"imageDigest"`
Platform containerengine.Platform `json:"platform"`
TargetPort int `json:"targetPort"`
TargetContainer string `json:"targetContainer"`
PreviousPort int `json:"previousPort"`
PreviousContainer string `json:"previousContainer"`
TargetHealthEndpoint string `json:"targetHealthEndpoint"`
StartLog bool `json:"startLog"`
GatewayBeforePath string `json:"gatewayBeforePath"`
GatewayAfterPath string `json:"gatewayAfterPath"`
GatewayReceiptPath string `json:"gatewayReceiptPath"`
// InputType 标识输入来源类型。
InputType string `json:"inputType"`
// ImageReference 用户提供的镜像引用(带标签)。
ImageReference string `json:"imageReference"`
// ImmutableReference 解析出的不可变镜像引用(带摘要)。
ImmutableReference string `json:"immutableReference"`
// ImageDigest 解析出的镜像仓库摘要。
ImageDigest string `json:"imageDigest"`
// Platform 镜像的精确平台信息。
Platform containerengine.Platform `json:"platform"`
// TargetPort 本次更新要切换到的目标端口。
TargetPort int `json:"targetPort"`
// TargetContainer 目标槽位的容器名。
TargetContainer string `json:"targetContainer"`
// PreviousPort 更新前 Nginx 上游指向的端口。
PreviousPort int `json:"previousPort"`
// PreviousContainer 更新前活动槽位的容器名,首次安装时为空。
PreviousContainer string `json:"previousContainer"`
// TargetHealthEndpoint 目标容器的健康检查端点。
TargetHealthEndpoint string `json:"targetHealthEndpoint"`
// StartLog 表示是否在启动阶段转发容器日志。
StartLog bool `json:"startLog"`
// GatewayBeforePath 切换前 Nginx 配置快照的文件路径。
GatewayBeforePath string `json:"gatewayBeforePath"`
// GatewayAfterPath 切换后 Nginx 配置快照的文件路径。
GatewayAfterPath string `json:"gatewayAfterPath"`
// GatewayReceiptPath Nginx 切换成功后的回执文件路径。
GatewayReceiptPath string `json:"gatewayReceiptPath"`
}
// UpdateContainerImage pulls one development image, freezes its repository
// digest, and updates the inactive Docker backend slot.
// UpdateContainerImage 拉取一个开发镜像,冻结其仓库摘要,并更新非活动的 Docker
// 后端槽位。
//
// 参数 imageReference 是必须带标签且不含多余空白的精确镜像引用;startLog 表示
// 是否转发容器启动日志;report 用于回传实时进度,可为 nil。返回值为本次更新
// 对应的事务记录以及错误。若已存在同镜像的活动事务则复用续跑。
func (u *Updater) UpdateContainerImage(ctx context.Context, imageReference string, startLog bool, report ProgressReporter) (transaction.Transaction, error) {
if u.containerExecutor == nil || u.engine == nil {
return transaction.Transaction{}, errors.New("container backend updater is not configured")
@@ -162,11 +183,13 @@ func (u *Updater) UpdateContainerImage(ctx context.Context, imageReference strin
return u.runContainerUpdate(ctx, record, request, created, report)
}
// resolveContainerSlots reconciles the committed deployment, gateway and both
// exact container names before selecting a target. A fresh installation has no
// deployment row, no committed container transaction history and no slot
// containers. It starts on the non-routed slot so traffic is exposed only after
// health passes.
// resolveContainerSlots 在选择目标槽位前,核对已提交的部署、网关以及两个精确
// 容器名是否一致。全新安装没有部署记录、没有已提交的容器事务历史,也没有槽位
// 容器,会从非路由槽位启动,这样只有健康检查通过后才会暴露流量。
//
// 返回值依次为目标端口、目标槽位以及上一容器名。activePort 为当前路由端口,
// activeSlot 为当前路由槽位,deployment 为已提交的部署记录,hasDeployment 与
// hasHistory 分别表示是否存在部署记录与容器事务历史。
func (u *Updater) resolveContainerSlots(
ctx context.Context,
activePort int,
@@ -224,12 +247,19 @@ func (u *Updater) resolveContainerSlots(
return inactivePort, inactiveSlot, "", nil
}
// resolvedImage 拉取并解析后得到的镜像信息。
type resolvedImage struct {
// ImmutableReference 带摘要的不可变镜像引用。
ImmutableReference string
Digest string
Platform containerengine.Platform
// Digest 镜像仓库摘要。
Digest string
// Platform 镜像的精确平台信息。
Platform containerengine.Platform
}
// pullAndResolveImage 解析镜像引用、确保其为带标签引用,拉取镜像并检查其平台
// 信息,随后从仓库摘要中解析出唯一的不可变引用。若仓库存在多个匹配摘要则报错,
// 以保证后续部署所用的引用是确定且唯一的。
func (u *Updater) pullAndResolveImage(ctx context.Context, imageReference string) (resolvedImage, error) {
named, err := reference.ParseNormalizedNamed(imageReference)
if err != nil {
@@ -276,6 +306,9 @@ func (u *Updater) pullAndResolveImage(ctx context.Context, imageReference string
return resolvedImage{}, errors.New("repository digest resolution produced no result")
}
// runContainerUpdate 根据事务当前状态执行容器后端更新的核心流程:启动目标容器,
// 再执行切换与提交。created 表示本调用是否新建了事务;report 用于回传进度。执行
// 失败时若事务尚未进入失败态则触发容器补偿。
func (u *Updater) runContainerUpdate(ctx context.Context, record transaction.Transaction, request persistedContainerRequest, created bool, report ProgressReporter) (transaction.Transaction, error) {
message := "Resuming container backend transaction " + record.ID
if created {
@@ -326,6 +359,9 @@ func (u *Updater) runContainerUpdate(ctx context.Context, record transaction.Tra
return u.store.Transaction(ctx, record.ID)
}
// switchAndCommitContainer 执行容器后端的流量切换与提交:切换 Nginx 上游到目标
// 端口、进入排空阶段、确认目标容器运行且镜像引用匹配,最后提交容器部署记录。
// 切换失败时触发容器补偿。
func (u *Updater) switchAndCommitContainer(ctx context.Context, transactionID string, request persistedContainerRequest, report ProgressReporter) error {
before, err := readGatewaySnapshot(request.GatewayBeforePath, request.PreviousPort)
if err != nil {
@@ -399,6 +435,9 @@ func (u *Updater) switchAndCommitContainer(ctx context.Context, transactionID st
}
}
// rollbackContainer 执行容器后端补偿:恢复 Nginx 上游、停止被补偿的目标容器,并
// 将事务迁入 RolledBack 状态。cause 为触发补偿的原始错误,会与补偿过程中的错误
// 合并后返回。
func (u *Updater) rollbackContainer(ctx context.Context, transactionID string, request persistedContainerRequest, before hostnginx.Snapshot, after hostnginx.Snapshot, cause error) error {
record, err := u.store.Transaction(ctx, transactionID)
if err != nil {
@@ -420,11 +459,15 @@ func (u *Updater) rollbackContainer(ctx context.Context, transactionID string, r
return errors.Join(cause, transitionErr)
}
// containerStopOperation 停止某个后端容器的事务操作。
type containerStopOperation struct {
// engine 容器引擎。
engine containerengine.Engine
name string
// name 待停止的容器名。
name string
}
// Apply 停止指定容器,若容器不存在则视为已满足(幂等成功)。
func (o *containerStopOperation) Apply(ctx context.Context) error {
err := o.engine.StopContainer(ctx, o.name)
if errors.Is(err, containerengine.ErrNotFound) {
@@ -433,6 +476,7 @@ func (o *containerStopOperation) Apply(ctx context.Context) error {
return err
}
// Inspect 检查容器状态:不存在或已停止视为已应用,仍运行视为未应用。
func (o *containerStopOperation) Inspect(ctx context.Context) (transaction.Inspection, error) {
record, err := o.engine.InspectContainer(ctx, o.name)
if errors.Is(err, containerengine.ErrNotFound) {
@@ -448,6 +492,8 @@ func (o *containerStopOperation) Inspect(ctx context.Context) (transaction.Inspe
return transaction.Inspection{Status: transaction.InspectionNotApplied, Result: result}, nil
}
// containerGatewaySwitchIntent 构造容器后端 Nginx 上游切换步骤的意图,记录切换
// 前后的端口。
func containerGatewaySwitchIntent(request persistedContainerRequest) transaction.StepIntent {
return stepIntent("backend.container.gateway.switch", "switch host Nginx to container backend", struct {
BeforePort int `json:"beforePort"`
@@ -455,24 +501,30 @@ func containerGatewaySwitchIntent(request persistedContainerRequest) transaction
}{request.PreviousPort, request.TargetPort})
}
// containerGatewayRestoreIntent 构造容器后端失败后恢复 Nginx 上游步骤的意图,记录
// 需恢复到的端口。
func containerGatewayRestoreIntent(request persistedContainerRequest) transaction.StepIntent {
return stepIntent("backend.container.gateway.restore", "restore host Nginx after container backend failure", struct {
Port int `json:"port"`
}{request.PreviousPort})
}
// stopPreviousContainerIntent 构造排空后停止前一容器步骤的意图,记录容器名。
func stopPreviousContainerIntent(request persistedContainerRequest) transaction.StepIntent {
return stepIntent("backend.previous-container.stop", "stop previous backend container after drain", struct {
ContainerName string `json:"containerName"`
}{request.PreviousContainer})
}
// stopTargetContainerIntent 构造停止被补偿容器步骤的意图,记录容器名。
func stopTargetContainerIntent(request persistedContainerRequest) transaction.StepIntent {
return stepIntent("backend.target-container.stop", "stop compensated backend container", struct {
ContainerName string `json:"containerName"`
}{request.TargetContainer})
}
// decodeContainerRequest 将持久化的容器后端更新请求 JSON 反序列化到目标结构体,
// 并禁止出现未知字段。
func decodeContainerRequest(content json.RawMessage, request *persistedContainerRequest) error {
decoder := json.NewDecoder(bytes.NewReader(content))
decoder.DisallowUnknownFields()
+49 -2
View File
@@ -20,9 +20,14 @@ import (
"yms-daemon/internal/transaction"
)
// containerTestImage 容器更新测试使用的镜像引用,指向 harbor 仓库的一个具体 tag。
const containerTestImage = "harbor.ymswell.asia/ymswell/glory-ymswell:20260813-184902-a37bf50d-v1.1.8.1"
// containerTestDigest 容器更新测试使用的镜像仓库摘要,用于与 containerTestImage 组合成不可变引用。
const containerTestDigest = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
// TestContainerUpdaterPullsSwitchesAndStopsPreviousSlot 验证在已存在运行中的 backend-8080 容器时,
// 更新镜像会切换到非活跃槽位 backend-8081、提交部署并停止旧容器 backend-8080。
func TestContainerUpdaterPullsSwitchesAndStopsPreviousSlot(t *testing.T) {
ctx := context.Background()
updater, store, engine, gateway := newContainerUpdaterFixture(t, map[string]containerengine.Container{
@@ -48,6 +53,9 @@ func TestContainerUpdaterPullsSwitchesAndStopsPreviousSlot(t *testing.T) {
assertCommittedContainerDeployment(t, store, record.ID, 8081, "backend-8081", "container-id-backend-8081")
}
// TestContainerUpdaterFirstInstallCreatesInactiveSlotBeforeSwitch 验证全新安装场景:
// 目标容器 backend-8081 必须先在非路由槽位创建并通过健康检查,之后才切换网关;
// 整个过程不得进入旧容器排水流程,也不得停止任何旧容器。
func TestContainerUpdaterFirstInstallCreatesInactiveSlotBeforeSwitch(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
@@ -92,6 +100,8 @@ func TestContainerUpdaterFirstInstallCreatesInactiveSlotBeforeSwitch(t *testing.
assertCommittedContainerDeployment(t, store, record.ID, 8081, "backend-8081", "container-id-backend-8081")
}
// TestContainerUpdaterRejectsMissingActiveWithPresentInactive 验证当活跃容器 backend-8080 缺失
// 而非活跃容器 backend-8081 仍在运行时,更新会被拒绝并返回明确的错误信息。
func TestContainerUpdaterRejectsMissingActiveWithPresentInactive(t *testing.T) {
updater, _, _, _ := newContainerUpdaterFixture(t, map[string]containerengine.Container{
"backend-8081": {
@@ -105,6 +115,9 @@ func TestContainerUpdaterRejectsMissingActiveWithPresentInactive(t *testing.T) {
}
}
// TestContainerUpdaterRecoversFailedRollbackThenRetriesSameImage 验证更新失败进入回滚后的恢复链路:
// 首次更新因网关切换失败而进入回滚并停止目标容器,再次调用恢复同一回滚直至完成,
// 最后重试同一镜像能够成功切换并提交。
func TestContainerUpdaterRecoversFailedRollbackThenRetriesSameImage(t *testing.T) {
ctx := context.Background()
updater, _, engine, gateway := newContainerUpdaterFixture(t, map[string]containerengine.Container{}, 0)
@@ -140,6 +153,8 @@ func TestContainerUpdaterRecoversFailedRollbackThenRetriesSameImage(t *testing.T
}
}
// TestContainerUpdaterRejectsMissingCommittedContainer 验证当已提交部署记录中的活跃容器 backend-8080 丢失时,
// 更新会将其识别为部署漂移并拒绝继续。
func TestContainerUpdaterRejectsMissingCommittedContainer(t *testing.T) {
ctx := context.Background()
updater, store, _, _ := newContainerUpdaterFixture(t, map[string]containerengine.Container{}, 0)
@@ -175,6 +190,8 @@ func TestContainerUpdaterRejectsMissingCommittedContainer(t *testing.T) {
}
}
// newContainerUpdaterFixture 构造容器更新器的测试夹具,返回更新器、事务存储、假容器引擎和内存网关。
// containers 指定引擎初始存在的容器;drain 指定旧容器停止前的排水等待时长。
func newContainerUpdaterFixture(
t *testing.T,
containers map[string]containerengine.Container,
@@ -242,6 +259,8 @@ func newContainerUpdaterFixture(
return updater, store, engine, gateway
}
// assertCreatedContainerSpec 校验传入容器引擎的创建规格,确认目标容器名称、镜像引用、运行时约束、
// 环境变量和挂载点均符合容器更新契约。
func assertCreatedContainerSpec(t *testing.T, request containerengine.ContainerSpec) {
t.Helper()
if request.Name != "backend-8081" || request.ImageReference != "harbor.ymswell.asia/ymswell/glory-ymswell@"+containerTestDigest {
@@ -258,6 +277,8 @@ func assertCreatedContainerSpec(t *testing.T, request containerengine.ContainerS
}
}
// assertCommittedContainerDeployment 校验事务存储中已提交的后端容器部署记录,
// 确认其活跃端口、容器名称、容器 ID、镜像摘要和事务 ID 均与预期一致。
func assertCommittedContainerDeployment(
t *testing.T,
store *transaction.Store,
@@ -276,6 +297,7 @@ func assertCommittedContainerDeployment(
}
}
// serverConfiguration8080 测试用的 host Nginx 配置片段,其活跃后端端口为 8080。
const serverConfiguration8080 = `http {
upstream yms-server {
# yms-update managed upstream begin
@@ -286,12 +308,17 @@ const serverConfiguration8080 = `http {
}
`
// containerUpdateRoundTripFunc 将普通函数适配为 http.RoundTripper
// 使测试可以用自定义逻辑响应后端的健康检查请求。
type containerUpdateRoundTripFunc func(*http.Request) (*http.Response, error)
// RoundTrip 实现 http.RoundTripper 接口,直接委托给底层函数处理请求。
func (function containerUpdateRoundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) {
return function(request)
}
// containerUpdateEngine 容器引擎的测试替身,在内存中模拟镜像与容器的生命周期,
// 并记录停止、创建和健康检查等交互,供断言验证更新行为。
type containerUpdateEngine struct {
image containerengine.Image
containers map[string]containerengine.Container
@@ -300,12 +327,21 @@ type containerUpdateEngine struct {
healthChecks int
}
func (e *containerUpdateEngine) Ping(context.Context) error { return nil }
func (e *containerUpdateEngine) PullImage(context.Context, string) error { return nil }
// Ping 返回 nil,模拟引擎连通性检查始终成功。
func (e *containerUpdateEngine) Ping(context.Context) error { return nil }
// PullImage 返回 nil,模拟镜像拉取始终成功。
func (e *containerUpdateEngine) PullImage(context.Context, string) error { return nil }
// LoadImage 返回 nil,模拟从流加载镜像始终成功。
func (e *containerUpdateEngine) LoadImage(context.Context, io.Reader) error { return nil }
// InspectImage 返回夹具预设的镜像信息,用于冻结镜像的平台与仓库摘要。
func (e *containerUpdateEngine) InspectImage(context.Context, string) (containerengine.Image, error) {
return e.image, nil
}
// CreateContainer 依据规格在内存中登记一个新容器,记录最后一次创建规格并返回该容器。
func (e *containerUpdateEngine) CreateContainer(_ context.Context, spec containerengine.ContainerSpec) (containerengine.Container, error) {
e.lastCreateSpec = spec
record := containerengine.Container{
@@ -326,6 +362,8 @@ func (e *containerUpdateEngine) CreateContainer(_ context.Context, spec containe
e.containers[spec.Name] = record
return record, nil
}
// StartContainer 将指定容器标记为运行中;若容器不存在则返回 containerengine.ErrNotFound。
func (e *containerUpdateEngine) StartContainer(_ context.Context, name string) error {
record, found := e.containers[name]
if !found {
@@ -337,9 +375,12 @@ func (e *containerUpdateEngine) StartContainer(_ context.Context, name string) e
return nil
}
// ContainerLogs 返回空的日志流,模拟容器日志读取。
func (e *containerUpdateEngine) ContainerLogs(context.Context, string) (io.ReadCloser, error) {
return io.NopCloser(strings.NewReader("")), nil
}
// StopContainer 记录被停止的容器名称并将该容器标记为已退出;若容器不存在则返回 containerengine.ErrNotFound。
func (e *containerUpdateEngine) StopContainer(_ context.Context, name string) error {
e.stopped = append(e.stopped, name)
record, found := e.containers[name]
@@ -351,6 +392,8 @@ func (e *containerUpdateEngine) StopContainer(_ context.Context, name string) er
e.containers[name] = record
return nil
}
// InspectContainer 返回内存中的指定容器;若容器不存在则返回 containerengine.ErrNotFound。
func (e *containerUpdateEngine) InspectContainer(_ context.Context, name string) (containerengine.Container, error) {
record, found := e.containers[name]
if !found {
@@ -358,6 +401,8 @@ func (e *containerUpdateEngine) InspectContainer(_ context.Context, name string)
}
return record, nil
}
// RemoveContainer 从内存中删除指定容器;若容器不存在则返回 containerengine.ErrNotFound。
func (e *containerUpdateEngine) RemoveContainer(_ context.Context, name string, _ bool) error {
if _, found := e.containers[name]; !found {
return containerengine.ErrNotFound
@@ -365,6 +410,8 @@ func (e *containerUpdateEngine) RemoveContainer(_ context.Context, name string,
delete(e.containers, name)
return nil
}
// Close 返回 nil,模拟引擎关闭无副作用。
func (e *containerUpdateEngine) Close() error { return nil }
var _ containerengine.Engine = (*containerUpdateEngine)(nil)
+13
View File
@@ -7,6 +7,8 @@ import (
"yms-daemon/internal/transaction"
)
// gatewaySwitchIntent 构造原生后端 Nginx 上游切换步骤的意图,记录切换前、
// 切换后各自的端口与配置摘要,供审计与回放时确认两侧快照一致。
func gatewaySwitchIntent(request persistedRequest, before hostnginx.Snapshot, after hostnginx.Snapshot) transaction.StepIntent {
return stepIntent("backend.gateway.switch", "switch host Nginx backend upstream", struct {
BeforePort int `json:"beforePort"`
@@ -16,6 +18,8 @@ func gatewaySwitchIntent(request persistedRequest, before hostnginx.Snapshot, af
}{before.ActivePort, snapshotDigest(before), after.ActivePort, snapshotDigest(after)})
}
// activeLinkIntent 构造替换原生后端兼容性链接步骤的意图,记录链接路径、
// 更新前状态以及即将指向的安装路径。
func activeLinkIntent(request persistedRequest, installedPath string) transaction.StepIntent {
return stepIntent("backend.native.active-link", "replace backend compatibility link", struct {
Path string `json:"path"`
@@ -24,12 +28,15 @@ func activeLinkIntent(request persistedRequest, installedPath string) transactio
}{request.ActiveJARPath, request.ActiveJARBefore, installedPath})
}
// stopPreviousUnitIntent 构造停止前一原生后端单元的步骤意图,记录待停止单元名。
func stopPreviousUnitIntent(request persistedRequest) transaction.StepIntent {
return stepIntent("backend.previous-unit.stop", "stop previous backend unit after drain", struct {
Unit string `json:"unit"`
}{request.PreviousUnit})
}
// activeLinkRestoreIntent 构造恢复原生后端兼容性链接步骤的意图,记录链接
// 路径及其更新前状态,供补偿流程回滚。
func activeLinkRestoreIntent(request persistedRequest) transaction.StepIntent {
return stepIntent("backend.native.active-link.restore", "restore backend compatibility path", struct {
Path string `json:"path"`
@@ -37,18 +44,22 @@ func activeLinkRestoreIntent(request persistedRequest) transaction.StepIntent {
}{request.ActiveJARPath, request.ActiveJARBefore})
}
// gatewayRestoreIntent 构造恢复原生后端 Nginx 上游步骤的意图,记录需恢复到的端口。
func gatewayRestoreIntent(request persistedRequest) transaction.StepIntent {
return stepIntent("backend.gateway.restore", "restore host Nginx backend upstream", struct {
Port int `json:"port"`
}{request.PreviousGatewayPort})
}
// stopTargetUnitIntent 构造停止被补偿的原生后端目标单元步骤的意图,记录待停止单元名。
func stopTargetUnitIntent(request persistedRequest) transaction.StepIntent {
return stepIntent("backend.target-unit.stop", "stop compensated backend target unit", struct {
Unit string `json:"unit"`
}{request.TargetUnit})
}
// restoreTargetSlotIntent 构造恢复被补偿的原生后端目标槽位步骤的意图,记录
// 槽位链接路径、其安装路径以及更新前的链接目标。
func restoreTargetSlotIntent(request persistedRequest, installedPath string) transaction.StepIntent {
return stepIntent("backend.target-slot.restore", "restore compensated backend target slot", struct {
Path string `json:"path"`
@@ -57,6 +68,8 @@ func restoreTargetSlotIntent(request persistedRequest, installedPath string) tra
}{request.TargetSlotJAR, installedPath, request.PreviousSlotTarget})
}
// stepIntent 以键名、显示名和任意值构造一个事务步骤意图。意图值会被序列化为
// JSON;若序列化失败则直接 panic,因为该失败属于程序缺陷而非运行期错误。
func stepIntent(key string, name string, value any) transaction.StepIntent {
content, err := json.Marshal(value)
if err != nil {
+86 -37
View File
@@ -2,57 +2,106 @@ package backendupdate
import "yms-daemon/internal/filestore"
// updateInput 一次原生后端更新(或重启)在进入事务前的内存态输入,由上层
// 入口(UpdateRepack、UpdateNativeJAR、Restart)组装后交给 Updater.update 执行。
// 其中持久化相关字段最终会编码进 persistedRequest 存入事务存储。
type updateInput struct {
IdempotencyKey string
InputType string
SourcePath string
SourceSHA256 string
CustomerCode string
VersionID string
ArtifactID int64
// IdempotencyKey 本次更新的幂等键,用于跨进程/跨调用识别同一逻辑更新。
IdempotencyKey string
// InputType 标识本次输入的来源类型,取值为 daemonapi 中定义的输入类型常量。
InputType string
// SourcePath 待部署构件在宿主机上的源路径。
SourcePath string
// SourceSHA256 源构件的 SHA-256 摘要,参与幂等识别与一致性校验。
SourceSHA256 string
// CustomerCode 构件所属客户编码,仅 repack 输入携带。
CustomerCode string
// VersionID 构件所属版本标识,仅 repack 输入携带。
VersionID string
// ArtifactID 构件标识,仅 repack 输入携带。
ArtifactID int64
// ArtifactFileName 构件文件名。
ArtifactFileName string
// ArtifactIdentity 构件的尺寸与摘要身份信息,用于校验落盘结果。
ArtifactIdentity filestore.Identity
ReleasePath string
Materialize func(string) error
// ReleasePath 构件在 release 目录下的相对安装路径。
ReleasePath string
// Materialize 将构件物化到指定目标路径,通常为解压或复制实现。
Materialize func(string) error
}
// persistedRequest 原生后端更新请求的持久化形态,以 JSON 存入事务记录。
// 事务在提交、重试或补偿时都依赖这些字段重建执行所需的完整上下文。
type persistedRequest struct {
InputType string `json:"inputType"`
SourcePath string `json:"sourcePath"`
SourceSHA256 string `json:"sourceSHA256"`
CustomerCode string `json:"customerCode,omitempty"`
VersionID string `json:"versionId,omitempty"`
ArtifactID int64 `json:"artifactId,omitempty"`
ArtifactFileName string `json:"artifactFileName"`
ArtifactPath string `json:"artifactPath"`
ArtifactIdentity filestore.Identity `json:"artifactIdentity"`
ReleasePath string `json:"releasePath"`
TargetPort int `json:"targetPort"`
TargetUnit string `json:"targetUnit"`
TargetSlotJAR string `json:"targetSlotJar"`
TargetHealthEndpoint string `json:"targetHealthEndpoint"`
PreviousSlotTarget string `json:"previousSlotTarget"`
PreviousGatewayPort int `json:"previousGatewayPort"`
PreviousUnit string `json:"previousUnit"`
GatewayBeforePath string `json:"gatewayBeforePath"`
GatewayAfterPath string `json:"gatewayAfterPath"`
GatewayReceiptPath string `json:"gatewayReceiptPath"`
ActiveJARPath string `json:"activeJarPath"`
ActiveJARBefore pathState `json:"activeJarBefore"`
// InputType 标识输入来源类型。
InputType string `json:"inputType"`
// SourcePath 构件源路径。
SourcePath string `json:"sourcePath"`
// SourceSHA256 构件 SHA-256 摘要。
SourceSHA256 string `json:"sourceSHA256"`
// CustomerCode 客户编码,为空时省略。
CustomerCode string `json:"customerCode,omitempty"`
// VersionID 版本标识,为空时省略。
VersionID string `json:"versionId,omitempty"`
// ArtifactID 构件标识,为零时省略。
ArtifactID int64 `json:"artifactId,omitempty"`
// ArtifactFileName 构件文件名。
ArtifactFileName string `json:"artifactFileName"`
// ArtifactPath 构件在事务工作目录中的物化路径。
ArtifactPath string `json:"artifactPath"`
// ArtifactIdentity 构件尺寸与摘要身份信息。
ArtifactIdentity filestore.Identity `json:"artifactIdentity"`
// ReleasePath 构件在 release 目录下的相对安装路径。
ReleasePath string `json:"releasePath"`
// TargetPort 本次更新要切换到的目标后端端口。
TargetPort int `json:"targetPort"`
// TargetUnit 目标端口对应的 systemd 单元名。
TargetUnit string `json:"targetUnit"`
// TargetSlotJAR 目标槽位的 JAR 链接路径。
TargetSlotJAR string `json:"targetSlotJar"`
// TargetHealthEndpoint 目标槽位的健康检查端点。
TargetHealthEndpoint string `json:"targetHealthEndpoint"`
// PreviousSlotTarget 目标槽位链接在更新前指向的目标,用于补偿恢复。
PreviousSlotTarget string `json:"previousSlotTarget"`
// PreviousGatewayPort 更新前 Nginx 上游指向的端口。
PreviousGatewayPort int `json:"previousGatewayPort"`
// PreviousUnit 更新前实际运行的后端单元名。
PreviousUnit string `json:"previousUnit"`
// GatewayBeforePath 切换前 Nginx 配置快照的文件路径。
GatewayBeforePath string `json:"gatewayBeforePath"`
// GatewayAfterPath 切换后 Nginx 配置快照的文件路径。
GatewayAfterPath string `json:"gatewayAfterPath"`
// GatewayReceiptPath Nginx 切换成功后的回执文件路径。
GatewayReceiptPath string `json:"gatewayReceiptPath"`
// ActiveJARPath 兼容性 JAR 链接路径。
ActiveJARPath string `json:"activeJarPath"`
// ActiveJARBefore 兼容性 JAR 链接更新前的状态快照,用于补偿恢复。
ActiveJARBefore pathState `json:"activeJarBefore"`
}
// pathKind 描述文件系统路径的状态种类。
type pathKind string
const (
pathKindAbsent pathKind = "absent"
// pathKindAbsent 表示路径应不存在。
pathKindAbsent pathKind = "absent"
// pathKindRegular 表示路径应是普通文件。
pathKindRegular pathKind = "regular"
// pathKindSymlink 表示路径应是符号链接。
pathKindSymlink pathKind = "symlink"
)
// pathState 文件系统路径在某个时刻的状态快照,同时作为“期望状态”与
// “历史状态”使用。applyPathState 依据 Kind 决定如何把路径调整到该状态。
type pathState struct {
Kind pathKind `json:"kind"`
Target string `json:"target,omitempty"`
BackupPath string `json:"backupPath,omitempty"`
Identity filestore.Identity `json:"identity,omitempty"`
Mode uint32 `json:"mode,omitempty"`
// Kind 路径状态种类,决定其它字段的取值。
Kind pathKind `json:"kind"`
// Target 符号链接目标路径,仅 Kind 为 pathKindSymlink 时有效。
Target string `json:"target,omitempty"`
// BackupPath 普通文件快照的备份路径,仅 Kind 为 pathKindRegular 时有效。
BackupPath string `json:"backupPath,omitempty"`
// Identity 普通文件快照的尺寸与摘要身份信息。
Identity filestore.Identity `json:"identity,omitempty"`
// Mode 普通文件的权限位,仅 Kind 为 pathKindRegular 时有效。
Mode uint32 `json:"mode,omitempty"`
}
+56 -6
View File
@@ -20,13 +20,21 @@ import (
"yms-daemon/internal/transaction"
)
// gatewayOperation 切换宿主 Nginx 后端上游的事务操作。Apply 应用 after 快照
// 并写入回执,Inspect 通过对比当前配置与 before/after 快照判断执行状态。
type gatewayOperation struct {
controller gatewayController
before hostnginx.Snapshot
after hostnginx.Snapshot
// controller 宿主 Nginx 配置的读取与应用控制器。
controller gatewayController
// before 切换前的 Nginx 配置快照。
before hostnginx.Snapshot
// after 切换后的 Nginx 配置快照。
after hostnginx.Snapshot
// receiptPath 切换成功后写入回执摘要的文件路径。
receiptPath string
}
// Apply 应用 after 快照到宿主 Nginx,并在成功后写入摘要回执文件。
// 回执文件用于 Inspect 区分“已应用”与“应用内容不一致”等状态。
func (o *gatewayOperation) Apply(ctx context.Context) error {
if err := o.controller.Apply(ctx, o.after); err != nil {
return err
@@ -34,6 +42,9 @@ func (o *gatewayOperation) Apply(ctx context.Context) error {
return writeImmutableFile(o.receiptPath, []byte(snapshotDigest(o.after)), 0o600)
}
// Inspect 读取当前 Nginx 配置并与 after/before 快照对比,返回该操作是否已应用、
// 未应用或状态未知。对比同时校验端口与配置内容;回执文件的存在与摘要一致用于
// 强化“已应用”判定。
func (o *gatewayOperation) Inspect(context.Context) (transaction.Inspection, error) {
current, err := o.controller.Read()
if err != nil {
@@ -58,16 +69,23 @@ func (o *gatewayOperation) Inspect(context.Context) (transaction.Inspection, err
return transaction.Inspection{Status: transaction.InspectionUnknown, Result: gatewayResult(current)}, nil
}
// pathOperation 将某文件系统路径调整到期望状态的事务操作。before 保存操作前
// 的路径状态,desired 保存期望达到的路径状态,Inspect 据此判断执行结果。
type pathOperation struct {
path string
before pathState
// path 被操作的路径。
path string
// before 操作前的路径状态,用于判断“未应用”。
before pathState
// desired 期望达到的路径状态,用于判断“已应用”。
desired pathState
}
// Apply 将目标路径调整到 desired 状态。
func (o *pathOperation) Apply(context.Context) error {
return applyPathState(o.path, o.desired)
}
// Inspect 判断路径当前是否已匹配 desired 或仍匹配 before,返回相应的执行状态。
func (o *pathOperation) Inspect(context.Context) (transaction.Inspection, error) {
desired, err := pathMatches(o.path, o.desired)
if err != nil {
@@ -86,15 +104,22 @@ func (o *pathOperation) Inspect(context.Context) (transaction.Inspection, error)
return transaction.Inspection{Status: transaction.InspectionUnknown}, nil
}
// unitStopOperation 停止某个 systemd 单元的事务操作。
type unitStopOperation struct {
// units systemd 管理器。
units systemd.Manager
name string
// name 待停止的单元名。
name string
}
// Apply 停止指定 systemd 单元。
func (o *unitStopOperation) Apply(ctx context.Context) error {
return o.units.Stop(ctx, o.name)
}
// Inspect 查询单元状态并判断是否已停止。inactive 与 failed 均视为已停止,因为
// systemd 可能把成功停止的遗留服务标记为 failed(其追踪的 JVM 以 SIGTERM 退出,
// 状态码 143),二者都表示已无活动进程,即本步骤所需结果。
func (o *unitStopOperation) Inspect(ctx context.Context) (transaction.Inspection, error) {
unit, err := o.units.Inspect(ctx, o.name)
if err != nil {
@@ -116,6 +141,9 @@ func (o *unitStopOperation) Inspect(ctx context.Context) (transaction.Inspection
return transaction.Inspection{Status: transaction.InspectionUnknown, Result: result}, nil
}
// snapshotPath 检查指定路径并把其当前状态快照为 pathState。若路径不存在则返回
// absent 状态;若为符号链接则记录其目标;若为普通文件则复制一份备份并记录其
// 尺寸与摘要身份,便于后续校验与恢复。
func snapshotPath(path string, backupPath string) (pathState, error) {
info, err := os.Lstat(path)
if errors.Is(err, os.ErrNotExist) {
@@ -141,6 +169,9 @@ func snapshotPath(path string, backupPath string) (pathState, error) {
return pathState{Kind: pathKindRegular, BackupPath: backupPath, Identity: identity, Mode: uint32(info.Mode().Perm())}, nil
}
// applyPathState 将目标路径调整到指定状态:absent 则删除,symlink 则原子地替换
// 为指向目标的新链接,regular 则校验备份后原子地恢复为普通文件。父目录必须是
// 直接目录。所有变更完成后都会同步父目录以确保持久化。
func applyPathState(path string, state pathState) error {
parent := filepath.Dir(path)
info, err := os.Lstat(parent)
@@ -179,6 +210,9 @@ func applyPathState(path string, state pathState) error {
}
}
// pathMatches 判断路径当前是否匹配指定状态:absent 状态要求路径不存在,symlink
// 状态要求为指向相同目标的符号链接,regular 状态要求为权限位与身份摘要都一致的
// 普通文件。
func pathMatches(path string, state pathState) (bool, error) {
info, err := os.Lstat(path)
if errors.Is(err, os.ErrNotExist) {
@@ -209,6 +243,8 @@ func pathMatches(path string, state pathState) (bool, error) {
}
}
// copyFileSnapshot 将源文件复制到目标路径并返回其尺寸与 SHA-256 身份信息。
// 复制采用先写临时文件、同步、再原子重命名的流程,并同步父目录,保证落盘一致。
func copyFileSnapshot(sourcePath string, destinationPath string, mode os.FileMode) (filestore.Identity, error) {
if err := os.MkdirAll(filepath.Dir(destinationPath), 0o750); err != nil {
return filestore.Identity{}, err
@@ -250,6 +286,8 @@ func copyFileSnapshot(sourcePath string, destinationPath string, mode os.FileMod
return filestore.Identity{Size: size, SHA256: hex.EncodeToString(digest.Sum(nil))}, nil
}
// copyFileAtomic 原子地将源文件复制到目标路径:先写入同目录临时文件,同步并
// 关闭后再重命名,最后同步父目录。用于普通文件路径状态的恢复。
func copyFileAtomic(sourcePath string, destinationPath string, mode os.FileMode) error {
source, err := os.Open(sourcePath)
if err != nil {
@@ -284,6 +322,9 @@ func copyFileAtomic(sourcePath string, destinationPath string, mode os.FileMode)
return syncDirectory(parent)
}
// writeImmutableFile 以排他方式写入不可变文件:若路径已存在则要求其内容与待写
// 内容完全一致,否则报错;若不存在则以指定权限原子地创建并同步。写入失败时会
// 清理残留文件。
func writeImmutableFile(path string, content []byte, mode os.FileMode) error {
if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil {
return err
@@ -326,6 +367,8 @@ func writeImmutableFile(path string, content []byte, mode os.FileMode) error {
return nil
}
// readGatewaySnapshot 读取持久化的 Nginx 配置快照文件,并校验其中活动端口与
// 期望端口一致,返回快照内容与端口。
func readGatewaySnapshot(path string, port int) (hostnginx.Snapshot, error) {
content, err := os.ReadFile(path)
if err != nil {
@@ -341,6 +384,8 @@ func readGatewaySnapshot(path string, port int) (hostnginx.Snapshot, error) {
return hostnginx.Snapshot{Content: content, ActivePort: port}, nil
}
// verifyFileIdentity 校验文件的实际尺寸与 SHA-256 摘要是否与给定身份一致,任何
// 不一致都返回错误。
func verifyFileIdentity(path string, identity filestore.Identity) error {
if err := identity.Validate(); err != nil {
return err
@@ -361,11 +406,14 @@ func verifyFileIdentity(path string, identity filestore.Identity) error {
return nil
}
// snapshotDigest 计算 Nginx 快照内容的 SHA-256 摘要(十六进制字符串),用于
// 回执与意图中的一致性标识。
func snapshotDigest(snapshot hostnginx.Snapshot) string {
digest := sha256.Sum256(snapshot.Content)
return hex.EncodeToString(digest[:])
}
// gatewayResult 将 Nginx 快照的活动端口与内容摘要序列化为事务检查结果的 JSON。
func gatewayResult(snapshot hostnginx.Snapshot) json.RawMessage {
result, _ := json.Marshal(struct {
ActivePort int `json:"activePort"`
@@ -374,6 +422,7 @@ func gatewayResult(snapshot hostnginx.Snapshot) json.RawMessage {
return result
}
// pathResult 将路径及其状态序列化为事务检查结果的 JSON。
func pathResult(path string, state pathState) json.RawMessage {
result, _ := json.Marshal(struct {
Path string `json:"path"`
@@ -382,6 +431,7 @@ func pathResult(path string, state pathState) json.RawMessage {
return result
}
// syncDirectory 打开目录并执行 fsync,将目录项变更持久化到磁盘。
func syncDirectory(directory string) error {
file, err := os.Open(directory)
if err != nil {
+11
View File
@@ -10,6 +10,8 @@ import (
"yms-daemon/internal/transaction"
)
// TestPathOperationReplacesRegularCompatibilityJarAndRestoresIt 验证 pathOperation 能把普通文件形态的
// 兼容 JAR 替换为目标软链接,随后又能依据快照恢复为原始普通文件内容。
func TestPathOperationReplacesRegularCompatibilityJarAndRestoresIt(t *testing.T) {
root := t.TempDir()
activePath := filepath.Join(root, "glory-soft-yms.jar")
@@ -47,6 +49,8 @@ func TestPathOperationReplacesRegularCompatibilityJarAndRestoresIt(t *testing.T)
}
}
// TestResolveCurrentUnitSupportsFirstLegacyMigrationAndTemplateRotation 验证 resolveCurrentUnit 在
// 首次从旧版单元迁移和模板单元轮换两种场景下,都能选出当前真正运行的单元。
func TestResolveCurrentUnitSupportsFirstLegacyMigrationAndTemplateRotation(t *testing.T) {
tests := []struct {
name string
@@ -72,6 +76,8 @@ func TestResolveCurrentUnitSupportsFirstLegacyMigrationAndTemplateRotation(t *te
}
}
// TestUnitStopOperationAcceptsSystemdFailedAsStopped 验证 unitStopOperation 的 Inspect 把 systemd 的
// failed 状态视为已停止(InspectionApplied),因为旧服务在 JVM 以 SIGTERM 退出后常表现为 failed。
func TestUnitStopOperationAcceptsSystemdFailedAsStopped(t *testing.T) {
units := &unitStateManager{units: map[string]systemd.Unit{
legacyUnit8081: {Name: legacyUnit8081, LoadState: "loaded", ActiveState: "failed"},
@@ -86,14 +92,19 @@ func TestUnitStopOperationAcceptsSystemdFailedAsStopped(t *testing.T) {
}
}
// unitStateManager systemd 单元管理器的只读测试替身,仅返回预先配置的单元状态,
// 用于隔离 resolveCurrentUnit 与 unitStopOperation 的检查逻辑。
type unitStateManager struct {
units map[string]systemd.Unit
}
// Inspect 返回指定名称的单元状态。
func (m *unitStateManager) Inspect(_ context.Context, name string) (systemd.Unit, error) {
return m.units[name], nil
}
// Start 空实现,测试中不涉及启动单元。
func (m *unitStateManager) Start(context.Context, string) error { return nil }
// Stop 空实现,测试中不涉及停止单元。
func (m *unitStateManager) Stop(context.Context, string) error { return nil }
+11 -4
View File
@@ -2,16 +2,23 @@ package backendupdate
import "yms-daemon/internal/transaction"
// Progress is one live update event sent to the local CLI. It is observational
// only: delivery failure must not change the persisted update transaction.
// Progress 一次更新过程中的单条实时进度事件,用于回传给本地 CLI。
// 它仅用于观测反馈:即使投递失败也不得改变已持久化的更新事务状态。
type Progress struct {
// TransactionID 事件所属的事务标识。
TransactionID string
State transaction.State
Message string
// State 事件发生时的事务状态。
State transaction.State
// Message 面向用户的进度描述文本。
Message string
}
// ProgressReporter 进度事件回调,接收一次更新过程中的单条进度。
// 实现必须可安全地并发调用,且失败不应影响更新主流程。
type ProgressReporter func(Progress)
// operationLabel 根据输入类型返回本次操作的中文可读标签:重启类输入返回
// "restart",其余输入返回 "update"。该标签用于拼装进度与提交消息。
func operationLabel(inputType string) string {
if inputType == inputTypeCurrentRelease {
return "restart"
+16 -2
View File
@@ -13,8 +13,13 @@ import (
"yms-daemon/internal/updatepackage"
)
// Restart performs a zero-downtime rotation with the exact release currently
// exposed by the compatibility JAR path.
// Restart 使用兼容性 JAR 链接当前指向的精确发行版执行一次零停机轮换:它把
// 当前运行中的发行版部署到非活动槽位,然后切换流量并停止前一单元,从而在
// 不更换版本的前提下完成一次重启。
//
// 参数 ctx 用于控制整个更新过程的取消;report 用于回传实时进度,可为 nil。
// 返回值为本次重启对应的事务记录以及错误。若后端类型为容器,则直接返回错误;
// 若已存在相关活动事务,则尝试复用并续跑。
func (u *Updater) Restart(ctx context.Context, report ProgressReporter) (transaction.Transaction, error) {
if u.config.Backend.Type == deploymentconfig.BackendTypeContainer {
return transaction.Transaction{}, errors.New("restart is not implemented for container backend")
@@ -67,6 +72,8 @@ func (u *Updater) Restart(ctx context.Context, report ProgressReporter) (transac
}, report)
}
// currentReleaseSource 解析兼容性 JAR 链接实际指向的源文件路径。若链接为
// 直接普通文件则原样返回;若为符号链接则要求其为绝对目标并解析到最终文件。
func currentReleaseSource(activeJAR string) (string, error) {
info, err := os.Lstat(activeJAR)
if err != nil {
@@ -92,6 +99,8 @@ func currentReleaseSource(activeJAR string) (string, error) {
return resolved, nil
}
// restartReleasePath 计算当前 JAR 在 release 目录下的相对安装路径。若当前 JAR
// 位于 release 目录内,则返回其相对路径;否则落到 direct 目录下的规范路径。
func (u *Updater) restartReleasePath(jar updatepackage.DirectNativeJAR) (string, error) {
resolvedReleaseDir, err := filepath.EvalSymlinks(u.config.Backend.ReleaseDir)
if err != nil {
@@ -107,6 +116,8 @@ func (u *Updater) restartReleasePath(jar updatepackage.DirectNativeJAR) (string,
return filepath.Join("direct", jar.SHA256[:directReleaseDigestLength], jar.FileName), nil
}
// persistedRestartInput 由已持久化的重启请求重建 updateInput,供续跑已存在的
// 重启事务使用。其中物化函数从 restartMaterializer 得到。
func (u *Updater) persistedRestartInput(request persistedRequest, idempotencyKey string) (updateInput, error) {
materialize, err := u.restartMaterializer(request)
if err != nil {
@@ -124,6 +135,9 @@ func (u *Updater) persistedRestartInput(request persistedRequest, idempotencyKey
}, nil
}
// restartMaterializer 为续跑的重启事务重建构件物化函数。若已物化的构件仍存在
// 且为直接普通文件,则返回一个表示“恢复期间构件消失”的失败函数;否则尝试从
// 源路径或 release 目录重新定位与身份匹配的 JAR 并提供其复制函数。
func (u *Updater) restartMaterializer(request persistedRequest) (func(string) error, error) {
if info, err := os.Lstat(request.ArtifactPath); err == nil {
if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 {
+2
View File
@@ -11,6 +11,8 @@ import (
"yms-daemon/internal/transaction"
)
// TestRestartRejectsUnfinishedBackendUpdate 验证当存在未完成的后端更新事务时,Restart 会被拒绝,
// 并返回 transaction.ErrActiveExists 及该活跃事务的记录。
func TestRestartRejectsUnfinishedBackendUpdate(t *testing.T) {
ctx := context.Background()
store, err := transaction.OpenStore(ctx, filepath.Join(t.TempDir(), "transactions.db"))
+106 -26
View File
@@ -1,4 +1,6 @@
// Package backendupdate orchestrates one native backend update through commit or compensation.
// Package backendupdate 负责在单台服务器上编排一次原生后端(或容器后端)更新,
// 覆盖从构件准备、切换流量到提交或补偿的完整事务流程。它通过 transaction 包维护
// 可恢复的更新事务,并在失败时执行补偿以回滚到更新前的状态。
package backendupdate
import (
@@ -27,49 +29,86 @@ import (
)
const (
serviceBackend = "backend"
sourceLocalCLI = "local-cli"
drainDuration = 5 * time.Second
// serviceBackend 本更新服务在事务中的服务标识名。
serviceBackend = "backend"
// sourceLocalCLI 事务来源标识,表示更新由本地命令行发起。
sourceLocalCLI = "local-cli"
// drainDuration 切换后旧单元/旧容器的默认排空时长。
drainDuration = 5 * time.Second
// directReleaseDigestLength 直接部署构件在 release 目录下的摘要路径截取长度。
directReleaseDigestLength = 12
inputTypeCurrentRelease = "current-native-release"
legacyUnit8080 = "yms.service"
legacyUnit8081 = "ymsback.service"
// inputTypeCurrentRelease 标识以当前运行发行版执行重启的输入类型。
inputTypeCurrentRelease = "current-native-release"
// legacyUnit8080 端口 8080 对应的遗留 systemd 单元名。
legacyUnit8080 = "yms.service"
// legacyUnit8081 端口 8081 对应的遗留 systemd 单元名。
legacyUnit8081 = "ymsback.service"
)
// Updater executes the current native backend contract on one server.
// Updater 在单台服务器上执行当前原生(或容器)后端契约。它持有部署配置、事务
// 存储、协调器以及各类执行器,是后端更新流程的核心入口与状态载体。
type Updater struct {
config deploymentconfig.Config
workRoot string
store *transaction.Store
coordinator *transaction.Coordinator
releaseStore *filestore.Store
units systemd.Manager
gateway gatewayController
executor nativeExecutor
containerExecutor containerExecutor
engine containerengine.Engine
// config 部署配置,描述后端类型、槽位与路径等信息。
config deploymentconfig.Config
// workRoot 事务工作目录的根路径。
workRoot string
// store 事务存储,用于创建、读取与推进更新事务。
store *transaction.Store
// coordinator 事务协调器,用于以可恢复方式执行单个步骤。
coordinator *transaction.Coordinator
// releaseStore 发行版文件存储,供执行器使用。
releaseStore *filestore.Store
// units systemd 管理器,用于检查与停止后端单元。
units systemd.Manager
// gateway 宿主 Nginx 配置控制器,用于读取与应用上游配置。
gateway gatewayController
// executor 原生后端执行器,负责运行与健康检查原生后端。
executor nativeExecutor
// containerExecutor 容器后端执行器,负责运行与健康检查容器后端。
containerExecutor containerExecutor
// engine 容器引擎,仅在容器后端更新时使用。
engine containerengine.Engine
// containerConfigSource 容器后端配置文件在宿主机上的源路径。
containerConfigSource string
// containerConfigTarget 容器后端配置文件在容器内的目标路径。
containerConfigTarget string
containerTmpSource string
containerTmpTarget string
logger *slog.Logger
drain time.Duration
// containerTmpSource 容器后端临时目录在宿主机上的源路径。
containerTmpSource string
// containerTmpTarget 容器后端临时目录在容器内的目标路径。
containerTmpTarget string
// logger 结构化日志记录器。
logger *slog.Logger
// drain 切换后旧单元/旧容器的排空时长。
drain time.Duration
}
// gatewayController 抽象宿主 Nginx 配置的读取与应用,便于测试与替换实现。
type gatewayController interface {
// Read 返回当前宿主 Nginx 配置快照。
Read() (hostnginx.Snapshot, error)
// Apply 将给定快照应用到宿主 Nginx。
Apply(context.Context, hostnginx.Snapshot) error
}
// nativeExecutor 抽象原生后端执行器的运行能力。
type nativeExecutor interface {
// Run 执行原生后端运行流程,request 携带构件与目标槽位等信息。
Run(context.Context, string, nativebackendexecutor.Request) error
}
// containerExecutor 抽象容器后端执行器的运行能力。
type containerExecutor interface {
// Run 执行容器后端运行流程,request 携带镜像与容器参数等信息。
Run(context.Context, string, backendexecutor.Request) error
}
// New creates the complete native backend update orchestrator.
// New 创建完整的原生后端更新编排器。
//
// 参数 config 为部署配置且必须校验通过且后端类型为 native;workRoot 必须是干净
// 的绝对路径;store 与 coordinator 提供事务能力;releaseStore 提供发行版存储;
// units 提供 systemd 管理;gateway 提供 Nginx 控制;httpClient 供执行器进行健康
// 检查;logger 可为 nil,缺省使用默认日志器。返回构造完成的 Updater,若参数非法
// 或执行器创建失败则返回错误。
func New(
config deploymentconfig.Config,
workRoot string,
@@ -114,7 +153,10 @@ func New(
}, nil
}
// NewContainer creates the Docker standalone backend update orchestrator.
// NewContainer 创建 Docker 独立容器后端更新编排器。
//
// 参数与 New 类似,但要求后端类型为 container 且 daemon 环境为 devengine 提供
// 容器引擎能力。返回构造完成的 Updater,若参数非法或执行器创建失败则返回错误。
func NewContainer(
config deploymentconfig.Config,
workRoot string,
@@ -157,7 +199,10 @@ func NewContainer(
}, nil
}
// UpdateRepack applies one repack ZIP selected by an absolute local path.
// UpdateRepack 应用由绝对本地路径指定的 repack ZIP 更新。
//
// 参数 packagePath 是 repack ZIP 的绝对路径;report 用于回传进度,可为 nil。
// 返回本次更新对应的事务记录以及错误。容器后端不支持该更新方式。
func (u *Updater) UpdateRepack(ctx context.Context, packagePath string, report ProgressReporter) (transaction.Transaction, error) {
if u.config.Backend.Type == deploymentconfig.BackendTypeContainer {
return transaction.Transaction{}, errors.New("repack ZIP update is not implemented for container backend")
@@ -184,7 +229,10 @@ func (u *Updater) UpdateRepack(ctx context.Context, packagePath string, report P
}, report)
}
// UpdateNativeJAR applies one JAR copied directly to the server.
// UpdateNativeJAR 应用一个直接复制到服务器的 JAR 更新。
//
// 参数 jarPath 是 JAR 的本地路径;report 用于回传进度,可为 nil。返回本次更新
// 对应的事务记录以及错误。仅原生后端支持该更新方式。
func (u *Updater) UpdateNativeJAR(ctx context.Context, jarPath string, report ProgressReporter) (transaction.Transaction, error) {
if u.config.Backend.Type == deploymentconfig.BackendTypeContainer {
return transaction.Transaction{}, errors.New("--native-jar requires backend.type = native")
@@ -207,6 +255,8 @@ func (u *Updater) UpdateNativeJAR(ctx context.Context, jarPath string, report Pr
}, report)
}
// update 原生后端更新(含重启)的统一执行入口:创建或恢复事务、物化构件、
// 运行执行器、再切换与提交。input 描述本次更新输入,report 用于回传进度。
func (u *Updater) update(ctx context.Context, input updateInput, report ProgressReporter) (transaction.Transaction, error) {
existing, request, created, err := u.createOrResume(ctx, input)
if err != nil {
@@ -269,6 +319,8 @@ func (u *Updater) update(ctx context.Context, input updateInput, report Progress
return u.store.Transaction(ctx, existing.ID)
}
// terminalResult 根据终态事务记录返回结果:已提交则正常返回,否则返回带有状态
// 信息的错误。
func terminalResult(record transaction.Transaction) (transaction.Transaction, error) {
if record.State == transaction.StateCommitted {
return record, nil
@@ -276,6 +328,11 @@ func terminalResult(record transaction.Transaction) (transaction.Transaction, er
return record, fmt.Errorf("backend update transaction %s is terminal in state %s", record.ID, record.State)
}
// createOrResume 为给定更新输入创建新事务,或在幂等键命中时恢复已存在事务。
//
// 该函数先读取当前 Nginx 快照以确定目标端口与槽位,再在事务工作目录下固化网关
// 快照与兼容性链接备份,最后创建持久化请求并写入事务存储。返回值为事务记录、
// 持久化请求、是否新建以及错误。
func (u *Updater) createOrResume(ctx context.Context, input updateInput) (transaction.Transaction, persistedRequest, bool, error) {
gatewayBefore, err := u.gateway.Read()
if err != nil {
@@ -372,6 +429,9 @@ func (u *Updater) createOrResume(ctx context.Context, input updateInput) (transa
return record, persisted, false, nil
}
// switchAndCommit 执行原生后端更新的切换与提交:切换 Nginx 上游到目标端口、更新
// 兼容性 JAR 链接、排空并停止前一单元,最终把事务迁入 Committed 状态。任何切换
// 阶段失败都会触发补偿。
func (u *Updater) switchAndCommit(ctx context.Context, transactionID string, request persistedRequest, report ProgressReporter) error {
operation := operationLabel(request.InputType)
before, err := readGatewaySnapshot(request.GatewayBeforePath, request.PreviousGatewayPort)
@@ -440,12 +500,16 @@ func (u *Updater) switchAndCommit(ctx context.Context, transactionID string, req
}
}
// reportProgress 在 report 非空时向其投递一条进度事件,report 为 nil 时静默忽略。
func reportProgress(report ProgressReporter, progress Progress) {
if report != nil {
report(progress)
}
}
// rollbackAfterPreparation 在切换阶段失败后执行原生后端补偿:恢复兼容性链接、
// 恢复 Nginx 上游、停止目标单元并恢复目标槽位,最后迁入 RolledBack 状态。cause
// 为触发补偿的原始错误,会与补偿过程中的错误合并返回。
func (u *Updater) rollbackAfterPreparation(ctx context.Context, transactionID string, request persistedRequest, before hostnginx.Snapshot, after hostnginx.Snapshot, cause error) error {
record, readErr := u.store.Transaction(ctx, transactionID)
if readErr != nil {
@@ -490,6 +554,8 @@ func (u *Updater) rollbackAfterPreparation(ctx context.Context, transactionID st
return errors.Join(cause, transitionErr)
}
// resolveCurrentUnit 确定当前活动端口实际运行的后端单元名。它同时检查配置单元与
// 遗留单元,要求二者恰好一个在运行,并返回运行中的那个。
func (u *Updater) resolveCurrentUnit(ctx context.Context, port int, configuredUnit string) (string, error) {
legacyUnit, err := legacyUnitForPort(port)
if err != nil {
@@ -514,16 +580,20 @@ func (u *Updater) resolveCurrentUnit(ctx context.Context, port int, configuredUn
return legacyUnit, nil
}
// fail 把事务迁入 Failed 状态并返回带错误的事务记录。
func (u *Updater) fail(ctx context.Context, transactionID string, cause error) (transaction.Transaction, error) {
_, transitionErr := u.store.Transition(ctx, transactionID, transaction.StateFailed, cause.Error())
return u.currentWithError(ctx, transactionID, errors.Join(cause, transitionErr))
}
// currentWithError 读取事务当前记录并把给定错误与读取错误合并返回,便于调用方在
// 出错时仍拿到最新事务状态。
func (u *Updater) currentWithError(ctx context.Context, transactionID string, cause error) (transaction.Transaction, error) {
record, err := u.store.Transaction(ctx, transactionID)
return record, errors.Join(cause, err)
}
// otherPort 返回给定后端端口的对侧端口:8080 与 8081 互换。
func otherPort(port int) int {
if port == deploymentconfig.BackendPort8080 {
return deploymentconfig.BackendPort8081
@@ -531,6 +601,8 @@ func otherPort(port int) int {
return deploymentconfig.BackendPort8080
}
// legacyUnitForPort 返回给定后端端口对应的遗留 systemd 单元名,仅支持 8080 与
// 8081 两个端口。
func legacyUnitForPort(port int) (string, error) {
switch port {
case deploymentconfig.BackendPort8080:
@@ -542,10 +614,13 @@ func legacyUnitForPort(port int) (string, error) {
}
}
// unitRunning 判断单元是否处于运行状态:既非 inactive 也非 failed 即视为运行。
func unitRunning(unit systemd.Unit) bool {
return unit.ActiveState != "inactive" && unit.ActiveState != "failed"
}
// readOptionalSymlink 读取指定路径的符号链接目标;若路径不存在则返回空字符串,
// 若路径存在但不是符号链接则报错。
func readOptionalSymlink(path string) (string, error) {
info, err := os.Lstat(path)
if errors.Is(err, os.ErrNotExist) {
@@ -564,6 +639,8 @@ func readOptionalSymlink(path string) (string, error) {
return target, nil
}
// ensureTransactionArtifact 确保构件已物化到事务工作路径:若已存在则校验其为直接
// 普通文件且身份匹配,否则调用 input.Materialize 进行物化。
func ensureTransactionArtifact(input updateInput, request persistedRequest) error {
info, err := os.Lstat(request.ArtifactPath)
if errors.Is(err, os.ErrNotExist) {
@@ -578,6 +655,8 @@ func ensureTransactionArtifact(input updateInput, request persistedRequest) erro
return verifyFileIdentity(request.ArtifactPath, request.ArtifactIdentity)
}
// decodePersistedRequest 将持久化的原生后端更新请求 JSON 反序列化到目标结构体,
// 并禁止出现未知字段。
func decodePersistedRequest(content json.RawMessage, request *persistedRequest) error {
decoder := json.NewDecoder(bytes.NewReader(content))
decoder.DisallowUnknownFields()
@@ -587,6 +666,7 @@ func decodePersistedRequest(content json.RawMessage, request *persistedRequest)
return nil
}
// waitContext 等待指定时长,或直到 ctx 被取消。返回 ctx 取消错误或 nil。
func waitContext(ctx context.Context, duration time.Duration) error {
timer := time.NewTimer(duration)
defer timer.Stop()
+25
View File
@@ -20,14 +20,21 @@ import (
"yms-daemon/internal/updatepackage"
)
// TestUpdaterMigratesLegacyBackendAndCommits 验证通过 repack 包更新时,能把仍在运行的旧版后端单元
// ymsback.service)迁移到模板单元并完成提交。
func TestUpdaterMigratesLegacyBackendAndCommits(t *testing.T) {
testUpdaterMigratesLegacyBackendAndCommits(t, false)
}
// TestUpdaterCommitsDirectNativeJAR 验证直接安装 native JAR 时能完成提交,
// 并进一步验证重启事务的创建、恢复与槽位旋转。
func TestUpdaterCommitsDirectNativeJAR(t *testing.T) {
testUpdaterMigratesLegacyBackendAndCommits(t, true)
}
// testUpdaterMigratesLegacyBackendAndCommits 上述两个测试共用的驱动函数。
// 当 direct 为 false 时走 repack 更新路径,为 true 时走直接 JAR 安装路径,
// 并额外验证重启事务的创建、恢复以及槽位旋转行为。
func testUpdaterMigratesLegacyBackendAndCommits(t *testing.T, direct bool) {
t.Helper()
ctx := context.Background()
@@ -164,6 +171,7 @@ func testUpdaterMigratesLegacyBackendAndCommits(t *testing.T, direct bool) {
}
}
// serverConfiguration8081 测试用的 host Nginx 配置片段,其活跃后端端口为 8081。
const serverConfiguration8081 = `http {
upstream yms-server {
# yms-update managed upstream begin
@@ -174,16 +182,20 @@ const serverConfiguration8081 = `http {
}
`
// memoryGateway 网关控制器的测试替身,在内存中保存 host Nginx 快照,
// 记录 Apply 调用次数,并支持在应用前注入失败以模拟切换故障。
type memoryGateway struct {
snapshot hostnginx.Snapshot
applyCount int
beforeApply func(hostnginx.Snapshot) error
}
// Read 返回当前保存的网关快照副本。
func (g *memoryGateway) Read() (hostnginx.Snapshot, error) {
return hostnginx.Snapshot{Content: append([]byte(nil), g.snapshot.Content...), ActivePort: g.snapshot.ActivePort}, nil
}
// Apply 在 beforeApply 钩子通过后,将给定快照保存为当前状态并累计应用次数。
func (g *memoryGateway) Apply(_ context.Context, snapshot hostnginx.Snapshot) error {
if g.beforeApply != nil {
if err := g.beforeApply(snapshot); err != nil {
@@ -195,15 +207,19 @@ func (g *memoryGateway) Apply(_ context.Context, snapshot hostnginx.Snapshot) er
return nil
}
// updateUnitManager systemd 单元管理器的测试替身,在内存中维护单元状态,
// 并记录最近一次被停止的单元名称。
type updateUnitManager struct {
units map[string]systemd.Unit
stopped string
}
// Inspect 返回指定名称的单元状态。
func (m *updateUnitManager) Inspect(_ context.Context, name string) (systemd.Unit, error) {
return m.units[name], nil
}
// Start 将指定单元置为 active。
func (m *updateUnitManager) Start(_ context.Context, name string) error {
unit := m.units[name]
unit.ActiveState = "active"
@@ -211,6 +227,7 @@ func (m *updateUnitManager) Start(_ context.Context, name string) error {
return nil
}
// Stop 将指定单元置为 failed 并记录其名称。
func (m *updateUnitManager) Stop(_ context.Context, name string) error {
unit := m.units[name]
unit.ActiveState = "failed"
@@ -219,12 +236,16 @@ func (m *updateUnitManager) Stop(_ context.Context, name string) error {
return nil
}
// preparingExecutor 原生后端执行器的测试替身:它把制品写入发布目录、
// 建立槽位软链接、启动目标单元并推进事务状态,模拟执行器在真实环境中的工作。
type preparingExecutor struct {
store *transaction.Store
releaseDir string
units *updateUnitManager
}
// Run 将请求中的制品落盘到发布目录,创建槽位软链接,启动目标单元,
// 并把事务依次推进到 Starting 状态,用于驱动后续切换与提交逻辑。
func (e *preparingExecutor) Run(ctx context.Context, transactionID string, request nativebackendexecutor.Request) error {
if err := os.MkdirAll(e.releaseDir, 0o750); err != nil {
return err
@@ -259,6 +280,8 @@ func (e *preparingExecutor) Run(ctx context.Context, transactionID string, reque
return nil
}
// writeNativeBackendPackage 生成一个包含 artifact-selection.json 清单和
// glory-soft-yms-test.jar 制品的 repack ZIP 包,返回其路径。
func writeNativeBackendPackage(t *testing.T, jar []byte) string {
t.Helper()
digest := sha256.Sum256(jar)
@@ -314,6 +337,8 @@ func writeNativeBackendPackage(t *testing.T, jar []byte) string {
return packagePath
}
// writeDirectBackendJAR 生成一个包含 BOOT-INF/classes/application.properties 的直接 native JAR
// 返回其路径,供直接安装更新路径使用。
func writeDirectBackendJAR(t *testing.T, content []byte) string {
t.Helper()
jarPath := filepath.Join(t.TempDir(), "glory-soft-yms.jar")