Skip to content

Commit 523bd38

Browse files
committed
MLE-20741 Now accounting for zones when creating forest replicas
This hooks up the host-to-zone mapping so that it's fed into the forest replica planner. Currently only being tested via manual testing, which is consistent with how "real" replicas have been created in the past. Enhanced the docker-compose file to create a 6-host cluster for manual testing purposes. Story isn't quite done though. Need to add some tests for creating forests on a single host, as that seems a bit off.
1 parent 80dfbef commit 523bd38

File tree

6 files changed

+212
-67
lines changed

6 files changed

+212
-67
lines changed

docker-compose.yaml

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,103 @@ services:
1414
ports:
1515
- "8000-8008:8000-8008"
1616
- "8028:8028"
17+
18+
# The six-node cluster defined below is used for manual testing of operations that require a cluster of 3 or more
19+
# hosts, such as creating forest replicas.
20+
node1:
21+
image: "progressofficial/marklogic-db:latest"
22+
platform: linux/amd64
23+
hostname: node1.local
24+
environment:
25+
- MARKLOGIC_INIT=true
26+
- MARKLOGIC_ADMIN_USERNAME=admin
27+
- MARKLOGIC_ADMIN_PASSWORD=admin
28+
volumes:
29+
- ./docker/marklogic/node1/logs:/var/opt/MarkLogic/Logs
30+
ports:
31+
- 8100-8103:8000-8003
32+
33+
node2:
34+
image: "progressofficial/marklogic-db:latest"
35+
platform: linux/amd64
36+
hostname: node2.local
37+
depends_on:
38+
- node1
39+
environment:
40+
- MARKLOGIC_INIT=true
41+
- MARKLOGIC_ADMIN_USERNAME=admin
42+
- MARKLOGIC_ADMIN_PASSWORD=admin
43+
- MARKLOGIC_JOIN_CLUSTER=true
44+
- MARKLOGIC_BOOTSTRAP_HOST=node1.local
45+
volumes:
46+
- ./docker/marklogic/node2/logs:/var/opt/MarkLogic/Logs
47+
ports:
48+
- 8200-8203:8000-8003
49+
50+
node3:
51+
image: "progressofficial/marklogic-db:latest"
52+
platform: linux/amd64
53+
hostname: node3.local
54+
depends_on:
55+
- node1
56+
environment:
57+
- MARKLOGIC_INIT=true
58+
- MARKLOGIC_ADMIN_USERNAME=admin
59+
- MARKLOGIC_ADMIN_PASSWORD=admin
60+
- MARKLOGIC_JOIN_CLUSTER=true
61+
- MARKLOGIC_BOOTSTRAP_HOST=node1.local
62+
volumes:
63+
- ./docker/marklogic/node3/logs:/var/opt/MarkLogic/Logs
64+
ports:
65+
- 8300-8303:8000-8003
66+
67+
node4:
68+
image: "progressofficial/marklogic-db:latest"
69+
platform: linux/amd64
70+
hostname: node4.local
71+
depends_on:
72+
- node1
73+
environment:
74+
- MARKLOGIC_INIT=true
75+
- MARKLOGIC_ADMIN_USERNAME=admin
76+
- MARKLOGIC_ADMIN_PASSWORD=admin
77+
- MARKLOGIC_JOIN_CLUSTER=true
78+
- MARKLOGIC_BOOTSTRAP_HOST=node1.local
79+
volumes:
80+
- ./docker/marklogic/node4/logs:/var/opt/MarkLogic/Logs
81+
ports:
82+
- 8500-8503:8000-8003
83+
84+
node5:
85+
image: "progressofficial/marklogic-db:latest"
86+
platform: linux/amd64
87+
hostname: node5.local
88+
depends_on:
89+
- node1
90+
environment:
91+
- MARKLOGIC_INIT=true
92+
- MARKLOGIC_ADMIN_USERNAME=admin
93+
- MARKLOGIC_ADMIN_PASSWORD=admin
94+
- MARKLOGIC_JOIN_CLUSTER=true
95+
- MARKLOGIC_BOOTSTRAP_HOST=node1.local
96+
volumes:
97+
- ./docker/marklogic/node5/logs:/var/opt/MarkLogic/Logs
98+
ports:
99+
- 8600-8603:8000-8003
100+
101+
node6:
102+
image: "progressofficial/marklogic-db:latest"
103+
platform: linux/amd64
104+
hostname: node6.local
105+
depends_on:
106+
- node1
107+
environment:
108+
- MARKLOGIC_INIT=true
109+
- MARKLOGIC_ADMIN_USERNAME=admin
110+
- MARKLOGIC_ADMIN_PASSWORD=admin
111+
- MARKLOGIC_JOIN_CLUSTER=true
112+
- MARKLOGIC_BOOTSTRAP_HOST=node1.local
113+
volumes:
114+
- ./docker/marklogic/node6/logs:/var/opt/MarkLogic/Logs
115+
ports:
116+
- 8700-8703:8000-8003

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

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -34,19 +34,18 @@
3434
import java.util.HashMap;
3535
import java.util.List;
3636
import java.util.Map;
37-
import java.util.stream.Collectors;
3837

