feat: complete daemon operations and recovery tooling
This commit is contained in:
@@ -38,6 +38,15 @@ type backendUpdater interface {
|
||||
Restart(context.Context, backendupdate.ProgressReporter) (transaction.Transaction, error)
|
||||
}
|
||||
|
||||
// backendDiagnoser 抽象了后端状态诊断与对账能力,供 status/doctor/reconcile 操作使用。
|
||||
// 它由 backendstatus 包在运行时注入,便于在测试中用轻量替身替换。
|
||||
type backendDiagnoser interface {
|
||||
// Diagnose 执行只读诊断,返回结构化诊断结果。
|
||||
Diagnose(context.Context) (daemonapi.Diagnosis, error)
|
||||
// Reconcile 生成修复计划或执行自动修复,apply 为 true 时执行修复动作。
|
||||
Reconcile(context.Context, bool) (daemonapi.Diagnosis, error)
|
||||
}
|
||||
|
||||
// Server 持有本地 Unix Socket 并把收到的请求分派给事务编排器。
|
||||
// 它负责连接的生命周期管理、请求解码与校验、执行进度回写以及最终结果的返回。
|
||||
type Server struct {
|
||||
@@ -45,24 +54,29 @@ type Server struct {
|
||||
socketPath string
|
||||
// updater 后端更新与重启动作的实际执行者。
|
||||
updater backendUpdater
|
||||
// diagnoser 后端状态诊断与对账动作的实际执行者。
|
||||
diagnoser backendDiagnoser
|
||||
// logger 用于记录服务运行与请求处理过程中的日志。
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
// New 构造一个 Server 实例并校验入参。
|
||||
// socketPath 必须是规范化的绝对路径,updater 不能为空,logger 为空时回退为默认日志器。
|
||||
// 当路径非法或 updater 为空时返回 error。
|
||||
func New(socketPath string, updater backendUpdater, logger *slog.Logger) (*Server, error) {
|
||||
// socketPath 必须是规范化的绝对路径,updater 与 diagnoser 不能为空,logger 为空时回退为默认日志器。
|
||||
// 当路径非法或依赖为空时返回 error。
|
||||
func New(socketPath string, updater backendUpdater, diagnoser backendDiagnoser, logger *slog.Logger) (*Server, error) {
|
||||
if !filepath.IsAbs(socketPath) || filepath.Clean(socketPath) != socketPath {
|
||||
return nil, errors.New("daemon Unix Socket path must be a clean absolute path")
|
||||
}
|
||||
if updater == nil {
|
||||
return nil, errors.New("backend updater is required")
|
||||
}
|
||||
if diagnoser == nil {
|
||||
return nil, errors.New("backend diagnoser is required")
|
||||
}
|
||||
if logger == nil {
|
||||
logger = slog.Default()
|
||||
}
|
||||
return &Server{socketPath: socketPath, updater: updater, logger: logger}, nil
|
||||
return &Server{socketPath: socketPath, updater: updater, diagnoser: diagnoser, logger: logger}, nil
|
||||
}
|
||||
|
||||
// Serve 监听 Unix Socket 直到 ctx 被取消,返回前会等待所有连接处理完成并清理 Socket 文件。
|
||||
@@ -176,8 +190,24 @@ func (s *Server) handle(ctx context.Context, connection net.Conn) {
|
||||
return
|
||||
}
|
||||
record, updateErr = s.updater.Restart(ctx, report)
|
||||
case daemonapi.OperationStatus, daemonapi.OperationDoctor:
|
||||
if request.InputType != "" || request.File != "" || request.ImageReference != "" || request.Apply {
|
||||
_ = s.writeResponse(connection, daemonapi.Response{Kind: daemonapi.ResponseResult, Error: request.Operation + " does not accept inputType, file, imageReference, or apply"})
|
||||
return
|
||||
}
|
||||
diagnosis, diagnosisErr := s.diagnoser.Diagnose(ctx)
|
||||
s.writeDiagnosis(ctx, connection, request.Operation, diagnosis, diagnosisErr)
|
||||
return
|
||||
case daemonapi.OperationReconcile:
|
||||
if request.InputType != "" || request.File != "" || request.ImageReference != "" {
|
||||
_ = s.writeResponse(connection, daemonapi.Response{Kind: daemonapi.ResponseResult, Error: "reconcile does not accept inputType, file, or imageReference"})
|
||||
return
|
||||
}
|
||||
diagnosis, diagnosisErr := s.diagnoser.Reconcile(ctx, request.Apply)
|
||||
s.writeDiagnosis(ctx, connection, request.Operation, diagnosis, diagnosisErr)
|
||||
return
|
||||
default:
|
||||
_ = s.writeResponse(connection, daemonapi.Response{Kind: daemonapi.ResponseResult, Error: "operation must be update or restart"})
|
||||
_ = s.writeResponse(connection, daemonapi.Response{Kind: daemonapi.ResponseResult, Error: "operation must be update, restart, status, doctor, or reconcile"})
|
||||
return
|
||||
}
|
||||
response := daemonapi.Response{Kind: daemonapi.ResponseResult, TransactionID: record.ID, State: string(record.State)}
|
||||
@@ -197,6 +227,22 @@ func (s *Server) writeResponse(connection net.Conn, response daemonapi.Response)
|
||||
return json.NewEncoder(connection).Encode(response)
|
||||
}
|
||||
|
||||
// writeDiagnosis 把 status/doctor/reconcile 的诊断结果写回客户端。
|
||||
// 诊断失败时返回携带错误信息的响应并清空诊断结果,同时记录相应日志。
|
||||
func (s *Server) writeDiagnosis(ctx context.Context, connection net.Conn, operation string, diagnosis daemonapi.Diagnosis, err error) {
|
||||
response := daemonapi.Response{Kind: daemonapi.ResponseResult, Diagnosis: &diagnosis}
|
||||
if err != nil {
|
||||
response.Diagnosis = nil
|
||||
response.Error = err.Error()
|
||||
s.logger.ErrorContext(ctx, "backend diagnosis failed", "operation", operation, "error", err)
|
||||
} else {
|
||||
s.logger.InfoContext(ctx, "backend diagnosis completed", "operation", operation, "healthy", diagnosis.Healthy, "items", len(diagnosis.Items))
|
||||
}
|
||||
if writeErr := s.writeResponse(connection, response); writeErr != nil {
|
||||
s.logger.ErrorContext(ctx, "write daemon diagnosis result", "operation", operation, "error", writeErr)
|
||||
}
|
||||
}
|
||||
|
||||
// decodeRequest 从 reader 中解码并校验一条客户端请求。
|
||||
// 它限制请求体大小不超过 maximumRequestBytes,禁止未知字段,并要求请求只能包含一个 JSON 值,
|
||||
// 且所有字符串字段不得带有前后空白字符;任一条不满足时返回 error。
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
func TestServerAcceptsBackendUpdateThroughUnixSocket(t *testing.T) {
|
||||
socketPath := shortSocketPath(t)
|
||||
updater := &fakeUpdater{record: transaction.Transaction{ID: "transaction-01", State: transaction.StateCommitted}}
|
||||
server, err := New(socketPath, updater, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||||
server, err := New(socketPath, updater, &fakeDiagnoser{}, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||||
if err != nil {
|
||||
t.Fatalf("create daemon server: %v", err)
|
||||
}
|
||||
@@ -65,7 +65,7 @@ func TestServerAcceptsBackendUpdateThroughUnixSocket(t *testing.T) {
|
||||
func TestServerAcceptsDirectNativeJARThroughUnixSocket(t *testing.T) {
|
||||
socketPath := shortSocketPath(t)
|
||||
updater := &fakeUpdater{record: transaction.Transaction{ID: "transaction-direct-01", State: transaction.StateCommitted}}
|
||||
server, err := New(socketPath, updater, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||||
server, err := New(socketPath, updater, &fakeDiagnoser{}, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||||
if err != nil {
|
||||
t.Fatalf("create daemon server: %v", err)
|
||||
}
|
||||
@@ -93,7 +93,7 @@ func TestServerAcceptsDirectNativeJARThroughUnixSocket(t *testing.T) {
|
||||
func TestServerAcceptsContainerImageThroughUnixSocket(t *testing.T) {
|
||||
socketPath := shortSocketPath(t)
|
||||
updater := &fakeUpdater{record: transaction.Transaction{ID: "transaction-container-01", State: transaction.StateCommitted}}
|
||||
server, err := New(socketPath, updater, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||||
server, err := New(socketPath, updater, &fakeDiagnoser{}, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||||
if err != nil {
|
||||
t.Fatalf("create daemon server: %v", err)
|
||||
}
|
||||
@@ -121,7 +121,7 @@ func TestServerReturnsTransactionFailure(t *testing.T) {
|
||||
record: transaction.Transaction{ID: "transaction-02", State: transaction.StateRolledBack},
|
||||
err: errors.New("health check failed"),
|
||||
}
|
||||
server, err := New(socketPath, updater, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||||
server, err := New(socketPath, updater, &fakeDiagnoser{}, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||||
if err != nil {
|
||||
t.Fatalf("create daemon server: %v", err)
|
||||
}
|
||||
@@ -143,7 +143,7 @@ func TestServerReturnsTransactionFailure(t *testing.T) {
|
||||
func TestServerAcceptsBackendRestartThroughUnixSocket(t *testing.T) {
|
||||
socketPath := shortSocketPath(t)
|
||||
updater := &fakeUpdater{record: transaction.Transaction{ID: "transaction-restart-01", State: transaction.StateCommitted}}
|
||||
server, err := New(socketPath, updater, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||||
server, err := New(socketPath, updater, &fakeDiagnoser{}, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||||
if err != nil {
|
||||
t.Fatalf("create daemon server: %v", err)
|
||||
}
|
||||
@@ -168,6 +168,55 @@ func TestServerAcceptsBackendRestartThroughUnixSocket(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestServerReturnsStatusThroughUnixSocket 验证服务端能通过 Unix Socket 处理 status 请求,
|
||||
// 并把诊断结果返回给客户端。
|
||||
func TestServerReturnsStatusThroughUnixSocket(t *testing.T) {
|
||||
socketPath := shortSocketPath(t)
|
||||
updater := &fakeUpdater{}
|
||||
diagnosis := daemonapi.Diagnosis{Service: "backend", Type: "container", Healthy: true, Items: []daemonapi.DiagnosisItem{{Level: daemonapi.DiagnosisLevelOK, Code: "ok", Message: "ok"}}}
|
||||
diagnoser := &fakeDiagnoser{diagnosis: diagnosis}
|
||||
server, err := New(socketPath, updater, diagnoser, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||||
if err != nil {
|
||||
t.Fatalf("create daemon server: %v", err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
serveResult := make(chan error, 1)
|
||||
go func() { serveResult <- server.Serve(ctx) }()
|
||||
waitForSocket(t, socketPath, serveResult)
|
||||
|
||||
result, err := daemonclient.Status(context.Background(), socketPath, "backend")
|
||||
if err != nil {
|
||||
t.Fatalf("submit status: %v", err)
|
||||
}
|
||||
if result.Service != "backend" || !result.Healthy {
|
||||
t.Fatalf("unexpected status result: %+v", result)
|
||||
}
|
||||
}
|
||||
|
||||
// TestServerReconcileForwardsApply 验证 reconcile --apply 会把 apply 标志正确传递给诊断器。
|
||||
func TestServerReconcileForwardsApply(t *testing.T) {
|
||||
socketPath := shortSocketPath(t)
|
||||
updater := &fakeUpdater{}
|
||||
diagnoser := &fakeDiagnoser{diagnosis: daemonapi.Diagnosis{Service: "backend", Type: "container", Healthy: true}}
|
||||
server, err := New(socketPath, updater, diagnoser, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||||
if err != nil {
|
||||
t.Fatalf("create daemon server: %v", err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
serveResult := make(chan error, 1)
|
||||
go func() { serveResult <- server.Serve(ctx) }()
|
||||
waitForSocket(t, socketPath, serveResult)
|
||||
|
||||
if _, err := daemonclient.Reconcile(context.Background(), socketPath, "backend", true); err != nil {
|
||||
t.Fatalf("submit reconcile apply: %v", err)
|
||||
}
|
||||
if !diagnoser.applied {
|
||||
t.Fatal("reconcile --apply was not forwarded to diagnoser")
|
||||
}
|
||||
}
|
||||
|
||||
// shortSocketPath 创建一个短目录并返回其下的 daemon.sock 路径,用于规避 Unix Socket 路径长度上限。
|
||||
// 目录会在测试结束时自动清理。
|
||||
func shortSocketPath(t *testing.T) string {
|
||||
@@ -259,3 +308,21 @@ func (u *fakeUpdater) Restart(_ context.Context, report backendupdate.ProgressRe
|
||||
}
|
||||
return u.record, u.err
|
||||
}
|
||||
|
||||
// fakeDiagnoser backendDiagnoser 接口的测试替身,返回预设的空诊断结果。
|
||||
type fakeDiagnoser struct {
|
||||
diagnosis daemonapi.Diagnosis
|
||||
err error
|
||||
applied bool
|
||||
}
|
||||
|
||||
// Diagnose 返回预设的诊断结果。
|
||||
func (d *fakeDiagnoser) Diagnose(context.Context) (daemonapi.Diagnosis, error) {
|
||||
return d.diagnosis, d.err
|
||||
}
|
||||
|
||||
// Reconcile 返回预设的诊断结果,并记录 apply 标志。
|
||||
func (d *fakeDiagnoser) Reconcile(_ context.Context, apply bool) (daemonapi.Diagnosis, error) {
|
||||
d.applied = apply
|
||||
return d.diagnosis, d.err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user