Merge remote-tracking branch 'upstream/main' into feat/base-service-delivery
# Conflicts: # server/.env.example # server/internal/config/config.go # server/internal/service/delivery.go # server/internal/service/delivery_test.go
This commit is contained in:
@@ -24,6 +24,7 @@ import (
|
||||
)
|
||||
|
||||
var dnsLabelPattern = regexp.MustCompile(`^[a-z0-9](?:[-a-z0-9]*[a-z0-9])?$`)
|
||||
var hostNamePattern = regexp.MustCompile(`^[a-zA-Z0-9](?:[-a-zA-Z0-9.]*[a-zA-Z0-9])?$`)
|
||||
|
||||
type MySQLDeliveryInput struct {
|
||||
BusinessLineID uint64 `json:"business_line_id" binding:"required"`
|
||||
@@ -34,9 +35,10 @@ type MySQLDeliveryInput struct {
|
||||
Topology string `json:"topology"`
|
||||
MySQLPort int `json:"mysql_port"`
|
||||
DataDisk string `json:"data_disk"`
|
||||
CPUCores int64 `json:"cpu_cores" binding:"required"`
|
||||
MemoryGB int64 `json:"memory_gb" binding:"required"`
|
||||
StorageGB int64 `json:"storage_gb" binding:"required"`
|
||||
TargetHost string `json:"target_host"`
|
||||
CPUCores int64 `json:"cpu_cores"`
|
||||
MemoryGB int64 `json:"memory_gb"`
|
||||
StorageGB int64 `json:"storage_gb"`
|
||||
ParamTemplate string `json:"param_template"`
|
||||
TimeZone string `json:"timezone"`
|
||||
LowerCaseTableNames int `json:"lower_case_table_names"`
|
||||
@@ -50,9 +52,9 @@ type MySQLDeliveryInput struct {
|
||||
LongQueryTime float64 `json:"long_query_time"`
|
||||
BinlogExpireLogsSeconds int64 `json:"binlog_expire_logs_seconds"`
|
||||
MaxBinlogSize string `json:"max_binlog_size"`
|
||||
CPUMilli int64 `json:"-"`
|
||||
MemoryMi int64 `json:"-"`
|
||||
StorageGi int64 `json:"-"`
|
||||
CPUMilli int64 `json:"cpu_milli"`
|
||||
MemoryMi int64 `json:"memory_mi"`
|
||||
StorageGi int64 `json:"storage_gi"`
|
||||
}
|
||||
|
||||
type deliveryPayload struct {
|
||||
@@ -118,31 +120,51 @@ func parseTargetMetadata(raw string) targetMetadata {
|
||||
return meta
|
||||
}
|
||||
|
||||
// firstFreeHost 返回候选池中第一个未被占用的节点。
|
||||
func firstFreeHost(hosts []targetHost, occupied []string) *targetHost {
|
||||
taken := make(map[string]bool, len(occupied))
|
||||
for _, h := range occupied {
|
||||
taken[h] = true
|
||||
// firstFreeHost 返回候选池中非失败任务数未达单机实例上限的第一个节点。
|
||||
func firstFreeHost(hosts []targetHost, occupied []string, limit int) *targetHost {
|
||||
if limit < 1 {
|
||||
limit = 1
|
||||
}
|
||||
for i := range hosts {
|
||||
if !taken[hosts[i].Name] {
|
||||
if hostTaskCount(occupied, hosts[i].Name) < limit {
|
||||
return &hosts[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func firstFreePort(occupied []int) int {
|
||||
taken := make(map[int]bool, len(occupied))
|
||||
for _, port := range occupied {
|
||||
taken[port] = true
|
||||
}
|
||||
for port := 13306; port <= 13999; port++ {
|
||||
if !taken[port] {
|
||||
return port
|
||||
func hostTaskCount(occupied []string, name string) int {
|
||||
count := 0
|
||||
for _, h := range occupied {
|
||||
if h == name {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return 0
|
||||
return count
|
||||
}
|
||||
|
||||
const (
|
||||
mysqlPortPoolStart = 13306
|
||||
mysqlPortPoolEnd = 13999
|
||||
)
|
||||
|
||||
func allocatePort(requested int, used []int) (int, error) {
|
||||
taken := make(map[int]bool, len(used))
|
||||
for _, p := range used {
|
||||
taken[p] = true
|
||||
}
|
||||
if requested != 0 {
|
||||
if taken[requested] {
|
||||
return 0, fmt.Errorf("mysql_port %d is already allocated on the target host", requested)
|
||||
}
|
||||
return requested, nil
|
||||
}
|
||||
for p := mysqlPortPoolStart; p <= mysqlPortPoolEnd; p++ {
|
||||
if !taken[p] {
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
return 0, fmt.Errorf("mysql port pool %d-%d is exhausted on the target host", mysqlPortPoolStart, mysqlPortPoolEnd)
|
||||
}
|
||||
|
||||
type DeliveryService struct {
|
||||
@@ -228,8 +250,11 @@ func (s *DeliveryService) CreateTask(ctx context.Context, userID uint64, isAdmin
|
||||
if idempotencyKey == "" || len(idempotencyKey) > 128 {
|
||||
return nil, false, fmt.Errorf("Idempotency-Key header is required and must not exceed 128 characters")
|
||||
}
|
||||
if input.DataDisk == "" && len(s.cfg.DeliveryDataDisks) > 0 {
|
||||
input.DataDisk = s.cfg.DeliveryDataDisks[0]
|
||||
}
|
||||
normalizeMySQLDeliveryInput(&input)
|
||||
if err := validateDeliveryInput(input); err != nil {
|
||||
if err := validateDeliveryInput(input, s.cfg.DeliveryDataDisks); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
@@ -290,9 +315,8 @@ func (s *DeliveryService) CreateTask(ctx context.Context, userID uint64, isAdmin
|
||||
return &task, false, nil
|
||||
}
|
||||
|
||||
var supportedMySQLVersions = map[string]bool{"8.0": true}
|
||||
var supportedMySQLVersions = map[string]bool{"8.0": true, "8.4": true}
|
||||
var supportedMySQLTopologies = map[string]bool{"standalone": true}
|
||||
var supportedDataDisks = map[string]bool{"/data": true, "/disk1": true, "/mnt/vol-1": true}
|
||||
var supportedParamTemplates = map[string]bool{"default": true, "high_performance": true, "high_safety": true}
|
||||
var supportedCharacterSets = map[string]bool{"utf8mb4": true, "utf8": true, "gbk": true, "latin1": true}
|
||||
var supportedCollations = map[string]bool{
|
||||
@@ -301,6 +325,7 @@ var supportedCollations = map[string]bool{
|
||||
}
|
||||
var supportedRedoLogCapacity = map[string]bool{"auto": true, "128M": true, "256M": true, "512M": true, "1G": true}
|
||||
var supportedMaxBinlogSize = map[string]bool{"128M": true, "256M": true, "512M": true, "1G": true}
|
||||
var timezonePattern = regexp.MustCompile(`^([+-](0\d|1[0-4]):[0-5]\d|SYSTEM|[A-Za-z]+(?:/[A-Za-z0-9_+-]+)+)$`)
|
||||
|
||||
func normalizeMySQLDeliveryInput(input *MySQLDeliveryInput) {
|
||||
if input.MySQLVersion == "" {
|
||||
@@ -348,9 +373,15 @@ func normalizeMySQLDeliveryInput(input *MySQLDeliveryInput) {
|
||||
if input.MaxBinlogSize == "" {
|
||||
input.MaxBinlogSize = "256M"
|
||||
}
|
||||
input.CPUMilli = input.CPUCores * 1000
|
||||
input.MemoryMi = input.MemoryGB * 1024
|
||||
input.StorageGi = input.StorageGB
|
||||
if input.CPUMilli == 0 && input.CPUCores != 0 {
|
||||
input.CPUMilli = input.CPUCores * 1000
|
||||
}
|
||||
if input.MemoryMi == 0 && input.MemoryGB != 0 {
|
||||
input.MemoryMi = input.MemoryGB * 1024
|
||||
}
|
||||
if input.StorageGi == 0 && input.StorageGB != 0 {
|
||||
input.StorageGi = input.StorageGB
|
||||
}
|
||||
}
|
||||
|
||||
func defaultCollation(characterSet string) string {
|
||||
@@ -366,38 +397,48 @@ func defaultCollation(characterSet string) string {
|
||||
}
|
||||
}
|
||||
|
||||
func validateDeliveryInput(input MySQLDeliveryInput) error {
|
||||
func validateDeliveryInput(input MySQLDeliveryInput, dataDisks []string) error {
|
||||
if len(input.Namespace) > 63 || !dnsLabelPattern.MatchString(input.Namespace) {
|
||||
return fmt.Errorf("namespace must be a valid Kubernetes DNS label")
|
||||
}
|
||||
if len(input.InstanceName) > 63 || !dnsLabelPattern.MatchString(input.InstanceName) {
|
||||
return fmt.Errorf("instance_name must be a valid Kubernetes DNS label")
|
||||
}
|
||||
if !oneOfInt64(input.CPUCores, []int64{1, 2, 4, 8, 16}) {
|
||||
return fmt.Errorf("cpu_cores must be one of 1, 2, 4, 8, 16")
|
||||
}
|
||||
if !oneOfInt64(input.MemoryGB, []int64{2, 4, 8, 16, 32, 64}) {
|
||||
return fmt.Errorf("memory_gb must be one of 2, 4, 8, 16, 32, 64")
|
||||
}
|
||||
if input.StorageGB < 20 || input.StorageGB > 2000 {
|
||||
return fmt.Errorf("storage_gb must be between 20 and 2000")
|
||||
if input.CPUMilli < 100 || input.CPUMilli > 64000 || input.MemoryMi < 2048 || input.MemoryMi > 65536 || input.StorageGi < 20 || input.StorageGi > 2000 {
|
||||
return fmt.Errorf("requested resources are outside the supported range (cpu: 100-64000m, memory: 2048-65536 MiB, storage: 20-2000 GiB)")
|
||||
}
|
||||
if input.MySQLVersion != "" && !supportedMySQLVersions[input.MySQLVersion] {
|
||||
return fmt.Errorf("unsupported mysql_version %q, supported: 8.0", input.MySQLVersion)
|
||||
return fmt.Errorf("unsupported mysql_version %q, supported: 8.0, 8.4 (5.6/5.7 are EOL and have no Ubuntu 24.04 packages)", input.MySQLVersion)
|
||||
}
|
||||
if input.Topology != "" && !supportedMySQLTopologies[input.Topology] {
|
||||
return fmt.Errorf("unsupported topology %q, supported: standalone", input.Topology)
|
||||
}
|
||||
if input.MySQLPort != 0 && (input.MySQLPort < 13306 || input.MySQLPort > 13999) {
|
||||
return fmt.Errorf("mysql_port must be empty for auto assignment or between 13306 and 13999")
|
||||
if input.MySQLPort != 0 && (input.MySQLPort < mysqlPortPoolStart || input.MySQLPort > mysqlPortPoolEnd) {
|
||||
return fmt.Errorf("mysql_port must be empty for auto assignment or between %d and %d", mysqlPortPoolStart, mysqlPortPoolEnd)
|
||||
}
|
||||
if !supportedDataDisks[input.DataDisk] {
|
||||
return fmt.Errorf("unsupported data_disk %q", input.DataDisk)
|
||||
if input.DataDisk != "" {
|
||||
allowed := dataDisks
|
||||
if len(allowed) == 0 {
|
||||
allowed = []string{"/data"}
|
||||
}
|
||||
found := false
|
||||
for _, disk := range allowed {
|
||||
if input.DataDisk == disk {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("data_disk %q is not in the allowed mount point list %v", input.DataDisk, allowed)
|
||||
}
|
||||
}
|
||||
if input.TargetHost != "" && (len(input.TargetHost) > 253 || !hostNamePattern.MatchString(input.TargetHost)) {
|
||||
return fmt.Errorf("target_host must be a valid inventory host name")
|
||||
}
|
||||
if !supportedParamTemplates[input.ParamTemplate] {
|
||||
return fmt.Errorf("unsupported param_template %q", input.ParamTemplate)
|
||||
}
|
||||
if !validTimeZone(input.TimeZone) {
|
||||
if !timezonePattern.MatchString(input.TimeZone) {
|
||||
return fmt.Errorf("unsupported timezone %q", input.TimeZone)
|
||||
}
|
||||
if input.LowerCaseTableNames != 0 && input.LowerCaseTableNames != 1 {
|
||||
@@ -593,20 +634,42 @@ func (s *DeliveryService) claimAndReserve(ctx context.Context) (*model.DeliveryT
|
||||
if err := tx.Model(&model.DeliveryTask{}).Where("target_id = ? AND target_host <> ? AND status NOT IN ?", task.TargetID, "", occupiedExclude).Pluck("target_host", &occupied).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
host := firstFreeHost(meta.Hosts, occupied)
|
||||
limit := s.cfg.DeliveryHostInstanceLimit
|
||||
var host *targetHost
|
||||
if payload.TargetHost != "" {
|
||||
for i := range meta.Hosts {
|
||||
if meta.Hosts[i].Name == payload.TargetHost {
|
||||
host = &meta.Hosts[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if host == nil {
|
||||
return s.failInTransaction(tx, &task, model.TaskValidationFailed, fmt.Sprintf("target_host %q is not in the candidate host pool", payload.TargetHost))
|
||||
}
|
||||
effectiveLimit := limit
|
||||
if effectiveLimit < 1 {
|
||||
effectiveLimit = 1
|
||||
}
|
||||
if hostTaskCount(occupied, host.Name) >= effectiveLimit {
|
||||
return fmt.Errorf("defer: pinned host %s reached the per-host instance limit %d", host.Name, effectiveLimit)
|
||||
}
|
||||
} else {
|
||||
host = firstFreeHost(meta.Hosts, occupied, limit)
|
||||
}
|
||||
if host == nil {
|
||||
return fmt.Errorf("defer: no free host available on target")
|
||||
}
|
||||
mysqlPort := payload.MySQLPort
|
||||
if mysqlPort == 0 {
|
||||
var occupiedPorts []int
|
||||
if err := tx.Model(&model.DeliveryTask{}).Where("target_id = ? AND target_host = ? AND mysql_port <> ? AND status NOT IN ?", task.TargetID, host.Name, 0, occupiedExclude).Pluck("mysql_port", &occupiedPorts).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
mysqlPort = firstFreePort(occupiedPorts)
|
||||
if mysqlPort == 0 {
|
||||
return fmt.Errorf("defer: no free MySQL port available on target host")
|
||||
}
|
||||
var usedPorts []int
|
||||
if err := tx.Model(&model.DeliveryTask{}).Where("target_id = ? AND target_host = ? AND mysql_port <> ? AND status NOT IN ?", task.TargetID, host.Name, 0, occupiedExclude).Pluck("mysql_port", &usedPorts).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
var instancePorts []int
|
||||
if err := tx.Model(&model.MySQLInstance{}).Where("target_id = ? AND node_name = ? AND status = ?", task.TargetID, host.Name, "active").Pluck("port", &instancePorts).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
mysqlPort, portErr := allocatePort(payload.MySQLPort, append(usedPorts, instancePorts...))
|
||||
if portErr != nil {
|
||||
return s.failInTransaction(tx, &task, model.TaskValidationFailed, portErr.Error())
|
||||
}
|
||||
reservation := model.ResourceReservation{TaskID: task.ID, BusinessLineID: task.BusinessLineID, TargetID: task.TargetID, CPUMilli: payload.CPUMilli, MemoryMi: payload.MemoryMi, StorageGi: payload.StorageGi, InstanceCount: 1, Status: "reserved", ExpiresAt: time.Now().Add(time.Duration(s.cfg.ReservationTTLMinutes) * time.Minute)}
|
||||
if err := tx.Create(&reservation).Error; err != nil {
|
||||
@@ -771,8 +834,10 @@ func (s *DeliveryService) CreateExecution(ctx context.Context, taskID, payloadHa
|
||||
"instance_name": payload.InstanceName, "mysql_port": task.MySQLPort,
|
||||
"data_disk": payload.DataDisk, "cpu_cores": payload.CPUCores,
|
||||
"memory_gb": payload.MemoryGB, "storage_gb": payload.StorageGB,
|
||||
"cpu_milli": payload.CPUMilli, "memory_mi": payload.MemoryMi, "storage_gi": payload.StorageGi,
|
||||
"mysql_version": payload.MySQLVersion, "param_template": payload.ParamTemplate,
|
||||
"timezone": payload.TimeZone, "lower_case_table_names": payload.LowerCaseTableNames,
|
||||
"target_host": payload.TargetHost,
|
||||
"timezone": payload.TimeZone, "lower_case_table_names": payload.LowerCaseTableNames,
|
||||
"character_set": payload.CharacterSet, "collation": payload.Collation,
|
||||
"max_connections": payload.MaxConnections, "innodb_redo_log_capacity": payload.InnoDBRedoLogCapacity,
|
||||
"innodb_flush_log_at_trx_commit": payload.InnoDBFlushLogAtTrxCommit, "sync_binlog": payload.SyncBinlog,
|
||||
|
||||
@@ -3,59 +3,123 @@ package service
|
||||
import "testing"
|
||||
|
||||
func TestValidateDeliveryInput(t *testing.T) {
|
||||
dataDisks := []string{"/data", "/disk1"}
|
||||
valid := MySQLDeliveryInput{BusinessLineID: 1, TargetID: 1, Namespace: "team-a", InstanceName: "mysql-01", CPUCores: 2, MemoryGB: 4, StorageGB: 50}
|
||||
normalizeMySQLDeliveryInput(&valid)
|
||||
if err := validateDeliveryInput(valid); err != nil {
|
||||
if err := validateDeliveryInput(valid, dataDisks); err != nil {
|
||||
t.Fatalf("valid input rejected: %v", err)
|
||||
}
|
||||
withVersion := valid
|
||||
withVersion.MySQLVersion = "8.0"
|
||||
if err := validateDeliveryInput(withVersion); err != nil {
|
||||
t.Fatalf("valid input with version 8.0 rejected: %v", err)
|
||||
full := valid
|
||||
full.MySQLVersion = "8.0"
|
||||
full.Topology = "standalone"
|
||||
full.MySQLPort = 13306
|
||||
full.DataDisk = "/disk1"
|
||||
full.TargetHost = "k8s-server-03"
|
||||
full.CPUMilli = 2000
|
||||
full.MemoryMi = 8192
|
||||
full.StorageGi = 2000
|
||||
full.TimeZone = "+08:00"
|
||||
full.LowerCaseTableNames = 0
|
||||
full.CharacterSet = "utf8mb4"
|
||||
full.Collation = "utf8mb4_general_ci"
|
||||
full.MaxConnections = "auto"
|
||||
full.InnoDBRedoLogCapacity = "256M"
|
||||
full.InnoDBFlushLogAtTrxCommit = 2
|
||||
full.SyncBinlog = 0
|
||||
full.InnoDBIOCapacity = 2000
|
||||
full.LongQueryTime = 0.5
|
||||
full.BinlogExpireLogsSeconds = 604800
|
||||
full.MaxBinlogSize = "512M"
|
||||
if err := validateDeliveryInput(full, dataDisks); err != nil {
|
||||
t.Fatalf("valid full input rejected: %v", err)
|
||||
}
|
||||
for name, input := range map[string]MySQLDeliveryInput{
|
||||
"uppercase namespace": valid,
|
||||
"bad instance": valid,
|
||||
"bad cpu cores": valid,
|
||||
"too much memory": valid,
|
||||
"too little storage": valid,
|
||||
"too much storage": valid,
|
||||
"unsupported version": valid,
|
||||
"unsupported topology": valid,
|
||||
"bad mysql port": valid,
|
||||
"bad data disk": valid,
|
||||
"unsupported charset": valid,
|
||||
"bad collation": valid,
|
||||
namedZone := valid
|
||||
namedZone.TimeZone = "Asia/Shanghai"
|
||||
if err := validateDeliveryInput(namedZone, dataDisks); err != nil {
|
||||
t.Fatalf("named timezone rejected: %v", err)
|
||||
}
|
||||
lts := valid
|
||||
lts.MySQLVersion = "8.4"
|
||||
if err := validateDeliveryInput(lts, dataDisks); err != nil {
|
||||
t.Fatalf("8.4 LTS rejected: %v", err)
|
||||
}
|
||||
for name, mutate := range map[string]func(*MySQLDeliveryInput){
|
||||
"uppercase namespace": func(in *MySQLDeliveryInput) { in.Namespace = "Team-A" },
|
||||
"bad instance": func(in *MySQLDeliveryInput) { in.InstanceName = "mysql_01" },
|
||||
"too little cpu": func(in *MySQLDeliveryInput) { in.CPUMilli = 50 },
|
||||
"too much cpu": func(in *MySQLDeliveryInput) { in.CPUMilli = 65000 },
|
||||
"too little memory": func(in *MySQLDeliveryInput) { in.MemoryMi = 1024 },
|
||||
"too much memory": func(in *MySQLDeliveryInput) { in.MemoryMi = 131072 },
|
||||
"too little storage": func(in *MySQLDeliveryInput) { in.StorageGi = 10 },
|
||||
"too much storage": func(in *MySQLDeliveryInput) { in.StorageGi = 4000 },
|
||||
"unsupported version": func(in *MySQLDeliveryInput) { in.MySQLVersion = "5.7" },
|
||||
"eol version": func(in *MySQLDeliveryInput) { in.MySQLVersion = "5.6" },
|
||||
"unsupported topology": func(in *MySQLDeliveryInput) { in.Topology = "mgr_3" },
|
||||
"port below pool": func(in *MySQLDeliveryInput) { in.MySQLPort = 3307 },
|
||||
"port above pool": func(in *MySQLDeliveryInput) { in.MySQLPort = 14000 },
|
||||
"data disk not in list": func(in *MySQLDeliveryInput) { in.DataDisk = "/mnt/other" },
|
||||
"bad target host": func(in *MySQLDeliveryInput) { in.TargetHost = "-bad-host" },
|
||||
"bad timezone": func(in *MySQLDeliveryInput) { in.TimeZone = "UTC+8" },
|
||||
"bad lower case": func(in *MySQLDeliveryInput) { in.LowerCaseTableNames = 2 },
|
||||
"bad charset": func(in *MySQLDeliveryInput) { in.CharacterSet = "big5" },
|
||||
"collation mismatch": func(in *MySQLDeliveryInput) { in.CharacterSet = "gbk"; in.Collation = "utf8mb4_general_ci" },
|
||||
"bad max connections": func(in *MySQLDeliveryInput) { in.MaxConnections = "300" },
|
||||
"bad redo capacity": func(in *MySQLDeliveryInput) { in.InnoDBRedoLogCapacity = "2G" },
|
||||
"bad flush log": func(in *MySQLDeliveryInput) { in.InnoDBFlushLogAtTrxCommit = 3 },
|
||||
"bad sync binlog": func(in *MySQLDeliveryInput) { in.SyncBinlog = 2 },
|
||||
"bad io capacity": func(in *MySQLDeliveryInput) { in.InnoDBIOCapacity = 500 },
|
||||
"bad long query time": func(in *MySQLDeliveryInput) { in.LongQueryTime = 3 },
|
||||
"bad binlog expire": func(in *MySQLDeliveryInput) { in.BinlogExpireLogsSeconds = 3600 },
|
||||
"bad max binlog size": func(in *MySQLDeliveryInput) { in.MaxBinlogSize = "64M" },
|
||||
} {
|
||||
switch name {
|
||||
case "uppercase namespace":
|
||||
input.Namespace = "Team-A"
|
||||
case "bad instance":
|
||||
input.InstanceName = "mysql_01"
|
||||
case "bad cpu cores":
|
||||
input.CPUCores = 3
|
||||
case "too much memory":
|
||||
input.MemoryGB = 128
|
||||
case "too little storage":
|
||||
input.StorageGB = 10
|
||||
case "too much storage":
|
||||
input.StorageGB = 3000
|
||||
case "unsupported version":
|
||||
input.MySQLVersion = "5.7"
|
||||
case "unsupported topology":
|
||||
input.Topology = "mgr"
|
||||
case "bad mysql port":
|
||||
input.MySQLPort = 3306
|
||||
case "bad data disk":
|
||||
input.DataDisk = "/"
|
||||
case "unsupported charset":
|
||||
input.CharacterSet = "sjis"
|
||||
case "bad collation":
|
||||
input.CharacterSet = "utf8"
|
||||
input.Collation = "utf8mb4_general_ci"
|
||||
}
|
||||
if err := validateDeliveryInput(input); err == nil {
|
||||
input := valid
|
||||
mutate(&input)
|
||||
if err := validateDeliveryInput(input, dataDisks); err == nil {
|
||||
t.Errorf("%s was accepted", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFirstFreeHost(t *testing.T) {
|
||||
hosts := []targetHost{{Name: "node-a"}, {Name: "node-b"}}
|
||||
if h := firstFreeHost(hosts, nil, 1); h == nil || h.Name != "node-a" {
|
||||
t.Fatalf("expected node-a on empty occupancy, got %+v", h)
|
||||
}
|
||||
if h := firstFreeHost(hosts, []string{"node-a"}, 1); h == nil || h.Name != "node-b" {
|
||||
t.Fatalf("expected node-b when node-a is full at limit 1, got %+v", h)
|
||||
}
|
||||
if h := firstFreeHost(hosts, []string{"node-a", "node-b"}, 1); h != nil {
|
||||
t.Fatalf("limit 1 with all hosts taken should return nil, got %+v", h)
|
||||
}
|
||||
if h := firstFreeHost(hosts, []string{"node-a", "node-b"}, 2); h == nil || h.Name != "node-a" {
|
||||
t.Fatalf("expected node-a for second round at limit 2, got %+v", h)
|
||||
}
|
||||
if h := firstFreeHost(hosts, []string{"node-a", "node-a", "node-b", "node-b"}, 2); h != nil {
|
||||
t.Fatalf("limit 2 with all hosts saturated should return nil, got %+v", h)
|
||||
}
|
||||
if h := firstFreeHost(hosts, []string{"node-a"}, 0); h == nil || h.Name != "node-b" {
|
||||
t.Fatalf("limit 0 should degrade to 1, got %+v", h)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllocatePort(t *testing.T) {
|
||||
if port, err := allocatePort(0, nil); err != nil || port != mysqlPortPoolStart {
|
||||
t.Fatalf("expected first pool port %d, got %d err=%v", mysqlPortPoolStart, port, err)
|
||||
}
|
||||
if port, err := allocatePort(0, []int{13306, 13307}); err != nil || port != 13308 {
|
||||
t.Fatalf("expected 13308 skipping occupied, got %d err=%v", port, err)
|
||||
}
|
||||
if port, err := allocatePort(13400, []int{13306}); err != nil || port != 13400 {
|
||||
t.Fatalf("expected requested port 13400, got %d err=%v", port, err)
|
||||
}
|
||||
if _, err := allocatePort(13306, []int{13306}); err == nil {
|
||||
t.Fatal("requested occupied port was accepted")
|
||||
}
|
||||
used := make([]int, 0, mysqlPortPoolEnd-mysqlPortPoolStart+1)
|
||||
for p := mysqlPortPoolStart; p <= mysqlPortPoolEnd; p++ {
|
||||
used = append(used, p)
|
||||
}
|
||||
if _, err := allocatePort(0, used); err == nil {
|
||||
t.Fatal("exhausted pool still allocated a port")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user