diff --git a/cmd/root.go b/cmd/root.go index be8af8c..53964cb 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -42,6 +42,9 @@ const ( flagStartS3Endpoint = "initial-s3-endpoint" flagStartS3Region = "initial-s3-region" flagStartS3ForcePathStyle = "initial-s3-force-path-style" + + // Filenode configuration. + flagStartFilenodeDefaultLimit = "initial-filenode-default-limit" ) var log = logger.NewNamed("cli") @@ -164,6 +167,16 @@ func buildStartFlags() []cli.Flag { EnvVars: []string{"ANY_SYNC_BUNDLE_INIT_S3_FORCE_PATH_STYLE"}, Usage: "Use path-style S3 URLs (required for MinIO)", }, + + // Filenode Configuration + &cli.Uint64Flag{ + Name: flagStartFilenodeDefaultLimit, + EnvVars: []string{"ANY_SYNC_BUNDLE_INIT_FILENODE_DEFAULT_LIMIT"}, + Usage: "Storage limit per space in bytes. " + + "Examples: 1 GiB = 1073741824, 10 GiB = 10737418240, " + + "150 GiB = 161061273600, 1 TiB = 1099511627776 (default), " + + "2 TiB = 2199023255552", + }, } } diff --git a/cmd/start.go b/cmd/start.go index 0661c72..d0eb388 100644 --- a/cmd/start.go +++ b/cmd/start.go @@ -159,6 +159,9 @@ func loadOrCreateConfig(cCtx *cli.Context, log logger.CtxLogger) *bundleConfig.C S3Endpoint: cCtx.String(flagStartS3Endpoint), S3Region: cCtx.String(flagStartS3Region), S3ForcePathStyle: cCtx.Bool(flagStartS3ForcePathStyle), + + // Filenode configuration + FilenodeDefaultLimit: cCtx.Uint64(flagStartFilenodeDefaultLimit), }) } diff --git a/compose.aio.yml b/compose.aio.yml index e89dd99..24d1ba6 100644 --- a/compose.aio.yml +++ b/compose.aio.yml @@ -18,3 +18,10 @@ services: environment: # Advertise addresses clients should use. Replace with your server hostname/IP. ANY_SYNC_BUNDLE_INIT_EXTERNAL_ADDRS: "192.168.100.9" + # Storage limit per space in bytes (default: 1 TiB) + # Examples: 1 GiB = 1073741824 + # 10 GiB = 10737418240 + # 150 GiB = 161061273600 + # 1 TiB = 1099511627776 (default) + # 2 TiB = 2199023255552 + # ANY_SYNC_BUNDLE_INIT_FILENODE_DEFAULT_LIMIT: "1099511627776" diff --git a/compose.external.yml b/compose.external.yml index 4676bb6..82aab4f 100644 --- a/compose.external.yml +++ b/compose.external.yml @@ -79,3 +79,10 @@ services: ANY_SYNC_BUNDLE_INIT_EXTERNAL_ADDRS: "192.168.100.9" ANY_SYNC_BUNDLE_INIT_MONGO_URI: "mongodb://mongo:27017/?replicaSet=rs0" ANY_SYNC_BUNDLE_INIT_REDIS_URI: "redis://redis:6379/" + # Storage limit per space in bytes (default: 1 TiB) + # Examples: 1 GiB = 1073741824 + # 10 GiB = 10737418240 + # 150 GiB = 161061273600 + # 1 TiB = 1099511627776 (default) + # 2 TiB = 2199023255552 + # ANY_SYNC_BUNDLE_INIT_FILENODE_DEFAULT_LIMIT: "1099511627776" diff --git a/compose.s3.yml b/compose.s3.yml index 6e93a89..27224de 100644 --- a/compose.s3.yml +++ b/compose.s3.yml @@ -68,3 +68,10 @@ services: # S3 Credentials (via standard AWS env vars) AWS_ACCESS_KEY_ID: "minioadmin" AWS_SECRET_ACCESS_KEY: "minioadmin" + # Storage limit per space in bytes (default: 1 TiB) + # Examples: 1 GiB = 1073741824 + # 10 GiB = 10737418240 + # 150 GiB = 161061273600 + # 1 TiB = 1099511627776 (default) + # 2 TiB = 2199023255552 + # ANY_SYNC_BUNDLE_INIT_FILENODE_DEFAULT_LIMIT: "1099511627776" diff --git a/config/bundle.go b/config/bundle.go index 1282287..0ede4dd 100644 --- a/config/bundle.go +++ b/config/bundle.go @@ -58,6 +58,12 @@ type ConsensusConfig struct { type FileNodeConfig struct { RedisConnect string `yaml:"redisConnect"` S3 *S3Config `yaml:"s3,omitempty"` // Optional: if present, use S3 storage instead of BadgerDB + // DefaultLimit is the storage limit per space in bytes. + // If not set (0), defaults to 1 TiB for backwards compatibility with old configs. + // + // TODO(bundleFormat:v2): Remove omitempty and runtime default. New configs already + // write 1 TiB explicitly; v2 can require the field to be present in config file. + DefaultLimit uint64 `yaml:"defaultLimit,omitempty"` } // S3Config configures S3-compatible storage backend for the filenode. @@ -148,6 +154,10 @@ type CreateOptions struct { S3Endpoint string S3Region string // Optional, defaults to "us-east-1" if empty S3ForcePathStyle bool + + // Filenode storage limit per space in bytes. + // If 0, defaults to 1 TiB at runtime. + FilenodeDefaultLimit uint64 } func CreateWrite(cfg *CreateOptions) *Config { @@ -217,6 +227,7 @@ func newBundleConfig(cfg *CreateOptions) *Config { }, FileNode: FileNodeConfig{ RedisConnect: cfg.RedisURI, + DefaultLimit: filenodeDefaultLimit(cfg.FilenodeDefaultLimit), }, } @@ -237,6 +248,16 @@ func newBundleConfig(cfg *CreateOptions) *Config { return defaultCfg } +// filenodeDefaultLimit returns the provided limit or 1 TiB if not set. +// This ensures new configs always have an explicit limit written to the file. +func filenodeDefaultLimit(limit uint64) uint64 { + const oneTiB = 1024 * 1024 * 1024 * 1024 + if limit == 0 { + return oneTiB + } + return limit +} + func newAcc(netKey crypto.PrivKey) accountservice.Config { signKey, _, err := crypto.GenerateRandomEd25519KeyPair() if err != nil { diff --git a/config/bundle_test.go b/config/bundle_test.go index ed94c51..229760e 100644 --- a/config/bundle_test.go +++ b/config/bundle_test.go @@ -420,3 +420,84 @@ filenode: assert.Equal(t, "https://s3.amazonaws.com", cfg.FileNode.S3.Endpoint) assert.True(t, cfg.FileNode.S3.ForcePathStyle) } + +// Filenode Default Limit Tests + +func TestCreateWrite_WithFilenodeDefaultLimit(t *testing.T) { + tmpDir := t.TempDir() + cfgPath := filepath.Join(tmpDir, "limit-config.yml") + + const tenGiB = 10 * 1024 * 1024 * 1024 // 10 GiB + + opts := &CreateOptions{ + CfgPath: cfgPath, + StorePath: filepath.Join(tmpDir, "storage"), + MongoURI: "mongodb://localhost:27017/", + RedisURI: "redis://localhost:6379/", + ExternalAddrs: []string{"192.168.1.100"}, + FilenodeDefaultLimit: tenGiB, + } + + cfg := CreateWrite(opts) + assert.Equal(t, uint64(tenGiB), cfg.FileNode.DefaultLimit) + + // Verify it persists and loads back correctly + loadedCfg := Load(cfgPath) + assert.Equal(t, uint64(tenGiB), loadedCfg.FileNode.DefaultLimit) +} + +func TestCreateWrite_WithoutFilenodeDefaultLimit(t *testing.T) { + tmpDir := t.TempDir() + cfgPath := filepath.Join(tmpDir, "no-limit-config.yml") + + const oneTiB = 1024 * 1024 * 1024 * 1024 // 1 TiB + + opts := &CreateOptions{ + CfgPath: cfgPath, + StorePath: filepath.Join(tmpDir, "storage"), + MongoURI: "mongodb://localhost:27017/", + RedisURI: "redis://localhost:6379/", + ExternalAddrs: []string{"192.168.1.100"}, + // FilenodeDefaultLimit not set (zero value) + } + + cfg := CreateWrite(opts) + assert.Equal(t, uint64(oneTiB), cfg.FileNode.DefaultLimit, + "DefaultLimit should be 1 TiB when not configured (written to config file)") +} + +func TestLoad_WithFilenodeDefaultLimit(t *testing.T) { + tmpDir := t.TempDir() + cfgPath := filepath.Join(tmpDir, "limit-config.yml") + + configWithLimit := `bundleVersion: "1.0.0" +bundleFormat: 1 +externalAddr: + - "192.168.1.100" +configId: "test-config-id" +networkId: "test-network-id" +storagePath: "./data/storage" +account: + peerId: "test-peer-id" + peerKey: "test-peer-key" + signingKey: "test-signing-key" +network: + listenTCPAddr: "0.0.0.0:33010" + listenUDPAddr: "0.0.0.0:33020" +coordinator: + mongoConnect: "mongodb://localhost:27017/" + mongoDatabase: "coordinator" +consensus: + mongoConnect: "mongodb://localhost:27017/?w=majority" + mongoDatabase: "consensus" +filenode: + redisConnect: "redis://localhost:6379/" + defaultLimit: 10737418240 +` + + err := os.WriteFile(cfgPath, []byte(configWithLimit), 0o600) + require.NoError(t, err) + + cfg := Load(cfgPath) + assert.Equal(t, uint64(10737418240), cfg.FileNode.DefaultLimit) +} diff --git a/config/convert.go b/config/convert.go index 0f849d2..1ed2758 100644 --- a/config/convert.go +++ b/config/convert.go @@ -146,6 +146,13 @@ func (bc *Config) consensusConfig(opts *nodeConfigOpts) *consensusconfig.Config func (bc *Config) filenodeConfig(opts *nodeConfigOpts) *filenodeconfig.Config { const oneTerabyte = 1024 * 1024 * 1024 * 1024 // 1 TiB in bytes + // Use configured limit or default to 1 TiB for backwards compatibility. + // TODO(bundleFormat:v2): Remove default, require explicit limit. + defaultLimit := bc.FileNode.DefaultLimit + if defaultLimit == 0 { + defaultLimit = oneTerabyte + } + cfg := &filenodeconfig.Config{ Account: bc.Account, Drpc: rpc.Config{ @@ -161,7 +168,7 @@ func (bc *Config) filenodeConfig(opts *nodeConfigOpts) *filenodeconfig.Config { Network: opts.networkCfg, NetworkStorePath: opts.pathNetworkStoreFilenode, NetworkUpdateIntervalSec: 0, - DefaultLimit: oneTerabyte, + DefaultLimit: defaultLimit, } // Configure S3 storage if S3 config is present diff --git a/config/convert_test.go b/config/convert_test.go index 8cbe7ea..f5d2bf5 100644 --- a/config/convert_test.go +++ b/config/convert_test.go @@ -174,3 +174,30 @@ func TestFilenodeConfig_WithoutS3(t *testing.T) { // S3Store should be empty (zero value) assert.Empty(t, nodeCfgs.Filenode.S3Store.Bucket) } + +// Filenode Default Limit Tests + +func TestFilenodeConfig_DefaultLimit_CustomValue(t *testing.T) { + const tenGiB = 10 * 1024 * 1024 * 1024 // 10 GiB + + cfg := newTestConfig() + cfg.FileNode.DefaultLimit = tenGiB + + nodeCfgs := cfg.NodeConfigs() + + require.NotNil(t, nodeCfgs.Filenode) + assert.Equal(t, uint64(tenGiB), nodeCfgs.Filenode.DefaultLimit) +} + +func TestFilenodeConfig_DefaultLimit_ZeroDefaultsTo1TiB(t *testing.T) { + const oneTiB = 1024 * 1024 * 1024 * 1024 // 1 TiB + + cfg := newTestConfig() + cfg.FileNode.DefaultLimit = 0 // Not set + + nodeCfgs := cfg.NodeConfigs() + + require.NotNil(t, nodeCfgs.Filenode) + assert.Equal(t, uint64(oneTiB), nodeCfgs.Filenode.DefaultLimit, + "Zero DefaultLimit should fallback to 1 TiB") +}