Skip to content

Commit bc34ae9

Browse files
authored
Add projectsDelta method to ClusterChangedEvent (#127697)
Similar to nodesDelta, the projectsDelta provides a centrailzed method to tell the added and removed projects so that cluster listeners/appliers do not need to compute it individually. Note the delta currently does not have updated. This needs to wait till we stop eagerly creating ProjectMetadata.Builder in Metadata.Builder.
1 parent c2561b5 commit bc34ae9

File tree

4 files changed

+104
-2
lines changed

4 files changed

+104
-2
lines changed

server/src/main/java/org/elasticsearch/cluster/ClusterChangedEvent.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@
1313
import org.elasticsearch.cluster.metadata.IndexGraveyard.IndexGraveyardDiff;
1414
import org.elasticsearch.cluster.metadata.IndexMetadata;
1515
import org.elasticsearch.cluster.metadata.Metadata;
16+
import org.elasticsearch.cluster.metadata.ProjectId;
1617
import org.elasticsearch.cluster.metadata.ProjectMetadata;
1718
import org.elasticsearch.cluster.node.DiscoveryNodes;
1819
import org.elasticsearch.cluster.routing.IndexRoutingTable;
20+
import org.elasticsearch.common.util.set.Sets;
1921
import org.elasticsearch.gateway.GatewayService;
2022
import org.elasticsearch.index.Index;
2123

@@ -41,6 +43,8 @@ public class ClusterChangedEvent {
4143

4244
private final DiscoveryNodes.Delta nodesDelta;
4345

46+
private final ProjectsDelta projectsDelta;
47+
4448
public ClusterChangedEvent(String source, ClusterState state, ClusterState previousState) {
4549
Objects.requireNonNull(source, "source must not be null");
4650
Objects.requireNonNull(state, "state must not be null");
@@ -49,6 +53,7 @@ public ClusterChangedEvent(String source, ClusterState state, ClusterState previ
4953
this.state = state;
5054
this.previousState = previousState;
5155
this.nodesDelta = state.nodes().delta(previousState.nodes());
56+
this.projectsDelta = calculateProjectDelta(previousState.metadata(), state.metadata());
5257
}
5358

5459
/**
@@ -237,6 +242,13 @@ public boolean nodesChanged() {
237242
return nodesRemoved() || nodesAdded();
238243
}
239244

245+
/**
246+
* Returns the {@link ProjectsDelta} between the previous cluster state and the new cluster state.
247+
*/
248+
public ProjectsDelta projectDelta() {
249+
return projectsDelta;
250+
}
251+
240252
/**
241253
* Determines whether or not the current cluster state represents an entirely
242254
* new cluster, either when a node joins a cluster for the first time or when
@@ -336,4 +348,32 @@ private List<Index> indicesDeletedFromTombstones() {
336348
.toList();
337349
}
338350

351+
private static ProjectsDelta calculateProjectDelta(Metadata previousMetadata, Metadata currentMetadata) {
352+
if (previousMetadata == currentMetadata
353+
|| (previousMetadata.projects().size() == 1
354+
&& previousMetadata.hasProject(ProjectId.DEFAULT)
355+
&& currentMetadata.projects().size() == 1
356+
&& currentMetadata.hasProject(ProjectId.DEFAULT))) {
357+
return ProjectsDelta.EMPTY;
358+
}
359+
360+
final Set<ProjectId> added = Collections.unmodifiableSet(
361+
Sets.difference(currentMetadata.projects().keySet(), previousMetadata.projects().keySet())
362+
);
363+
final Set<ProjectId> removed = Collections.unmodifiableSet(
364+
Sets.difference(previousMetadata.projects().keySet(), currentMetadata.projects().keySet())
365+
);
366+
// TODO: Enable the following assertions once tests no longer add or remove default projects
367+
// assert added.contains(ProjectId.DEFAULT) == false;
368+
// assert removed.contains(ProjectId.DEFAULT) == false;
369+
return new ProjectsDelta(added, removed);
370+
}
371+
372+
public record ProjectsDelta(Set<ProjectId> added, Set<ProjectId> removed) {
373+
private static final ProjectsDelta EMPTY = new ProjectsDelta(Set.of(), Set.of());
374+
375+
public boolean isEmpty() {
376+
return added.isEmpty() && removed.isEmpty();
377+
}
378+
}
339379
}

server/src/test/java/org/elasticsearch/cluster/ClusterChangedEventTests.java

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
import org.elasticsearch.cluster.metadata.IndexGraveyard;
1515
import org.elasticsearch.cluster.metadata.IndexMetadata;
1616
import org.elasticsearch.cluster.metadata.Metadata;
17+
import org.elasticsearch.cluster.metadata.ProjectId;
1718
import org.elasticsearch.cluster.metadata.ProjectMetadata;
19+
import org.elasticsearch.cluster.metadata.ReservedStateMetadata;
1820
import org.elasticsearch.cluster.node.DiscoveryNode;
1921
import org.elasticsearch.cluster.node.DiscoveryNodeRole;
2022
import org.elasticsearch.cluster.node.DiscoveryNodeUtils;
@@ -520,6 +522,65 @@ public void testChangedCustomMetadataSetMultiProject() {
520522
assertEquals(Set.of(IndexGraveyard.TYPE, project2Custom.getWriteableName()), event.changedCustomProjectMetadataSet());
521523
}
522524

525+
public void testProjectsDelta() {
526+
final var state0 = ClusterState.builder(TEST_CLUSTER_NAME).build();
527+
528+
// No project changes
529+
final var state1 = ClusterState.builder(state0)
530+
.metadata(Metadata.builder(state0.metadata()).put(ReservedStateMetadata.builder("test").build()))
531+
.build();
532+
ClusterChangedEvent event = new ClusterChangedEvent("test", state1, state0);
533+
assertTrue(event.projectDelta().isEmpty());
534+
535+
// Add projects
536+
final List<ProjectId> projectIds = randomList(1, 5, ESTestCase::randomUniqueProjectId);
537+
Metadata.Builder metadataBuilder = Metadata.builder(state1.metadata());
538+
for (ProjectId projectId : projectIds) {
539+
metadataBuilder.put(ProjectMetadata.builder(projectId));
540+
}
541+
final var state2 = ClusterState.builder(state1).metadata(metadataBuilder.build()).build();
542+
event = new ClusterChangedEvent("test", state2, state1);
543+
assertThat(event.projectDelta().added(), containsInAnyOrder(projectIds.toArray()));
544+
assertThat(event.projectDelta().removed(), empty());
545+
546+
// Add more projects and delete one
547+
final var removedProjectIds = randomNonEmptySubsetOf(projectIds);
548+
final List<ProjectId> moreProjectIds = randomList(1, 3, ESTestCase::randomUniqueProjectId);
549+
metadataBuilder = Metadata.builder(state2.metadata());
550+
GlobalRoutingTable.Builder routingTableBuilder = GlobalRoutingTable.builder(state2.globalRoutingTable());
551+
for (ProjectId projectId : removedProjectIds) {
552+
metadataBuilder.removeProject(projectId);
553+
routingTableBuilder.removeProject(projectId);
554+
}
555+
for (ProjectId projectId : moreProjectIds) {
556+
metadataBuilder.put(ProjectMetadata.builder(projectId));
557+
}
558+
559+
final var state3 = ClusterState.builder(state2).metadata(metadataBuilder.build()).routingTable(routingTableBuilder.build()).build();
560+
561+
event = new ClusterChangedEvent("test", state3, state2);
562+
assertThat(event.projectDelta().added(), containsInAnyOrder(moreProjectIds.toArray()));
563+
assertThat(event.projectDelta().removed(), containsInAnyOrder(removedProjectIds.toArray()));
564+
565+
// Remove all projects
566+
final List<ProjectId> remainingProjects = state3.metadata()
567+
.projects()
568+
.keySet()
569+
.stream()
570+
.filter(projectId -> ProjectId.DEFAULT.equals(projectId) == false)
571+
.toList();
572+
metadataBuilder = Metadata.builder(state3.metadata());
573+
routingTableBuilder = GlobalRoutingTable.builder(state3.globalRoutingTable());
574+
for (ProjectId projectId : remainingProjects) {
575+
metadataBuilder.removeProject(projectId);
576+
routingTableBuilder.removeProject(projectId);
577+
}
578+
final var state4 = ClusterState.builder(state3).metadata(metadataBuilder.build()).routingTable(routingTableBuilder.build()).build();
579+
event = new ClusterChangedEvent("test", state4, state3);
580+
assertThat(event.projectDelta().added(), empty());
581+
assertThat(event.projectDelta().removed(), containsInAnyOrder(remainingProjects.toArray()));
582+
}
583+
523584
private static class CustomClusterMetadata2 extends TestClusterCustomMetadata {
524585
protected CustomClusterMetadata2(String data) {
525586
super(data);

x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/persistence/TrainedModelCacheMetadataServiceTests.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ public void testRefreshCacheVersionOnMasterNode() {
7474
ClusterState clusterState = mock(ClusterState.class);
7575
when(clusterState.clusterRecovered()).thenReturn(true);
7676
when(clusterState.nodes()).thenReturn(clusterNodes);
77+
when(clusterState.metadata()).thenReturn(Metadata.EMPTY_METADATA);
7778

7879
modelCacheMetadataService.clusterChanged(new ClusterChangedEvent("test", clusterState, ClusterState.EMPTY_STATE));
7980

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/QueryableBuiltInRolesSynchronizerTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -330,8 +330,8 @@ public void testSecurityIndexDeleted() {
330330

331331
synchronizer.clusterChanged(event(currentClusterState, previousClusterState));
332332

333-
verify(previousClusterState, times(1)).metadata();
334-
verify(currentClusterState, times(1)).metadata();
333+
verify(previousClusterState, times(2)).metadata();
334+
verify(currentClusterState, times(2)).metadata();
335335
verifyNoMoreInteractions(nativeRolesStore, featureService, taskQueue, reservedRolesProvider, threadPool, clusterService);
336336
}
337337

0 commit comments

Comments
 (0)