Skip to content

Commit b52a424

Browse files
committed
MLE-20741 Forest replica creation now considers host zone
This isn't fully baked yet, but it gets the plumbing in place for accounting for a zone value on each host. ForestReplicaPlanner was created almost entirely by Copilot, and it's handling the work of determining which replicas to build and on what hosts. Next step will be to enhance DeployForestsCommand to actually build the map of hosts to zones, which will then enable the feature. A final step, and in a separate ticket, will be to figure out what to do with ConfigureForestReplicasCommand. That hopefully is no longer needed, given that DeployForestsCommand is able to create replicas as well.
1 parent 4ac5617 commit b52a424

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)