Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,6 @@ known_hosts

# Stray native artifacts (not part of this Go operator)
chdb.hpp

# Linked git worktrees
.worktrees/
3 changes: 2 additions & 1 deletion api/v1alpha1/hyperbytedbcluster_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`

Expand Down
7 changes: 2 additions & 5 deletions api/v1alpha1/hyperbytedbcluster_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 2 additions & 3 deletions api/v1alpha1/hyperbytedbcluster_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 6 additions & 1 deletion internal/hyperbytedb/configmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
57 changes: 57 additions & 0 deletions internal/hyperbytedb/configmap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,63 @@ 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)
}
}

// 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{
Expand Down
2 changes: 1 addition & 1 deletion internal/hyperbytedb/statefulset.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading