refactor: use nginx -s reload instead of systemd
- doc: add comment
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
// Package nativebackendexecutor prepares and starts one explicitly configured native backend slot.
|
||||
// Gateway switching is deliberately outside this package.
|
||||
// Package nativebackendexecutor 负责准备并启动一个由外部显式配置的原生后端槽位。
|
||||
// 它把后端 JAR 安装到发布目录、把槽位软链接指向新版本、启动对应的 systemd 单元并等待健康检查通过,
|
||||
// 从而把持久化的事务推进到 switching 状态。网关流量切换被刻意排除在本包职责之外。
|
||||
package nativebackendexecutor
|
||||
|
||||
import (
|
||||
@@ -22,40 +23,66 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
healthPath = "/yms/actuator/health"
|
||||
healthTimeout = 120 * time.Second
|
||||
healthInterval = time.Second
|
||||
stepInstallJar = "backend.native.jar.install"
|
||||
stepBindSlot = "backend.native.slot.bind"
|
||||
stepStartUnit = "backend.native.service.start"
|
||||
// healthPath 健康检查端点必须精确匹配的 URL 路径。
|
||||
healthPath = "/yms/actuator/health"
|
||||
// healthTimeout 等待原生后端 Actuator 健康检查就绪的最长时限。
|
||||
healthTimeout = 120 * time.Second
|
||||
// healthInterval 健康检查器轮询端点时的间隔。
|
||||
healthInterval = time.Second
|
||||
// stepInstallJar “安装不可变原生后端 JAR”事务步骤的持久化键。
|
||||
stepInstallJar = "backend.native.jar.install"
|
||||
// stepBindSlot “绑定非活跃槽位软链接”事务步骤的持久化键。
|
||||
stepBindSlot = "backend.native.slot.bind"
|
||||
// stepStartUnit “启动非活跃原生后端 systemd 单元”事务步骤的持久化键。
|
||||
stepStartUnit = "backend.native.service.start"
|
||||
// stepCheckHealth “等待原生后端 Actuator 健康检查”事务步骤的持久化键。
|
||||
stepCheckHealth = "backend.native.health"
|
||||
stepStopUnit = "backend.native.service.stop"
|
||||
// stepStopUnit “停止失败的非活跃原生后端 systemd 单元”事务步骤的持久化键。
|
||||
stepStopUnit = "backend.native.service.stop"
|
||||
// stepRestoreSlot “恢复非活跃槽位软链接”事务步骤的持久化键。
|
||||
stepRestoreSlot = "backend.native.slot.restore"
|
||||
activeState = "active"
|
||||
inactiveState = "inactive"
|
||||
failedState = "failed"
|
||||
// activeState systemd 单元的“活跃”运行状态标识。
|
||||
activeState = "active"
|
||||
// inactiveState systemd 单元的“非活跃”运行状态标识。
|
||||
inactiveState = "inactive"
|
||||
// failedState systemd 单元的“失败”运行状态标识。
|
||||
failedState = "failed"
|
||||
)
|
||||
|
||||
// Request contains exact values from the immutable update request and local deployment configuration.
|
||||
// UnitName, SlotJarPath and PreviousSlotTarget are opaque and are never derived from filenames or ports.
|
||||
// Request 汇集了不可变更新请求中的精确值与本地部署配置。
|
||||
// 其中 UnitName、SlotJarPath 与 PreviousSlotTarget 都是不透明值,绝不从文件名或端口号推导得出。
|
||||
type Request struct {
|
||||
ArtifactPath string
|
||||
ArtifactIdentity filestore.Identity
|
||||
ReleasePath string
|
||||
SlotJarPath string
|
||||
// ArtifactPath 待安装后端 JAR 的绝对源路径。
|
||||
ArtifactPath string
|
||||
// ArtifactIdentity 后端 JAR 的期望身份(大小与 SHA-256),用于校验与发布存储。
|
||||
ArtifactIdentity filestore.Identity
|
||||
// ReleasePath 发布存储内的本地相对路径。
|
||||
ReleasePath string
|
||||
// SlotJarPath 槽位软链接的绝对路径。
|
||||
SlotJarPath string
|
||||
// PreviousSlotTarget 槽位软链接此前指向的绝对路径,首次部署时可为空。
|
||||
PreviousSlotTarget string
|
||||
UnitName string
|
||||
Port int
|
||||
HealthEndpoint string
|
||||
Progress func(transaction.State, string)
|
||||
// UnitName 非活跃后端对应的精确 systemd 单元名。
|
||||
UnitName string
|
||||
// Port 后端监听的端口,只允许 8080 或 8081。
|
||||
Port int
|
||||
// HealthEndpoint 健康检查的 HTTP URL,路径必须精确等于 healthPath。
|
||||
HealthEndpoint string
|
||||
// Progress 可选的回调,用于把事务状态与进度消息上报给上层;为空时不回调。
|
||||
Progress func(transaction.State, string)
|
||||
}
|
||||
|
||||
// actuatorChecker 抽象了 Actuator 健康检查器,供 Executor 依赖注入使用。
|
||||
// 它把真实的 healthcheck.ActuatorChecker 与测试替身统一起来。
|
||||
type actuatorChecker interface {
|
||||
// Check 对给定端点执行一次健康检查并立即返回报告、就绪标志与错误。
|
||||
Check(context.Context, string, healthcheck.RunningProbe) (healthcheck.ActuatorReport, bool, error)
|
||||
// Wait 反复检查端点直到就绪或超过给定时限,并返回最终报告与错误。
|
||||
Wait(context.Context, string, time.Duration, healthcheck.RunningProbe) (healthcheck.ActuatorReport, error)
|
||||
}
|
||||
|
||||
// Executor drives the persisted transaction up to SWITCHING after the inactive native backend is healthy.
|
||||
// Executor 在非活跃原生后端健康之后,把持久化事务推进到 switching 状态。
|
||||
// 它负责校验、安装、绑定槽位、启动单元与健康检查,但不负责切换网关流量。
|
||||
type Executor struct {
|
||||
store *transaction.Store
|
||||
coordinator *transaction.Coordinator
|
||||
@@ -64,6 +91,9 @@ type Executor struct {
|
||||
checker actuatorChecker
|
||||
}
|
||||
|
||||
// New 构造一个原生后端执行器。
|
||||
// store、coordinator、releaseStore、units 为 nil 时返回错误;httpClient 用于构造 Actuator 健康检查器。
|
||||
// 返回值是就绪可用的 *Executor;错误只在缺少必要依赖或健康检查器构造失败时非空。
|
||||
func New(store *transaction.Store, coordinator *transaction.Coordinator, releaseStore *filestore.Store, units systemd.Manager, httpClient *http.Client) (*Executor, error) {
|
||||
if store == nil {
|
||||
return nil, errors.New("transaction store is required")
|
||||
@@ -84,7 +114,10 @@ func New(store *transaction.Store, coordinator *transaction.Coordinator, release
|
||||
return &Executor{store: store, coordinator: coordinator, releaseStore: releaseStore, units: units, checker: checker}, nil
|
||||
}
|
||||
|
||||
// Run resumes from the transaction's persisted state. It does not switch gateway traffic.
|
||||
// Run 从事务持久化的状态恢复并继续推进,直到 switching 状态后返回。
|
||||
// ctx 用于取消与超时;transactionID 是待推进的事务 ID,空白时返回错误;request 携带不可变请求与部署配置。
|
||||
// 它通过协调器的排他执行保证同一事务不会并发运行,且不切换网关流量。
|
||||
// 返回值是执行过程中的错误;若事务已达到 switching 状态则返回 nil。
|
||||
func (e *Executor) Run(ctx context.Context, transactionID string, request Request) error {
|
||||
if strings.TrimSpace(transactionID) == "" {
|
||||
return errors.New("transaction ID is required")
|
||||
@@ -94,6 +127,10 @@ func (e *Executor) Run(ctx context.Context, transactionID string, request Reques
|
||||
})
|
||||
}
|
||||
|
||||
// run 在排他锁内的实际状态机,根据持久化事务状态执行对应步骤。
|
||||
// ctx 用于取消与超时;transactionID 定位事务;request 携带不可变请求与部署配置。
|
||||
// 它循环读取事务状态并推进:校验、安装与绑定、启动与健康检查、补偿恢复,直至 switching 或终止状态。
|
||||
// 返回值是推进过程中的错误;可恢复错误会被原样返回以便重试,不可恢复错误会写入 failed 状态。
|
||||
func (e *Executor) run(ctx context.Context, transactionID string, request Request) error {
|
||||
for {
|
||||
record, err := e.store.Transaction(ctx, transactionID)
|
||||
@@ -161,6 +198,10 @@ func (e *Executor) run(ctx context.Context, transactionID string, request Reques
|
||||
}
|
||||
}
|
||||
|
||||
// validate 校验请求与部署前置条件。
|
||||
// ctx 用于取消;request 携带待校验的请求与配置。
|
||||
// 校验内容包括:请求字段合法性、JAR 源文件为普通文件且大小匹配、槽位软链接初始状态正确、
|
||||
// 以及目标 systemd 单元必须处于非活跃或失败状态。返回错误时不会改动任何文件或事务状态。
|
||||
func (e *Executor) validate(ctx context.Context, request Request) error {
|
||||
if err := validateRequest(request); err != nil {
|
||||
return err
|
||||
@@ -188,6 +229,9 @@ func (e *Executor) validate(ctx context.Context, request Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// prepare 通过协调器执行安装 JAR 与绑定槽位两个步骤,具备幂等性。
|
||||
// ctx 用于取消;transactionID 定位事务;request 携带源路径、发布路径、身份与槽位配置。
|
||||
// 返回已安装 JAR 的绝对路径;错误来自任一事务步骤或发布存储检查失败。
|
||||
func (e *Executor) prepare(ctx context.Context, transactionID string, request Request) (string, error) {
|
||||
installOperation := &installJarOperation{
|
||||
store: e.releaseStore,
|
||||
@@ -217,6 +261,9 @@ func (e *Executor) prepare(ctx context.Context, transactionID string, request Re
|
||||
return installed.Path, nil
|
||||
}
|
||||
|
||||
// startAndCheck 通过协调器执行启动 systemd 单元与等待健康检查两个步骤。
|
||||
// ctx 用于取消;transactionID 定位事务;request 提供单元名、健康端点与进度回调。
|
||||
// 返回值是任一事务步骤的错误;健康检查成功后还会上报“状态为 UP”的进度。
|
||||
func (e *Executor) startAndCheck(ctx context.Context, transactionID string, request Request) error {
|
||||
reportProgress(request, transaction.StateStarting, "Starting native backend unit "+request.UnitName)
|
||||
startOperation := &unitStartOperation{units: e.units, name: request.UnitName}
|
||||
@@ -238,12 +285,19 @@ func (e *Executor) startAndCheck(ctx context.Context, transactionID string, requ
|
||||
return err
|
||||
}
|
||||
|
||||
// reportProgress 在 request.Progress 非空时向调用方上报进度。
|
||||
// request 提供 Progress 回调;state 是当前事务状态;message 是进度描述。
|
||||
// 该函数无返回值且无副作用(除了可选回调),Progress 为空时直接跳过。
|
||||
func reportProgress(request Request, state transaction.State, message string) {
|
||||
if request.Progress != nil {
|
||||
request.Progress(state, message)
|
||||
}
|
||||
}
|
||||
|
||||
// rollbackBeforeSwitch 在切换网关前因启动或健康检查失败而启动补偿流程。
|
||||
// ctx 用于取消;transactionID 定位事务;request 提供单元名与槽位配置;
|
||||
// installedPath 本次已安装的 JAR 路径;cause 是触发补偿的原始错误。
|
||||
// 它先把事务置为 rolling back,再补偿并置为 rolled back,最终总是返回 cause 以保留原始错误上下文。
|
||||
func (e *Executor) rollbackBeforeSwitch(ctx context.Context, transactionID string, request Request, installedPath string, cause error) error {
|
||||
if _, err := e.store.Transition(ctx, transactionID, transaction.StateRollingBack, "native backend preparation failed; compensation started"); err != nil {
|
||||
return errors.Join(cause, err)
|
||||
@@ -257,6 +311,9 @@ func (e *Executor) rollbackBeforeSwitch(ctx context.Context, transactionID strin
|
||||
return cause
|
||||
}
|
||||
|
||||
// compensateBeforeSwitch 执行切换前的补偿:停止后端单元并把槽位软链接恢复到先前的目标。
|
||||
// ctx 用于取消;transactionID 定位事务;request 提供单元名与槽位配置;installedPath 是本次安装的 JAR 路径。
|
||||
// 返回值是停止或恢复步骤的错误;两个步骤都通过协调器执行以保持幂等。
|
||||
func (e *Executor) compensateBeforeSwitch(ctx context.Context, transactionID string, request Request, installedPath string) error {
|
||||
stopOperation := &unitStopOperation{units: e.units, name: request.UnitName}
|
||||
if _, err := e.coordinator.ExecuteStep(ctx, transactionID, stopIntent(request), stopOperation); err != nil {
|
||||
@@ -273,6 +330,9 @@ func (e *Executor) compensateBeforeSwitch(ctx context.Context, transactionID str
|
||||
return nil
|
||||
}
|
||||
|
||||
// failUnlessRecoverable 判断错误是否可恢复:可恢复时原样返回,否则把事务置为 failed 状态。
|
||||
// ctx 用于取消;transactionID 定位事务;cause 是待判定的错误。
|
||||
// 返回值为 cause 与(可选的)状态转换错误的合并结果。
|
||||
func (e *Executor) failUnlessRecoverable(ctx context.Context, transactionID string, cause error) error {
|
||||
if recoverable(cause) {
|
||||
return cause
|
||||
@@ -281,11 +341,15 @@ func (e *Executor) failUnlessRecoverable(ctx context.Context, transactionID stri
|
||||
return errors.Join(cause, transitionErr)
|
||||
}
|
||||
|
||||
// recoverable 判断错误是否允许重试,即是否为不确定步骤错误或步骤冲突错误。
|
||||
// cause 待判定的错误。若可通过重试恢复则返回 true,否则返回 false。
|
||||
func recoverable(cause error) bool {
|
||||
var uncertain *transaction.UncertainStepError
|
||||
return errors.As(cause, &uncertain) || errors.Is(cause, transaction.ErrStepConflict)
|
||||
}
|
||||
|
||||
// validateRequest 校验 Request 中所有字段的合法性,不产生任何副作用。
|
||||
// request 待校验的请求。任一字段不满足约束即返回错误;全部通过则返回 nil。
|
||||
func validateRequest(request Request) error {
|
||||
if !filepath.IsAbs(request.ArtifactPath) {
|
||||
return errors.New("native backend JAR path must be absolute")
|
||||
@@ -318,6 +382,9 @@ func validateRequest(request Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// inspectInitialSlot 校验槽位软链接的初始状态与期望的先前目标一致。
|
||||
// path 槽位软链接路径;previousTarget 是期望指向的先前目标,首次部署时可为空。
|
||||
// 返回错误的条件包括:软链接缺失但与期望不符、路径不是软链接、目标不符,或先前目标不是普通文件。
|
||||
func inspectInitialSlot(path, previousTarget string) error {
|
||||
info, err := os.Lstat(path)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
@@ -351,6 +418,8 @@ func inspectInitialSlot(path, previousTarget string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// installIntent 构造“安装不可变原生后端 JAR”的事务步骤意图。
|
||||
// request 提供源路径、发布路径与身份。返回值是序列化后的事务步骤意图。
|
||||
func installIntent(request Request) transaction.StepIntent {
|
||||
return intent(stepInstallJar, "install immutable native backend JAR", struct {
|
||||
SourcePath string `json:"sourcePath"`
|
||||
@@ -359,6 +428,8 @@ func installIntent(request Request) transaction.StepIntent {
|
||||
}{request.ArtifactPath, request.ReleasePath, request.ArtifactIdentity})
|
||||
}
|
||||
|
||||
// bindIntent 构造“绑定非活跃槽位软链接”的事务步骤意图。
|
||||
// request 提供槽位路径与先前目标;installedPath 是本次安装后的 JAR 路径。返回值是事务步骤意图。
|
||||
func bindIntent(request Request, installedPath string) transaction.StepIntent {
|
||||
return intent(stepBindSlot, "bind inactive native backend slot", struct {
|
||||
SlotJarPath string `json:"slotJarPath"`
|
||||
@@ -367,12 +438,16 @@ func bindIntent(request Request, installedPath string) transaction.StepIntent {
|
||||
}{request.SlotJarPath, request.PreviousSlotTarget, installedPath})
|
||||
}
|
||||
|
||||
// startIntent 构造“启动非活跃原生后端 systemd 单元”的事务步骤意图。
|
||||
// request 提供单元名。返回值是事务步骤意图。
|
||||
func startIntent(request Request) transaction.StepIntent {
|
||||
return intent(stepStartUnit, "start inactive native backend systemd unit", struct {
|
||||
UnitName string `json:"unitName"`
|
||||
}{request.UnitName})
|
||||
}
|
||||
|
||||
// healthIntent 构造“等待原生后端 Actuator 健康检查”的事务步骤意图。
|
||||
// request 提供单元名与端点,超时使用包级常量 healthTimeout。返回值是事务步骤意图。
|
||||
func healthIntent(request Request) transaction.StepIntent {
|
||||
return intent(stepCheckHealth, "wait for native backend Actuator health", struct {
|
||||
UnitName string `json:"unitName"`
|
||||
@@ -381,12 +456,16 @@ func healthIntent(request Request) transaction.StepIntent {
|
||||
}{request.UnitName, request.HealthEndpoint, healthTimeout})
|
||||
}
|
||||
|
||||
// stopIntent 构造“停止失败的非活跃原生后端 systemd 单元”的事务步骤意图。
|
||||
// request 提供单元名。返回值是事务步骤意图。
|
||||
func stopIntent(request Request) transaction.StepIntent {
|
||||
return intent(stepStopUnit, "stop failed inactive native backend systemd unit", struct {
|
||||
UnitName string `json:"unitName"`
|
||||
}{request.UnitName})
|
||||
}
|
||||
|
||||
// restoreIntent 构造“恢复非活跃槽位软链接”的事务步骤意图。
|
||||
// request 提供槽位路径与先前目标;installedPath 是本次安装后需被替换的 JAR 路径。返回值是事务步骤意图。
|
||||
func restoreIntent(request Request, installedPath string) transaction.StepIntent {
|
||||
return intent(stepRestoreSlot, "restore inactive native backend slot", struct {
|
||||
SlotJarPath string `json:"slotJarPath"`
|
||||
@@ -395,6 +474,9 @@ func restoreIntent(request Request, installedPath string) transaction.StepIntent
|
||||
}{request.SlotJarPath, installedPath, request.PreviousSlotTarget})
|
||||
}
|
||||
|
||||
// intent 用键、名称与值构造事务步骤意图,其中值会被序列化为 JSON。
|
||||
// key 步骤的持久化键;name 是人类可读的步骤名;value 是待序列化的载荷。
|
||||
// 序列化失败时直接 panic(内部载荷应始终可序列化)。返回值是事务步骤意图。
|
||||
func intent(key, name string, value any) transaction.StepIntent {
|
||||
payload, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
|
||||
@@ -20,6 +20,8 @@ import (
|
||||
"yms-daemon/internal/transaction"
|
||||
)
|
||||
|
||||
// TestExecutorInstallsJarStartsExactUnitAndReachesSwitching 验证成功路径:安装 JAR、启动精确单元、
|
||||
// 槽位软链接指向新版本、事务推进到 switching,且重复运行不会再次启动单元。
|
||||
func TestExecutorInstallsJarStartsExactUnitAndReachesSwitching(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
executor, store, releaseStore, units, request, previousTarget := testNativeExecutor(t)
|
||||
@@ -65,6 +67,8 @@ func TestExecutorInstallsJarStartsExactUnitAndReachesSwitching(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestExecutorRollsBackSlotAndStopsUnitWhenHealthFails 验证健康检查失败时:事务回滚、槽位软链接恢复、
|
||||
// 单元被停止且状态回到非活跃。
|
||||
func TestExecutorRollsBackSlotAndStopsUnitWhenHealthFails(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
executor, store, _, units, request, previousTarget := testNativeExecutor(t)
|
||||
@@ -96,6 +100,7 @@ func TestExecutorRollsBackSlotAndStopsUnitWhenHealthFails(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestExecutorResumesPersistedRollback 验证从持久化的 rolling back 状态恢复补偿:恢复槽位并停止单元。
|
||||
func TestExecutorResumesPersistedRollback(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
executor, store, _, units, request, previousTarget := testNativeExecutor(t)
|
||||
@@ -134,6 +139,7 @@ func TestExecutorResumesPersistedRollback(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestExecutorRollbackRemovesFirstDeploymentSlotLink 验证首次部署(无先前目标)失败时,回滚会移除槽位软链接。
|
||||
func TestExecutorRollbackRemovesFirstDeploymentSlotLink(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
executor, store, _, _, request, _ := testNativeExecutor(t)
|
||||
@@ -159,6 +165,8 @@ func TestExecutorRollbackRemovesFirstDeploymentSlotLink(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestExecutorRecoversRecordedSlotIntentWithoutChangingRequest 验证崩溃恢复:已记录的槽位意图不依赖请求变更,
|
||||
// 执行器能直接复用并完成后续步骤。
|
||||
func TestExecutorRecoversRecordedSlotIntentWithoutChangingRequest(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
executor, store, releaseStore, units, request, _ := testNativeExecutor(t)
|
||||
@@ -197,6 +205,7 @@ func TestExecutorRecoversRecordedSlotIntentWithoutChangingRequest(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
// TestExecutorRejectsChangedRecoveryIntentAndPreservesStartingState 验证请求变更导致步骤冲突时被拒绝,且事务停留在 starting 状态。
|
||||
func TestExecutorRejectsChangedRecoveryIntentAndPreservesStartingState(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
executor, store, _, _, request, _ := testNativeExecutor(t)
|
||||
@@ -221,6 +230,7 @@ func TestExecutorRejectsChangedRecoveryIntentAndPreservesStartingState(t *testin
|
||||
}
|
||||
}
|
||||
|
||||
// TestExecutorRejectsActiveUnitBeforeChangingFiles 验证单元已活跃时校验失败:事务进入 failed,且不改动发布存储与槽位软链接。
|
||||
func TestExecutorRejectsActiveUnitBeforeChangingFiles(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
executor, store, releaseStore, units, request, previousTarget := testNativeExecutor(t)
|
||||
@@ -246,6 +256,7 @@ func TestExecutorRejectsActiveUnitBeforeChangingFiles(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestUnitStopOperationAcceptsSystemdFailedAsStopped 验证 unitStopOperation 把 systemd 的 failed 状态视为已停止。
|
||||
func TestUnitStopOperationAcceptsSystemdFailedAsStopped(t *testing.T) {
|
||||
units := &fakeUnitManager{unit: systemd.Unit{
|
||||
Name: "yms-backend@8080.service",
|
||||
@@ -263,6 +274,8 @@ func TestUnitStopOperationAcceptsSystemdFailedAsStopped(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// testNativeExecutor 构造一套完整且可复用的原生后端测试夹具。
|
||||
// t 用于失败报告与清理注册。返回执行器、事务存储、发布存储、伪造的单元管理器、请求与先前槽位目标。
|
||||
func testNativeExecutor(t *testing.T) (*Executor, *transaction.Store, *filestore.Store, *fakeUnitManager, Request, string) {
|
||||
t.Helper()
|
||||
store, err := transaction.OpenStore(context.Background(), filepath.Join(t.TempDir(), "transactions.db"))
|
||||
@@ -320,6 +333,8 @@ func testNativeExecutor(t *testing.T) (*Executor, *transaction.Store, *filestore
|
||||
return executor, store, releaseStore, units, request, previousTarget
|
||||
}
|
||||
|
||||
// createNativeTransaction 在测试事务存储中创建一条原生后端事务记录。
|
||||
// t 用于失败报告;store 是事务存储;suffix 用于构造唯一的事务 ID。返回创建的事务记录。
|
||||
func createNativeTransaction(t *testing.T, store *transaction.Store, suffix string) transaction.Transaction {
|
||||
t.Helper()
|
||||
record, _, err := store.CreateTransaction(context.Background(), transaction.CreateRequest{
|
||||
@@ -334,6 +349,8 @@ func createNativeTransaction(t *testing.T, store *transaction.Store, suffix stri
|
||||
return record
|
||||
}
|
||||
|
||||
// transitionNativeToPrepared 把事务依次推进到 validating 与 prepared 状态,供测试预置前置步骤。
|
||||
// t 用于失败报告;store 是事务存储;transactionID 定位事务。
|
||||
func transitionNativeToPrepared(t *testing.T, store *transaction.Store, transactionID string) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
@@ -345,11 +362,14 @@ func transitionNativeToPrepared(t *testing.T, store *transaction.Store, transact
|
||||
}
|
||||
}
|
||||
|
||||
// testIdentity 根据内容计算文件身份(大小与 SHA-256),用于测试夹具与校验。
|
||||
// content 文件内容。返回对应的 filestore.Identity。
|
||||
func testIdentity(content []byte) filestore.Identity {
|
||||
digest := sha256.Sum256(content)
|
||||
return filestore.Identity{Size: int64(len(content)), SHA256: hex.EncodeToString(digest[:])}
|
||||
}
|
||||
|
||||
// fakeUnitManager systemd.Manager 的测试替身,记录单元状态与启动/停止调用次数。
|
||||
type fakeUnitManager struct {
|
||||
mu sync.Mutex
|
||||
unit systemd.Unit
|
||||
@@ -360,6 +380,8 @@ type fakeUnitManager struct {
|
||||
startedName string
|
||||
}
|
||||
|
||||
// Inspect 返回单元状态:名称不匹配时返回 ErrUnitNotFound。
|
||||
// 参数 ctx 与 name 用于取消与定位,name 决定返回哪个单元。返回单元状态与错误。
|
||||
func (m *fakeUnitManager) Inspect(_ context.Context, name string) (systemd.Unit, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
@@ -369,6 +391,8 @@ func (m *fakeUnitManager) Inspect(_ context.Context, name string) (systemd.Unit,
|
||||
return m.unit, nil
|
||||
}
|
||||
|
||||
// Start 模拟启动单元:记录调用与名称,成功置为活跃,失败置为 failed 并返回 startErr。
|
||||
// 参数 ctx 与 name 用于取消与定位,name 决定记录的名称。返回值是启动错误(若配置了 startErr)。
|
||||
func (m *fakeUnitManager) Start(_ context.Context, name string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
@@ -384,6 +408,8 @@ func (m *fakeUnitManager) Start(_ context.Context, name string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop 模拟停止单元:记录调用,成功置为非活跃,失败返回 stopErr。
|
||||
// 参数 ctx 与 name 用于取消与定位。返回值是停止错误(若配置了 stopErr)。
|
||||
func (m *fakeUnitManager) Stop(context.Context, string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
@@ -396,6 +422,7 @@ func (m *fakeUnitManager) Stop(context.Context, string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// fakeActuatorChecker actuatorChecker 的测试替身,返回可配置的健康报告与错误。
|
||||
type fakeActuatorChecker struct {
|
||||
waitReport healthcheck.ActuatorReport
|
||||
waitErr error
|
||||
@@ -404,6 +431,8 @@ type fakeActuatorChecker struct {
|
||||
checkErr error
|
||||
}
|
||||
|
||||
// Wait 模拟等待健康检查:先执行运行探针,未运行返回 ErrWorkloadStopped,否则返回 waitReport 与 waitErr。
|
||||
// ctx 用于取消;running 是运行探针。返回值是健康报告与错误。
|
||||
func (c *fakeActuatorChecker) Wait(ctx context.Context, _ string, _ time.Duration, running healthcheck.RunningProbe) (healthcheck.ActuatorReport, error) {
|
||||
isRunning, err := running(ctx)
|
||||
if err != nil {
|
||||
@@ -415,6 +444,8 @@ func (c *fakeActuatorChecker) Wait(ctx context.Context, _ string, _ time.Duratio
|
||||
return c.waitReport, c.waitErr
|
||||
}
|
||||
|
||||
// Check 模拟单次健康检查:先执行运行探针,未运行返回 ErrWorkloadStopped,否则返回 checkReport、checkReady 与 checkErr。
|
||||
// ctx 用于取消;running 是运行探针。返回值是健康报告、就绪标志与错误。
|
||||
func (c *fakeActuatorChecker) Check(ctx context.Context, _ string, running healthcheck.RunningProbe) (healthcheck.ActuatorReport, bool, error) {
|
||||
isRunning, err := running(ctx)
|
||||
if err != nil {
|
||||
@@ -426,5 +457,6 @@ func (c *fakeActuatorChecker) Check(ctx context.Context, _ string, running healt
|
||||
return c.checkReport, c.checkReady, c.checkErr
|
||||
}
|
||||
|
||||
// 以下编译期断言确保测试替身实现了相应接口。
|
||||
var _ systemd.Manager = (*fakeUnitManager)(nil)
|
||||
var _ actuatorChecker = (*fakeActuatorChecker)(nil)
|
||||
|
||||
@@ -16,6 +16,8 @@ import (
|
||||
"yms-daemon/internal/transaction"
|
||||
)
|
||||
|
||||
// installJarOperation “安装后端 JAR 到发布存储”的事务操作,实现 transaction.Operation。
|
||||
// 它把源文件按给定身份提交到发布存储,并通过 Inspect 判断是否已生效。
|
||||
type installJarOperation struct {
|
||||
store *filestore.Store
|
||||
sourcePath string
|
||||
@@ -23,6 +25,8 @@ type installJarOperation struct {
|
||||
identity filestore.Identity
|
||||
}
|
||||
|
||||
// Apply 执行安装:打开源 JAR 并以指定身份提交到发布存储。
|
||||
// 参数 ctx 未使用,仅用于满足接口签名。返回值是打开或提交失败时的错误。
|
||||
func (o *installJarOperation) Apply(context.Context) error {
|
||||
source, err := os.Open(o.sourcePath)
|
||||
if err != nil {
|
||||
@@ -33,6 +37,9 @@ func (o *installJarOperation) Apply(context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Inspect 检查安装是否已生效,返回事务检查结果。
|
||||
// 参数 ctx 未使用,仅用于满足接口签名。目标冲突时返回未知状态,未找到时返回未应用,
|
||||
// 找到时返回已应用并携带结果 JSON;其余情况返回错误。
|
||||
func (o *installJarOperation) Inspect(context.Context) (transaction.Inspection, error) {
|
||||
file, found, err := o.store.Inspect(o.releasePath, o.identity)
|
||||
if errors.Is(err, filestore.ErrDestinationConflict) {
|
||||
@@ -47,12 +54,16 @@ func (o *installJarOperation) Inspect(context.Context) (transaction.Inspection,
|
||||
return transaction.Inspection{Status: transaction.InspectionApplied, Result: resultJSON(file)}, nil
|
||||
}
|
||||
|
||||
// slotLinkOperation “替换槽位软链接”的事务操作,实现 transaction.Operation。
|
||||
// 它把槽位软链接原子地替换为期望目标,并在 desiredTarget 为空时移除软链接。
|
||||
type slotLinkOperation struct {
|
||||
path string
|
||||
desiredTarget string
|
||||
previousTarget string
|
||||
}
|
||||
|
||||
// Apply 原子地替换槽位软链接指向 desiredTarget,空目标则移除软链接。
|
||||
// ctx 用于 Inspect 调用。返回值是检查、目录校验或文件操作失败时的错误。
|
||||
func (o *slotLinkOperation) Apply(ctx context.Context) error {
|
||||
inspection, err := o.Inspect(ctx)
|
||||
if err != nil {
|
||||
@@ -118,6 +129,9 @@ func (o *slotLinkOperation) Apply(ctx context.Context) error {
|
||||
return syncDirectory(parent)
|
||||
}
|
||||
|
||||
// Inspect 判断槽位软链接当前状态与期望是否一致,返回事务检查结果。
|
||||
// 参数 ctx 未使用,仅用于满足接口签名。软链接指向 desiredTarget 时为已应用,
|
||||
// 指向 previousTarget 时为未应用,其余情况为未知;错误时返回错误。
|
||||
func (o *slotLinkOperation) Inspect(context.Context) (transaction.Inspection, error) {
|
||||
info, err := os.Lstat(o.path)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
@@ -155,15 +169,21 @@ func (o *slotLinkOperation) Inspect(context.Context) (transaction.Inspection, er
|
||||
return transaction.Inspection{Status: transaction.InspectionUnknown, Result: result}, nil
|
||||
}
|
||||
|
||||
// unitStartOperation “启动 systemd 单元”的事务操作,实现 transaction.Operation。
|
||||
// 它启动指定单元,并通过 Inspect 依据单元活跃状态判断是否已生效。
|
||||
type unitStartOperation struct {
|
||||
units systemd.Manager
|
||||
name string
|
||||
}
|
||||
|
||||
// Apply 启动指定单元。
|
||||
// ctx 用于取消。返回值是启动失败时的错误。
|
||||
func (o *unitStartOperation) Apply(ctx context.Context) error {
|
||||
return o.units.Start(ctx, o.name)
|
||||
}
|
||||
|
||||
// Inspect 检查单元是否已启动:活跃为已应用,非活跃或失败为未应用,其余为未知。
|
||||
// ctx 用于取消。返回值是事务检查结果;检查单元失败时返回错误。
|
||||
func (o *unitStartOperation) Inspect(ctx context.Context) (transaction.Inspection, error) {
|
||||
unit, err := o.units.Inspect(ctx, o.name)
|
||||
if err != nil {
|
||||
@@ -180,15 +200,21 @@ func (o *unitStartOperation) Inspect(ctx context.Context) (transaction.Inspectio
|
||||
}
|
||||
}
|
||||
|
||||
// unitStopOperation “停止 systemd 单元”的事务操作,实现 transaction.Operation。
|
||||
// 它停止指定单元,并通过 Inspect 依据单元活跃状态判断是否已停止。
|
||||
type unitStopOperation struct {
|
||||
units systemd.Manager
|
||||
name string
|
||||
}
|
||||
|
||||
// Apply 停止指定单元。
|
||||
// ctx 用于取消。返回值是停止失败时的错误。
|
||||
func (o *unitStopOperation) Apply(ctx context.Context) error {
|
||||
return o.units.Stop(ctx, o.name)
|
||||
}
|
||||
|
||||
// Inspect 检查单元是否已停止:非活跃或失败为已应用,活跃为未应用,其余为未知。
|
||||
// ctx 用于取消。返回值是事务检查结果;检查单元失败时返回错误。
|
||||
func (o *unitStopOperation) Inspect(ctx context.Context) (transaction.Inspection, error) {
|
||||
unit, err := o.units.Inspect(ctx, o.name)
|
||||
if err != nil {
|
||||
@@ -205,6 +231,8 @@ func (o *unitStopOperation) Inspect(ctx context.Context) (transaction.Inspection
|
||||
}
|
||||
}
|
||||
|
||||
// healthOperation “等待后端 Actuator 健康检查”的事务操作,实现 transaction.Operation。
|
||||
// 它等待健康检查就绪,并通过互斥锁缓存已确认的报告以避免重复探测。
|
||||
type healthOperation struct {
|
||||
units systemd.Manager
|
||||
checker actuatorChecker
|
||||
@@ -217,6 +245,8 @@ type healthOperation struct {
|
||||
confirmed bool
|
||||
}
|
||||
|
||||
// Apply 阻塞等待健康检查就绪,成功后缓存已确认的报告。
|
||||
// ctx 用于取消与超时。返回值是等待失败时的错误。
|
||||
func (o *healthOperation) Apply(ctx context.Context) error {
|
||||
report, err := o.checker.Wait(ctx, o.endpoint, o.timeout, o.running)
|
||||
if err != nil {
|
||||
@@ -229,6 +259,8 @@ func (o *healthOperation) Apply(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Inspect 检查健康状态:若已有确认报告则直接返回,否则执行一次即时健康检查。
|
||||
// ctx 用于取消。返回值是事务检查结果;检查器错误由 healthInspection 归一化后返回。
|
||||
func (o *healthOperation) Inspect(ctx context.Context) (transaction.Inspection, error) {
|
||||
o.mu.Lock()
|
||||
if o.confirmed {
|
||||
@@ -241,6 +273,8 @@ func (o *healthOperation) Inspect(ctx context.Context) (transaction.Inspection,
|
||||
return healthInspection(report, ready, err)
|
||||
}
|
||||
|
||||
// running 判断后端单元当前是否处于活跃状态,作为健康检查的探针。
|
||||
// ctx 用于取消。单元不存在时返回 false 且无错误;其他检查失败时返回错误。
|
||||
func (o *healthOperation) running(ctx context.Context) (bool, error) {
|
||||
unit, err := o.units.Inspect(ctx, o.unitName)
|
||||
if errors.Is(err, systemd.ErrUnitNotFound) {
|
||||
@@ -252,6 +286,8 @@ func (o *healthOperation) running(ctx context.Context) (bool, error) {
|
||||
return unit.ActiveState == activeState, nil
|
||||
}
|
||||
|
||||
// healthInspection 把健康检查结果归一化为事务检查状态。
|
||||
// report 健康报告;ready 是就绪标志;err 是检查错误。工作负载停止、出错或未就绪均为未应用,否则为已应用。
|
||||
func healthInspection(report healthcheck.ActuatorReport, ready bool, err error) (transaction.Inspection, error) {
|
||||
result := resultJSON(report)
|
||||
if errors.Is(err, healthcheck.ErrWorkloadStopped) {
|
||||
@@ -263,6 +299,8 @@ func healthInspection(report healthcheck.ActuatorReport, ready bool, err error)
|
||||
return transaction.Inspection{Status: transaction.InspectionApplied, Result: result}, nil
|
||||
}
|
||||
|
||||
// linkResult 把槽位路径与目标序列化为 JSON 结果。
|
||||
// path 槽位软链接路径;target 是软链接指向的目标。返回值是序列化后的 JSON。
|
||||
func linkResult(path, target string) json.RawMessage {
|
||||
return resultJSON(struct {
|
||||
Path string `json:"path"`
|
||||
@@ -270,6 +308,8 @@ func linkResult(path, target string) json.RawMessage {
|
||||
}{path, target})
|
||||
}
|
||||
|
||||
// unitResult 把 systemd 单元状态序列化为 JSON 结果。
|
||||
// unit 待序列化的单元。返回值是序列化后的 JSON。
|
||||
func unitResult(unit systemd.Unit) json.RawMessage {
|
||||
return resultJSON(struct {
|
||||
Name string `json:"name"`
|
||||
@@ -279,6 +319,8 @@ func unitResult(unit systemd.Unit) json.RawMessage {
|
||||
}{unit.Name, unit.LoadState, unit.ActiveState, unit.SubState})
|
||||
}
|
||||
|
||||
// resultJSON 把任意值序列化为 JSON 原始消息。
|
||||
// value 待序列化的值。序列化失败时直接 panic(内部结果应始终可序列化)。返回值是序列化后的 JSON。
|
||||
func resultJSON(value any) json.RawMessage {
|
||||
payload, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
@@ -287,6 +329,8 @@ func resultJSON(value any) json.RawMessage {
|
||||
return payload
|
||||
}
|
||||
|
||||
// syncDirectory 打开目录并同步其元数据到磁盘,确保重命名或删除持久化。
|
||||
// path 待刷新的目录路径。返回值是打开、同步或关闭失败时的错误。
|
||||
func syncDirectory(path string) error {
|
||||
directory, err := os.Open(path)
|
||||
if err != nil {
|
||||
@@ -300,6 +344,7 @@ func syncDirectory(path string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 以下编译期断言确保各操作类型都实现了 transaction.Operation 接口。
|
||||
var _ transaction.Operation = (*installJarOperation)(nil)
|
||||
var _ transaction.Operation = (*slotLinkOperation)(nil)
|
||||
var _ transaction.Operation = (*unitStartOperation)(nil)
|
||||
|
||||
Reference in New Issue
Block a user