This project maintains a set of conventions to keep contributions consistent and maintainable.
LVMSync is a high-performance tool for streaming LVM snapshots across hosts. The codebase targets
production reliability, favoring small, single-purpose components with clear interfaces. Each
function should do one thing well, include dedicated tests, and rely on dependency injection where
practical. Break down large functions into focused helpers and test each helper individually.
Logging and configuration are fully structured to keep behavior predictable across
command-line use, environment variables, and config.yaml files.
- Avoid package-level function variables for stubbing dependencies.
- Use a
Runnerstruct to hold external interactions and provideNewRunnerandNewRunnerWithDepsconstructors so tests can inject mocks. - Callers and subcommands should invoke methods on a
Runnerinstance rather than modifying global variables. - Privilege checks rely on a
privilege.Escalatorinterface.cmd/root.ConfigureWithEscalatoraccepts anEscalatorso tests can stub privilege escalation without invoking realsudo.
DetectorchestratesdetectFileDevice,detectLVMDevice, anddetectRawDevice.- Each helper includes dedicated tests for success and error paths.
- Non-root runs escalate LVM operations using
--lvm-escalation(defaultsudo -n). The command is validated at startup and detection fails if escalation is unavailable.
- Use zap for structured logging.
- Zap is the sole logging backend; avoid
logorfmt.Print*for progress output. - Always flush logs using the
SyncLoggerhelper (e.g.,defer rootcmd.SyncLogger(logger)) sologger.Sync()errors are recorded. - Transport constructors must accept a
*zap.Logger; avoid package-level loggers. - Pass loggers explicitly to commands and helpers; do not use
zap.L()or other globals. - Use
zap.NewNop()instead ofnilwhen no logging is needed. - Constructors and other exported functions must require a non-nil logger; callers
must always supply one (use
zap.NewNop()to disable logging). - Log connection lifecycle events and errors with
snake_casefields including units (e.g.,bytes_transferred,duration_ms). - Do not log secrets or authentication tokens; scrub sensitive values before emitting them.
- Callers using transports should
defer rootcmd.SyncLogger(logger)to ensure logs are flushed.
TLSVersionStringreturns"unknown"or the numeric TLS version when the value is unrecognized to avoid empty strings in logs.
- Use
snake_casefor all field keys. - Include the unit in the name when relevant:
resource_idfor identifiersduration_msfor durations in millisecondssize_bytesfor sizes
Debugfor verbose details useful during development.Infofor lifecycle events and high-level progress.Warnfor unexpected situations that do not stop execution.Errorfor failures that require user action.- Avoid
Fatalin libraries; return an error instead.
logger.Info("snapshot complete",
zap.String("resource_id", snapshotID),
zap.Int64("duration_ms", time.Since(start).Milliseconds()),
)- Prefer
pflagfor flag parsing. - Bind flags to
viperto support configuration from flags, config files, and environment variables. - Group related options into
FlagSets to share common configuration across commands. - Define flags within
pflag.FlagSets and bind them toviper; the standard libraryflagpackage is not used. - Expose configuration via both config files and environment variables for easy automation.
- New flag groups must include tests demonstrating configuration precedence.
--fs-freeze-commandand--fs-thaw-commandvalues are split with shell-style quoting; tests should cover paths with spaces and README examples must quote such arguments.
func initConfig() (*viper.Viper, string, string) {
v := viper.New()
general := pflag.NewFlagSet("general", pflag.ExitOnError)
general.Bool("progress", true, "show progress")
pflag.CommandLine.AddFlagSet(general)
v.BindPFlags(pflag.CommandLine)
v.SetConfigName("config")
v.AddConfigPath(".")
v.SetEnvPrefix("LVMSYNC")
v.AutomaticEnv()
pflag.Parse()
args := pflag.Args()
if len(args) < 2 {
pflag.Usage()
os.Exit(1)
}
return v, args[0], args[1]
}Example invocation:
lvmsync /dev/vg0/snap0 /mnt/backupExample usage selecting transports and ports:
lvmsync --transport ssh,tcp+tls,h2,quic --tcp-port 9443BDP-based autotuning keeps roughly one to two times the bandwidth–delay product
in flight. Override the autotuned value with --concurrency.
- TLS 1.3 with mutual authentication is required by default;
AllowInsecureis for development only.--allow-insecure(defaults to false).
The throughput preset favors maximal transfer rates:
- transport order
ssh,tcp+tls,h2,quic - concurrency
8 - hybrid dedup with 2 MiB fixed chunks and CDC range 256 KiB–8 MiB
- compression
auto - enables
--odirect --sync-interval=1 GiB,--checkpoint-bytes=1 GiB,--checkpoint-interval=10s- QUIC congestion control
bbr
- Sample 8 KiB from each chunk to estimate the compression ratio.
- Skip compression when the ratio is greater than or equal to
--compress-threshold. - Auto mode selects LZ4 for chunks under 256 KiB and Zstd for larger chunks on CPUs with AVX2 or NEON support.
- Choose the algorithm with
--compress {auto|lz4|zstd|none}and tune levels using--zstd-level 1..5or--lz4-level {fast|hc}.
Example configuration:
lvmsync --compress auto --zstd-level 2 --compress-threshold 0.85- Prefer CPU-accelerated implementations such as
github.com/zeebo/blake3for hashing andgithub.com/klauspost/compressfor Zstd and LZ4 compression. - Choose libraries that leverage vector instructions when available to maximize throughput.
- Register transports via
transport.Registerusing short, descriptive names. - Each transport must implement
SenderandReceiverinterfaces and include an integration test. - Avoid global state; pass configuration and loggers explicitly.
- Dedup strategies should expose pure functions and persist state deterministically.
- Provide tests for Bloom filter sizing and CDC window boundaries.
- Document tunables
--cdc-min,--cdc-avg,--cdc-max, and--bloom-mbits. - Verify configuration precedence (flags > env vars > config file) and handle invalid YAML or value parse errors.
- Detect CPU features before selecting algorithms.
- Sample 8 KiB per chunk to estimate ratios and honour
--compress-threshold. - Benchmarks must cover algorithm choice and cache resets.
- Clients perform a handshake sending
sector_size,alignment,max_concurrency, and dedup/compression support. - Sessions exchange ephemeral X.509 certificates and a pre-shared key.
- Resume bitmaps are streamed with a session ID to continue interrupted transfers.
- Each session maintains a bidirectional
Ackstream for pings and acknowledgements. - Finalization requests close the session by ID.
- Resume state files record the BLAKE3 digest of the last CDC chunk.
- Checkpoint the resume file every 1 GiB of raw data or every 10 s.
- Exchange final manifests containing chunk hashes and a final SHA-256 digest to verify completion.
- Ensure
golangci-lintv2 passes before submitting changes.
- Run
go test -cover ./...to generate coverage statistics. - For a file
coverage.out, view details withgo tool cover -func=coverage.out. - Ensure overall test coverage remains at or above 50%; CI enforces this threshold and fails if the total coverage falls below it.
- Every function must have a dedicated unit test.
- Cover both successful and failing paths to verify correctness.
- Where external commands would normally execute, inject test hooks (e.g.,
the
privilegepackage acceptsexec.Commandsubstitutes) to stub side effects during tests.
- Where external commands would normally execute, inject test hooks (e.g.,
the
- The snapshot monitoring goroutine closes its error channel on exit; cleanup
must only cancel monitoring.
TestCreateSnapshotCleanupNoPanicverifies this behavior.
- Keep packages and functions focused on a single task.
- Break up large files or components when functionality grows.
- Document every CLI flag, environment variable, and configuration option in
README.md. - Update examples and default values to match new options.
- Deduplication modes (
fixed,cdc,hybrid) expose tunables--cdc-min,--cdc-avg,--cdc-max, and Bloom filter sizing with--bloom-mbitsfor the mmap-backed index. - Keep transport sections and flag-to-env tables (QUIC, HTTP/2, TCP+TLS, SSH) in sync with code changes.
-
Tags follow
vX.Y.Zsemantic versioning. -
Pushing a tag triggers
.github/workflows/release.ymlwhich builds and publishes artifacts. -
Release binaries omit the
rsynctransport unless built withGOFLAGS='-tags rsync'. -
Each release must include a
CHANGELOG.mdentry using the Keep a Changelog format:## [vX.Y.Z] - YYYY-MM-DD ### Added - ... ### Fixed - ...
-
Every pull request must update the
[Unreleased]section inCHANGELOG.mdwith entries underAddedorFixed.
- Follow the guidelines in
CONTRIBUTING.md. - Commit messages must follow the Conventional Commits format:
type(scope): description.
Run these commands locally before opening a pull request:
go build ./...go test -coverprofile=coverage.out ./...golangci-lint rungo tool cover -func=coverage.outand ensure total coverage is at least 50%- Include unit tests covering success and failure paths for new functionality.
- Use
zapfor all structured logging and callrootcmd.SyncLogger(logger)on shutdown. - Parse configuration with
pflagandviper; expose every option via CLI flags,LVMSYNC_*environment variables, and theconfig.yamlfile. - Group related CLI options into dedicated
FlagSets with clear descriptions. - Keep packages and functions single-purpose and inject dependencies for testability.
- Provide unit tests for every function, covering success and error paths.
- Run
go build ./...,go test -cover ./..., andgolangci-lint runbefore merging. - Document new flags, environment variables, and configuration options in
README.md.
- Track known issues using GitHub issues rather than repository files.
- When a gap is discovered:
- Open a new issue describing the problem and any missing test coverage.
- Optionally add a
TODO(#issue)comment in the code referencing the issue number. - Address the gap in a follow-up change and close the issue once tests pass.
- Remote command validation uses a precompiled regex
^[a-zA-Z0-9._-]+$(remoteCmdReinremote/remote.go). Maintain this pattern when checking remote commands.
-
Audit logging: ensure
zapis used withsnake_casefields, include units, and callrootcmd.SyncLogger(logger)before exit (block size logs now usesize_bytes). -
Remove package-wide loggers; transport constructors such as
ssh.New(cfg, logger)now require an explicit*zap.Loggerparameter. -
Replace any remaining
fmt.Print*calls with structuredzaplogs to keep progress output fully structured. -
device: expand mountinfo parsing to handle bind mounts and multiple entries.
-
Review the QUIC transport constructor and expand tests for both sender and receiver paths.
-
LVM device support: plumb snapshot creation through the device abstraction, allow raw device fallbacks, and unit test LVM vs. file paths.
-
Transport logging: emit connection handshake and teardown events with
snake_casefields and ensure callersdefer rootcmd.SyncLogger(logger). -
Manifest rebuild: add a subcommand to regenerate chunk digests when manifests are missing or out of date, exercising rebuild logic in tests.
-
Verify command: compare source and destination devices against manifest entries and surface mismatched digests with structured logs.
-
Manifest: return error when block size is 0 during creation.
-
Expand unit test coverage for remote execution and client signal handling, and run coverage reports (transports coverage ≥50%).
go test -cover ./... -
Run lint checks to keep style and correctness issues from creeping in for transports.
golangci-lint run-
Add precedence tests for
ssh_host_key_pathflag, environment variable, and YAML configuration (seeinternal/config/ssh_host_key_path_test.go). -
Ensure SSH agent connections use context timeouts and cover SSHManager reuse in tests.
-
common: add endianness mismatch handshake validation test.
-
Add privilege escalation tests covering success and error paths (tests require root; skipped otherwise).
-
Expand coverage for configuration precedence across flags, environment variables, and config files.
-
Keep README configuration examples and precedence tests in sync.
-
Keep modules single-purpose; maintain the
transferpackage decomposition (progress.go,handshake.go,block_writer.go,resume.go,worker.go,writer.go,apply.go,transfer.go). Future contributions should keep files small and focused. -
Keep
READMEconfiguration documentation current with code changes. -
Keep transport documentation and configuration references (flags and env vars) up to date.
-
Track decomposition of large files like
transfer/transfer.go. -
Ensure progress logging uses
zapexclusively. -
Ensure every new function has a dedicated unit test.
-
Run
go build ./...,go test -cover ./..., andgolangci-lint runbefore merging changes. -
Maintain tests for compression detection, ensuring benchmark and cache logic remain correct.
-
Add end-to-end tests for resume and verify workflows.
-
Document flag grouping patterns and expand coverage for pflag/viper bindings.
-
Maintain tests for buffer alignment, hole punching, and NUMA pinning.
-
Enforce modular, single-responsibility design across packages.
-
Document each new CLI flag, environment variable, and configuration option in
README.md. -
Refactor
main.gointo smaller modules. -
cmd/dumphandles snapshot dumping and transport selection, receiving configuration and loggers explicitly. -
cmd/rootconfigures the application and wirescmd/dump. -
Verify configuration precedence.
-
Maintain tests for dedup configuration precedence and error paths (invalid YAML, parse errors).
-
Keep README dedup configuration options in sync with code.
-
Document feature changes in
README.md. -
Implement full transport registry with working QUIC, HTTP/2, TCP+TLS, and SSH backends and accompanying tests.
-
Finalize hybrid deduplication and document CDC tuning knobs.
-
Integrate adaptive compression sampling with configurable thresholds and unit benchmarks.
-
Fail fast when unsupported transports are requested and document
--transportas reserved. -
Add validation for CDC chunker parameters and corresponding tests.
- Covered by
config/validation.gotests.
- Covered by
-
Ensure all commands default to a non-nil
zap.Logger(usezap.NewNop()), eliminating nil checks.
- Introduce pluggable data plane with transport registry supporting QUIC, HTTP/2, TLS/TCP, and SSH.
- Add hybrid deduplication combining fixed-size and content-defined chunking (FastCDC).
- Implement adaptive compression with CPU feature detection and per-chunk sampling.
- Optimize transfer pipeline for concurrency autotuning, large in-flight windows, and efficient I/O paths.
- Provide
FlagSet-grouped CLI options bound to Viper, with parity across flags, environment variables, and config files. - Ensure each new function includes unit tests and documentation updates in README.md.
- Maintain modular, single-responsibility design to ease future maintenance.
- Document file layout, naming, and single-purpose design for new packages.
- Include unit tests for success and failure paths.
- Run
go build ./...,go test -coverprofile=coverage.out ./..., andgolangci-lint runbefore submitting patches.
- Use zap for structured logging with snake_case fields.
- Call
rootcmd.SyncLogger(logger)on shutdown and avoidfmt.Print*for progress output.