From 44fdeb5b2a7b3be48a2cfa56bed85bbe49abe030 Mon Sep 17 00:00:00 2001 From: Austin Barrington Date: Fri, 4 Sep 2026 22:53:22 +0100 Subject: [PATCH 1/3] Allow sharding on a one-replica cluster. A CR with replicas=1 and sharding.enabled now writes [cluster] enabled=true instead of being rejected or running cluster-off. --- .gitignore | 3 +++ api/v1alpha1/hyperbytedbcluster_types.go | 3 ++- api/v1alpha1/hyperbytedbcluster_webhook.go | 7 ++--- .../hyperbytedbcluster_webhook_test.go | 5 ++-- internal/hyperbytedb/configmap.go | 7 ++++- internal/hyperbytedb/configmap_test.go | 26 +++++++++++++++++++ internal/hyperbytedb/statefulset.go | 2 +- 7 files changed, 42 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 2215a37..e17a3ce 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,6 @@ known_hosts # Stray native artifacts (not part of this Go operator) chdb.hpp + +# Linked git worktrees +.worktrees/ diff --git a/api/v1alpha1/hyperbytedbcluster_types.go b/api/v1alpha1/hyperbytedbcluster_types.go index 14c2c22..ee235c4 100644 --- a/api/v1alpha1/hyperbytedbcluster_types.go +++ b/api/v1alpha1/hyperbytedbcluster_types.go @@ -77,7 +77,8 @@ type HyperbytedbClusterSpec struct { Cluster ClusterTuningSpec `json:"cluster,omitempty"` // Experimental series sharding. When set, the operator writes a `[sharding]` - // block into config.toml. Enabling it requires replicas > 1 (cluster mode). + // block into config.toml. A 1-replica CR with sharding.enabled is a + // 1-member cluster ([cluster] enabled=true). Defaults stay off. // +optional Sharding *ShardingSpec `json:"sharding,omitempty"` diff --git a/api/v1alpha1/hyperbytedbcluster_webhook.go b/api/v1alpha1/hyperbytedbcluster_webhook.go index ce3b4a7..33db06f 100644 --- a/api/v1alpha1/hyperbytedbcluster_webhook.go +++ b/api/v1alpha1/hyperbytedbcluster_webhook.go @@ -150,21 +150,18 @@ func validateCluster(cluster *HyperbytedbCluster) (admission.Warnings, error) { warnings = append(warnings, "2-node clusters cannot tolerate any node failure; consider 3+ replicas") } - if err := validateSharding(cluster, replicas); err != nil { + if err := validateSharding(cluster); err != nil { return warnings, err } return warnings, nil } -func validateSharding(cluster *HyperbytedbCluster, replicas int32) error { +func validateSharding(cluster *HyperbytedbCluster) error { s := cluster.Spec.Sharding if s == nil { return nil } - if s.Enabled && replicas < 2 { - return fmt.Errorf("sharding.enabled requires replicas > 1 (cluster mode)") - } if s.RegionMergeSeries > 0 && s.RegionSplitSeries > 0 && s.RegionMergeSeries >= s.RegionSplitSeries { return fmt.Errorf("sharding.regionMergeSeries (%d) must be < regionSplitSeries (%d)", s.RegionMergeSeries, s.RegionSplitSeries) diff --git a/api/v1alpha1/hyperbytedbcluster_webhook_test.go b/api/v1alpha1/hyperbytedbcluster_webhook_test.go index ececc13..7c4b89f 100644 --- a/api/v1alpha1/hyperbytedbcluster_webhook_test.go +++ b/api/v1alpha1/hyperbytedbcluster_webhook_test.go @@ -18,10 +18,9 @@ func TestValidateSharding(t *testing.T) { }{ {name: "nil sharding", replicas: 1}, { - name: "enabled requires cluster", + name: "enabled on single replica is a one-member cluster", replicas: 1, sharding: &ShardingSpec{Enabled: true}, - wantErr: "replicas > 1", }, { name: "enabled on 3 replicas", @@ -74,7 +73,7 @@ func TestValidateSharding(t *testing.T) { Sharding: tt.sharding, }, } - err := validateSharding(cluster, tt.replicas) + err := validateSharding(cluster) if tt.wantErr == "" { if err != nil { t.Fatalf("unexpected error: %v", err) diff --git a/internal/hyperbytedb/configmap.go b/internal/hyperbytedb/configmap.go index 32cdc07..4763cb6 100644 --- a/internal/hyperbytedb/configmap.go +++ b/internal/hyperbytedb/configmap.go @@ -58,7 +58,12 @@ func clusterMetadataEnabled(cluster *v1alpha1.HyperbytedbCluster) bool { if cluster.Spec.Replicas != nil { replicas = *cluster.Spec.Replicas } - return replicas > 1 + if replicas > 1 { + return true + } + // A 1-replica CR with explicit sharding is a 1-member cluster, not + // cluster-off. Defaults stay off when sharding is omitted or disabled. + return cluster.Spec.Sharding != nil && cluster.Spec.Sharding.Enabled } func renderConfigTOMLWithClusterEnabled(cluster *v1alpha1.HyperbytedbCluster, clusterEnabled bool) string { diff --git a/internal/hyperbytedb/configmap_test.go b/internal/hyperbytedb/configmap_test.go index bac0e7a..ae17d2f 100644 --- a/internal/hyperbytedb/configmap_test.go +++ b/internal/hyperbytedb/configmap_test.go @@ -110,6 +110,32 @@ func TestConfigHash_ignoresReplicaCount(t *testing.T) { } } +func TestRenderConfigTOML_oneReplicaExplicitShardingIsCluster(t *testing.T) { + cluster := &v1alpha1.HyperbytedbCluster{ + Spec: v1alpha1.HyperbytedbClusterSpec{ + Replicas: ptr.To(int32(1)), + Sharding: &v1alpha1.ShardingSpec{Enabled: true}, + }, + } + out := renderConfigTOML(cluster) + if !strings.Contains(out, "[cluster]") { + t.Fatal("expected [cluster] section") + } + // First `enabled = true` in the file is [cluster] (sharding follows). + clusterIdx := strings.Index(out, "[cluster]") + shardIdx := strings.Index(out, "[sharding]") + if clusterIdx < 0 || shardIdx < 0 || shardIdx < clusterIdx { + t.Fatalf("expected [cluster] then [sharding]\n%s", out) + } + clusterBlock := out[clusterIdx:shardIdx] + if !strings.Contains(clusterBlock, "enabled = true") { + t.Fatalf("1-replica explicit sharding must set [cluster] enabled=true\n%s", clusterBlock) + } + if !strings.Contains(out[shardIdx:], "enabled = true") { + t.Fatalf("expected sharding.enabled=true\n%s", out) + } +} + func TestRenderConfigTOML_sharding(t *testing.T) { cluster := &v1alpha1.HyperbytedbCluster{ Spec: v1alpha1.HyperbytedbClusterSpec{ diff --git a/internal/hyperbytedb/statefulset.go b/internal/hyperbytedb/statefulset.go index 6643c24..2250bb0 100644 --- a/internal/hyperbytedb/statefulset.go +++ b/internal/hyperbytedb/statefulset.go @@ -36,7 +36,7 @@ func BuildStatefulSet(cluster *v1alpha1.HyperbytedbCluster, configHash string) * replicas = *cluster.Spec.Replicas } port := serverPort(cluster) - clusterEnabled := replicas > 1 + clusterEnabled := clusterMetadataEnabled(cluster) headlessSvc := HeadlessServiceName(cluster) image := ResolveHyperbytedbImage(cluster) From 5ae40aa1b006a49135adb50288f442eccea6327b Mon Sep 17 00:00:00 2001 From: Austin Barrington Date: Sun, 6 Sep 2026 08:22:12 +0100 Subject: [PATCH 2/3] Regenerate the CRD for the one-replica sharding rule (P1.7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `controller-gen` was not re-run after the `Sharding` field doc changed, so the shipped CRD still told users that enabling sharding requires replicas > 1 — the rule the previous commit removed. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019Te8YUXjjLssk3hxwoE5Db --- .../bases/hyperbytedb.hyperbyte.cloud_hyperbytedbclusters.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/crd/bases/hyperbytedb.hyperbyte.cloud_hyperbytedbclusters.yaml b/config/crd/bases/hyperbytedb.hyperbyte.cloud_hyperbytedbclusters.yaml index 64b6f6d..b76eab2 100644 --- a/config/crd/bases/hyperbytedb.hyperbyte.cloud_hyperbytedbclusters.yaml +++ b/config/crd/bases/hyperbytedb.hyperbyte.cloud_hyperbytedbclusters.yaml @@ -3592,7 +3592,8 @@ spec: sharding: description: |- Experimental series sharding. When set, the operator writes a `[sharding]` - block into config.toml. Enabling it requires replicas > 1 (cluster mode). + block into config.toml. A 1-replica CR with sharding.enabled is a + 1-member cluster ([cluster] enabled=true). Defaults stay off. properties: bootstrapTimeoutMs: description: Sync bootstrap RPC timeout. From b2a726d0a858fcb4e56550dfed2623183a404afe Mon Sep 17 00:00:00 2001 From: Austin Barrington Date: Sun, 6 Sep 2026 08:23:00 +0100 Subject: [PATCH 3/3] Cover the sharding opt-out at one replica (P1.7) Only *explicit* sharding turns a 1-replica CR into a one-member cluster. The enabling half was tested; the opt-out half was not, so a regression making `clusterMetadataEnabled` ignore the `Enabled` field would have gone unnoticed. Assert both an omitted and an explicitly disabled sharding block leave the config cluster-off. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019Te8YUXjjLssk3hxwoE5Db --- internal/hyperbytedb/configmap_test.go | 31 ++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/internal/hyperbytedb/configmap_test.go b/internal/hyperbytedb/configmap_test.go index ae17d2f..9d5e95a 100644 --- a/internal/hyperbytedb/configmap_test.go +++ b/internal/hyperbytedb/configmap_test.go @@ -136,6 +136,37 @@ func TestRenderConfigTOML_oneReplicaExplicitShardingIsCluster(t *testing.T) { } } +// The opt-out half of the one-member-cluster rule: only *explicit* sharding +// turns a 1-replica CR into a cluster. Disabling sharding must leave it +// cluster-off, exactly as omitting the block does. +func TestRenderConfigTOML_oneReplicaDisabledShardingStaysClusterOff(t *testing.T) { + for name, sharding := range map[string]*v1alpha1.ShardingSpec{ + "omitted": nil, + "disabled": {Enabled: false}, + } { + t.Run(name, func(t *testing.T) { + cluster := &v1alpha1.HyperbytedbCluster{ + Spec: v1alpha1.HyperbytedbClusterSpec{ + Replicas: ptr.To(int32(1)), + Sharding: sharding, + }, + } + out := renderConfigTOML(cluster) + clusterIdx := strings.Index(out, "[cluster]") + if clusterIdx < 0 { + t.Fatalf("expected [cluster] section\n%s", out) + } + end := len(out) + if shardIdx := strings.Index(out, "[sharding]"); shardIdx > clusterIdx { + end = shardIdx + } + if !strings.Contains(out[clusterIdx:end], "enabled = false") { + t.Fatalf("1-replica without explicit sharding must stay cluster-off\n%s", out[clusterIdx:end]) + } + }) + } +} + func TestRenderConfigTOML_sharding(t *testing.T) { cluster := &v1alpha1.HyperbytedbCluster{ Spec: v1alpha1.HyperbytedbClusterSpec{