From 1e5d978f0ba8dececa1f533cb3f385de9170df8c Mon Sep 17 00:00:00 2001 From: hayzam Date: Thu, 26 Mar 2026 21:19:23 +0530 Subject: [PATCH] main: startup: gate initialization --- cmd/sylve/main.go | 38 +++++++--- cmd/sylve/startup_mode.go | 37 ++++++++++ cmd/sylve/startup_mode_test.go | 122 +++++++++++++++++++++++++++++++++ 3 files changed, 189 insertions(+), 8 deletions(-) create mode 100644 cmd/sylve/startup_mode.go create mode 100644 cmd/sylve/startup_mode_test.go diff --git a/cmd/sylve/main.go b/cmd/sylve/main.go index 3e63b825..179f55f1 100644 --- a/cmd/sylve/main.go +++ b/cmd/sylve/main.go @@ -22,6 +22,7 @@ import ( "github.com/alchemillahq/sylve/internal/cmd" "github.com/alchemillahq/sylve/internal/config" "github.com/alchemillahq/sylve/internal/db" + dbModels "github.com/alchemillahq/sylve/internal/db/models" clusterModels "github.com/alchemillahq/sylve/internal/db/models/cluster" "github.com/alchemillahq/sylve/internal/handlers" "github.com/alchemillahq/sylve/internal/logger" @@ -134,22 +135,43 @@ func main() { defer initCancel() err = sS.Initialize(aS.(*auth.Service), initContext, qCtx) - - go sysS.StartNetlinkWatcher(qCtx) - go sysS.NetlinkEventsCleaner(qCtx) - go libvirtSvc.StartLifecycleWatcher(qCtx) - go db.StartQueue(qCtx) - if err != nil { logger.L.Fatal().Err(err).Msg("Failed to initialize at startup") - } else { - logger.L.Info().Msg("Basic initializations complete") + } + + logger.L.Info().Msg("Basic initializations complete") + + startAdvancedStartupWorkers, basicSettings, settingsErr := shouldStartAdvancedStartupWorkers(func() (dbModels.BasicSettings, error) { + var settings dbModels.BasicSettings + if err := d.First(&settings).Error; err != nil { + return dbModels.BasicSettings{}, err + } + return settings, nil + }) + if settingsErr != nil { + logger.L.Fatal().Err(settingsErr).Msg("Failed to evaluate startup readiness") + } + + go db.StartQueue(qCtx) + + if startAdvancedStartupWorkers { + go sysS.StartNetlinkWatcher(qCtx) + go sysS.NetlinkEventsCleaner(qCtx) + + if libvirtSvc.IsVirtualizationEnabled() { + go libvirtSvc.StartLifecycleWatcher(qCtx) + } enqueueCtx, enqueueCancel := context.WithTimeout(context.Background(), 10*time.Second) if enqueueErr := lifecycleSvc.EnqueueStartupAutostart(enqueueCtx); enqueueErr != nil { logger.L.Warn().Err(enqueueErr).Msg("failed_to_enqueue_guest_autostart_sequence") } enqueueCancel() + } else { + logger.L.Info(). + Bool("initialized", basicSettings.Initialized). + Bool("restarted", basicSettings.Restarted). + Msg("System startup not finalized; skipping advanced watchers and autostart queue") } err = cS.InitRaft(fsm) diff --git a/cmd/sylve/startup_mode.go b/cmd/sylve/startup_mode.go new file mode 100644 index 00000000..440c00e3 --- /dev/null +++ b/cmd/sylve/startup_mode.go @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: BSD-2-Clause +// +// Copyright (c) 2025 The FreeBSD Foundation. +// +// This software was developed by Hayzam Sherif +// of Alchemilla Ventures Pvt. Ltd. , +// under sponsorship from the FreeBSD Foundation. + +package main + +import ( + "errors" + "fmt" + + "github.com/alchemillahq/sylve/internal/db/models" + + "gorm.io/gorm" +) + +type basicSettingsLookup func() (models.BasicSettings, error) + +func shouldStartAdvancedStartupWorkers(lookup basicSettingsLookup) (bool, models.BasicSettings, error) { + if lookup == nil { + return false, models.BasicSettings{}, fmt.Errorf("basic_settings_lookup_required") + } + + settings, err := lookup() + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return false, models.BasicSettings{}, nil + } + + return false, models.BasicSettings{}, fmt.Errorf("failed_to_fetch_basic_settings: %w", err) + } + + return settings.Initialized && settings.Restarted, settings, nil +} diff --git a/cmd/sylve/startup_mode_test.go b/cmd/sylve/startup_mode_test.go new file mode 100644 index 00000000..87b2cb05 --- /dev/null +++ b/cmd/sylve/startup_mode_test.go @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: BSD-2-Clause +// +// Copyright (c) 2025 The FreeBSD Foundation. +// +// This software was developed by Hayzam Sherif +// of Alchemilla Ventures Pvt. Ltd. , +// under sponsorship from the FreeBSD Foundation. + +package main + +import ( + "errors" + "strings" + "testing" + + "github.com/alchemillahq/sylve/internal/db/models" + "gorm.io/gorm" +) + +func TestShouldStartAdvancedStartupWorkersMissingLookup(t *testing.T) { + _, _, err := shouldStartAdvancedStartupWorkers(nil) + if err == nil { + t.Fatalf("expected error when lookup is nil") + } + + if !strings.Contains(err.Error(), "basic_settings_lookup_required") { + t.Fatalf("expected missing lookup error, got: %v", err) + } +} + +func TestShouldStartAdvancedStartupWorkersNoBasicSettingsYet(t *testing.T) { + enabled, settings, err := shouldStartAdvancedStartupWorkers(func() (models.BasicSettings, error) { + return models.BasicSettings{}, gorm.ErrRecordNotFound + }) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if enabled { + t.Fatalf("expected advanced startup workers to stay disabled when settings are missing") + } + if settings.Initialized || settings.Restarted { + t.Fatalf("expected zero-value settings when basic settings are missing, got %+v", settings) + } +} + +func TestShouldStartAdvancedStartupWorkersRequiresBothFlags(t *testing.T) { + cases := []struct { + name string + settings models.BasicSettings + enabled bool + }{ + { + name: "initialized false restarted false", + settings: models.BasicSettings{ + Initialized: false, + Restarted: false, + }, + enabled: false, + }, + { + name: "initialized true restarted false", + settings: models.BasicSettings{ + Initialized: true, + Restarted: false, + }, + enabled: false, + }, + { + name: "initialized false restarted true", + settings: models.BasicSettings{ + Initialized: false, + Restarted: true, + }, + enabled: false, + }, + { + name: "initialized true restarted true", + settings: models.BasicSettings{ + Initialized: true, + Restarted: true, + }, + enabled: true, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + enabled, settings, err := shouldStartAdvancedStartupWorkers(func() (models.BasicSettings, error) { + return tc.settings, nil + }) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if enabled != tc.enabled { + t.Fatalf("expected enabled=%t, got %t", tc.enabled, enabled) + } + if settings.Initialized != tc.settings.Initialized || settings.Restarted != tc.settings.Restarted { + t.Fatalf("expected settings %+v, got %+v", tc.settings, settings) + } + }) + } +} + +func TestShouldStartAdvancedStartupWorkersLookupFailure(t *testing.T) { + lookupErr := errors.New("db_timeout") + + enabled, _, err := shouldStartAdvancedStartupWorkers(func() (models.BasicSettings, error) { + return models.BasicSettings{}, lookupErr + }) + if err == nil { + t.Fatalf("expected error, got nil") + } + if enabled { + t.Fatalf("expected advanced startup workers disabled on lookup failure") + } + if !strings.Contains(err.Error(), "failed_to_fetch_basic_settings") { + t.Fatalf("expected wrapped lookup error, got: %v", err) + } + if !errors.Is(err, lookupErr) { + t.Fatalf("expected wrapped error %v, got %v", lookupErr, err) + } +}