Skip to content

Commit e046429

Browse files
Fix single project GetClusterState API response for multiproject (#133936)
As it was noted in #132332 we have a general principle that API output for a single project should be the same regardless of whether the cluster is non-MP or MP. (However, the reserved metadata merge logic was never properly implemented to support this) As a result of changes in that PR, response formats diverged. This PR makes the single project GetClusterState API response for MP clusters match the non-MP format.
1 parent ecf7eae commit e046429

File tree

8 files changed

+197
-88
lines changed

8 files changed

+197
-88
lines changed

server/src/main/java/org/elasticsearch/action/admin/cluster/state/ClusterStateRequest.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ public class ClusterStateRequest extends LocalClusterStateRequest implements Ind
3939
private TimeValue waitForTimeout = DEFAULT_WAIT_FOR_NODE_TIMEOUT;
4040
private String[] indices = Strings.EMPTY_ARRAY;
4141
private IndicesOptions indicesOptions = IndicesOptions.lenientExpandOpen();
42+
private boolean multiproject = false;
4243

4344
public ClusterStateRequest(TimeValue masterNodeTimeout) {
4445
super(masterNodeTimeout);
@@ -140,6 +141,15 @@ public boolean customs() {
140141
return customs;
141142
}
142143

144+
public ClusterStateRequest multiproject(boolean multiproject) {
145+
this.multiproject = multiproject;
146+
return this;
147+
}
148+
149+
public boolean multiproject() {
150+
return multiproject;
151+
}
152+
143153
public TimeValue waitForTimeout() {
144154
return waitForTimeout;
145155
}
@@ -200,5 +210,4 @@ public String getDescription() {
200210
stringBuilder.append("master timeout [").append(masterTimeout()).append("]]");
201211
return stringBuilder.toString();
202212
}
203-
204213
}

server/src/main/java/org/elasticsearch/action/admin/cluster/state/ClusterStateRequestBuilder.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,4 +108,12 @@ public ClusterStateRequestBuilder setWaitForTimeOut(TimeValue waitForTimeout) {
108108
request.waitForTimeout(waitForTimeout);
109109
return this;
110110
}
111+
112+
/**
113+
* When set then the response will be in multi-project format
114+
*/
115+
public ClusterStateRequestBuilder setMultiproject(boolean multiproject) {
116+
request.multiproject(multiproject);
117+
return this;
118+
}
111119
}

server/src/main/java/org/elasticsearch/action/admin/cluster/state/TransportClusterStateAction.java

Lines changed: 73 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
import org.elasticsearch.cluster.metadata.Metadata;
2929
import org.elasticsearch.cluster.metadata.ProjectId;
3030
import org.elasticsearch.cluster.metadata.ProjectMetadata;
31+
import org.elasticsearch.cluster.metadata.ReservedStateHandlerMetadata;
32+
import org.elasticsearch.cluster.metadata.ReservedStateMetadata;
3133
import org.elasticsearch.cluster.project.ProjectResolver;
3234
import org.elasticsearch.cluster.project.ProjectStateRegistry;
3335
import org.elasticsearch.cluster.routing.GlobalRoutingTable;
@@ -47,7 +49,9 @@
4749

4850
import java.io.IOException;
4951
import java.util.Collection;
52+
import java.util.HashMap;
5053
import java.util.Map;
54+
import java.util.Objects;
5155
import java.util.Set;
5256
import java.util.function.BiPredicate;
5357
import java.util.function.Predicate;
@@ -189,12 +193,12 @@ private static Map<String, Set<String>> getClusterFeatures(ClusterState clusterS
189193
}
190194

