aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-02-28 05:39:38 -0800
committerFuwn <[email protected]>2026-02-28 05:39:38 -0800
commit2525861018ccd4db2f683a8778e5784a9e2942f5 (patch)
tree6232a03b8ff0633adf920bafdcb946b2c1b62928
parentchore(compose): add thin mode override (diff)
downloadplutia-test-2525861018ccd4db2f683a8778e5784a9e2942f5.tar.xz
plutia-test-2525861018ccd4db2f683a8778e5784a9e2942f5.zip
feat(config): allow optional config file with default fallback
-rw-r--r--README.md2
-rw-r--r--config.default.yaml2
-rw-r--r--docker-compose.yml2
-rw-r--r--internal/config/config.go12
-rw-r--r--internal/config/config_load_test.go82
5 files changed, 93 insertions, 7 deletions
diff --git a/README.md b/README.md
index bfc9953..07fbac6 100644
--- a/README.md
+++ b/README.md
@@ -211,6 +211,8 @@ See [`config.default.yaml`](./config.default.yaml). All supported config keys:
- `rate_limit.proof_rps`
- `rate_limit.proof_burst`
+Config files are optional. If `--config` points to a missing file, Plutia falls back to internal defaults and then applies any `PLUTIA_*` environment overrides.
+
### Example `docker-compose.yml`
```yaml
diff --git a/config.default.yaml b/config.default.yaml
index 9782b06..a9d1da7 100644
--- a/config.default.yaml
+++ b/config.default.yaml
@@ -6,7 +6,7 @@ zstd_level: 9
block_size_mb: 8
checkpoint_interval: 100000
commit_batch_size: 128
-verify_workers: 10
+# verify_workers: 16
export_page_size: 1000
replay_trace: false
thin_cache_ttl: 24h
diff --git a/docker-compose.yml b/docker-compose.yml
index 3fad1b3..5c115ba 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -22,7 +22,7 @@ services:
PLUTIA_THIN_CACHE_MAX_ENTRIES: ${THIN_CACHE_MAX_ENTRIES:-100000}
PLUTIA_CHECKPOINT_INTERVAL: ${CHECKPOINT_INTERVAL:-100000}
PLUTIA_COMMIT_BATCH_SIZE: ${COMMIT_BATCH_SIZE:-128}
- PLUTIA_VERIFY_WORKERS: ${VERIFY_WORKERS:-8}
+ PLUTIA_VERIFY_WORKERS: ${VERIFY_WORKERS:-}
PLUTIA_EXPORT_PAGE_SIZE: "${EXPORT_PAGE_SIZE:-1000}"
PLUTIA_REPLAY_TRACE: "${REPLAY_TRACE:-false}"
PLUTIA_LISTEN_ADDR: "${LISTEN_ADDR:-:8080}"
diff --git a/internal/config/config.go b/internal/config/config.go
index 69173fd..47f5842 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -89,11 +89,13 @@ func Load(path string) (Config, error) {
b, err := os.ReadFile(path)
if err != nil {
- return Config{}, fmt.Errorf("read config: %w", err)
- }
-
- if err := yaml.Unmarshal(b, &cfg); err != nil {
- return Config{}, fmt.Errorf("parse config: %w", err)
+ if !errors.Is(err, os.ErrNotExist) {
+ return Config{}, fmt.Errorf("read config: %w", err)
+ }
+ } else {
+ if err := yaml.Unmarshal(b, &cfg); err != nil {
+ return Config{}, fmt.Errorf("parse config: %w", err)
+ }
}
}
diff --git a/internal/config/config_load_test.go b/internal/config/config_load_test.go
new file mode 100644
index 0000000..516dd93
--- /dev/null
+++ b/internal/config/config_load_test.go
@@ -0,0 +1,82 @@
+package config
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+)
+
+func TestLoadMissingConfigFileFallsBackToDefaults(t *testing.T) {
+ cfg, err := Load(filepath.Join(t.TempDir(), "missing.yaml"))
+ if err != nil {
+ t.Fatalf("load missing config: %v", err)
+ }
+
+ def := Default()
+ if cfg.Mode != def.Mode {
+ t.Fatalf("mode mismatch: got %q want %q", cfg.Mode, def.Mode)
+ }
+ if cfg.VerifyPolicy != def.VerifyPolicy {
+ t.Fatalf("verify policy mismatch: got %q want %q", cfg.VerifyPolicy, def.VerifyPolicy)
+ }
+ if cfg.CheckpointInterval != def.CheckpointInterval {
+ t.Fatalf("checkpoint interval mismatch: got %d want %d", cfg.CheckpointInterval, def.CheckpointInterval)
+ }
+ if cfg.VerifyWorkers != def.VerifyWorkers {
+ t.Fatalf("verify workers mismatch: got %d want %d", cfg.VerifyWorkers, def.VerifyWorkers)
+ }
+}
+
+func TestLoadPartialConfigRetainsDefaultValues(t *testing.T) {
+ configPath := filepath.Join(t.TempDir(), "partial.yaml")
+ content := []byte("mode: thin\nverify: full\n")
+
+ if err := os.WriteFile(configPath, content, 0o644); err != nil {
+ t.Fatalf("write partial config: %v", err)
+ }
+
+ cfg, err := Load(configPath)
+ if err != nil {
+ t.Fatalf("load partial config: %v", err)
+ }
+
+ if cfg.Mode != ModeThin {
+ t.Fatalf("mode mismatch: got %q want %q", cfg.Mode, ModeThin)
+ }
+ if cfg.VerifyPolicy != VerifyFull {
+ t.Fatalf("verify policy mismatch: got %q want %q", cfg.VerifyPolicy, VerifyFull)
+ }
+
+ def := Default()
+ if cfg.PLCSource != def.PLCSource {
+ t.Fatalf("plc_source mismatch: got %q want %q", cfg.PLCSource, def.PLCSource)
+ }
+ if cfg.RequestTimeout != 10*time.Second {
+ t.Fatalf("request_timeout mismatch: got %s want 10s", cfg.RequestTimeout)
+ }
+ if cfg.CommitBatchSize != def.CommitBatchSize {
+ t.Fatalf("commit_batch_size mismatch: got %d want %d", cfg.CommitBatchSize, def.CommitBatchSize)
+ }
+}
+
+func TestLoadMissingConfigFileAppliesEnvOverrides(t *testing.T) {
+ t.Setenv("PLUTIA_MODE", ModeThin)
+ t.Setenv("PLUTIA_VERIFY", VerifyFull)
+ t.Setenv("PLUTIA_REQUEST_TIMEOUT", "15s")
+
+ cfg, err := Load(filepath.Join(t.TempDir(), "missing.yaml"))
+ if err != nil {
+ t.Fatalf("load missing config with env overrides: %v", err)
+ }
+
+ if cfg.Mode != ModeThin {
+ t.Fatalf("mode mismatch: got %q want %q", cfg.Mode, ModeThin)
+ }
+ if cfg.VerifyPolicy != VerifyFull {
+ t.Fatalf("verify policy mismatch: got %q want %q", cfg.VerifyPolicy, VerifyFull)
+ }
+ if cfg.RequestTimeout != 15*time.Second {
+ t.Fatalf("request_timeout mismatch: got %s want 15s", cfg.RequestTimeout)
+ }
+}