3938
/**
40-
* Command for configuring - i.e. creating and setting - replica forests for existing databases.
41-
* <p>
42-
* Very useful for the out-of-the-box forests such as Security, Schemas, App-Services, and Meters, which normally need
43-
* replicas for failover in a cluster.
39+
* Command for configuring - i.e. creating and setting - replica forests for existing databases. The expectation is that
40+
* {@code DeployForestsCommand} is used for creating primary forests, while this command is used for creating replica
41+
* forests based on existing primary forests.
4442
*/
4543
public class ConfigureForestReplicasCommand extends AbstractUndoableCommand {
4644

4745
private Map<String, Integer> databaseNamesAndReplicaCounts = new HashMap<>();
4846
private boolean deleteReplicasOnUndo = true;
4947
private GroupHostNamesProvider groupHostNamesProvider;
48+
private ForestBuilder forestBuilder = new ForestBuilder();
5049

5150
public ConfigureForestReplicasCommand() {
5251
setExecuteSortOrder(SortOrderConstants.DEPLOY_FOREST_REPLICAS);
@@ -128,39 +127,47 @@ protected void deleteReplicas(String forestName, ForestManager forestMgr) {
128127
* @param hostNames
129128
* @param context
130129
*/
131-
protected void configureDatabaseReplicaForests(String databaseName, int replicaCount, List<String> hostNames, CommandContext context) {
130+
private void configureDatabaseReplicaForests(String databaseName, int replicaCount, List<String> hostNames, CommandContext context) {
132131
List<Forest> forestsNeedingReplicas = determineForestsNeedingReplicas(databaseName, context);
133132

134-
ForestBuilder forestBuilder = new ForestBuilder();
135133
List<String> selectedHostNames = getHostNamesForDatabaseForests(databaseName, hostNames, context);
136-
ForestPlan forestPlan = new ForestPlan(databaseName, selectedHostNames).withReplicaCount(replicaCount);
137134
List<String> dataDirectories = forestBuilder.determineDataDirectories(databaseName, context.getAppConfig());
135+
136+
final ForestPlan forestPlan = new ForestPlan(databaseName, selectedHostNames)
137+
.withHostsToZones(new HostManager(context.getManageClient()).getHostNamesAndZones())
138+
.withReplicaCount(replicaCount);
139+
138140
forestBuilder.addReplicasToForests(forestsNeedingReplicas, forestPlan, context.getAppConfig(), dataDirectories);
139141

142+
// Trim off all forests details so only the replicas are saved.
140143
List<Forest> forestsWithOnlyReplicas = forestsNeedingReplicas.stream().map(forest -> {
141144
Forest forestWithOnlyReplicas = new Forest();
142145
forestWithOnlyReplicas.setForestName(forest.getForestName());
143146
forestWithOnlyReplicas.setForestReplica(forest.getForestReplica());
144147
return forestWithOnlyReplicas;
145-
}).collect(Collectors.toList());
148+
}).toList();
146149

147150
// As of 4.5.3, try CMA first so that this can be done in a single request instead of one request per forest.
148151
if (context.getAppConfig().getCmaConfig().isDeployForests()) {
149152
try {
150-
Configuration config = new Configuration();
151-
forestsWithOnlyReplicas.forEach(forest -> {
152-
config.addForest(forest.toObjectNode());
153-
});
154-
new Configurations(config).submit(context.getManageClient());
153+
saveReplicasViaCma(forestsWithOnlyReplicas, context);
155154
return;
156155
} catch (Exception ex) {
157-
logger.warn("Unable to create forest replicas via CMA; cause: " + ex.getMessage() + "; will " +
158-
"fall back to using /manage/v2.");
156+
logger.warn("Unable to create forest replicas via CMA; cause: {}; will fall back to using /manage/v2.", ex.getMessage());
159157
}
160158
}
161159

162-
// If we get here, either CMA usage is disabled or an error occurred with CMA. Just use /manage/v2 to submit
163-
// each forest one-by-one.
160+
// If we get here, either CMA usage is disabled or an error occurred with CMA.
161+
saveReplicasOneByOne(forestsWithOnlyReplicas, context);
162+
}
163+
164+
private void saveReplicasViaCma(List<Forest> forestsWithOnlyReplicas, CommandContext context) {
165+
Configuration config = new Configuration();
166+
forestsWithOnlyReplicas.forEach(forest -> config.addForest(forest.toObjectNode()));
167+
new Configurations(config).submit(context.getManageClient());
168+
}
169+
170+
private void saveReplicasOneByOne(List<Forest> forestsWithOnlyReplicas, CommandContext context) {
164171
ForestManager forestManager = new ForestManager(context.getManageClient());
165172
forestsWithOnlyReplicas.forEach(forest -> {
166173
String forestName = forest.getForestName();

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

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import com.marklogic.mgmt.resource.databases.DatabaseManager;
2929
import com.marklogic.mgmt.resource.forests.ForestManager;
3030
import com.marklogic.mgmt.resource.hosts.DefaultHostNameProvider;
31+
import com.marklogic.mgmt.resource.hosts.HostManager;
3132
import org.springframework.util.StringUtils;
3233

3334
import java.io.File;
@@ -122,11 +123,11 @@ protected void createForestsViaForestEndpoint(CommandContext context, List<Fores
122123
* these commands to construct a list of many forests that can be created via CMA in one request.
123124
*
124125
* @param context
125-
* @param includeReplicas This command currently doesn't make use of this feature; it's here so that other clients
126-
* can get a preview of the forests to be created, including replicas.
126+
* @param previewingForestCreation When previewing forest creation, replicas will be built. If not previewing
127+
* forest creation, then only primary forests will be built.
127128
* @return
128129
*/
129-
public List<Forest> buildForests(CommandContext context, boolean includeReplicas) {
130+
public List<Forest> buildForests(CommandContext context, boolean previewingForestCreation) {
130131
// Need to know what primary forests exist already in case more need to be added, or a new host has been added
131132
List<Forest> existingPrimaryForests = null;
132133

@@ -141,36 +142,35 @@ public List<Forest> buildForests(CommandContext context, boolean includeReplicas
141142
existingPrimaryForests = getExistingPrimaryForests(context, this.databaseName);
142143
}
143144

144-
return buildForests(context, includeReplicas, existingPrimaryForests);
145+
return buildForests(context, previewingForestCreation, existingPrimaryForests);
145146
}
146147

147148
/**
148149
* @param context
149-
* @param includeReplicas
150+
* @param previewingForestCreation
150151
* @param existingPrimaryForests
151152
* @return
152153
*/
153-
protected List<Forest> buildForests(CommandContext context, boolean includeReplicas, List<Forest> existingPrimaryForests) {
154+
protected List<Forest> buildForests(CommandContext context, boolean previewingForestCreation, List<Forest> existingPrimaryForests) {
154155
ForestHostNames forestHostNames = determineHostNamesForForest(context, existingPrimaryForests);
155156

156157
final String template = buildForestTemplate(context, new ForestManager(context.getManageClient()));
157158

158159
ForestPlan forestPlan = new ForestPlan(this.databaseName, forestHostNames.getPrimaryForestHostNames())
159-
.withReplicaHostNames(forestHostNames.getReplicaForestHostNames())
160160
.withTemplate(template)
161161
.withForestsPerDataDirectory(this.forestsPerHost)
162162
.withExistingForests(existingPrimaryForests);
163163

164-
if (includeReplicas) {
164+
if (previewingForestCreation) {
165165
Map<String, Integer> map = context.getAppConfig().getDatabaseNamesAndReplicaCounts();
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.
173-
forestPlan.withReplicaCount(count);
169+
// If replicas are needed while previewing forest creation, we need to fetch some additional data
170+
// to ensure the correct replicas are previewed.
171+
forestPlan.withReplicaCount(count)
172+
.withHostsToZones(new HostManager(context.getManageClient()).getHostNamesAndZones())
173+
.withReplicaHostNames(forestHostNames.getReplicaForestHostNames());
174174
}
175175
}
176176
}
@@ -232,7 +232,7 @@ protected String buildForestTemplate(CommandContext context, ForestManager fores
232232
* @param existingPrimaryForests
233233
* @return a ForestHostNames instance that defines the list of host names that can be used for primary forests and
234234
* that can be used for replica forests. As of 4.1.0, the only reason these will differ is when a database is
235-
* configured to only have forests on one host, or when the deprecated setCreateForestsOnOneHost method is used.
235+
* configured to only have forests on one host.
236236
*/
237237
protected ForestHostNames determineHostNamesForForest(CommandContext context, List<Forest> existingPrimaryForests) {
238238
if (hostCalculator == null) {

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,23 @@ public ForestPlan withReplicaCount(int count) {
6868
return this;
6969
}
7070

71+
/**
72+
* Confusingly, this is only used - at least as of 6.0.0 - when previewing forest creation. It is not used when
73+
* actually configuring forest replicas.
74+
*
75+
* @param replicaHostNames
76+
* @return
77+
*/
7178
public ForestPlan withReplicaHostNames(List<String> replicaHostNames) {
7279
this.replicaHostNames = replicaHostNames;
7380
return this;
7481
}
7582

83+
/**
84+
* @param hostsToZones a mapping of each host name to an optional zone value for each host. The zone value can be
85+
* null for a host.
86+
* @since 6.0.0
87+
*/
7688
public ForestPlan withHostsToZones(Map<String, String> hostsToZones) {
7789
this.hostsToZones = hostsToZones;
7890
return this;
@@ -106,6 +118,10 @@ public List<String> getReplicaHostNames() {
106118
return replicaHostNames;
107119
}
108120

121+
/**
122+
* @return
123+
* @since 6.0.0
124+
*/
109125
public Map<String, String> getHostsToZones() {
110126
return hostsToZones;
111127
}

ml-app-deployer/src/main/java/com/marklogic/mgmt/resource/hosts/HostManager.java

Lines changed: 48 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -28,48 +28,67 @@
2828

2929
public class HostManager extends AbstractManager {
3030

31-
private ManageClient client;
31+
private ManageClient manageClient;
3232

33-
public HostManager(ManageClient client) {
34-
this.client = client;
35-
}
33+
public HostManager(ManageClient manageClient) {
34+
this.manageClient = manageClient;
35+
}
3636

37-
public List<String> getHostIds() {
38-
return getHosts().getElementValues("/h:host-default-list/h:list-items/h:list-item/h:idref");
39-
}
37+
public List<String> getHostIds() {
38+
return getHosts().getElementValues("/h:host-default-list/h:list-items/h:list-item/h:idref");
39+
}
4040

41-
public List<String> getHostNames() {
42-
return getHosts().getElementValues("/h:host-default-list/h:list-items/h:list-item/h:nameref");
43-
}
41+
public List<String> getHostNames() {
42+
return getHosts().getElementValues("/h:host-default-list/h:list-items/h:list-item/h:nameref");
43+
}
44+
45+
/**
46+
* @return a map of host names to optional zones. For performance reasons, as soon as a host is found to not have
47+
* a zone, then an empty map will be returned. Currently, the only use case for this map is to create forest replicas
48+
* across zones. But if any host does not have a zone, then zones do not matter.
49+
* @since 6.0.0
50+
*/
51+
public Map<String, String> getHostNamesAndZones() {
52+
Map<String, String> map = new LinkedHashMap<>();
53+
for (String hostName : getHostNames()) {
54+
String json = manageClient.getJson("/manage/v2/hosts/%s/properties".formatted(hostName));
55+
String zone = payloadParser.getPayloadFieldValue(json, "zone");
56+
if (zone == null || zone.trim().isEmpty()) {
57+
return map;
58+
}
59+
map.put(hostName, zone);
60+
}
61+
return map;
62+
}
4463

4564
/**
4665
* @return a map with an entry for each host, with the key of the host being the host ID and the value being the
4766
* host name
4867
*/
4968
public Map<String, String> getHostIdsAndNames() {
50-
Fragment xml = getHosts();
51-
Map<String, String> map = new LinkedHashMap<>();
52-
Namespace ns = Namespace.getNamespace("http://marklogic.com/manage/hosts");
53-
for (Element el : xml.getElements("/h:host-default-list/h:list-items/h:list-item")) {
54-
String hostId = el.getChildText("idref", ns);
55-
String hostName = el.getChildText("nameref", ns);
56-
map.put(hostId, hostName);
57-
}
58-
return map;
59-
}
69+
Fragment xml = getHosts();
70+
Map<String, String> map = new LinkedHashMap<>();
71+
Namespace ns = Namespace.getNamespace("http://marklogic.com/manage/hosts");
72+
for (Element el : xml.getElements("/h:host-default-list/h:list-items/h:list-item")) {
73+
String hostId = el.getChildText("idref", ns);
74+
String hostName = el.getChildText("nameref", ns);
75+
map.put(hostId, hostName);
76+
}
77+
return map;
78+
}
6079

61-
public Fragment getHosts() {
62-
return client.getXml("/manage/v2/hosts");
63-
}
80+
public Fragment getHosts() {
81+
return manageClient.getXml("/manage/v2/hosts");
82+
}
6483

65-
public ResponseEntity<String> setHostToGroup(String hostIdOrName, String groupIdOrName) {
66-
String json = format("{\"group\":\"%s\"}", groupIdOrName);
67-
String url = format("/manage/v2/hosts/%s/properties", hostIdOrName);
68-
return client.putJson(url, json);
69-
}
84+
public ResponseEntity<String> setHostToGroup(String hostIdOrName, String groupIdOrName) {
85+
String json = format("{\"group\":\"%s\"}", groupIdOrName);
86+
String url = format("/manage/v2/hosts/%s/properties", hostIdOrName);
87+
return manageClient.putJson(url, json);
88+
}
7089

7190
public String getAssignedGroupName(String hostIdOrName) {
7291
String url = format("/manage/v2/hosts/%s/properties", hostIdOrName);
73-
return payloadParser.getPayloadFieldValue(client.getJson(url), "group");
92+
return payloadParser.getPayloadFieldValue(manageClient.getJson(url), "group");
7493
}
7594
}

0 commit comments

Comments
 (0)