diff --git a/cmd/server/server.go b/cmd/server/server.go index 7f39f20bb4..fdff54215f 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -350,8 +350,8 @@ func runServer(cmd *cobra.Command, _ []string) error { flightrecorder.Module(flightRecorderCfg), // Provide application module appModule, - // Cold storage module (conditional on driver) - bootstrap.ColdStorageModule(cfg.ColdStorageConfig.Driver), + // Cold storage module (conditional on driver; never in restore mode) + bootstrap.ColdStorageModule(cfg.ColdStorageConfig.Driver, cfg.Restore), } defer func() { diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index 55da1fe50b..cc95e486de 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -87,14 +87,18 @@ type nodeProvideResult struct { FreshStart walFreshStart } -// coldStorageModule conditionally provides ColdStorage, ColdReader, and Archiver -// when cold storage is enabled (driver != "none"). When disabled, these components -// are not added to the fx graph and archiving is rejected at the admission layer. // ColdStorageModule conditionally provides ColdStorage, ColdReader, and Archiver // when cold storage is enabled (driver != "none"). When disabled, these components // are not added to the fx graph and archiving is rejected at the admission layer. -func ColdStorageModule(coldStorageDriver string) fx.Option { - if coldStorageDriver == "none" { +// +// Restore mode never gets the module: the Archiver consumes the runtime graph +// (*dal.Store, *state.Machine, ctrl.Admission, *node.Node) that RestoreModule +// deliberately does not provide — including it makes the whole fx graph +// unbuildable and the server exits at boot. A restoring server has no use for +// it either: it neither archives nor reads cold data, and backup downloads +// carry their own storage configuration per request. +func ColdStorageModule(coldStorageDriver string, restore bool) fx.Option { + if coldStorageDriver == "none" || restore { return fx.Options() } diff --git a/internal/bootstrap/module_restore_test.go b/internal/bootstrap/module_restore_test.go new file mode 100644 index 0000000000..6fc7ee3a03 --- /dev/null +++ b/internal/bootstrap/module_restore_test.go @@ -0,0 +1,64 @@ +package bootstrap + +import ( + "testing" + + "github.com/stretchr/testify/require" + "go.uber.org/fx" + + logging "github.com/formancehq/go-libs/v5/pkg/observe/log" + + "github.com/formancehq/ledger/v3/internal/infra/coldstorage" +) + +// restoreGraphOptions assembles the restore-mode fx graph the way +// cmd/server/server.go does: the restore app module plus the conditional +// cold-storage module. +func restoreGraphOptions(t *testing.T, coldStorageGated bool) fx.Option { + t.Helper() + + cfg := Config{ + ClusterID: "restore-graph-test", + DataDir: t.TempDir(), + Restore: true, + ColdStorageConfig: coldstorage.Config{ + Driver: "s3", + S3Bucket: "archives", + S3Region: "us-east-1", + }, + } + + restore := cfg.Restore + if !coldStorageGated { + restore = false + } + + return fx.Options( + fx.Supply(cfg), + fx.Provide(func() logging.Logger { return logging.Testing() }), + RestoreModule(), + ColdStorageModule(cfg.ColdStorageConfig.Driver, restore), + ) +} + +// TestRestoreModeGraph_WithColdStorageEnabled pins the disaster-recovery +// regression: a server with cold storage enabled must be able to build its fx +// graph in restore mode. The Archiver consumes the runtime graph (*dal.Store, +// *state.Machine, ctrl.Admission, *node.Node) that RestoreModule deliberately +// does not provide, so the cold-storage module must stay out of restore mode. +func TestRestoreModeGraph_WithColdStorageEnabled(t *testing.T) { + t.Parallel() + + require.NoError(t, fx.ValidateApp(restoreGraphOptions(t, true))) +} + +// TestRestoreModeGraph_UngatedColdStorageIsUnbuildable documents why the gate +// exists: without it, the restore-mode graph cannot be built at all — the shape +// a cold-storage-enabled node hits at boot when asked to restore. +func TestRestoreModeGraph_UngatedColdStorageIsUnbuildable(t *testing.T) { + t.Parallel() + + err := fx.ValidateApp(restoreGraphOptions(t, false)) + require.Error(t, err) + require.Contains(t, err.Error(), "missing type") +}