Repository navigation
feat(cli): compact stopped datadirs with client commands #351
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
3638b34
feat(client): compact nethermind and besu with RocksDB ldb
qu0b 80034f6
feat(client): fail a geth compaction whose journal holds more than 12…
qu0b 125db09
feat(cmd): add `benchmarkoor db compact` to compact a datadir outside…
qu0b 3b028de
feat(runner): make the geth journal check opt-in in a run; on by defa…
qu0b cc9861e
feat(client): log each column family's levels and SST bytes before an…
qu0b ae87520
feat(client): compact reth with reth db copy --compact; name why ethr…
qu0b ec92d69
fix(client): reth's --datadir goes on db, not on stats/copy
qu0b 1765da1
fix(client): let mdbx_env_copy create reth's destination directory
qu0b 3df7227
fix(client): reth's compacted copy is one file; swap it in as db/mdbx…
qu0b d47c060
db: fail compaction when RocksDB properties cannot be read
qu0b f9997f6
docs: include Reth stats in compaction inspection table
qu0b d6995ba
refactor: restrict compaction CLI to existing client commands
qu0b 5b1ada3
fix(cli): reject empty compaction datadir and non-positive timeout
qu0b File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "os/signal" | ||
| "syscall" | ||
| "time" | ||
|
|
||
| "github.com/ethpandaops/benchmarkoor/pkg/client" | ||
| "github.com/ethpandaops/benchmarkoor/pkg/config" | ||
| "github.com/ethpandaops/benchmarkoor/pkg/docker" | ||
| "github.com/ethpandaops/benchmarkoor/pkg/runner" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
|
|
||
| var dbCompactFlags struct { | ||
| client string | ||
| datadir string | ||
| prepare []string | ||
| image string | ||
| extraArgs []string | ||
| timeout string | ||
| noInspect bool | ||
| } | ||
|
|
||
| var dbCmd = &cobra.Command{ | ||
| Use: "db", | ||
| Short: "Offline client database maintenance", | ||
| } | ||
|
|
||
| var dbCompactCmd = &cobra.Command{ | ||
| Use: "compact", | ||
| Short: "Compact a stopped client's datadir, as db_compaction does in a run", | ||
| Long: `Compact a stopped client's datadir using the client's own offline command. | ||
| Supported clients are geth and erigon. Use --image to select the same client | ||
| version that wrote the datadir. Other clients return an unsupported error.`, | ||
| Args: cobra.NoArgs, | ||
| RunE: runDBCompact, | ||
| } | ||
|
|
||
| func init() { | ||
| rootCmd.AddCommand(dbCmd) | ||
| dbCmd.AddCommand(dbCompactCmd) | ||
|
|
||
| f := dbCompactCmd.Flags() | ||
| f.StringVar(&dbCompactFlags.client, "client", "", "client that wrote the datadir (geth or erigon)") | ||
| f.StringVar(&dbCompactFlags.datadir, "datadir", "", "host path of the datadir; the client must be stopped") | ||
| f.StringArrayVar(&dbCompactFlags.prepare, "prepare", nil, "preparation step to run before the compaction (repeatable, e.g. erigon's seg-retire)") | ||
| f.StringVar(&dbCompactFlags.image, "image", "", "client image (default: the client's default image)") | ||
| f.StringArrayVar(&dbCompactFlags.extraArgs, "extra-arg", nil, "argument appended to the compaction command (repeatable)") | ||
| f.StringVar(&dbCompactFlags.timeout, "timeout", config.DefaultDBCompactionTimeout, "time limit for the whole compaction") | ||
| f.BoolVar(&dbCompactFlags.noInspect, "no-inspect", false, "skip the database inspection before and after") | ||
|
|
||
| _ = dbCompactCmd.MarkFlagRequired("client") | ||
| _ = dbCompactCmd.MarkFlagRequired("datadir") | ||
| } | ||
|
|
||
| func runDBCompact(cmd *cobra.Command, _ []string) error { | ||
| ct := client.ClientType(dbCompactFlags.client) | ||
| if !client.SupportsDBCompaction(ct) { | ||
| return fmt.Errorf("client %q has no offline database compaction", ct) | ||
| } | ||
|
|
||
| if dbCompactFlags.datadir == "" { | ||
| return fmt.Errorf("--datadir must not be empty") | ||
| } | ||
|
|
||
| timeout, err := time.ParseDuration(dbCompactFlags.timeout) | ||
| if err != nil { | ||
| return fmt.Errorf("--timeout: %w", err) | ||
| } | ||
|
|
||
| if timeout <= 0 { | ||
| return fmt.Errorf("--timeout must be positive") | ||
| } | ||
|
|
||
| inspect := !dbCompactFlags.noInspect | ||
| cfg := &config.DBCompactionConfig{ | ||
| Enabled: true, | ||
| Prepare: dbCompactFlags.prepare, | ||
| ExtraArgs: dbCompactFlags.extraArgs, | ||
| Timeout: dbCompactFlags.timeout, | ||
| Inspect: &inspect, | ||
| } | ||
|
|
||
| ctx, stop := signal.NotifyContext(cmd.Context(), syscall.SIGINT, syscall.SIGTERM) | ||
| defer stop() | ||
|
|
||
| mgr, err := docker.NewManager(log) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| if err := mgr.Start(ctx); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| defer func() { _ = mgr.Stop() }() | ||
|
|
||
| return runner.CompactDatadir(ctx, log, mgr, &runner.DatadirCompaction{ | ||
| Client: ct, | ||
| DataDir: dbCompactFlags.datadir, | ||
| Image: dbCompactFlags.image, | ||
| Cfg: cfg, | ||
| }) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "context" | ||
| "path/filepath" | ||
| "testing" | ||
|
|
||
| "github.com/spf13/cobra" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| func TestDBCompactRejectsInvalidInputs(t *testing.T) { | ||
| original := dbCompactFlags | ||
| t.Cleanup(func() { dbCompactFlags = original }) | ||
|
|
||
| // Invalid input must be rejected without a reachable container engine. | ||
| t.Setenv("DOCKER_HOST", "unix://"+filepath.Join(t.TempDir(), "missing.sock")) | ||
|
|
||
| for _, tc := range []struct { | ||
| name string | ||
| datadir string | ||
| timeout string | ||
| want string | ||
| }{ | ||
| {"empty datadir", "", "1m", "--datadir must not be empty"}, | ||
| {"zero timeout", t.TempDir(), "0", "--timeout must be positive"}, | ||
| {"negative timeout", t.TempDir(), "-1s", "--timeout must be positive"}, | ||
| {"malformed timeout", t.TempDir(), "invalid", "--timeout:"}, | ||
| } { | ||
| t.Run(tc.name, func(t *testing.T) { | ||
| dbCompactFlags.client = "geth" | ||
| dbCompactFlags.datadir = tc.datadir | ||
| dbCompactFlags.timeout = tc.timeout | ||
| cmd := &cobra.Command{} | ||
| cmd.SetContext(context.Background()) | ||
|
|
||
| require.ErrorContains(t, runDBCompact(cmd, nil), tc.want) | ||
| }) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| package runner | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "io" | ||
| "testing" | ||
|
|
||
| "github.com/ethpandaops/benchmarkoor/pkg/client" | ||
| "github.com/ethpandaops/benchmarkoor/pkg/config" | ||
| "github.com/ethpandaops/benchmarkoor/pkg/docker" | ||
| "github.com/sirupsen/logrus" | ||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| type cliCompactionMgr struct { | ||
| fakeDBMaintenanceMgr | ||
| pulled []string | ||
| specs []*docker.ContainerSpec | ||
| pullErr error | ||
| } | ||
|
|
||
| func (m *cliCompactionMgr) PullImage(_ context.Context, image, _ string) error { | ||
| m.pulled = append(m.pulled, image) | ||
|
|
||
| return m.pullErr | ||
| } | ||
|
|
||
| func (m *cliCompactionMgr) RunInitContainer( | ||
| ctx context.Context, spec *docker.ContainerSpec, stdout, stderr io.Writer, | ||
| ) error { | ||
| m.specs = append(m.specs, spec) | ||
|
|
||
| return m.fakeDBMaintenanceMgr.RunInitContainer(ctx, spec, stdout, stderr) | ||
| } | ||
|
|
||
| func TestCompactDatadir_ClientCommands(t *testing.T) { | ||
| for _, ct := range []client.ClientType{client.ClientGeth, client.ClientErigon} { | ||
| t.Run(string(ct), func(t *testing.T) { | ||
| dir := t.TempDir() | ||
| mgr := &cliCompactionMgr{} | ||
| cfg := &config.DBCompactionConfig{ExtraArgs: []string{"--example-option"}} | ||
| require.NoError(t, CompactDatadir(context.Background(), logrus.New(), mgr, &DatadirCompaction{ | ||
| Client: ct, DataDir: dir, Image: "client:pinned", Cfg: cfg, | ||
| })) | ||
| assert.Equal(t, []string{"client:pinned"}, mgr.pulled) | ||
| require.Len(t, mgr.specs, 3) | ||
|
|
||
| for _, spec := range mgr.specs { | ||
| assert.Equal(t, "client:pinned", spec.Image) | ||
| assert.Empty(t, spec.Entrypoint, "use the client image's entrypoint") | ||
| require.Len(t, spec.Mounts, 1) | ||
| assert.Equal(t, docker.Mount{Type: "bind", Source: dir, Target: "/data"}, spec.Mounts[0]) | ||
| } | ||
|
|
||
| assert.Equal(t, []string{"db", "compact", "--datadir=/data", "--example-option"}, mgr.specs[1].Command) | ||
| assert.NotContains(t, mgr.specs[0].Command, "--example-option") | ||
| assert.NotContains(t, mgr.specs[2].Command, "--example-option") | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestCompactDatadir_RejectsUnsupportedClients(t *testing.T) { | ||
| for _, ct := range []client.ClientType{client.ClientBesu, client.ClientNethermind, client.ClientReth, client.ClientEthrex} { | ||
| t.Run(string(ct), func(t *testing.T) { | ||
| mgr := &cliCompactionMgr{} | ||
| err := CompactDatadir(context.Background(), logrus.New(), mgr, &DatadirCompaction{Client: ct}) | ||
| require.ErrorContains(t, err, "no database compaction command") | ||
| assert.Empty(t, mgr.pulled) | ||
| assert.Empty(t, mgr.specs) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestCompactDatadir_Failures(t *testing.T) { | ||
| for _, stage := range []string{"pull", "compact"} { | ||
| t.Run(stage, func(t *testing.T) { | ||
| mgr := &cliCompactionMgr{} | ||
| if stage == "pull" { | ||
| mgr.pullErr = fmt.Errorf("image unavailable") | ||
| } else { | ||
| mgr.failCommand = "db" | ||
| } | ||
|
|
||
| inspect := false | ||
| err := CompactDatadir(context.Background(), logrus.New(), mgr, &DatadirCompaction{ | ||
| Client: client.ClientGeth, DataDir: t.TempDir(), Image: "client:pinned", | ||
| Cfg: &config.DBCompactionConfig{Inspect: &inspect}, | ||
| }) | ||
| require.Error(t, err) | ||
|
|
||
| if stage == "pull" { | ||
| require.ErrorContains(t, err, "image unavailable") | ||
| assert.Empty(t, mgr.specs) | ||
| } else { | ||
| require.ErrorContains(t, err, "exit status 1") | ||
| require.Len(t, mgr.specs, 1) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestCompactDatadir_RejectsEmptyDatadir(t *testing.T) { | ||
| mgr := &cliCompactionMgr{} | ||
| err := CompactDatadir(context.Background(), logrus.New(), mgr, &DatadirCompaction{ | ||
| Client: client.ClientGeth, Image: "client:pinned", | ||
| Cfg: &config.DBCompactionConfig{}, | ||
| }) | ||
| require.ErrorContains(t, err, "datadir must not be empty") | ||
| assert.Empty(t, mgr.pulled) | ||
| assert.Empty(t, mgr.specs) | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 empty --datadir silently compacts the working directory
--datadir=still sets the flag, so Cobra'sMarkFlagRequired("datadir")passes, andfilepath.Abs("")returns the process working directory; CompactDatadir then bind-mounts that directory at/dataand runs the client's compaction against it. Reject an empty DataDir before Abs — the run path always supplies a non-empty mount source, so only the CLI can hit this.