Skip to content

Commit c15fd70

Browse files
authored
Merge pull request #757 from marklogic/feature/20741-forests-on-one-host
MLE-20741 Zone-aware replicas created when forests on single host
2 parents 8d69d19 + 83392c4 commit c15fd70

File tree

5 files changed

+257
-210
lines changed

5 files changed

+257
-210
lines changed

ml-app-deployer/src/main/java/com/marklogic/appdeployer/command/forests/ForestReplicaPlanner.java

Lines changed: 184 additions & 169 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
import com.marklogic.mgmt.api.forest.Forest;
77
import com.marklogic.mgmt.api.forest.ForestReplica;
8+
import org.slf4j.Logger;
9+
import org.slf4j.LoggerFactory;
810

911
import java.util.ArrayList;
1012
import java.util.HashSet;
@@ -13,173 +15,186 @@
1315

1416
class ForestReplicaPlanner {
1517

16-
static class Host {
17-
String name;
18-
String zone;
19-
List<Forest> forests;
20-
21-
Host(String hostName, String zone, List<Forest> forests) {
22-
this.name = hostName;
23-
this.zone = zone;
24-
this.forests = forests;
25-
}
26-
27-
Host(String hostName, String zone, String... forests) {
28-
this.name = hostName;
29-
this.zone = zone;
30-
this.forests = new ArrayList<>();
31-
for (String forest : forests) {
32-
this.forests.add(new Forest(hostName, forest));
33-
}
34-
}
35-
}
36-
37-
static class ReplicaAssignment {
38-
String forest;
39-
String originalHost;
40-
List<String> replicaHosts;
41-
42-
ReplicaAssignment(String forest, String originalHost) {
43-
this.forest = forest;
44-
this.originalHost = originalHost;
45-
this.replicaHosts = new ArrayList<>();
46-
}
47-
48-
void addReplicaHost(String host) {
49-
replicaHosts.add(host);
50-
}
51-
}
52-
53-
static List<ReplicaAssignment> assignReplicas(List<Host> hosts, int replicaCount) {
54-
return assignReplicas(hosts, replicaCount, null);
55-
}
56-
57-
/**
58-
* @param hosts
59-
* @param replicaCount
60-
* @param allAvailableHosts is not null for a scenario where e.g. forests for a database are only created on one
61-
* host, such as for a modules or schemas database. In that scenario, the caller needs to
62-
* pass in a list of all available hosts in the cluster, so that replicas can be created
63-
* on those hosts.
64-
* @return
65-
*/
66-
static List<ReplicaAssignment> assignReplicas(List<Host> hosts, int replicaCount, List<String> allAvailableHosts) {
67-
final List<Host> replicaHosts = buildReplicaHostsList(hosts, allAvailableHosts);
68-
final boolean ignoreZones = shouldIgnoreZones(replicaHosts);
69-
70-
int differentZoneIndex = 0;
71-
int sameZoneIndex = 0;
72-
final List<ReplicaAssignment> assignments = new ArrayList<>();
73-
74-
for (final Host host : hosts) {
75-
int forestIndex = 0;
76-
for (Forest forest : host.forests) {
77-
ReplicaAssignment assignment = new ReplicaAssignment(forest.getForestName(), host.name);
78-
List<Host> eligibleHosts = buildEligibleHostsList(host, replicaHosts, ignoreZones);
79-
80-
if (ignoreZones) {
81-
assignReplicasIgnoringZones(assignment, eligibleHosts, replicaCount, forestIndex);
82-
} else {
83-
assignReplicasWithZoneAwareness(assignment, eligibleHosts, host, replicaCount, differentZoneIndex, sameZoneIndex);
84-
differentZoneIndex += replicaCount;
85-
sameZoneIndex += replicaCount;
86-
}
87-
88-
addReplicasToForest(forest, assignment);
89-
assignments.add(assignment);
90-
forestIndex++;
91-
}
92-
}
93-
94-
return assignments;
95-
}
96-
97-
private static List<Host> buildReplicaHostsList(List<Host> hosts, List<String> allAvailableHosts) {
98-
List<Host> replicaHosts = new ArrayList<>(hosts);
99-
if (allAvailableHosts != null) {
100-
for (String availableHost : allAvailableHosts) {
101-
boolean hostAlreadyExists = hosts.stream().anyMatch(h -> h.name.equals(availableHost));
102-
if (!hostAlreadyExists) {
103-
replicaHosts.add(new Host(availableHost, null, new ArrayList<>()));
104-
}
105-
}
106-
}
107-
return replicaHosts;
108-
}
109-
110-
private static boolean shouldIgnoreZones(List<Host> replicaHosts) {
111-
return replicaHosts.stream().anyMatch(h -> h.zone == null);
112-
}
113-
114-
private static List<Host> buildEligibleHostsList(Host sourceHost, List<Host> replicaHosts, boolean ignoreZones) {
115-
List<Host> eligibleHosts = new ArrayList<>();
116-
if (ignoreZones) {
117-
int sourceIndex = replicaHosts.indexOf(sourceHost);
118-
for (int i = 1; i < replicaHosts.size(); i++) {
119-
Host candidate = replicaHosts.get((sourceIndex + i) % replicaHosts.size());
120-
eligibleHosts.add(candidate);
121-
}
122-
} else {
123-
eligibleHosts = replicaHosts.stream()
124-
.filter(h -> !h.name.equals(sourceHost.name))
125-
.toList();
126-
}
127-
return eligibleHosts;
128-
}
129-
130-
private static void assignReplicasIgnoringZones(ReplicaAssignment assignment, List<Host> eligibleHosts, int replicaCount, int forestIndex) {
131-
Set<String> usedHosts = new HashSet<>();
132-
for (int i = 0; i < replicaCount && i < eligibleHosts.size(); i++) {
133-
Host targetHost = eligibleHosts.get((forestIndex + i) % eligibleHosts.size());
134-
if (!usedHosts.contains(targetHost.name)) {
135-
assignment.addReplicaHost(targetHost.name);
136-
usedHosts.add(targetHost.name);
137-
}
138-
}
139-
}
140-
141-
private static void assignReplicasWithZoneAwareness(ReplicaAssignment assignment, List<Host> eligibleHosts, Host sourceHost, int replicaCount, int differentZoneIndex, int sameZoneIndex) {
142-
List<Host> differentZoneHosts = new ArrayList<>();
143-
List<Host> sameZoneHosts = new ArrayList<>();
144-
145-
for (Host h : eligibleHosts) {
146-
if (h.zone.equals(sourceHost.zone)) {
147-
sameZoneHosts.add(h);
148-
} else {
149-
differentZoneHosts.add(h);
150-
}
151-
}
152-
153-
Set<String> usedHosts = new HashSet<>();
154-
int replicasAssigned = 0;
155-
156-
// First, try to assign from different zones
157-
replicasAssigned = assignFromHostList(assignment, differentZoneHosts, replicaCount, differentZoneIndex, usedHosts, replicasAssigned);
158-
159-
// If we still need more replicas, use same-zone hosts
160-
assignFromHostList(assignment, sameZoneHosts, replicaCount, sameZoneIndex, usedHosts, replicasAssigned);
161-
}
162-
163-
private static int assignFromHostList(ReplicaAssignment assignment, List<Host> hostList, int replicaCount, int startIndex, Set<String> usedHosts, int replicasAssigned) {
164-
while (replicasAssigned < replicaCount && !hostList.isEmpty() && usedHosts.size() < hostList.size()) {
165-
Host targetHost = hostList.get(startIndex % hostList.size());
166-
startIndex++;
167-
168-
if (!usedHosts.contains(targetHost.name)) {
169-
assignment.addReplicaHost(targetHost.name);
170-
usedHosts.add(targetHost.name);
171-
replicasAssigned++;
172-
}
173-
}
174-
return replicasAssigned;
175-
}
176-
177-
private static void addReplicasToForest(Forest forest, ReplicaAssignment assignment) {
178-
forest.setForestReplica(new ArrayList<>());
179-
assignment.replicaHosts.forEach(replicaHost -> {
180-
ForestReplica replica = new ForestReplica();
181-
replica.setHost(replicaHost);
182-
forest.getForestReplica().add(replica);
183-
});
184-
}
18+
private static final Logger logger = LoggerFactory.getLogger(ForestReplicaPlanner.class);
19+
20+
static class Host {
21+
String name;
22+
String zone;
23+
List<Forest> forests;
24+
25+
Host(String hostName, String zone, List<Forest> forests) {
26+
this.name = hostName;
27+
this.zone = zone;
28+
this.forests = forests;
29+
}
30+
31+
Host(String hostName, String zone, String... forests) {
32+
this.name = hostName;
33+
this.zone = zone;
34+
this.forests = new ArrayList<>();
35+
for (String forest : forests) {
36+
this.forests.add(new Forest(hostName, forest));
37+
}
38+
}
39+
40+
@Override
41+
public String toString() {
42+
return "[Host: %s; zone: %s]".formatted(name, zone);
43+
}
44+
}
45+
46+
static class ReplicaAssignment {
47+
String forest;
48+
String originalHost;
49+
List<String> replicaHosts;
50+
51+
ReplicaAssignment(String forest, String originalHost) {
52+
this.forest = forest;
53+
this.originalHost = originalHost;
54+
this.replicaHosts = new ArrayList<>();
55+
}
56+
57+
void addReplicaHost(String host) {
58+
replicaHosts.add(host);
59+
}
60+
}
61+
62+
static List<ReplicaAssignment> assignReplicas(List<Host> hosts, int replicaCount) {
63+
return assignReplicas(hosts, replicaCount, null);
64+
}
65+
66+
/**
67+
* @param hosts list of hosts containing primary forests.
68+
* @param replicaCount number of replica forests to create for each primary forest.
69+
* @param allAvailableHosts is not null for a scenario where e.g. forests for a database are only created on one
70+
* host, such as for a modules or schemas database. In that scenario, the caller needs to
71+
* pass in a list of all available hosts in the cluster, so that replicas can be created
72+
* on those hosts.
73+
* @return
74+
*/
75+
static List<ReplicaAssignment> assignReplicas(List<Host> hosts, int replicaCount, List<Host> allAvailableHosts) {
76+
final List<Host> replicaHosts = buildReplicaHostsList(hosts, allAvailableHosts);
77+
final boolean ignoreZones = shouldIgnoreZones(replicaHosts);
78+
79+
int differentZoneIndex = 0;
80+
int sameZoneIndex = 0;
81+
final List<ReplicaAssignment> assignments = new ArrayList<>();
82+
83+
for (final Host host : hosts) {
84+
int forestIndex = 0;
85+
for (Forest forest : host.forests) {
86+
ReplicaAssignment assignment = new ReplicaAssignment(forest.getForestName(), host.name);
87+
List<Host> eligibleHosts = buildEligibleHostsList(host, replicaHosts, ignoreZones);
88+
89+
if (ignoreZones) {
90+
assignReplicasIgnoringZones(assignment, eligibleHosts, replicaCount, forestIndex);
91+
} else {
92+
assignReplicasWithZoneAwareness(assignment, eligibleHosts, host, replicaCount, differentZoneIndex, sameZoneIndex);
93+
differentZoneIndex += replicaCount;
94+
sameZoneIndex += replicaCount;
95+
}
96+
97+
addReplicasToForest(forest, assignment);
98+
assignments.add(assignment);
99+
forestIndex++;
100+
}
101+
}
102+
103+
return assignments;
104+
}
105+
106+
private static List<Host> buildReplicaHostsList(List<Host> hosts, List<Host> allAvailableHosts) {
107+
List<Host> replicaHosts = new ArrayList<>(hosts);
108+
if (allAvailableHosts != null) {
109+
for (Host availableHost : allAvailableHosts) {
110+
boolean hostAlreadyExists = hosts.stream().anyMatch(h -> h.name.equals(availableHost.name));
111+
if (!hostAlreadyExists) {
112+
replicaHosts.add(availableHost);
113+
}
114+
}
115+
}
116+
return replicaHosts;
117+
}
118+
119+
private static boolean shouldIgnoreZones(List<Host> replicaHosts) {
120+
return replicaHosts.stream().anyMatch(h -> h.zone == null);
121+
}
122+
123+
private static List<Host> buildEligibleHostsList(Host sourceHost, List<Host> replicaHosts, boolean ignoreZones) {
124+
List<Host> eligibleHosts = new ArrayList<>();
125+
if (ignoreZones) {
126+
int sourceIndex = replicaHosts.indexOf(sourceHost);
127+
for (int i = 1; i < replicaHosts.size(); i++) {
128+
Host candidate = replicaHosts.get((sourceIndex + i) % replicaHosts.size());
129+
eligibleHosts.add(candidate);
130+
}
131+
} else {
132+
eligibleHosts = replicaHosts.stream()
133+
.filter(h -> !h.name.equals(sourceHost.name))
134+
.toList();
135+
}
136+
return eligibleHosts;
137+
}
138+
139+
private static void assignReplicasIgnoringZones(ReplicaAssignment assignment, List<Host> eligibleHosts, int replicaCount, int forestIndex) {
140+
if (logger.isDebugEnabled()) {
141+
logger.debug("Assigning replicas without considering host zones.");
142+
}
143+
Set<String> usedHosts = new HashSet<>();
144+
for (int i = 0; i < replicaCount && i < eligibleHosts.size(); i++) {
145+
Host targetHost = eligibleHosts.get((forestIndex + i) % eligibleHosts.size());
146+
if (!usedHosts.contains(targetHost.name)) {
147+
assignment.addReplicaHost(targetHost.name);
148+
usedHosts.add(targetHost.name);
149+
}
150+
}
151+
}
152+
153+
private static void assignReplicasWithZoneAwareness(ReplicaAssignment assignment, List<Host> eligibleHosts, Host sourceHost, int replicaCount, int differentZoneIndex, int sameZoneIndex) {
154+
if (logger.isDebugEnabled()) {
155+
logger.debug("Assigning replicas while taking host zones into account.");
156+
}
157+
List<Host> differentZoneHosts = new ArrayList<>();
158+
List<Host> sameZoneHosts = new ArrayList<>();
159+
160+
for (Host h : eligibleHosts) {
161+
if (h.zone.equals(sourceHost.zone)) {
162+
sameZoneHosts.add(h);
163+
} else {
164+
differentZoneHosts.add(h);
165+
}
166+
}
167+
168+
Set<String> usedHosts = new HashSet<>();
169+
int replicasAssigned = 0;
170+
171+
// First, try to assign from different zones
172+
replicasAssigned = assignFromHostList(assignment, differentZoneHosts, replicaCount, differentZoneIndex, usedHosts, replicasAssigned);
173+
174+
// If we still need more replicas, use same-zone hosts
175+
assignFromHostList(assignment, sameZoneHosts, replicaCount, sameZoneIndex, usedHosts, replicasAssigned);
176+
}
177+
178+
private static int assignFromHostList(ReplicaAssignment assignment, List<Host> hostList, int replicaCount, int startIndex, Set<String> usedHosts, int replicasAssigned) {
179+
while (replicasAssigned < replicaCount && !hostList.isEmpty() && usedHosts.size() < hostList.size()) {
180+
Host targetHost = hostList.get(startIndex % hostList.size());
181+
startIndex++;
182+
183+
if (!usedHosts.contains(targetHost.name)) {
184+
assignment.addReplicaHost(targetHost.name);
185+
usedHosts.add(targetHost.name);
186+
replicasAssigned++;
187+
}
188+
}
189+
return replicasAssigned;
190+
}
191+
192+
private static void addReplicasToForest(Forest forest, ReplicaAssignment assignment) {
193+
forest.setForestReplica(new ArrayList<>());
194+
assignment.replicaHosts.forEach(replicaHost -> {
195+
ForestReplica replica = new ForestReplica();
196+
replica.setHost(replicaHost);
197+
forest.getForestReplica().add(replica);
198+
});
199+
}
185200
}

