Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
df3d4d6
Projects reserved state is moved to ProjectStateRegistry
alexey-ivanov-es Aug 1, 2025
d136933
Change the order of fields in a serialised version for consistency
alexey-ivanov-es Aug 1, 2025
97850c9
Merge branch 'main' into ES-12183
alexey-ivanov-es Aug 1, 2025
18d6a0c
Merge branch 'main' into ES-12183
alexey-ivanov-es Aug 1, 2025
6088f4a
[CI] Auto commit changes from spotless
Aug 1, 2025
84b0c9e
Fix build
alexey-ivanov-es Aug 1, 2025
a18c390
Fix tests
alexey-ivanov-es Aug 1, 2025
d96df29
[CI] Auto commit changes from spotless
Aug 1, 2025
9045018
Fix tests
alexey-ivanov-es Aug 1, 2025
7fd7234
Merge branch 'main' into ES-12183
alexey-ivanov-es Aug 1, 2025
1a49726
[CI] Auto commit changes from spotless
Aug 1, 2025
79d943a
Merge branch 'main' into ES-12183
alexey-ivanov-es Aug 1, 2025
4156e8f
Address some review comments
alexey-ivanov-es Aug 4, 2025
e3911dd
[CI] Auto commit changes from spotless
Aug 4, 2025
ff8eb63
Merge branch 'main' into ES-12183
alexey-ivanov-es Aug 8, 2025
a869c06
Address review comments
alexey-ivanov-es Aug 21, 2025
1dcc91a
Merge branch 'main' into ES-12183
alexey-ivanov-es Aug 21, 2025
9198794
Merge branch 'main' into ES-12183
alexey-ivanov-es Aug 21, 2025
7c54ef3
Fix projects state registry filtering in GetClusterState API
alexey-ivanov-es Aug 22, 2025
28b46ce
Update docs/changelog/133401.yaml
alexey-ivanov-es Aug 22, 2025
44ade8f
Merge branch 'main' into fix/project_state_registry_filter
alexey-ivanov-es Aug 22, 2025
f33aa53
Delete docs/changelog/133401.yaml
alexey-ivanov-es Aug 22, 2025
8152173
[CI] Auto commit changes from spotless
Aug 22, 2025
e750926
Merge remote-tracking branch 'upstream/main' into ES-12183
alexey-ivanov-es Aug 22, 2025
8237c89
Merge remote-tracking branch 'origin/fix/project_state_registry_filte…
alexey-ivanov-es Aug 22, 2025
7bf5c23
Merge branch 'main' into ES-12183
alexey-ivanov-es Aug 26, 2025
5c9b780
Merge remote-tracking branch 'origin/ES-12183' into ES-12183
alexey-ivanov-es Aug 26, 2025
b473065
Address minor review comments
alexey-ivanov-es Aug 28, 2025
8ec5bdc
Merge branch 'main' into ES-12183
alexey-ivanov-es Aug 28, 2025
b227579
Merge branch 'main' into ES-12183
alexey-ivanov-es Aug 29, 2025
d181842
Merge remote-tracking branch 'upstream/main' into ES-12183
alexey-ivanov-es Aug 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,7 @@ static TransportVersion def(int id) {
public static final TransportVersion ESQL_SAMPLE_OPERATOR_STATUS = def(9_127_0_00);
public static final TransportVersion ALLOCATION_DECISION_NOT_PREFERRED = def(9_145_0_00);
public static final TransportVersion ESQL_QUALIFIERS_IN_ATTRIBUTES = def(9_146_0_00);
public static final TransportVersion PROJECT_RESERVED_STATE_MOVE_TO_REGISTRY = def(9_147_0_00);

/*
* STOP! READ THIS FIRST! No, really,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.elasticsearch.cluster.block.ClusterBlockException;
import org.elasticsearch.cluster.block.ClusterBlockLevel;
import org.elasticsearch.cluster.project.ProjectResolver;
import org.elasticsearch.cluster.project.ProjectStateRegistry;
import org.elasticsearch.cluster.service.ClusterService;
import org.elasticsearch.common.util.concurrent.EsExecutors;
import org.elasticsearch.injection.guice.Inject;
Expand Down Expand Up @@ -91,7 +92,7 @@ protected void validateForReservedState(DeleteRepositoryRequest request, Cluster
super.validateForReservedState(request, state);

validateForReservedState(
projectResolver.getProjectMetadata(state).reservedStateMetadata().values(),
ProjectStateRegistry.get(state).reservedStateMetadata(projectResolver.getProjectId()).values(),
reservedStateHandlerName().get(),
modifiedKeys(request),
request.toString()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.elasticsearch.cluster.block.ClusterBlockLevel;
import org.elasticsearch.cluster.metadata.MetadataIndexTemplateService;
import org.elasticsearch.cluster.project.ProjectResolver;
import org.elasticsearch.cluster.project.ProjectStateRegistry;
import org.elasticsearch.cluster.service.ClusterService;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.io.stream.StreamInput;
Expand Down Expand Up @@ -95,7 +96,7 @@ protected void validateForReservedState(Request request, ClusterState state) {
super.validateForReservedState(request, state);

validateForReservedState(
projectResolver.getProjectMetadata(state).reservedStateMetadata().values(),
ProjectStateRegistry.get(state).reservedStateMetadata(projectResolver.getProjectId()).values(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add a convenience method to get reservedStateMetadata to ProjectState as we've done for settings so this could just be projectResolver.getProjectState(state).reservedStateMetadata()?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not strongly oppose it, but I don't think reserved state metadata is needed in a project state - for example, Settings can be used by many components, but there are not so many things that are interested in the reserved state

reservedStateHandlerName().get(),
modifiedKeys(request),
request.toString()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.elasticsearch.cluster.metadata.MetadataIndexTemplateService;
import org.elasticsearch.cluster.metadata.Template;
import org.elasticsearch.cluster.project.ProjectResolver;
import org.elasticsearch.cluster.project.ProjectStateRegistry;
import org.elasticsearch.cluster.service.ClusterService;
import org.elasticsearch.common.settings.IndexScopedSettings;
import org.elasticsearch.common.settings.Settings;
Expand Down Expand Up @@ -130,7 +131,7 @@ protected void validateForReservedState(PutComponentTemplateAction.Request reque
super.validateForReservedState(request, state);

validateForReservedState(
projectResolver.getProjectMetadata(state).reservedStateMetadata().values(),
ProjectStateRegistry.get(state).reservedStateMetadata(projectResolver.getProjectId()).values(),
reservedStateHandlerName().get(),
modifiedKeys(request),
request.toString()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.elasticsearch.cluster.metadata.ProjectId;
import org.elasticsearch.cluster.metadata.ReservedStateMetadata;
import org.elasticsearch.cluster.project.ProjectResolver;
import org.elasticsearch.cluster.project.ProjectStateRegistry;
import org.elasticsearch.cluster.service.ClusterService;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.io.stream.StreamInput;
Expand Down Expand Up @@ -87,7 +88,10 @@ protected void masterOperation(
) {
ProjectId projectId = projectResolver.getProjectId();
verifyIfUsingReservedComponentTemplates(request, state.metadata().reservedStateMetadata().values());
verifyIfUsingReservedComponentTemplates(request, state.metadata().getProject(projectId).reservedStateMetadata().values());
verifyIfUsingReservedComponentTemplates(
request,
ProjectStateRegistry.get(state).reservedStateMetadata(projectResolver.getProjectId()).values()
);
ComposableIndexTemplate indexTemplate = request.indexTemplate();
indexTemplateService.putIndexTemplateV2(
request.cause(),
Expand Down Expand Up @@ -138,7 +142,7 @@ protected void validateForReservedState(Request request, ClusterState state) {
super.validateForReservedState(request, state);

validateForReservedState(
projectResolver.getProjectMetadata(state).reservedStateMetadata().values(),
ProjectStateRegistry.get(state).reservedStateMetadata(projectResolver.getProjectId()).values(),
reservedStateHandlerName().get(),
modifiedKeys(request),
request.toString()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import org.elasticsearch.cluster.block.ClusterBlockException;
import org.elasticsearch.cluster.block.ClusterBlockLevel;
import org.elasticsearch.cluster.project.ProjectResolver;
import org.elasticsearch.cluster.project.ProjectStateRegistry;
import org.elasticsearch.common.util.concurrent.EsExecutors;
import org.elasticsearch.ingest.IngestService;
import org.elasticsearch.injection.guice.Inject;
Expand Down Expand Up @@ -80,7 +81,7 @@ protected void validateForReservedState(PutPipelineRequest request, ClusterState
super.validateForReservedState(request, state);

validateForReservedState(
projectResolver.getProjectMetadata(state).reservedStateMetadata().values(),
ProjectStateRegistry.get(state).reservedStateMetadata(projectResolver.getProjectId()).values(),
reservedStateHandlerName().get(),
modifiedKeys(request),
request.toString()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.BiConsumer;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
Expand Down Expand Up @@ -799,12 +798,6 @@ private Iterator<? extends ToXContent> toXContentChunkedWithSingleProjectFormat(
@FixForMultiProject
final ProjectMetadata project = projectMetadata.values().iterator().next();

// need to combine reserved state together into a single block so we don't get duplicate keys
// and not include it in the project xcontent output (through the lack of multi-project params)
// use a tree map so the order is deterministic
final Map<String, ReservedStateMetadata> clusterReservedState = new TreeMap<>(reservedStateMetadata);
clusterReservedState.putAll(project.reservedStateMetadata());
Comment on lines -802 to -806
Copy link
Member

@ywangd ywangd Aug 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The intention of this code is keep the output from the GetClusterState API identical in non-MP and MP (when the API is invoked via a project URL, i.e. single project context). Removing it without any replace means the responses will now differ, i.e.

// non-MP
{
  "metadata": {
    "reserved_state": { ... }
  }
}

vs

// MP when a single project is requested
{
  "metadata": { ... },
  "projects_registry": {
    "projects": [
      { 
        "reserved_state": { ... }
      }
    ]
  }
}

Maybe it's OK for them to be different. But we need to make an explicit decision here.

Also, there is an existing issue that we don't filter ProjectStateRegistry for the requested project in TransportClusterStateAction so that it currently shows all projects when only a single project is requested. (EDIT: This is not an unique issue for ProjectStateRegistry. For example, blocks also have the same issue. So no need to fix it in this PR. The other point about different output formats between stateful and MP still stands).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On a second thought, the difference in ClusterState API output format between non-MP and MP setups can also be addressed in future PRs. There should not be any BWC concern to change the MP output in future. So nothing is needed in this PR other than maybe raise a JIRA issue so that we don't forget about it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for highlighting this. I'll address the project filtering issue in a separate PR since it's straightforward, and I don't want to leave it for later.
Regarding the reserved state looking the same as before for a single project: we still have Metadata-level reserved state for cluster-level reserved state, and for all existing clusters it will still contain all the reserved state

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll address the project filtering issue in a separate PR

👍

Regarding the reserved state looking the same as before for a single project: we still have Metadata-level reserved state for cluster-level reserved state, and for all existing clusters it will still contain all the reserved state

Yes it is not an issue for existing clusters (non-MP). My question is about MP cluster. We have a general principle that the API output for a single project should be the same regardless whether the cluster is non-MP or MP. For the GetClusterState API, non-MP already sets the convention that reserved state is printed under Metadata. So my question is whether a MP cluster should always do the same as oppose to print them under cluster level customs. Note we already intentionally hide the whole concept of ProjectMetadata in the output from a MP cluster with the intention to keep it consistent with non-MP output.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR for the filtering issue: #133401

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it is not an issue for existing clusters (non-MP). My question is about MP cluster. We have a general principle that the API output for a single project should be the same regardless whether the cluster is non-MP or MP. For the GetClusterState API, non-MP already sets the convention that reserved state is printed under Metadata. So my question is whether a MP cluster should always do the same as oppose to print them under cluster level customs. Note we already intentionally hide the whole concept of ProjectMetadata in the output from a MP cluster with the intention to keep it consistent with non-MP output.

I see now, thanks for the clarification. I'm a bit concerned we're going to make a mistake in one of these many branches eventually, but I'll support the convention. However, I'd prefer to handle it at the transport action level. I think that's cleaner, and I believe we don't want to store project's reserved state metadata to disk

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll do it in a separate PR, because turned out that I need changes from both this PR and #133401 😕

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer to handle it at the transport action level

That might be an option as well. I don't have strong opinion.

I'll do it in a separate PR

Yeah that's what I meant. No need for this PR. Since it affects only MP cluster, there is no real BWC either.


// Similarly, combine cluster and project persistent tasks and report them under a single key
Iterator<ToXContent> customs = Iterators.flatMap(customs().entrySet().iterator(), entry -> {
if (entry.getValue().context().contains(context) && ClusterPersistentTasksCustomMetadata.TYPE.equals(entry.getKey()) == false) {
Expand All @@ -824,13 +817,20 @@ private Iterator<? extends ToXContent> toXContentChunkedWithSingleProjectFormat(
);
}

// make order deterministic
Iterator<ReservedStateMetadata> reservedStateMetadataIterator = reservedStateMetadata.entrySet()
.stream()
.sorted(Map.Entry.comparingByKey())
.map(Map.Entry::getValue)
.iterator();

return Iterators.concat(
start,
clusterCoordination,
persistentSettings,
project.toXContentChunked(p),
customs,
ChunkedToXContentHelper.object("reserved_state", clusterReservedState.values().iterator()),
ChunkedToXContentHelper.object("reserved_state", reservedStateMetadataIterator),
ChunkedToXContentHelper.endObject()
);
}
Expand All @@ -845,6 +845,7 @@ private static class MetadataDiff implements Diff<Metadata> {
private final Settings persistentSettings;
private final Diff<DiffableStringMap> hashesOfConsistentSettings;
private final ProjectMetadata.ProjectMetadataDiff singleProject;

private final MapDiff<ProjectId, ProjectMetadata, Map<ProjectId, ProjectMetadata>> multiProject;
private final MapDiff<String, ClusterCustom, ImmutableOpenMap<String, ClusterCustom>> clusterCustoms;
private final MapDiff<String, ReservedStateMetadata, ImmutableOpenMap<String, ReservedStateMetadata>> reservedStateMetadata;
Expand Down Expand Up @@ -981,7 +982,7 @@ private MetadataDiff(StreamInput in) throws IOException {
RESERVED_DIFF_VALUE_READER
);

singleProject = new ProjectMetadata.ProjectMetadataDiff(indices, templates, projectCustoms, DiffableUtils.emptyDiff());
singleProject = new ProjectMetadata.ProjectMetadataDiff(indices, templates, projectCustoms);
multiProject = null;
} else {
fromNodeBeforeMultiProjectsSupport = false;
Expand Down Expand Up @@ -1048,7 +1049,7 @@ public void writeTo(StreamOutput out) throws IOException {
singleProject.indices().writeTo(out);
singleProject.templates().writeTo(out);
buildUnifiedCustomDiff().writeTo(out);
buildUnifiedReservedStateMetadataDiff().writeTo(out);
reservedStateMetadata.writeTo(out);
} else {
clusterCustoms.writeTo(out);
reservedStateMetadata.writeTo(out);
Expand Down Expand Up @@ -1094,15 +1095,6 @@ public void writeTo(StreamOutput out) throws IOException {
}
}

private Diff<Map<String, ReservedStateMetadata>> buildUnifiedReservedStateMetadataDiff() {
return DiffableUtils.merge(
reservedStateMetadata,
singleProject.reservedStateMetadata(),
DiffableUtils.getStringKeySerializer(),
RESERVED_DIFF_VALUE_READER
);
}

@Override
public Metadata apply(Metadata part) {
if (empty) {
Expand Down Expand Up @@ -1304,12 +1296,7 @@ public void writeTo(StreamOutput out) throws IOException {
);
VersionedNamedWriteable.writeVersionedWriteables(out, combinedCustoms);

List<ReservedStateMetadata> combinedMetadata = new ArrayList<>(
reservedStateMetadata.size() + singleProject.reservedStateMetadata().size()
);
combinedMetadata.addAll(reservedStateMetadata.values());
combinedMetadata.addAll(singleProject.reservedStateMetadata().values());
out.writeCollection(combinedMetadata);
out.writeCollection(reservedStateMetadata.values());
} else {
VersionedNamedWriteable.writeVersionedWriteables(out, customs.values());

Expand Down
Loading