From 77208756d84cd770d9f1adb61bbe36cf03feca68 Mon Sep 17 00:00:00 2001 From: Mike Horgan Date: Fri, 1 May 2026 12:45:50 -0400 Subject: [PATCH] add crc64-nvme support for aws checksum path Allow staged CRC64 rollout by enabling crc64-nvme in CLI and s3-aws while failing fast for unsupported netty/rdma driver selections. --- cli/README.md | 2 +- cli/cmd/envdefaults.go | 2 +- cli/cmd/run.go | 9 ++-- cli/cmd/run_scenario_test.go | 54 +++++++++++++++++++ cli/docs/SPT_SYNTAX.md | 4 +- cli/internal/scenario/constants.go | 2 + cli/internal/scenario/defaults_test.go | 26 +++++++++ cli/internal/scenario/types.go | 2 +- .../implementations/s3-aws/README.md | 2 +- .../coop/aws/s3/S3AwsStorageDriver.java | 2 + .../coop/aws/s3/S3AwsStorageDriverTest.java | 33 ++++++++++++ 11 files changed, 129 insertions(+), 9 deletions(-) diff --git a/cli/README.md b/cli/README.md index 889571be..4b1785c2 100644 --- a/cli/README.md +++ b/cli/README.md @@ -359,7 +359,7 @@ Executes a benchmark test with the specified workload type. - `--threads, -t`: Number of parallel client threads (default: 1) - `--object-size, -o`: Size of each object (e.g., 1MB, 256KB, 4GB) - `--part-size`: Enable S3 multipart upload with the given part size (e.g., 5MB, 64MB). When set, `load.batch.size` is forced to `1`. The engine automatically retries individual parts (up to 3 times) and aborts incomplete uploads on failure. Per-part checksums are applied when checksum is enabled. See [`SPT_SYNTAX.md`](docs/SPT_SYNTAX.md) for details -- `--checksum`: Enable S3 checksum validation with the specified algorithm: `crc32`, `crc32c`, `sha1`, `sha256`. When used with `--part-size`, checksums are applied per part. (env: `SPT_CHECKSUM`) +- `--checksum`: Enable S3 checksum validation with the specified algorithm: `crc32`, `crc32c`, `sha1`, `sha256`, `crc64-nvme`. When used with `--part-size`, checksums are applied per part. `crc64-nvme` currently requires `--s3-driver aws`. (env: `SPT_CHECKSUM`) - `--object-data-compressibility`: Target compressibility percentage for generated object data, 0-100 (default: 0 = fully random). Each 4KB chunk is split into random and zero-filled portions according to the percentage. (env: `SPT_OBJECT_DATA_COMPRESSIBILITY`) - `--object-data-dedupable`: Whether generated data remains dedupe-friendly (default: true). Set `false` to stamp every 4KB with a unique object-id + offset header that defeats inline deduplication. Incompatible with `--items-file` / file-based data input. (env: `SPT_OBJECT_DATA_DEDUPABLE`) - `--cleanup`: Delete all created objects after test completion diff --git a/cli/cmd/envdefaults.go b/cli/cmd/envdefaults.go index db16ba48..b5827cf4 100644 --- a/cli/cmd/envdefaults.go +++ b/cli/cmd/envdefaults.go @@ -157,7 +157,7 @@ func applyEnvDefaultsToRunFlags(cmd *cobra.Command) error { } } - // Checksum algorithm: SPT_CHECKSUM env var (values: crc32, crc32c, sha1, sha256) + // Checksum algorithm: SPT_CHECKSUM env var (values: crc32, crc32c, sha1, sha256, crc64-nvme) _ = setIf("checksum", constants.EnvChecksum) // Object data shaping diff --git a/cli/cmd/run.go b/cli/cmd/run.go index b9569160..fb6f6037 100644 --- a/cli/cmd/run.go +++ b/cli/cmd/run.go @@ -929,7 +929,7 @@ Shorthand: --use-rdma is equivalent to --s3-driver rdma. (env: SPT_S3_DRIVER)`) // Checksum Options runCmd.Flags().String("checksum", "", - `Enable checksum validation with the specified algorithm: crc32, crc32c, sha1, sha256. + `Enable checksum validation with the specified algorithm: crc32, crc32c, sha1, sha256, crc64-nvme. Omit to disable checksums. (env: SPT_CHECKSUM)`) // S3 Tables Options @@ -1169,10 +1169,13 @@ func buildScenarioParams(workloadType string, cmd *cobra.Command) (scenario.Para checksumAlgo = strings.ToLower(strings.TrimSpace(checksumAlgo)) switch checksumAlgo { case scenario.ChecksumCRC32, scenario.ChecksumCRC32C, - scenario.ChecksumSHA1, scenario.ChecksumSHA256: + scenario.ChecksumSHA1, scenario.ChecksumSHA256, scenario.ChecksumCRC64NVME: // valid default: - return params, fmt.Errorf("invalid --checksum value %q: must be one of: crc32, crc32c, sha1, sha256", checksumAlgo) + return params, fmt.Errorf("invalid --checksum value %q: must be one of: crc32, crc32c, sha1, sha256, crc64-nvme", checksumAlgo) + } + if checksumAlgo == scenario.ChecksumCRC64NVME && s3Driver != scenario.S3DriverAws { + return params, fmt.Errorf("invalid --checksum value %q for --s3-driver %q: crc64-nvme requires --s3-driver aws", checksumAlgo, s3Driver) } params.Checksum = checksumAlgo } diff --git a/cli/cmd/run_scenario_test.go b/cli/cmd/run_scenario_test.go index 85342e8a..fefbb9cd 100644 --- a/cli/cmd/run_scenario_test.go +++ b/cli/cmd/run_scenario_test.go @@ -455,6 +455,7 @@ func TestBuildScenarioParams(t *testing.T) { cmd.Flags().String("rdma-device", "auto", "") cmd.Flags().String("rdma-log-level", "WARN", "") cmd.Flags().Int64("rdma-timeout-ms", 30000, "") + cmd.Flags().String("checksum", "", "") cmd.Flags().Float64("object-data-compressibility", 0.0, "") cmd.Flags().Bool("object-data-dedupable", true, "") @@ -576,6 +577,7 @@ func TestBuildScenarioParamsEdgeCases(t *testing.T) { cmd.Flags().String("rdma-device", "auto", "") cmd.Flags().String("rdma-log-level", "WARN", "") cmd.Flags().Int64("rdma-timeout-ms", 30000, "") + cmd.Flags().String("checksum", "", "") cmd.Flags().Float64("object-data-compressibility", 0.0, "") cmd.Flags().Bool("object-data-dedupable", true, "") @@ -627,6 +629,7 @@ func TestBuildScenarioParamsServiceThreads(t *testing.T) { cmd.Flags().String("rdma-device", "auto", "") cmd.Flags().String("rdma-log-level", "WARN", "") cmd.Flags().Int64("rdma-timeout-ms", 30000, "") + cmd.Flags().String("checksum", "", "") _ = cmd.Flags().Set("service-threads", fmt.Sprintf("%d", tt.value)) @@ -666,6 +669,7 @@ func TestBuildScenarioParams_S3DriverFlag(t *testing.T) { cmd.Flags().String("rdma-device", "auto", "") cmd.Flags().String("rdma-log-level", "WARN", "") cmd.Flags().Int64("rdma-timeout-ms", 30000, "") + cmd.Flags().String("checksum", "", "") cmd.Flags().Float64("object-data-compressibility", 0.0, "") cmd.Flags().Bool("object-data-dedupable", true, "") return cmd @@ -730,6 +734,56 @@ func TestBuildScenarioParams_S3DriverFlag(t *testing.T) { } }) + t.Run("--checksum crc64-nvme with --s3-driver aws is accepted", func(t *testing.T) { + cmd := newCmd() + _ = cmd.Flags().Set("s3-driver", "aws") + _ = cmd.Flags().Set("checksum", "crc64-nvme") + p, err := buildScenarioParams("mock", cmd) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if p.Checksum != "crc64-nvme" { + t.Errorf("Checksum = %q, want %q", p.Checksum, "crc64-nvme") + } + }) + + t.Run("--checksum crc64-nvme with default driver fails fast", func(t *testing.T) { + cmd := newCmd() + _ = cmd.Flags().Set("checksum", "crc64-nvme") + _, err := buildScenarioParams("mock", cmd) + if err == nil { + t.Fatal("expected validation error") + } + if !strings.Contains(err.Error(), "requires --s3-driver aws") { + t.Errorf("expected aws conflict message, got: %v", err) + } + }) + + t.Run("--checksum crc64-nvme with rdma fails fast", func(t *testing.T) { + cmd := newCmd() + _ = cmd.Flags().Set("s3-driver", "rdma") + _ = cmd.Flags().Set("checksum", "crc64-nvme") + _, err := buildScenarioParams("mock", cmd) + if err == nil { + t.Fatal("expected validation error") + } + if !strings.Contains(err.Error(), "requires --s3-driver aws") { + t.Errorf("expected aws conflict message, got: %v", err) + } + }) + + t.Run("invalid --checksum value lists crc64-nvme", func(t *testing.T) { + cmd := newCmd() + _ = cmd.Flags().Set("checksum", "blake3") + _, err := buildScenarioParams("mock", cmd) + if err == nil { + t.Fatal("expected validation error") + } + if !strings.Contains(err.Error(), "crc64-nvme") { + t.Errorf("expected crc64-nvme in validation list, got: %v", err) + } + }) + t.Run("object data knobs are parsed", func(t *testing.T) { cmd := newCmd() _ = cmd.Flags().Set("object-data-compressibility", "75") diff --git a/cli/docs/SPT_SYNTAX.md b/cli/docs/SPT_SYNTAX.md index 109cc38f..be1a34a3 100644 --- a/cli/docs/SPT_SYNTAX.md +++ b/cli/docs/SPT_SYNTAX.md @@ -40,7 +40,7 @@ You can use these variables to avoid repeating sensitive or commonly used parame - **Docker:** `SPT_SKIP_IMAGE_PULL` (skip pulling the engine image) - **Engine tuning:** `SPT_SERVICE_THREADS` (virtual-thread carrier parallelism) - **Multipart upload:** `SPT_PART_SIZE` (part size, e.g. `64MB`) -- **Checksum:** `SPT_CHECKSUM` (algorithm: `crc32`, `crc32c`, `sha1`, `sha256`) +- **Checksum:** `SPT_CHECKSUM` (algorithm: `crc32`, `crc32c`, `sha1`, `sha256`, `crc64-nvme`; `crc64-nvme` requires `SPT_S3_DRIVER=aws` or `--s3-driver aws`) - **Data shaping:** `SPT_OBJECT_DATA_COMPRESSIBILITY` (0-100, default 0), `SPT_OBJECT_DATA_DEDUPABLE` (true/false, default true) - **Storage driver:** `SPT_S3_DRIVER` (driver backend: `default`, `aws`, `rdma`) - **RDMA:** `SPT_RDMA_ENABLED`, `RDMA_LOCAL_IP`, `RDMA_DEVICE`, `RDMA_LOG_LEVEL`, `RDMA_THRESHOLD_BYTES`, `RDMA_TIMEOUT_MS`, `RDMA_FALLBACK_ENABLED` @@ -97,7 +97,7 @@ Required for S3 workloads, optional/ignored for `mock`. | `--object-count` | `-n` | `0` | Fixed number of objects to process | | `--duration` | `-d` | `""` | Fixed time duration (e.g., `5m`, `1h`) | | `--seed-objects` | | `2500` | Objects to pre-create for `read` benchmarks | -| `--checksum` | | `""` | Enable S3 checksum validation with the specified algorithm: `crc32`, `crc32c`, `sha1`, `sha256`. Omit to disable checksums. When set with `--part-size`, checksums are applied per part. (env: `SPT_CHECKSUM`) | +| `--checksum` | | `""` | Enable S3 checksum validation with the specified algorithm: `crc32`, `crc32c`, `sha1`, `sha256`, `crc64-nvme`. Omit to disable checksums. When set with `--part-size`, checksums are applied per part. `crc64-nvme` currently requires `--s3-driver aws`. (env: `SPT_CHECKSUM`) | | `--object-data-compressibility` | | `0` | Target compressibility percentage for generated object data (0-100). Each 4KB chunk is split into random and zero-filled portions. 0 = fully random, 100 = fully compressible. (env: `SPT_OBJECT_DATA_COMPRESSIBILITY`) | | `--object-data-dedupable` | | `true` | Whether generated data remains dedupe-friendly. Set `false` to stamp every 4KB with a 16-byte object-id + offset header that practically eliminates inline deduplication. Incompatible with file-based data input. (env: `SPT_OBJECT_DATA_DEDUPABLE`) | | `--save-items` | | `false` | Save `items.csv` listing created objects (`write` only) | diff --git a/cli/internal/scenario/constants.go b/cli/internal/scenario/constants.go index 2d860b11..a0041071 100644 --- a/cli/internal/scenario/constants.go +++ b/cli/internal/scenario/constants.go @@ -30,6 +30,8 @@ const ( ChecksumSHA1 = "sha1" // ChecksumSHA256 selects SHA-256 checksum validation. ChecksumSHA256 = "sha256" + // ChecksumCRC64NVME selects CRC64-NVME checksum validation. + ChecksumCRC64NVME = "crc64-nvme" itemTypeData = "data" itemTypePath = "path" diff --git a/cli/internal/scenario/defaults_test.go b/cli/internal/scenario/defaults_test.go index a5934db2..a0a7a4bc 100644 --- a/cli/internal/scenario/defaults_test.go +++ b/cli/internal/scenario/defaults_test.go @@ -967,6 +967,32 @@ func TestGenerateDefaults(t *testing.T) { } }, }, + { + name: "checksum crc64-nvme", + params: Params{ + WorkloadType: "write", + Endpoint: "http://s3.example.com", + AccessKey: "key", + SecretKey: "secret", + Bucket: "bucket", + Threads: 4, + Checksum: "crc64-nvme", + }, + wantErr: false, + checkOutput: func(t *testing.T, data []byte) { + t.Helper() + var config DefaultsConfig + if err := yaml.Unmarshal(data, &config); err != nil { + t.Fatalf("Failed to unmarshal YAML: %v", err) + } + if config.Storage.Checksum == nil { + t.Fatal("Expected storage.checksum section") + } + if config.Storage.Checksum.Algorithm != "crc64-nvme" { + t.Errorf("Expected algorithm 'crc64-nvme', got %q", config.Storage.Checksum.Algorithm) + } + }, + }, { name: "no checksum omits checksum section", params: Params{ diff --git a/cli/internal/scenario/types.go b/cli/internal/scenario/types.go index c13ab9b8..01219993 100644 --- a/cli/internal/scenario/types.go +++ b/cli/internal/scenario/types.go @@ -42,7 +42,7 @@ type Params struct { RdmaTimeoutMs int64 // RDMA operation timeout (default: 30000) // Checksum validation - Checksum string // Checksum algorithm: crc32, crc32c, sha1, sha256 (empty = disabled) + Checksum string // Checksum algorithm: crc32, crc32c, sha1, sha256, crc64-nvme (empty = disabled) ObjectDataCompressibility float64 // Object data compressibility percentage [0..100] ObjectDataDedupable bool // True keeps repeating ring-buffer data dedupe-friendly; false enables anti-dedupe stamping diff --git a/engine/extensions/storage-drivers/implementations/s3-aws/README.md b/engine/extensions/storage-drivers/implementations/s3-aws/README.md index 2e31621c..2e97d2a0 100644 --- a/engine/extensions/storage-drivers/implementations/s3-aws/README.md +++ b/engine/extensions/storage-drivers/implementations/s3-aws/README.md @@ -160,7 +160,7 @@ S3StorageDriver driver = S3StorageDriverFactory.create(config); | connectionTimeout | int | 10000 | Connection timeout in milliseconds | | enableRequestMetrics | boolean | false | Enable AWS SDK request metrics | | checksumEnabled | boolean | false | Compute and send a checksum on write requests | -| checksumAlgorithm | String | null | Checksum algorithm: `crc32`, `crc32c`, `sha1`, `sha256`. MD5 is not supported as a flexible checksum by the AWS SDK | +| checksumAlgorithm | String | null | Checksum algorithm: `crc32`, `crc32c`, `sha1`, `sha256`, `crc64-nvme`. MD5 is not supported as a flexible checksum by the AWS SDK | ## API Methods diff --git a/engine/extensions/storage-drivers/implementations/s3-aws/src/main/java/com/dell/spt/storage/driver/coop/aws/s3/S3AwsStorageDriver.java b/engine/extensions/storage-drivers/implementations/s3-aws/src/main/java/com/dell/spt/storage/driver/coop/aws/s3/S3AwsStorageDriver.java index 0af6197a..39cac462 100644 --- a/engine/extensions/storage-drivers/implementations/s3-aws/src/main/java/com/dell/spt/storage/driver/coop/aws/s3/S3AwsStorageDriver.java +++ b/engine/extensions/storage-drivers/implementations/s3-aws/src/main/java/com/dell/spt/storage/driver/coop/aws/s3/S3AwsStorageDriver.java @@ -194,6 +194,8 @@ static ChecksumAlgorithm resolveChecksumAlgorithm(final String algorithm) { return ChecksumAlgorithm.SHA1; case "sha256": return ChecksumAlgorithm.SHA256; + case "crc64-nvme": + return ChecksumAlgorithm.CRC64_NVME; default: return null; } diff --git a/engine/extensions/storage-drivers/implementations/s3-aws/src/test/java/com/dell/spt/storage/driver/coop/aws/s3/S3AwsStorageDriverTest.java b/engine/extensions/storage-drivers/implementations/s3-aws/src/test/java/com/dell/spt/storage/driver/coop/aws/s3/S3AwsStorageDriverTest.java index fe365eb7..ab2c6dfa 100644 --- a/engine/extensions/storage-drivers/implementations/s3-aws/src/test/java/com/dell/spt/storage/driver/coop/aws/s3/S3AwsStorageDriverTest.java +++ b/engine/extensions/storage-drivers/implementations/s3-aws/src/test/java/com/dell/spt/storage/driver/coop/aws/s3/S3AwsStorageDriverTest.java @@ -1510,12 +1510,18 @@ void sha256() { assertEquals(ChecksumAlgorithm.SHA256, S3AwsStorageDriver.resolveChecksumAlgorithm("sha256")); } + @Test + void crc64Nvme() { + assertEquals(ChecksumAlgorithm.CRC64_NVME, S3AwsStorageDriver.resolveChecksumAlgorithm("crc64-nvme")); + } + @Test void caseInsensitive() { assertEquals(ChecksumAlgorithm.CRC32, S3AwsStorageDriver.resolveChecksumAlgorithm("CRC32")); assertEquals(ChecksumAlgorithm.CRC32_C, S3AwsStorageDriver.resolveChecksumAlgorithm("CRC32C")); assertEquals(ChecksumAlgorithm.SHA1, S3AwsStorageDriver.resolveChecksumAlgorithm("SHA1")); assertEquals(ChecksumAlgorithm.SHA256, S3AwsStorageDriver.resolveChecksumAlgorithm("SHA256")); + assertEquals(ChecksumAlgorithm.CRC64_NVME, S3AwsStorageDriver.resolveChecksumAlgorithm("CRC64-NVME")); } @Test @@ -1610,6 +1616,33 @@ void initiateMultipartUpload_includesChecksumWhenEnabled() throws Exception { assertEquals(ChecksumAlgorithm.CRC32, cap.getValue().checksumAlgorithm()); } + @SuppressWarnings("unchecked") + @Test + void initiateMultipartUpload_includesCrc64NvmeWhenEnabled() throws Exception { + setChecksumFields(drv, true, ChecksumAlgorithm.CRC64_NVME); + + CreateMultipartUploadResponse mockResponse = CreateMultipartUploadResponse.builder() + .uploadId("test-upload-id-crc64") + .build(); + + when(mockS3Client.createMultipartUpload(any(CreateMultipartUploadRequest.class))) + .thenReturn(CompletableFuture.completedFuture(mockResponse)); + + @SuppressWarnings("rawtypes") + CompositeDataOperation compositeOp = mock(CompositeDataOperation.class); + DataItem item = mock(DataItem.class); + when(compositeOp.item()).thenReturn(item); + when(compositeOp.type()).thenReturn(OpType.CREATE); + when(compositeOp.dstPath()).thenReturn("/test-bucket"); + when(item.name()).thenReturn("test-key"); + + drv.execute((Operation) compositeOp).join(); + + ArgumentCaptor cap = ArgumentCaptor.forClass(CreateMultipartUploadRequest.class); + verify(mockS3Client).createMultipartUpload(cap.capture()); + assertEquals(ChecksumAlgorithm.CRC64_NVME, cap.getValue().checksumAlgorithm()); + } + @SuppressWarnings("unchecked") @Disabled("Requires complex setup with real CompositeDataOperation/PartialDataOperation instances - tested through integration") @Test