191195
private ClusterStateResponse buildResponse(final ClusterStateRequest request, final ClusterState rawState) {
192-
final ClusterState currentState = filterClusterState(rawState);
196+
final ClusterState filteredState = filterClusterState(rawState);
193197

194198
ThreadPool.assertCurrentThreadPool(ThreadPool.Names.MANAGEMENT); // too heavy to construct & serialize cluster state without forking
195199

196200
if (request.blocks() == false) {
197-
final var blockException = currentState.blocks().globalBlockedException(ClusterBlockLevel.METADATA_READ);
201+
final var blockException = filteredState.blocks().globalBlockedException(ClusterBlockLevel.METADATA_READ);
198202
if (blockException != null) {
199203
// There's a METADATA_READ block in place, but we aren't returning it to the caller, and yet the caller needs to know that
200204
// this block exists (e.g. it's the STATE_NOT_RECOVERED_BLOCK, so the rest of the state is known to be incomplete). Thus we
@@ -203,22 +207,22 @@ private ClusterStateResponse buildResponse(final ClusterStateRequest request, fi
203207
}
204208
}
205209

206-
logger.trace("Serving cluster state request using version {}", currentState.version());
207-
ClusterState.Builder builder = ClusterState.builder(currentState.getClusterName());
208-
builder.version(currentState.version());
209-
builder.stateUUID(currentState.stateUUID());
210+
logger.trace("Serving cluster state request using version {}", filteredState.version());
211+
ClusterState.Builder builder = ClusterState.builder(filteredState.getClusterName());
212+
builder.version(filteredState.version());
213+
builder.stateUUID(filteredState.stateUUID());
210214

211215
if (request.nodes()) {
212-
builder.nodes(currentState.nodes());
213-
builder.nodeIdsToCompatibilityVersions(getCompatibilityVersions(currentState));
214-
builder.nodeFeatures(getClusterFeatures(currentState));
216+
builder.nodes(filteredState.nodes());
217+
builder.nodeIdsToCompatibilityVersions(getCompatibilityVersions(filteredState));
218+
builder.nodeFeatures(getClusterFeatures(filteredState));
215219
}
216220
if (request.routingTable()) {
217221
if (request.indices().length > 0) {
218-
final GlobalRoutingTable.Builder globalRoutingTableBuilder = GlobalRoutingTable.builder(currentState.globalRoutingTable())
222+
final GlobalRoutingTable.Builder globalRoutingTableBuilder = GlobalRoutingTable.builder(filteredState.globalRoutingTable())
219223
.clear();
220-
for (ProjectMetadata project : currentState.metadata().projects().values()) {
221-
RoutingTable projectRouting = currentState.routingTable(project.id());
224+
for (ProjectMetadata project : filteredState.metadata().projects().values()) {
225+
RoutingTable projectRouting = filteredState.routingTable(project.id());
222226
RoutingTable.Builder routingTableBuilder = RoutingTable.builder();
223227
String[] indices = indexNameExpressionResolver.concreteIndexNames(project, request);
224228
for (String filteredIndex : indices) {
@@ -230,18 +234,18 @@ private ClusterStateResponse buildResponse(final ClusterStateRequest request, fi
230234
}
231235
builder.routingTable(globalRoutingTableBuilder.build());
232236
} else {
233-
builder.routingTable(currentState.globalRoutingTable());
237+
builder.routingTable(filteredState.globalRoutingTable());
234238
}
235239
} else {
236240
builder.routingTable(GlobalRoutingTable.builder().build());
237241
}
238242
if (request.blocks()) {
239-
builder.blocks(currentState.blocks());
243+
builder.blocks(filteredState.blocks());
240244
}
241245

242246
Metadata.Builder mdBuilder = Metadata.builder();
243-
mdBuilder.clusterUUID(currentState.metadata().clusterUUID());
244-
mdBuilder.coordinationMetadata(currentState.coordinationMetadata());
247+
mdBuilder.clusterUUID(filteredState.metadata().clusterUUID());
248+
mdBuilder.coordinationMetadata(filteredState.coordinationMetadata());
245249

246250
if (request.metadata()) {
247251
// filter out metadata that shouldn't be returned by the API
@@ -250,14 +254,30 @@ private ClusterStateResponse buildResponse(final ClusterStateRequest request, fi
250254
if (request.indices().length > 0) {
251255
// if the request specified index names, then we don't want the whole metadata, just the version and projects (which will
252256
// be filtered (below) to only include the relevant indices)
253-
mdBuilder.version(currentState.metadata().version());
257+
mdBuilder.version(filteredState.metadata().version());
254258
} else {
255259
// If there are no requested indices, then we want all the metadata, except for customs that aren't exposed via the API
256-
mdBuilder = Metadata.builder(currentState.metadata());
260+
mdBuilder = Metadata.builder(filteredState.metadata());
257261
mdBuilder.removeCustomIf(notApi);
262+
263+
if (projectResolver.supportsMultipleProjects() && request.multiproject() == false) {
264+
ProjectStateRegistry projectStateRegistry = ProjectStateRegistry.get(filteredState);
265+
if (projectStateRegistry.size() > 1) {
266+
throw new Metadata.MultiProjectPendingException(
267+
"There are multiple projects " + projectStateRegistry.knownProjects()
268+
);
269+
}
270+
var reservedStateMetadata = new HashMap<>(filteredState.metadata().reservedStateMetadata());
271+
var singleProjectReservedStateMetadata = projectStateRegistry.reservedStateMetadata(projectResolver.getProjectId());
272+
singleProjectReservedStateMetadata.forEach(
273+
(key, value) -> reservedStateMetadata.merge(key, value, this::mergeReservedStateMetadata)
274+
);
275+
276+
mdBuilder.put(reservedStateMetadata);
277+
}
258278
}
259279

260-
for (ProjectMetadata project : currentState.metadata().projects().values()) {
280+
for (ProjectMetadata project : filteredState.metadata().projects().values()) {
261281
ProjectMetadata.Builder pBuilder;
262282
if (request.indices().length > 0) {
263283
// if the request specified index names, then only include the project-id and indices
@@ -289,7 +309,7 @@ private ClusterStateResponse buildResponse(final ClusterStateRequest request, fi
289309
mdBuilder.put(pBuilder);
290310
}
291311
} else {
292-
for (ProjectId project : currentState.metadata().projects().keySet()) {
312+
for (ProjectId project : filteredState.metadata().projects().keySet()) {
293313
// Request doesn't want to retrieve metadata, so we just fill in empty projects
294314
// (because we can't have a truly empty Metadata)
295315
mdBuilder.put(ProjectMetadata.builder(project));
@@ -298,14 +318,45 @@ private ClusterStateResponse buildResponse(final ClusterStateRequest request, fi
298318
builder.metadata(mdBuilder);
299319

300320
if (request.customs()) {
301-
for (Map.Entry<String, ClusterState.Custom> custom : currentState.customs().entrySet()) {
321+
for (Map.Entry<String, ClusterState.Custom> custom : filteredState.customs().entrySet()) {
302322
if (custom.getValue().isPrivate() == false) {
303323
builder.putCustom(custom.getKey(), custom.getValue());
304324
}
305325
}
306326
}
307327

308-
return new ClusterStateResponse(currentState.getClusterName(), builder.build(), false);
328+
return new ClusterStateResponse(filteredState.getClusterName(), builder.build(), false);
309329
}
310330

331+
private ReservedStateMetadata mergeReservedStateMetadata(
332+
ReservedStateMetadata clusterReservedMetadata,
333+
ReservedStateMetadata projectReservedMetadata
334+
) {
335+
if (Objects.equals(clusterReservedMetadata.version(), projectReservedMetadata.version()) == false) {
336+
logger.info(
337+
"Reserved state metadata version is different for Metadata ({}) and the requested project ({})",
338+
clusterReservedMetadata.version(),
339+
projectReservedMetadata.version()
340+
);
341+
}
342+
ReservedStateMetadata.Builder builder = ReservedStateMetadata.builder(clusterReservedMetadata.namespace())
343+
.version(Math.max(clusterReservedMetadata.version(), projectReservedMetadata.version()));
344+
345+
for (ReservedStateHandlerMetadata handler : clusterReservedMetadata.handlers().values()) {
346+
builder.putHandler(handler);
347+
}
348+
for (Map.Entry<String, ReservedStateHandlerMetadata> handlerEntry : projectReservedMetadata.handlers().entrySet()) {
349+
assert clusterReservedMetadata.handlers().containsKey(handlerEntry.getKey()) == false
350+
: "Duplicate of handler: " + handlerEntry.getKey();
351+
builder.putHandler(handlerEntry.getValue());
352+
}
353+
354+
if (projectReservedMetadata.errorMetadata() != null) {
355+
builder.errorMetadata(projectReservedMetadata.errorMetadata());
356+
} else if (clusterReservedMetadata.errorMetadata() != null) {
357+
builder.errorMetadata(clusterReservedMetadata.errorMetadata());
358+
}
359+
360+
return builder.build();
361+
}
311362
}

server/src/main/java/org/elasticsearch/cluster/project/ProjectStateRegistry.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,8 +204,7 @@ public long getProjectsMarkedForDeletionGeneration() {
204204
return projectsMarkedForDeletionGeneration;
205205
}
206206

207-
// visible for testing
208-
Set<ProjectId> knownProjects() {
207+
public Set<ProjectId> knownProjects() {
209208
return projectsEntries.keySet();
210209
}
211210

server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestClusterStateAction.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ public RestChannelConsumer prepareRequest(final RestRequest request, final NodeC
112112
final Map<String, String> params;
113113
if (request.paramAsBoolean("multi_project", false)) {
114114
params = Map.of(Metadata.CONTEXT_MODE_PARAM, Metadata.CONTEXT_MODE_API, "multi-project", "true");
115+
clusterStateRequest.multiproject(true);
115116
} else {
116117
params = Map.of(Metadata.CONTEXT_MODE_PARAM, Metadata.CONTEXT_MODE_API);
117118
}

server/src/test/java/org/elasticsearch/action/admin/cluster/state/TransportClusterStateActionTests.java

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import org.elasticsearch.cluster.metadata.Metadata;
2020
import org.elasticsearch.cluster.metadata.ProjectId;
2121
import org.elasticsearch.cluster.metadata.ProjectMetadata;
22+
import org.elasticsearch.cluster.metadata.ReservedStateHandlerMetadata;
23+
import org.elasticsearch.cluster.metadata.ReservedStateMetadata;
2224
import org.elasticsearch.cluster.node.VersionInformation;
2325
import org.elasticsearch.cluster.project.DefaultProjectResolver;
2426
import org.elasticsearch.cluster.project.ProjectResolver;
@@ -94,7 +96,7 @@ public void testGetClusterStateWithDefaultProjectOnly() throws Exception {
9496
final ProjectResolver projectResolver = DefaultProjectResolver.INSTANCE;
9597

9698
final Set<String> indexNames = randomSet(1, 8, () -> randomAlphaOfLengthBetween(4, 12));
97-
final ClusterStateRequest request = buildRandomRequest(indexNames);
99+
final ClusterStateRequest request = buildRandomRequest(indexNames, false);
98100
final String[] expectedIndices = getExpectedIndices(request, indexNames);
99101

100102
final ProjectId projectId = Metadata.DEFAULT_PROJECT_ID;
@@ -112,7 +114,7 @@ public void testGetClusterStateForOneProjectOfMany() throws Exception {
112114
final ProjectId projectId = randomUniqueProjectId();
113115

114116
final ProjectResolver projectResolver = TestProjectResolvers.singleProject(projectId);
115-
final ClusterStateRequest request = buildRandomRequest(indexNames);
117+
final ClusterStateRequest request = buildRandomRequest(indexNames, false);
116118
final String[] expectedIndices = getExpectedIndices(request, indexNames);
117119

118120
final int numberOfProjects = randomIntBetween(2, 5);
@@ -141,7 +143,7 @@ public void testGetClusterStateForManyProjects() throws Exception {
141143
final ClusterState state = buildClusterState(projects);
142144

143145
final ProjectResolver projectResolver = TestProjectResolvers.allProjects();
144-
final ClusterStateRequest request = buildRandomRequest(indexNames);
146+
final ClusterStateRequest request = buildRandomRequest(indexNames, true);
145147
final Set<String> requestedIndices = Set.of(getExpectedIndices(request, indexNames));
146148

147149
final ClusterStateResponse response = executeAction(projectResolver, request, state);
@@ -190,6 +192,22 @@ private static void assertSingleProjectResponse(
190192
assertThat(metadata.projects().keySet(), contains(projectId));
191193
if (request.metadata()) {
192194
assertThat(metadata.getProject(projectId).indices().keySet(), containsInAnyOrder(expectedIndices));
195+
196+
if (request.indices().length == 0) {
197+
Map<String, ReservedStateMetadata> reservedStateMetadataMap = metadata.reservedStateMetadata();
198+
assertThat(reservedStateMetadataMap, aMapWithSize(1));
199+
ReservedStateMetadata fileSettings = reservedStateMetadataMap.get("file_settings");
200+
assertNotNull(fileSettings);
201+
assertThat(fileSettings.version(), equalTo(43L));
202+
Map<String, ReservedStateHandlerMetadata> handlers = fileSettings.handlers();
203+
assertThat(handlers, aMapWithSize(2));
204+
ReservedStateHandlerMetadata clusterSettingsHandler = handlers.get("cluster_settings");
205+
assertNotNull(clusterSettingsHandler);
206+
assertThat(clusterSettingsHandler.keys(), containsInAnyOrder("setting_1", "setting_2"));
207+
ReservedStateHandlerMetadata projectSettingsHandler = handlers.get("project_settings");
208+
assertNotNull(projectSettingsHandler);
209+
assertThat(projectSettingsHandler.keys(), containsInAnyOrder("setting_1"));
210+
}
193211
} else {
194212
assertThat(metadata.getProject(projectId).indices(), anEmptyMap());
195213
}
@@ -235,7 +253,7 @@ private static String[] getExpectedIndices(ClusterStateRequest request, Set<Stri
235253
}
236254
}
237255

238-
private static ClusterStateRequest buildRandomRequest(Set<String> indexNames) {
256+
private static ClusterStateRequest buildRandomRequest(Set<String> indexNames, boolean multipleProjects) {
239257
final ClusterStateRequest request = new ClusterStateRequest(TEST_REQUEST_TIMEOUT);
240258
if (randomBoolean()) {
241259
final int numberSelectedIndices = randomIntBetween(1, indexNames.size());
@@ -248,18 +266,31 @@ private static ClusterStateRequest buildRandomRequest(Set<String> indexNames) {
248266
request.routingTable(randomBoolean());
249267
request.blocks(randomBoolean());
250268
request.customs(true);
269+
request.multiproject(multipleProjects);
251270
return request;
252271
}
253272

254273
private static ClusterState buildClusterState(ProjectMetadata.Builder... projects) {
255274
final Metadata.Builder metadataBuilder = Metadata.builder();
275+
metadataBuilder.put(
276+
ReservedStateMetadata.builder("file_settings")
277+
.version(43L)
278+
.putHandler(new ReservedStateHandlerMetadata("cluster_settings", Set.of("setting_1", "setting_2")))
279+
.build()
280+
);
256281
Arrays.stream(projects).forEach(metadataBuilder::put);
257282
final var metadata = metadataBuilder.build();
258283

259284
ClusterState.Builder csBuilder = ClusterState.builder(new ClusterName(randomAlphaOfLengthBetween(4, 12)));
260285
ProjectStateRegistry.Builder psBuilder = ProjectStateRegistry.builder();
261286
for (ProjectMetadata.Builder project : projects) {
262-
psBuilder.putProjectSettings(project.getId(), Settings.builder().put("setting_1", randomIdentifier()).build());
287+
psBuilder.putReservedStateMetadata(
288+
project.getId(),
289+
ReservedStateMetadata.builder("file_settings")
290+
.version(43L)
291+
.putHandler(new ReservedStateHandlerMetadata("project_settings", Set.of("setting_1")))
292+
.build()
293+
).putProjectSettings(project.getId(), Settings.builder().put("setting_1", randomIdentifier()).build());
263294
}
264295
return csBuilder.metadata(metadata)
265296
.routingTable(GlobalRoutingTableTestHelper.buildRoutingTable(metadata, RoutingTable.Builder::addAsNew))

0 commit comments

Comments
 (0)