ml-app-deployer/src/main/java/com/marklogic/appdeployer/command/forests/ZoneAwareReplicaBuilderStrategy.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,22 @@ public void buildReplicas(List<Forest> forests, ForestPlan forestPlan, AppConfig
3030
}
3131
}
3232

33-
List<ForestReplicaPlanner.Host> hosts = new ArrayList<>();
33+
final List<ForestReplicaPlanner.Host> hosts = new ArrayList<>();
3434
hostToForests.forEach((host, hostForests) -> {
3535
final String zone = forestPlan.getHostsToZones() != null ? forestPlan.getHostsToZones().get(host) : null;
3636
hosts.add(new ForestReplicaPlanner.Host(host, zone, hostForests));
3737
});
3838

39-
ForestReplicaPlanner.assignReplicas(hosts, forestPlan.getReplicaCount(), forestPlan.getReplicaHostNames());
39+
List<ForestReplicaPlanner.Host> allAvailableHosts = null;
40+
if (forestPlan.getReplicaHostNames() != null) {
41+
allAvailableHosts = new ArrayList<>();
42+
for (String hostName : forestPlan.getReplicaHostNames()) {
43+
final String zone = forestPlan.getHostsToZones() != null ? forestPlan.getHostsToZones().get(hostName) : null;
44+
allAvailableHosts.add(new ForestReplicaPlanner.Host(hostName, zone));
45+
}
46+
}
47+
48+
ForestReplicaPlanner.assignReplicas(hosts, forestPlan.getReplicaCount(), allAvailableHosts);
4049

4150
for (Forest forest : forests) {
4251
if (forest.getForestReplica() == null) {

0 commit comments

Comments
 (0)