Skip to content

Commit 80dfbef

Browse files
authored
Merge pull request #755 from marklogic/feature/20741-forest-host-zone
MLE-20741 Forest replica creation now considers host zone
2 parents 4ac5617 + b52a424 commit 80dfbef

File tree

8 files changed

+592
-19
lines changed

8 files changed

+592
-19
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,10 @@ protected List<Forest> buildForests(CommandContext context, boolean includeRepli
166166
if (map != null && map.containsKey(this.databaseName)) {
167167
int count = map.get(this.databaseName);
168168
if (count > 0) {
169+
// Need to pass in host-to-zone mapping.
170+
// And what to do about ConfigureForestReplicasCommand??? We may be better off removing that as part of
171+
// the 6.0 release so that we only have replica creation in one place. Would need to verify that this command
172+
// still works for OOTB databases.
169173
forestPlan.withReplicaCount(count);
170174
}
171175
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public ForestBuilder() {
4040

4141
public ForestBuilder(ForestNamingStrategy forestNamingStrategy) {
4242
this.forestNamingStrategy = forestNamingStrategy;
43-
this.replicaBuilderStrategy = new DistributedReplicaBuilderStrategy();
43+
this.replicaBuilderStrategy = new ZoneAwareReplicaBuilderStrategy();
4444
this.resourceMapper = new DefaultResourceMapper(new API(null));
4545
}
4646

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.util.ArrayList;
2121
import java.util.Arrays;
2222
import java.util.List;
23+
import java.util.Map;
2324

2425
public class ForestPlan {
2526

@@ -30,6 +31,7 @@ public class ForestPlan {
3031
private int forestsPerDataDirectory = 1;
3132
private List<Forest> existingForests = new ArrayList<>();
3233
private int replicaCount = 0;
34+
private Map<String, String> hostsToZones;
3335

3436
public ForestPlan(String databaseName, String... hostNames) {
3537
this(databaseName, Arrays.asList(hostNames));
@@ -71,6 +73,11 @@ public ForestPlan withReplicaHostNames(List<String> replicaHostNames) {
7173
return this;
7274
}
7375

76+
public ForestPlan withHostsToZones(Map<String, String> hostsToZones) {
77+
this.hostsToZones = hostsToZones;
78+
return this;
79+
}
80+
7481
public String getDatabaseName() {
7582
return databaseName;
7683
}
@@ -98,4 +105,8 @@ public List<Forest> getExistingForests() {
98105
public List<String> getReplicaHostNames() {
99106
return replicaHostNames;
100107
}
108+
109+
public Map<String, String> getHostsToZones() {
110+
return hostsToZones;
111+
}
101112
}
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
/*
2+
* Copyright © 2025 MarkLogic Corporation. All Rights Reserved.
3+
*/
4+
package com.marklogic.appdeployer.command.forests;
5+
6+
import com.marklogic.mgmt.api.forest.Forest;
7+
import com.marklogic.mgmt.api.forest.ForestReplica;
8+
9+
import java.util.ArrayList;
10+
import java.util.HashSet;
11+
import java.util.List;
12+
import java.util.Set;
13+
14+
class ForestReplicaPlanner {
15+
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+
}
185+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright © 2025 MarkLogic Corporation. All Rights Reserved.
3+
*/
4+
package com.marklogic.appdeployer.command.forests;
5+
6+
import com.marklogic.appdeployer.AppConfig;
7+
import com.marklogic.mgmt.api.forest.Forest;
8+
import com.marklogic.mgmt.api.forest.ForestReplica;
9+
10+
import java.util.*;
11+
12+
/**
13+
* Temporarily using this for the logic in MLE-20741. Will eventually replace DistributedReplicaBuilderStrategy with
14+
* this, once we know this is all working properly.
15+
*/
16+
class ZoneAwareReplicaBuilderStrategy extends AbstractReplicaBuilderStrategy {
17+
18+
@Override
19+
public void buildReplicas(List<Forest> forests, ForestPlan forestPlan, AppConfig appConfig,
20+
List<String> replicaDataDirectories, ForestNamingStrategy forestNamingStrategy) {
21+
Map<String, List<Forest>> hostToForests = new LinkedHashMap<>();
22+
for (Forest f : forests) {
23+
String host = f.getHost();
24+
if (hostToForests.containsKey(host)) {
25+
hostToForests.get(host).add(f);
26+
} else {
27+
ArrayList<Forest> hostForests = new ArrayList<>();
28+
hostForests.add(f);
29+
hostToForests.put(host, hostForests);
30+
}
31+
}
32+
33+
List<ForestReplicaPlanner.Host> hosts = new ArrayList<>();
34+
hostToForests.forEach((host, hostForests) -> {
35+
final String zone = forestPlan.getHostsToZones() != null ? forestPlan.getHostsToZones().get(host) : null;
36+
hosts.add(new ForestReplicaPlanner.Host(host, zone, hostForests));
37+
});
38+
39+
ForestReplicaPlanner.assignReplicas(hosts, forestPlan.getReplicaCount(), forestPlan.getReplicaHostNames());
40+
41+
for (Forest forest : forests) {
42+
if (forest.getForestReplica() == null) {
43+
continue;
44+
}
45+
DataDirectoryIterator dataDirectoryIterator = new DataDirectoryIterator(replicaDataDirectories);
46+
for (int i = 0; i < forest.getForestReplica().size(); i++) {
47+
ForestReplica replica = forest.getForestReplica().get(i);
48+
String replicaName = forestNamingStrategy.getReplicaName(forestPlan.getDatabaseName(), forest.getForestName(), i + 1, appConfig);
49+
replica.setReplicaName(replicaName);
50+
replica.setDataDirectory(dataDirectoryIterator.next());
51+
configureReplica(replica, forestPlan.getDatabaseName(), appConfig);
52+
}
53+
}
54+
}
55+
56+
private static class DataDirectoryIterator implements Iterator<String> {
57+
58+
private final List<String> dataDirectories;
59+
private int index = 0;
60+
61+
public DataDirectoryIterator(List<String> dataDirectories) {
62+
this.dataDirectories = dataDirectories;
63+
}
64+
65+
@Override
66+
public boolean hasNext() {
67+
return true;
68+
}
69+
70+
@Override
71+
public String next() {
72+
String value = dataDirectories.get(index);
73+
index = (index + 1) % dataDirectories.size();
74+
return value;
75+
}
76+
}
77+
}

0 commit comments

Comments
 (0)