Skip to content

Commit 8d69d19

Browse files
authored
Merge pull request #756 from marklogic/feature/20741-consolidate
MLE-20741 Now accounting for zones when creating forest replicas
2 parents 80dfbef + 523bd38 commit 8d69d19

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)