Skip to content

Add projectsDelta method to ClusterChangedEvent #127697

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@
import org.elasticsearch.cluster.metadata.IndexGraveyard.IndexGraveyardDiff;
import org.elasticsearch.cluster.metadata.IndexMetadata;
import org.elasticsearch.cluster.metadata.Metadata;
import org.elasticsearch.cluster.metadata.ProjectId;
import org.elasticsearch.cluster.metadata.ProjectMetadata;
import org.elasticsearch.cluster.node.DiscoveryNodes;
import org.elasticsearch.cluster.routing.IndexRoutingTable;
import org.elasticsearch.common.util.set.Sets;
import org.elasticsearch.gateway.GatewayService;
import org.elasticsearch.index.Index;

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

private final DiscoveryNodes.Delta nodesDelta;

private final ProjectsDelta projectsDelta;

public ClusterChangedEvent(String source, ClusterState state, ClusterState previousState) {
Objects.requireNonNull(source, "source must not be null");
Objects.requireNonNull(state, "state must not be null");
Expand All @@ -49,6 +53,7 @@ public ClusterChangedEvent(String source, ClusterState state, ClusterState previ
this.state = state;
this.previousState = previousState;
this.nodesDelta = state.nodes().delta(previousState.nodes());
this.projectsDelta = calculateProjectDelta(previousState.metadata(), state.metadata());
}

/**
Expand Down Expand Up @@ -237,6 +242,13 @@ public boolean nodesChanged() {
return nodesRemoved() || nodesAdded();
}

/**
* Returns the {@link ProjectsDelta} between the previous cluster state and the new cluster state.
*/
public ProjectsDelta projectDelta() {
return projectsDelta;
}

/**
* Determines whether or not the current cluster state represents an entirely
* new cluster, either when a node joins a cluster for the first time or when
Expand Down Expand Up @@ -336,4 +348,32 @@ private List<Index> indicesDeletedFromTombstones() {
.toList();
}

private static ProjectsDelta calculateProjectDelta(Metadata previousMetadata, Metadata currentMetadata) {
if (previousMetadata == currentMetadata
|| (previousMetadata.projects().size() == 1
&& previousMetadata.hasProject(ProjectId.DEFAULT)
&& currentMetadata.projects().size() == 1
&& currentMetadata.hasProject(ProjectId.DEFAULT))) {
return ProjectsDelta.EMPTY;
}

final Set<ProjectId> added = Collections.unmodifiableSet(
Sets.difference(currentMetadata.projects().keySet(), previousMetadata.projects().keySet())
);
final Set<ProjectId> removed = Collections.unmodifiableSet(
Sets.difference(previousMetadata.projects().keySet(), currentMetadata.projects().keySet())
);
// TODO: Enable the following assertions once tests no longer add or remove default projects
// assert added.contains(ProjectId.DEFAULT) == false;
// assert removed.contains(ProjectId.DEFAULT) == false;
return new ProjectsDelta(added, removed);
}

public record ProjectsDelta(Set<ProjectId> added, Set<ProjectId> removed) {
private static final ProjectsDelta EMPTY = new ProjectsDelta(Set.of(), Set.of());

public boolean isEmpty() {
return added.isEmpty() && removed.isEmpty();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
import org.elasticsearch.cluster.metadata.IndexGraveyard;
import org.elasticsearch.cluster.metadata.IndexMetadata;
import org.elasticsearch.cluster.metadata.Metadata;
import org.elasticsearch.cluster.metadata.ProjectId;
import org.elasticsearch.cluster.metadata.ProjectMetadata;
import org.elasticsearch.cluster.metadata.ReservedStateMetadata;
import org.elasticsearch.cluster.node.DiscoveryNode;
import org.elasticsearch.cluster.node.DiscoveryNodeRole;
import org.elasticsearch.cluster.node.DiscoveryNodeUtils;
Expand Down Expand Up @@ -520,6 +522,65 @@ public void testChangedCustomMetadataSetMultiProject() {
assertEquals(Set.of(IndexGraveyard.TYPE, project2Custom.getWriteableName()), event.changedCustomProjectMetadataSet());
}

public void testProjectsDelta() {
final var state0 = ClusterState.builder(TEST_CLUSTER_NAME).build();

// No project changes
final var state1 = ClusterState.builder(state0)
.metadata(Metadata.builder(state0.metadata()).put(ReservedStateMetadata.builder("test").build()))
.build();
ClusterChangedEvent event = new ClusterChangedEvent("test", state1, state0);
assertTrue(event.projectDelta().isEmpty());

// Add projects
final List<ProjectId> projectIds = randomList(1, 5, ESTestCase::randomUniqueProjectId);
Metadata.Builder metadataBuilder = Metadata.builder(state1.metadata());
for (ProjectId projectId : projectIds) {
metadataBuilder.put(ProjectMetadata.builder(projectId));
}
final var state2 = ClusterState.builder(state1).metadata(metadataBuilder.build()).build();
event = new ClusterChangedEvent("test", state2, state1);
assertThat(event.projectDelta().added(), containsInAnyOrder(projectIds.toArray()));
assertThat(event.projectDelta().removed(), empty());

// Add more projects and delete one
final var removedProjectIds = randomNonEmptySubsetOf(projectIds);
final List<ProjectId> moreProjectIds = randomList(1, 3, ESTestCase::randomUniqueProjectId);
metadataBuilder = Metadata.builder(state2.metadata());
GlobalRoutingTable.Builder routingTableBuilder = GlobalRoutingTable.builder(state2.globalRoutingTable());
for (ProjectId projectId : removedProjectIds) {
metadataBuilder.removeProject(projectId);
routingTableBuilder.removeProject(projectId);
}
for (ProjectId projectId : moreProjectIds) {
metadataBuilder.put(ProjectMetadata.builder(projectId));
}

final var state3 = ClusterState.builder(state2).metadata(metadataBuilder.build()).routingTable(routingTableBuilder.build()).build();

event = new ClusterChangedEvent("test", state3, state2);
assertThat(event.projectDelta().added(), containsInAnyOrder(moreProjectIds.toArray()));
assertThat(event.projectDelta().removed(), containsInAnyOrder(removedProjectIds.toArray()));

// Remove all projects
final List<ProjectId> remainingProjects = state3.metadata()
.projects()
.keySet()
.stream()
.filter(projectId -> ProjectId.DEFAULT.equals(projectId) == false)
.toList();
metadataBuilder = Metadata.builder(state3.metadata());
routingTableBuilder = GlobalRoutingTable.builder(state3.globalRoutingTable());
for (ProjectId projectId : remainingProjects) {
metadataBuilder.removeProject(projectId);
routingTableBuilder.removeProject(projectId);
}
final var state4 = ClusterState.builder(state3).metadata(metadataBuilder.build()).routingTable(routingTableBuilder.build()).build();
event = new ClusterChangedEvent("test", state4, state3);
assertThat(event.projectDelta().added(), empty());
assertThat(event.projectDelta().removed(), containsInAnyOrder(remainingProjects.toArray()));
}

private static class CustomClusterMetadata2 extends TestClusterCustomMetadata {
protected CustomClusterMetadata2(String data) {
super(data);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ public void testRefreshCacheVersionOnMasterNode() {
ClusterState clusterState = mock(ClusterState.class);
when(clusterState.clusterRecovered()).thenReturn(true);
when(clusterState.nodes()).thenReturn(clusterNodes);
when(clusterState.metadata()).thenReturn(Metadata.EMPTY_METADATA);

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -330,8 +330,8 @@ public void testSecurityIndexDeleted() {

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

verify(previousClusterState, times(1)).metadata();
verify(currentClusterState, times(1)).metadata();
verify(previousClusterState, times(2)).metadata();
verify(currentClusterState, times(2)).metadata();
verifyNoMoreInteractions(nativeRolesStore, featureService, taskQueue, reservedRolesProvider, threadPool, clusterService);
}

Expand Down