Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions cmd/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
14 changes: 9 additions & 5 deletions internal/bootstrap/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

Expand Down
64 changes: 64 additions & 0 deletions internal/bootstrap/module_restore_test.go
Original file line number Diff line number Diff line change
@@ -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")
}