fix(server): 显式 target_hosts 时主机指向用户指定的主节点
- 显式列表校验通过后令 host = nodes[0],task.TargetHost/IP、台账、 CloudDM 注册与凭据地址不再指向此前默认挑选的空闲主机 - 显式列表解析抽为纯函数 resolvePinnedPrimaryReplicaNodes, 行为与原内联逻辑一致(校验失败终态/单机上限 defer 重试) - 新增测试覆盖主节点非池首空闲节点及数量/池外/重复/占满场景
This commit is contained in:
@@ -335,6 +335,40 @@ func hostTaskCount(occupied []string, name string) int {
|
||||
return count
|
||||
}
|
||||
|
||||
// resolvePinnedPrimaryReplicaNodes 按用户显式指定的有序 target_hosts 解析 primary_replica 节点列表,
|
||||
// 首节点为主节点。返回值:节点列表、参数校验失败原因(终态)、延后调度错误(defer: 前缀,可重试)。
|
||||
func resolvePinnedPrimaryReplicaNodes(hosts []targetHost, requested []string, occupied []string, limit int, nodeCount int64, pinnedPrimary string) ([]*targetHost, string, error) {
|
||||
if int64(len(requested)) != nodeCount {
|
||||
return nil, fmt.Sprintf("primary_replica requires %d target_hosts, got %d", nodeCount, len(requested)), nil
|
||||
}
|
||||
nodes := make([]*targetHost, 0, len(requested))
|
||||
seen := map[string]struct{}{}
|
||||
for index, requestedHost := range requested {
|
||||
var selected *targetHost
|
||||
for i := range hosts {
|
||||
if hosts[i].Name == requestedHost {
|
||||
selected = &hosts[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if selected == nil {
|
||||
return nil, fmt.Sprintf("target_hosts[%d] %q is not in the candidate host pool", index, requestedHost), nil
|
||||
}
|
||||
if _, exists := seen[selected.Name]; exists {
|
||||
return nil, fmt.Sprintf("target_hosts contains duplicate host %q", selected.Name), nil
|
||||
}
|
||||
if hostTaskCount(occupied, selected.Name) >= max(limit, 1) {
|
||||
return nil, "", fmt.Errorf("defer: pinned host %s reached the per-host instance limit %d", selected.Name, max(limit, 1))
|
||||
}
|
||||
seen[selected.Name] = struct{}{}
|
||||
nodes = append(nodes, selected)
|
||||
}
|
||||
if pinnedPrimary != "" && nodes[0].Name != pinnedPrimary {
|
||||
return nil, "target_host must match the first primary_replica target_hosts entry", nil
|
||||
}
|
||||
return nodes, "", nil
|
||||
}
|
||||
|
||||
func topologyNodeCount(input MySQLDeliveryInput) int64 {
|
||||
if input.Topology == "primary_replica" {
|
||||
replicas := input.ReplicaCount
|
||||
@@ -1757,34 +1791,17 @@ func (s *DeliveryService) claimAndReserve(ctx context.Context) (*model.DeliveryT
|
||||
nodes := []*targetHost{host}
|
||||
if nodeCount > 1 {
|
||||
if len(payload.TargetHosts) > 0 {
|
||||
if int64(len(payload.TargetHosts)) != nodeCount {
|
||||
return s.failInTransaction(tx, &task, model.TaskValidationFailed, fmt.Sprintf("primary_replica requires %d target_hosts, got %d", nodeCount, len(payload.TargetHosts)))
|
||||
pinned, validationMsg, deferErr := resolvePinnedPrimaryReplicaNodes(meta.Hosts, payload.TargetHosts, occupied, limit, nodeCount, payload.TargetHost)
|
||||
if deferErr != nil {
|
||||
return deferErr
|
||||
}
|
||||
nodes = nodes[:0]
|
||||
seen := map[string]struct{}{}
|
||||
for index, requestedHost := range payload.TargetHosts {
|
||||
var selected *targetHost
|
||||
for i := range meta.Hosts {
|
||||
if meta.Hosts[i].Name == requestedHost {
|
||||
selected = &meta.Hosts[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if selected == nil {
|
||||
return s.failInTransaction(tx, &task, model.TaskValidationFailed, fmt.Sprintf("target_hosts[%d] %q is not in the candidate host pool", index, requestedHost))
|
||||
}
|
||||
if _, exists := seen[selected.Name]; exists {
|
||||
return s.failInTransaction(tx, &task, model.TaskValidationFailed, fmt.Sprintf("target_hosts contains duplicate host %q", selected.Name))
|
||||
}
|
||||
if hostTaskCount(occupied, selected.Name) >= max(limit, 1) {
|
||||
return fmt.Errorf("defer: pinned host %s reached the per-host instance limit %d", selected.Name, max(limit, 1))
|
||||
}
|
||||
seen[selected.Name] = struct{}{}
|
||||
nodes = append(nodes, selected)
|
||||
}
|
||||
if payload.TargetHost != "" && nodes[0].Name != payload.TargetHost {
|
||||
return s.failInTransaction(tx, &task, model.TaskValidationFailed, "target_host must match the first primary_replica target_hosts entry")
|
||||
if validationMsg != "" {
|
||||
return s.failInTransaction(tx, &task, model.TaskValidationFailed, validationMsg)
|
||||
}
|
||||
// 显式列表以首节点为主节点:覆盖此前默认挑选的空闲主机,
|
||||
// 避免 task.TargetHost/IP 与台账、凭据指向未部署节点。
|
||||
nodes = pinned
|
||||
host = nodes[0]
|
||||
} else {
|
||||
for i := range meta.Hosts {
|
||||
candidate := &meta.Hosts[i]
|
||||
|
||||
@@ -138,6 +138,35 @@ func TestFirstFreeHost(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolvePinnedPrimaryReplicaNodes(t *testing.T) {
|
||||
pool := []targetHost{{Name: "node-a", IP: "10.0.0.1"}, {Name: "node-b", IP: "10.0.0.2"}, {Name: "node-c", IP: "10.0.0.3"}}
|
||||
|
||||
// 主节点不是池首空闲节点(firstFreeHost 会选 node-a):首节点必须以用户列表为准。
|
||||
nodes, validationMsg, err := resolvePinnedPrimaryReplicaNodes(pool, []string{"node-c", "node-a"}, nil, 4, 2, "")
|
||||
if err != nil || validationMsg != "" {
|
||||
t.Fatalf("unexpected rejection: msg=%q err=%v", validationMsg, err)
|
||||
}
|
||||
if nodes[0].Name != "node-c" || nodes[0].IP != "10.0.0.3" || nodes[1].Name != "node-a" {
|
||||
t.Fatalf("primary must follow the user-specified order, got %+v", nodes)
|
||||
}
|
||||
|
||||
if _, msg, _ := resolvePinnedPrimaryReplicaNodes(pool, []string{"node-a"}, nil, 4, 2, ""); msg == "" {
|
||||
t.Fatal("host count mismatch was accepted")
|
||||
}
|
||||
if _, msg, _ := resolvePinnedPrimaryReplicaNodes(pool, []string{"node-a", "node-x"}, nil, 4, 2, ""); msg == "" {
|
||||
t.Fatal("host outside the pool was accepted")
|
||||
}
|
||||
if _, msg, _ := resolvePinnedPrimaryReplicaNodes(pool, []string{"node-a", "node-a"}, nil, 4, 2, ""); msg == "" {
|
||||
t.Fatal("duplicate host was accepted")
|
||||
}
|
||||
if _, msg, _ := resolvePinnedPrimaryReplicaNodes(pool, []string{"node-b", "node-c"}, nil, 4, 2, "node-a"); msg == "" {
|
||||
t.Fatal("target_host mismatching the first entry was accepted")
|
||||
}
|
||||
if _, _, err := resolvePinnedPrimaryReplicaNodes(pool, []string{"node-a", "node-b"}, []string{"node-b"}, 1, 2, ""); err == nil {
|
||||
t.Fatal("saturated pinned host must defer scheduling")
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user