feat: complete Windows client control and recovery paths
This commit is contained in:
@@ -12,10 +12,13 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
rvboxv1 "github.com/rvbox/rvbox/gen/go/rvbox/v1"
|
||||
"github.com/rvbox/rvbox/internal/agentproto"
|
||||
"github.com/rvbox/rvbox/internal/config"
|
||||
"github.com/rvbox/rvbox/internal/domain"
|
||||
"github.com/rvbox/rvbox/internal/observability"
|
||||
"github.com/rvbox/rvbox/internal/server/control"
|
||||
"github.com/rvbox/rvbox/internal/server/session"
|
||||
"github.com/rvbox/rvbox/internal/server/store"
|
||||
@@ -24,12 +27,26 @@ import (
|
||||
|
||||
func main() {
|
||||
var configPath string
|
||||
var checkConfig bool
|
||||
flag.StringVar(&configPath, "config", "", "absolute server TOML configuration path")
|
||||
flag.BoolVar(&checkConfig, "check-config", false, "validate server configuration and exit")
|
||||
flag.Parse()
|
||||
if configPath == "" {
|
||||
log.Print("rvbox-server: --config is required")
|
||||
os.Exit(2)
|
||||
}
|
||||
if checkConfig {
|
||||
configured, err := config.LoadServer(configPath)
|
||||
if err != nil {
|
||||
log.Printf("rvbox-server: invalid configuration: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if _, err := fmt.Fprintf(os.Stdout, "valid server configuration: data_dir=%s agent_listen=%s control_socket=%s observability_listen=%s json_rpc_enabled=%t\n", configured.Server.DataDir, configured.Server.AgentListen, configured.Server.ControlSocket, configured.Observability.Listen, configured.JSONRPC.Enabled); err != nil {
|
||||
log.Printf("rvbox-server: write check result: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err := run(configPath); err != nil {
|
||||
log.Printf("rvbox-server: %v", err)
|
||||
os.Exit(1)
|
||||
@@ -58,6 +75,43 @@ func run(configPath string) error {
|
||||
return err
|
||||
}
|
||||
defer persistence.Close()
|
||||
health := observability.New()
|
||||
healthServer := &http.Server{Handler: health.Handler(observability.Paths{Liveness: configured.Observability.LivenessPath, Readiness: configured.Observability.ReadinessPath, Metrics: configured.Observability.MetricsPath}), ReadHeaderTimeout: configured.Flow.WriteDeadline}
|
||||
// Health is deliberately best effort. A port collision or a temporary
|
||||
// listener failure must not prevent the control/agent service from starting;
|
||||
// operators still get the failure in the daemon log.
|
||||
var healthListener net.Listener
|
||||
if configured.Observability.Listen != "" {
|
||||
healthListener, err = net.Listen("tcp", configured.Observability.Listen)
|
||||
if err != nil {
|
||||
log.Printf("rvbox-server: observability endpoint unavailable (continuing without it): %v", err)
|
||||
}
|
||||
}
|
||||
if healthListener != nil {
|
||||
defer healthListener.Close()
|
||||
}
|
||||
recoveryContext, cancelRecovery := context.WithCancel(context.Background())
|
||||
defer cancelRecovery()
|
||||
// Recovery is deliberately asynchronous: liveness and incident inspection
|
||||
// remain available while committed-range checks run. Readiness becomes
|
||||
// true only after the real SQLite/segment recovery completes.
|
||||
go func() {
|
||||
if _, recoverErr := persistence.RecoverCommandSegments(recoveryContext); recoverErr != nil {
|
||||
health.SetDirty(true)
|
||||
if incidentID, idErr := domain.NewUUIDv7(); idErr == nil {
|
||||
if _, incidentErr := persistence.RecordIncident(context.Background(), store.IncidentInput{
|
||||
IncidentUUID: incidentID, DetectedAt: time.Now().UTC(), Kind: store.IncidentSQLiteIntegrity,
|
||||
Scope: store.IncidentScopeGlobal, ScopeKey: "server-startup-recovery", Summary: "startup storage recovery failed",
|
||||
Evidence: []byte(recoverErr.Error()), AutomaticallyRepairable: false,
|
||||
}); incidentErr != nil {
|
||||
log.Printf("rvbox-server: could not persist recovery incident: %v", incidentErr)
|
||||
}
|
||||
}
|
||||
log.Printf("rvbox-server: storage recovery left readiness disabled: %v", recoverErr)
|
||||
return
|
||||
}
|
||||
health.SetReady(true)
|
||||
}()
|
||||
|
||||
listener, err := net.Listen("tcp", configured.Server.AgentListen)
|
||||
if err != nil {
|
||||
@@ -106,9 +160,12 @@ func run(configPath string) error {
|
||||
HeartbeatIdle: configured.Protocol.HeartbeatIdle, LivenessTimeout: configured.Protocol.LivenessTimeout,
|
||||
}
|
||||
httpServer := &http.Server{Handler: agent, ReadHeaderTimeout: configured.Flow.WriteDeadline}
|
||||
serveError := make(chan error, 3)
|
||||
serveError := make(chan error, 4)
|
||||
go func() { serveError <- httpServer.Serve(listener) }()
|
||||
go func() { serveError <- grpcServer.Serve(controlListener) }()
|
||||
if healthListener != nil {
|
||||
go func() { serveError <- healthServer.Serve(healthListener) }()
|
||||
}
|
||||
if rpcServer != nil {
|
||||
go func() { serveError <- rpcServer.Serve(rpcListener) }()
|
||||
}
|
||||
@@ -119,6 +176,9 @@ func run(configPath string) error {
|
||||
select {
|
||||
case err := <-serveError:
|
||||
if errors.Is(err, http.ErrServerClosed) {
|
||||
if healthListener != nil {
|
||||
_ = healthServer.Close()
|
||||
}
|
||||
if rpcServer != nil {
|
||||
_ = rpcServer.Close()
|
||||
}
|
||||
@@ -126,6 +186,9 @@ func run(configPath string) error {
|
||||
return nil
|
||||
}
|
||||
_ = httpServer.Close()
|
||||
if healthListener != nil {
|
||||
_ = healthServer.Close()
|
||||
}
|
||||
if rpcServer != nil {
|
||||
_ = rpcServer.Close()
|
||||
}
|
||||
@@ -135,6 +198,13 @@ func run(configPath string) error {
|
||||
shutdownContext, cancel := context.WithTimeout(context.Background(), configured.Server.ShutdownGrace)
|
||||
defer cancel()
|
||||
httpErr := httpServer.Shutdown(shutdownContext)
|
||||
var healthErr error
|
||||
if healthListener != nil {
|
||||
healthErr = healthServer.Shutdown(shutdownContext)
|
||||
}
|
||||
if httpErr == nil {
|
||||
httpErr = healthErr
|
||||
}
|
||||
if rpcServer != nil {
|
||||
if err := rpcServer.Shutdown(shutdownContext); httpErr == nil {
|
||||
httpErr = err
|
||||
|
||||
Reference in New Issue
Block a user