Browse Source

Projects reserved state is moved to ProjectStateRegistry (#132332)

This commit refactors how project reserved state is stored and accessed in Elasticsearch. Instead of storing reserved state metadata directly within each ProjectMetadata object, the commit moves it to a centralized ProjectStateRegistry.

Note: In mixed-version multiproject clusters, this may cause existing reserved state metadata for projects to temporarily disappear until all nodes have been upgraded and restarted.

It also maintains the logic for resetting reserved state metadata during snapshot restore, ensuring that the reserved state will be reread and re-applied to the cluster state.
Alexey Ivanov 1 month ago
parent
commit
61382f36e0
21 changed files with 279 additions and 169 deletions
  1. 1 0
      server/src/main/java/org/elasticsearch/TransportVersions.java
  2. 2 1
      server/src/main/java/org/elasticsearch/action/admin/cluster/repositories/delete/TransportDeleteRepositoryAction.java
  3. 2 1
      server/src/main/java/org/elasticsearch/action/admin/indices/template/delete/TransportDeleteComposableIndexTemplateAction.java
  4. 2 1
      server/src/main/java/org/elasticsearch/action/admin/indices/template/put/TransportPutComponentTemplateAction.java
  5. 6 2
      server/src/main/java/org/elasticsearch/action/admin/indices/template/put/TransportPutComposableIndexTemplateAction.java
  6. 2 1
      server/src/main/java/org/elasticsearch/action/ingest/PutPipelineTransportAction.java
  7. 12 25
      server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java
  8. 23 66
      server/src/main/java/org/elasticsearch/cluster/metadata/ProjectMetadata.java
  9. 99 14
      server/src/main/java/org/elasticsearch/cluster/project/ProjectStateRegistry.java
  10. 12 3
      server/src/main/java/org/elasticsearch/reservedstate/service/FileSettingsService.java
  11. 2 1
      server/src/main/java/org/elasticsearch/reservedstate/service/ReservedClusterStateService.java
  12. 6 3
      server/src/main/java/org/elasticsearch/reservedstate/service/ReservedProjectStateUpdateTask.java
  13. 10 5
      server/src/main/java/org/elasticsearch/reservedstate/service/ReservedStateErrorTask.java
  14. 1 1
      server/src/main/java/org/elasticsearch/snapshots/RestoreService.java
  15. 11 8
      server/src/test/java/org/elasticsearch/action/admin/indices/template/reservedstate/ReservedComposableIndexTemplateActionTests.java
  16. 51 6
      server/src/test/java/org/elasticsearch/cluster/ClusterStateTests.java
  17. 0 4
      server/src/test/java/org/elasticsearch/cluster/metadata/MetadataTests.java
  18. 1 7
      server/src/test/java/org/elasticsearch/cluster/metadata/ProjectMetadataTests.java
  19. 2 5
      server/src/test/java/org/elasticsearch/cluster/metadata/ToAndFromJsonMetadataTests.java
  20. 2 2
      server/src/test/java/org/elasticsearch/reservedstate/service/FileSettingsServiceTests.java
  21. 32 13
      server/src/test/java/org/elasticsearch/reservedstate/service/ReservedClusterStateServiceTests.java

+ 1 - 0
server/src/main/java/org/elasticsearch/TransportVersions.java

@@ -357,6 +357,7 @@ public class TransportVersions {
     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,

+ 2 - 1
server/src/main/java/org/elasticsearch/action/admin/cluster/repositories/delete/TransportDeleteRepositoryAction.java

@@ -19,6 +19,7 @@ import org.elasticsearch.cluster.ClusterState;
 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;
@@ -91,7 +92,7 @@ public class TransportDeleteRepositoryAction extends AcknowledgedTransportMaster
         super.validateForReservedState(request, state);
 
         validateForReservedState(
-            projectResolver.getProjectMetadata(state).reservedStateMetadata().values(),
+            ProjectStateRegistry.get(state).reservedStateMetadata(projectResolver.getProjectId()).values(),
             reservedStateHandlerName().get(),
             modifiedKeys(request),
             request.toString()

+ 2 - 1
server/src/main/java/org/elasticsearch/action/admin/indices/template/delete/TransportDeleteComposableIndexTemplateAction.java

@@ -22,6 +22,7 @@ import org.elasticsearch.cluster.block.ClusterBlockException;
 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;
@@ -95,7 +96,7 @@ public class TransportDeleteComposableIndexTemplateAction extends AcknowledgedTr
         super.validateForReservedState(request, state);
 
         validateForReservedState(
-            projectResolver.getProjectMetadata(state).reservedStateMetadata().values(),
+            ProjectStateRegistry.get(state).reservedStateMetadata(projectResolver.getProjectId()).values(),
             reservedStateHandlerName().get(),
             modifiedKeys(request),
             request.toString()

+ 2 - 1
server/src/main/java/org/elasticsearch/action/admin/indices/template/put/TransportPutComponentTemplateAction.java

@@ -22,6 +22,7 @@ import org.elasticsearch.cluster.metadata.IndexMetadata;
 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;
@@ -130,7 +131,7 @@ public class TransportPutComponentTemplateAction extends AcknowledgedTransportMa
         super.validateForReservedState(request, state);
 
         validateForReservedState(
-            projectResolver.getProjectMetadata(state).reservedStateMetadata().values(),
+            ProjectStateRegistry.get(state).reservedStateMetadata(projectResolver.getProjectId()).values(),
             reservedStateHandlerName().get(),
             modifiedKeys(request),
             request.toString()

+ 6 - 2
server/src/main/java/org/elasticsearch/action/admin/indices/template/put/TransportPutComposableIndexTemplateAction.java

@@ -28,6 +28,7 @@ import org.elasticsearch.cluster.metadata.MetadataIndexTemplateService;
 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;
@@ -87,7 +88,10 @@ public class TransportPutComposableIndexTemplateAction extends AcknowledgedTrans
     ) {
         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(),
@@ -138,7 +142,7 @@ public class TransportPutComposableIndexTemplateAction extends AcknowledgedTrans
         super.validateForReservedState(request, state);
 
         validateForReservedState(
-            projectResolver.getProjectMetadata(state).reservedStateMetadata().values(),
+            ProjectStateRegistry.get(state).reservedStateMetadata(projectResolver.getProjectId()).values(),
             reservedStateHandlerName().get(),
             modifiedKeys(request),
             request.toString()

+ 2 - 1
server/src/main/java/org/elasticsearch/action/ingest/PutPipelineTransportAction.java

@@ -18,6 +18,7 @@ import org.elasticsearch.cluster.ClusterState;
 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;
@@ -80,7 +81,7 @@ public class PutPipelineTransportAction extends AcknowledgedTransportMasterNodeA
         super.validateForReservedState(request, state);
 
         validateForReservedState(
-            projectResolver.getProjectMetadata(state).reservedStateMetadata().values(),
+            ProjectStateRegistry.get(state).reservedStateMetadata(projectResolver.getProjectId()).values(),
             reservedStateHandlerName().get(),
             modifiedKeys(request),
             request.toString()

+ 12 - 25
server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java

@@ -61,7 +61,6 @@ import java.util.Map;
 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;
@@ -799,12 +798,6 @@ public class Metadata implements Diffable<Metadata>, ChunkedToXContent {
         @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());
-
         // 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) {
@@ -824,13 +817,20 @@ public class Metadata implements Diffable<Metadata>, ChunkedToXContent {
             );
         }
 
+        // 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()
         );
     }
@@ -845,6 +845,7 @@ public class Metadata implements Diffable<Metadata>, ChunkedToXContent {
         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;
@@ -981,7 +982,7 @@ public class Metadata implements Diffable<Metadata>, ChunkedToXContent {
                     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;
@@ -1048,7 +1049,7 @@ public class Metadata implements Diffable<Metadata>, ChunkedToXContent {
                 singleProject.indices().writeTo(out);
                 singleProject.templates().writeTo(out);
                 buildUnifiedCustomDiff().writeTo(out);
-                buildUnifiedReservedStateMetadataDiff().writeTo(out);
+                reservedStateMetadata.writeTo(out);
             } else {
                 clusterCustoms.writeTo(out);
                 reservedStateMetadata.writeTo(out);
@@ -1094,15 +1095,6 @@ public class Metadata implements Diffable<Metadata>, ChunkedToXContent {
             }
         }
 
-        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) {
@@ -1304,12 +1296,7 @@ public class Metadata implements Diffable<Metadata>, ChunkedToXContent {
             );
             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());
 

+ 23 - 66
server/src/main/java/org/elasticsearch/cluster/metadata/ProjectMetadata.java

@@ -36,6 +36,7 @@ import org.elasticsearch.common.xcontent.ChunkedToXContent;
 import org.elasticsearch.common.xcontent.ChunkedToXContentHelper;
 import org.elasticsearch.common.xcontent.XContentHelper;
 import org.elasticsearch.common.xcontent.XContentParserUtils;
+import org.elasticsearch.core.FixForMultiProject;
 import org.elasticsearch.core.Nullable;
 import org.elasticsearch.index.Index;
 import org.elasticsearch.index.IndexMode;
@@ -78,6 +79,7 @@ import java.util.stream.Stream;
 
 import static org.elasticsearch.cluster.metadata.LifecycleExecutionState.ILM_CUSTOM_METADATA_KEY;
 import static org.elasticsearch.cluster.metadata.Metadata.ALL;
+import static org.elasticsearch.cluster.project.ProjectStateRegistry.RESERVED_DIFF_VALUE_READER;
 import static org.elasticsearch.index.IndexSettings.PREFER_ILM_SETTING;
 
 public class ProjectMetadata implements Iterable<IndexMetadata>, Diffable<ProjectMetadata>, ChunkedToXContent {
@@ -91,7 +93,6 @@ public class ProjectMetadata implements Iterable<IndexMetadata>, Diffable<Projec
     private final ImmutableOpenMap<String, Set<Index>> aliasedIndices;
     private final ImmutableOpenMap<String, IndexTemplateMetadata> templates;
     private final ImmutableOpenMap<String, Metadata.ProjectCustom> customs;
-    private final ImmutableOpenMap<String, ReservedStateMetadata> reservedStateMetadata;
 
     private final int totalNumberOfShards;
     private final int totalOpenIndexShards;
@@ -125,7 +126,6 @@ public class ProjectMetadata implements Iterable<IndexMetadata>, Diffable<Projec
         ImmutableOpenMap<String, Set<Index>> aliasedIndices,
         ImmutableOpenMap<String, IndexTemplateMetadata> templates,
         ImmutableOpenMap<String, Metadata.ProjectCustom> customs,
-        ImmutableOpenMap<String, ReservedStateMetadata> reservedStateMetadata,
         int totalNumberOfShards,
         int totalOpenIndexShards,
         String[] allIndices,
@@ -143,7 +143,6 @@ public class ProjectMetadata implements Iterable<IndexMetadata>, Diffable<Projec
         this.aliasedIndices = aliasedIndices;
         this.templates = templates;
         this.customs = customs;
-        this.reservedStateMetadata = reservedStateMetadata;
         this.totalNumberOfShards = totalNumberOfShards;
         this.totalOpenIndexShards = totalOpenIndexShards;
         this.allIndices = allIndices;
@@ -224,7 +223,6 @@ public class ProjectMetadata implements Iterable<IndexMetadata>, Diffable<Projec
             aliasedIndices,
             templates,
             customs,
-            reservedStateMetadata,
             totalNumberOfShards,
             totalOpenIndexShards,
             allIndices,
@@ -257,7 +255,6 @@ public class ProjectMetadata implements Iterable<IndexMetadata>, Diffable<Projec
             aliasedIndices,
             templates,
             customs,
-            reservedStateMetadata,
             totalNumberOfShards,
             totalOpenIndexShards,
             allIndices,
@@ -291,7 +288,6 @@ public class ProjectMetadata implements Iterable<IndexMetadata>, Diffable<Projec
             aliasedIndices,
             templates,
             customs,
-            reservedStateMetadata,
             totalNumberOfShards,
             totalOpenIndexShards,
             allIndices,
@@ -379,7 +375,6 @@ public class ProjectMetadata implements Iterable<IndexMetadata>, Diffable<Projec
             updatedAliases,
             templates,
             customs,
-            reservedStateMetadata,
             totalNumberOfShards + index.getTotalNumberOfShards(),
             totalOpenIndexShards + (index.getState() == IndexMetadata.State.OPEN ? index.getTotalNumberOfShards() : 0),
             updatedAllIndices,
@@ -933,10 +928,6 @@ public class ProjectMetadata implements Iterable<IndexMetadata>, Diffable<Projec
         return customs;
     }
 
-    public Map<String, ReservedStateMetadata> reservedStateMetadata() {
-        return reservedStateMetadata;
-    }
-
     public Map<String, DataStream> dataStreams() {
         return custom(DataStreamMetadata.TYPE, DataStreamMetadata.EMPTY).dataStreams();
     }
@@ -1143,7 +1134,6 @@ public class ProjectMetadata implements Iterable<IndexMetadata>, Diffable<Projec
         private final ImmutableOpenMap.Builder<String, IndexMetadata> indices;
         private final ImmutableOpenMap.Builder<String, IndexTemplateMetadata> templates;
         private final ImmutableOpenMap.Builder<String, Metadata.ProjectCustom> customs;
-        private final ImmutableOpenMap.Builder<String, ReservedStateMetadata> reservedStateMetadata;
 
         private SortedMap<String, IndexAbstraction> previousIndicesLookup;
 
@@ -1160,7 +1150,6 @@ public class ProjectMetadata implements Iterable<IndexMetadata>, Diffable<Projec
             this.indices = ImmutableOpenMap.builder(projectMetadata.indices);
             this.templates = ImmutableOpenMap.builder(projectMetadata.templates);
             this.customs = ImmutableOpenMap.builder(projectMetadata.customs);
-            this.reservedStateMetadata = ImmutableOpenMap.builder(projectMetadata.reservedStateMetadata);
             this.previousIndicesLookup = projectMetadata.indicesLookup;
             this.mappingsByHash = new HashMap<>(projectMetadata.mappingsByHash);
             this.checkForUnusedMappings = false;
@@ -1174,7 +1163,6 @@ public class ProjectMetadata implements Iterable<IndexMetadata>, Diffable<Projec
             indices = ImmutableOpenMap.builder(indexCountHint);
             templates = ImmutableOpenMap.builder();
             customs = ImmutableOpenMap.builder();
-            reservedStateMetadata = ImmutableOpenMap.builder();
             previousIndicesLookup = null;
             this.mappingsByHash = new HashMap<>(mappingsByHash);
             indexGraveyard(IndexGraveyard.builder().build()); // create new empty index graveyard to initialize
@@ -1519,21 +1507,6 @@ public class ProjectMetadata implements Iterable<IndexMetadata>, Diffable<Projec
             return this;
         }
 
-        public Builder put(Map<String, ReservedStateMetadata> reservedStateMetadata) {
-            this.reservedStateMetadata.putAllFromMap(reservedStateMetadata);
-            return this;
-        }
-
-        public Builder put(ReservedStateMetadata metadata) {
-            reservedStateMetadata.put(metadata.namespace(), metadata);
-            return this;
-        }
-
-        public Builder removeReservedState(ReservedStateMetadata metadata) {
-            reservedStateMetadata.remove(metadata.namespace());
-            return this;
-        }
-
         public Builder indexGraveyard(final IndexGraveyard indexGraveyard) {
             return putCustom(IndexGraveyard.TYPE, indexGraveyard);
         }
@@ -1682,7 +1655,6 @@ public class ProjectMetadata implements Iterable<IndexMetadata>, Diffable<Projec
                 aliasedIndices,
                 templates.build(),
                 customs.build(),
-                reservedStateMetadata.build(),
                 totalNumberOfShards,
                 totalOpenIndexShards,
                 allIndicesArray,
@@ -2087,6 +2059,7 @@ public class ProjectMetadata implements Iterable<IndexMetadata>, Diffable<Projec
             return true;
         }
 
+        @FixForMultiProject(description = "Remove reading reserved_state and settings") // ES-12795
         public static ProjectMetadata fromXContent(XContentParser parser) throws IOException {
             XContentParser.Token token = parser.currentToken();
             if (token == null) {
@@ -2107,9 +2080,10 @@ public class ProjectMetadata implements Iterable<IndexMetadata>, Diffable<Projec
                     }
                 } else if (token == XContentParser.Token.START_OBJECT) {
                     switch (currentFieldName) {
+                        // Remove this (ES-12795)
                         case "reserved_state" -> {
                             while (parser.nextToken() != XContentParser.Token.END_OBJECT) {
-                                projectBuilder.put(ReservedStateMetadata.fromXContent(parser));
+                                ReservedStateMetadata.fromXContent(parser);
                             }
                         }
                         case "indices" -> {
@@ -2122,6 +2096,7 @@ public class ProjectMetadata implements Iterable<IndexMetadata>, Diffable<Projec
                                 projectBuilder.put(IndexTemplateMetadata.Builder.fromXContent(parser, parser.currentName()));
                             }
                         }
+                        // Remove this (ES-12795)
                         case "settings" -> {
                             Settings.fromXContent(parser);
                         }
@@ -2169,10 +2144,7 @@ public class ProjectMetadata implements Iterable<IndexMetadata>, Diffable<Projec
                 )
             ),
             indices,
-            customs,
-            multiProject
-                ? ChunkedToXContentHelper.object("reserved_state", reservedStateMetadata().values().iterator())
-                : Collections.emptyIterator()
+            customs
         );
     }
 
@@ -2199,9 +2171,11 @@ public class ProjectMetadata implements Iterable<IndexMetadata>, Diffable<Projec
 
         readProjectCustoms(in, builder);
 
-        int reservedStateSize = in.readVInt();
-        for (int i = 0; i < reservedStateSize; i++) {
-            builder.put(ReservedStateMetadata.readFrom(in));
+        if (in.getTransportVersion().before(TransportVersions.PROJECT_RESERVED_STATE_MOVE_TO_REGISTRY)) {
+            int reservedStateSize = in.readVInt();
+            for (int i = 0; i < reservedStateSize; i++) {
+                ReservedStateMetadata.readFrom(in);
+            }
         }
 
         if (in.getTransportVersion()
@@ -2238,7 +2212,9 @@ public class ProjectMetadata implements Iterable<IndexMetadata>, Diffable<Projec
         }
         out.writeCollection(templates.values());
         VersionedNamedWriteable.writeVersionedWriteables(out, customs.values());
-        out.writeCollection(reservedStateMetadata.values());
+        if (out.getTransportVersion().before(TransportVersions.PROJECT_RESERVED_STATE_MOVE_TO_REGISTRY)) {
+            out.writeCollection(Collections.emptySet());
+        }
 
         if (out.getTransportVersion()
             .between(TransportVersions.PROJECT_METADATA_SETTINGS, TransportVersions.CLUSTER_STATE_PROJECTS_SETTINGS)) {
@@ -2253,23 +2229,16 @@ public class ProjectMetadata implements Iterable<IndexMetadata>, Diffable<Projec
             new DiffableUtils.DiffableValueReader<>(IndexMetadata::readFrom, IndexMetadata::readDiffFrom);
         private static final DiffableUtils.DiffableValueReader<String, IndexTemplateMetadata> TEMPLATES_DIFF_VALUE_READER =
             new DiffableUtils.DiffableValueReader<>(IndexTemplateMetadata::readFrom, IndexTemplateMetadata::readDiffFrom);
-        private static final DiffableUtils.DiffableValueReader<String, ReservedStateMetadata> RESERVED_DIFF_VALUE_READER =
-            new DiffableUtils.DiffableValueReader<>(ReservedStateMetadata::readFrom, ReservedStateMetadata::readDiffFrom);
 
         private final DiffableUtils.MapDiff<String, IndexMetadata, ImmutableOpenMap<String, IndexMetadata>> indices;
         private final DiffableUtils.MapDiff<String, IndexTemplateMetadata, ImmutableOpenMap<String, IndexTemplateMetadata>> templates;
         private final DiffableUtils.MapDiff<String, Metadata.ProjectCustom, ImmutableOpenMap<String, Metadata.ProjectCustom>> customs;
-        private final DiffableUtils.MapDiff<
-            String,
-            ReservedStateMetadata,
-            ImmutableOpenMap<String, ReservedStateMetadata>> reservedStateMetadata;
 
         private ProjectMetadataDiff(ProjectMetadata before, ProjectMetadata after) {
             if (before == after) {
                 indices = DiffableUtils.emptyDiff();
                 templates = DiffableUtils.emptyDiff();
                 customs = DiffableUtils.emptyDiff();
-                reservedStateMetadata = DiffableUtils.emptyDiff();
             } else {
                 indices = DiffableUtils.diff(before.indices, after.indices, DiffableUtils.getStringKeySerializer());
                 templates = DiffableUtils.diff(before.templates, after.templates, DiffableUtils.getStringKeySerializer());
@@ -2279,35 +2248,26 @@ public class ProjectMetadata implements Iterable<IndexMetadata>, Diffable<Projec
                     DiffableUtils.getStringKeySerializer(),
                     PROJECT_CUSTOM_VALUE_SERIALIZER
                 );
-                reservedStateMetadata = DiffableUtils.diff(
-                    before.reservedStateMetadata,
-                    after.reservedStateMetadata,
-                    DiffableUtils.getStringKeySerializer()
-                );
             }
         }
 
         ProjectMetadataDiff(
             DiffableUtils.MapDiff<String, IndexMetadata, ImmutableOpenMap<String, IndexMetadata>> indices,
             DiffableUtils.MapDiff<String, IndexTemplateMetadata, ImmutableOpenMap<String, IndexTemplateMetadata>> templates,
-            DiffableUtils.MapDiff<String, Metadata.ProjectCustom, ImmutableOpenMap<String, Metadata.ProjectCustom>> customs,
-            DiffableUtils.MapDiff<String, ReservedStateMetadata, ImmutableOpenMap<String, ReservedStateMetadata>> reservedStateMetadata
+            DiffableUtils.MapDiff<String, Metadata.ProjectCustom, ImmutableOpenMap<String, Metadata.ProjectCustom>> customs
         ) {
             this.indices = indices;
             this.templates = templates;
             this.customs = customs;
-            this.reservedStateMetadata = reservedStateMetadata;
         }
 
         ProjectMetadataDiff(StreamInput in) throws IOException {
             indices = DiffableUtils.readImmutableOpenMapDiff(in, DiffableUtils.getStringKeySerializer(), INDEX_METADATA_DIFF_VALUE_READER);
             templates = DiffableUtils.readImmutableOpenMapDiff(in, DiffableUtils.getStringKeySerializer(), TEMPLATES_DIFF_VALUE_READER);
             customs = DiffableUtils.readImmutableOpenMapDiff(in, DiffableUtils.getStringKeySerializer(), PROJECT_CUSTOM_VALUE_SERIALIZER);
-            reservedStateMetadata = DiffableUtils.readImmutableOpenMapDiff(
-                in,
-                DiffableUtils.getStringKeySerializer(),
-                RESERVED_DIFF_VALUE_READER
-            );
+            if (in.getTransportVersion().before(TransportVersions.PROJECT_RESERVED_STATE_MOVE_TO_REGISTRY)) {
+                DiffableUtils.readImmutableOpenMapDiff(in, DiffableUtils.getStringKeySerializer(), RESERVED_DIFF_VALUE_READER);
+            }
             if (in.getTransportVersion()
                 .between(TransportVersions.PROJECT_METADATA_SETTINGS, TransportVersions.CLUSTER_STATE_PROJECTS_SETTINGS)) {
                 Settings.readSettingsDiffFromStream(in);
@@ -2326,16 +2286,14 @@ public class ProjectMetadata implements Iterable<IndexMetadata>, Diffable<Projec
             return customs;
         }
 
-        DiffableUtils.MapDiff<String, ReservedStateMetadata, ImmutableOpenMap<String, ReservedStateMetadata>> reservedStateMetadata() {
-            return reservedStateMetadata;
-        }
-
         @Override
         public void writeTo(StreamOutput out) throws IOException {
             indices.writeTo(out);
             templates.writeTo(out);
             customs.writeTo(out);
-            reservedStateMetadata.writeTo(out);
+            if (out.getTransportVersion().before(TransportVersions.PROJECT_RESERVED_STATE_MOVE_TO_REGISTRY)) {
+                DiffableUtils.emptyDiff().writeTo(out);
+            }
             if (out.getTransportVersion()
                 .between(TransportVersions.PROJECT_METADATA_SETTINGS, TransportVersions.CLUSTER_STATE_PROJECTS_SETTINGS)) {
                 Settings.EMPTY_DIFF.writeTo(out);
@@ -2344,7 +2302,7 @@ public class ProjectMetadata implements Iterable<IndexMetadata>, Diffable<Projec
 
         @Override
         public ProjectMetadata apply(ProjectMetadata part) {
-            if (indices.isEmpty() && templates.isEmpty() && customs.isEmpty() && reservedStateMetadata.isEmpty()) {
+            if (indices.isEmpty() && templates.isEmpty() && customs.isEmpty()) {
                 // nothing to do
                 return part;
             }
@@ -2354,7 +2312,6 @@ public class ProjectMetadata implements Iterable<IndexMetadata>, Diffable<Projec
             builder.indices(updatedIndices);
             builder.templates(templates.apply(part.templates));
             builder.customs(customs.apply(part.customs));
-            builder.put(reservedStateMetadata.apply(part.reservedStateMetadata));
             if (part.indices == updatedIndices
                 && builder.dataStreamMetadata() == part.custom(DataStreamMetadata.TYPE, DataStreamMetadata.EMPTY)) {
                 builder.previousIndicesLookup = part.indicesLookup;

+ 99 - 14
server/src/main/java/org/elasticsearch/cluster/project/ProjectStateRegistry.java

@@ -21,6 +21,7 @@ import org.elasticsearch.cluster.NamedDiff;
 import org.elasticsearch.cluster.NamedDiffable;
 import org.elasticsearch.cluster.SimpleDiffable;
 import org.elasticsearch.cluster.metadata.ProjectId;
+import org.elasticsearch.cluster.metadata.ReservedStateMetadata;
 import org.elasticsearch.common.collect.ImmutableOpenMap;
 import org.elasticsearch.common.collect.Iterators;
 import org.elasticsearch.common.io.stream.StreamInput;
@@ -29,6 +30,7 @@ import org.elasticsearch.common.io.stream.Writeable;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.util.set.Sets;
 import org.elasticsearch.xcontent.ToXContent;
+import org.elasticsearch.xcontent.ToXContentFragment;
 import org.elasticsearch.xcontent.XContentBuilder;
 
 import java.io.IOException;
@@ -38,6 +40,7 @@ import java.util.Iterator;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import java.util.function.UnaryOperator;
 import java.util.stream.Collectors;
 
 /**
@@ -46,7 +49,10 @@ import java.util.stream.Collectors;
 public class ProjectStateRegistry extends AbstractNamedDiffable<Custom> implements Custom, NamedDiffable<Custom> {
     public static final String TYPE = "projects_registry";
     public static final ProjectStateRegistry EMPTY = new ProjectStateRegistry(Collections.emptyMap(), Collections.emptySet(), 0);
-    private static final Entry EMPTY_ENTRY = new Entry(Settings.EMPTY);
+    private static final Entry EMPTY_ENTRY = new Entry(Settings.EMPTY, ImmutableOpenMap.of());
+
+    public static final DiffableUtils.DiffableValueReader<String, ReservedStateMetadata> RESERVED_DIFF_VALUE_READER =
+        new DiffableUtils.DiffableValueReader<>(ReservedStateMetadata::readFrom, ReservedStateMetadata::readDiffFrom);
 
     private final Map<ProjectId, Entry> projectsEntries;
     // Projects that have been marked for deletion based on their file-based setting
@@ -63,7 +69,9 @@ public class ProjectStateRegistry extends AbstractNamedDiffable<Custom> implemen
             projectsEntries = in.readMap(ProjectId::readFrom, Entry::readFrom);
         } else {
             Map<ProjectId, Settings> settingsMap = in.readMap(ProjectId::readFrom, Settings::readSettingsFromStream);
-            projectsEntries = settingsMap.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> new Entry(e.getValue())));
+            projectsEntries = settingsMap.entrySet()
+                .stream()
+                .collect(Collectors.toMap(Map.Entry::getKey, e -> new Entry(e.getValue(), ImmutableOpenMap.of())));
         }
         if (in.getTransportVersion().onOrAfter(TransportVersions.PROJECT_STATE_REGISTRY_RECORDS_DELETIONS)) {
             projectsMarkedForDeletion = in.readCollectionAsImmutableSet(ProjectId::readFrom);
@@ -105,6 +113,10 @@ public class ProjectStateRegistry extends AbstractNamedDiffable<Custom> implemen
         return projectsEntries.getOrDefault(projectId, EMPTY_ENTRY).settings;
     }
 
+    public Map<String, ReservedStateMetadata> reservedStateMetadata(ProjectId projectId) {
+        return projectsEntries.getOrDefault(projectId, EMPTY_ENTRY).reservedStateMetadata;
+    }
+
     public Set<ProjectId> getProjectsMarkedForDeletion() {
         return projectsMarkedForDeletion;
     }
@@ -304,14 +316,22 @@ public class ProjectStateRegistry extends AbstractNamedDiffable<Custom> implemen
             this.projectsMarkedForDeletionGeneration = original.projectsMarkedForDeletionGeneration;
         }
 
-        public Builder putProjectSettings(ProjectId projectId, Settings settings) {
+        private void updateEntry(ProjectId projectId, UnaryOperator<Entry> modifier) {
             Entry entry = projectsEntries.get(projectId);
             if (entry == null) {
-                entry = new Entry(settings);
-            } else {
-                entry = entry.withSettings(settings);
+                entry = new Entry();
             }
+            entry = modifier.apply(entry);
             projectsEntries.put(projectId, entry);
+        }
+
+        public Builder putProjectSettings(ProjectId projectId, Settings settings) {
+            updateEntry(projectId, entry -> entry.withSettings(settings));
+            return this;
+        }
+
+        public Builder putReservedStateMetadata(ProjectId projectId, ReservedStateMetadata reservedStateMetadata) {
+            updateEntry(projectId, entry -> entry.withReservedStateMetadata(reservedStateMetadata));
             return this;
         }
 
@@ -343,25 +363,66 @@ public class ProjectStateRegistry extends AbstractNamedDiffable<Custom> implemen
         }
     }
 
-    private record Entry(Settings settings) implements Writeable, Diffable<Entry> {
+    private record Entry(Settings settings, ImmutableOpenMap<String, ReservedStateMetadata> reservedStateMetadata)
+        implements
+            ToXContentFragment,
+            Writeable,
+            Diffable<Entry> {
+
+        Entry() {
+            this(Settings.EMPTY, ImmutableOpenMap.of());
+        }
 
         public static Entry readFrom(StreamInput in) throws IOException {
-            return new Entry(Settings.readSettingsFromStream(in));
+            Settings settings = Settings.readSettingsFromStream(in);
+
+            ImmutableOpenMap<String, ReservedStateMetadata> reservedStateMetadata;
+            if (in.getTransportVersion().onOrAfter(TransportVersions.PROJECT_RESERVED_STATE_MOVE_TO_REGISTRY)) {
+                int reservedStateSize = in.readVInt();
+                ImmutableOpenMap.Builder<String, ReservedStateMetadata> builder = ImmutableOpenMap.builder(reservedStateSize);
+                for (int i = 0; i < reservedStateSize; i++) {
+                    ReservedStateMetadata r = ReservedStateMetadata.readFrom(in);
+                    builder.put(r.namespace(), r);
+                }
+                reservedStateMetadata = builder.build();
+            } else {
+                reservedStateMetadata = ImmutableOpenMap.of();
+            }
+
+            return new Entry(settings, reservedStateMetadata);
         }
 
         public Entry withSettings(Settings settings) {
-            return new Entry(settings);
+            return new Entry(settings, reservedStateMetadata);
+        }
+
+        public Entry withReservedStateMetadata(ReservedStateMetadata reservedStateMetadata) {
+            ImmutableOpenMap<String, ReservedStateMetadata> reservedStateMetadataMap = ImmutableOpenMap.builder(this.reservedStateMetadata)
+                .fPut(reservedStateMetadata.namespace(), reservedStateMetadata)
+                .build();
+            return new Entry(settings, reservedStateMetadataMap);
         }
 
         @Override
         public void writeTo(StreamOutput out) throws IOException {
             out.writeWriteable(settings);
+            if (out.getTransportVersion().onOrAfter(TransportVersions.PROJECT_RESERVED_STATE_MOVE_TO_REGISTRY)) {
+                out.writeCollection(reservedStateMetadata.values());
+            }
         }
 
-        public void toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException {
+        @Override
+        public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException {
             builder.startObject("settings");
             settings.toXContent(builder, new ToXContent.MapParams(Collections.singletonMap("flat_settings", "true")));
             builder.endObject();
+
+            builder.startObject("reserved_state");
+            for (ReservedStateMetadata reservedStateMetadata : reservedStateMetadata.values()) {
+                reservedStateMetadata.toXContent(builder, params);
+            }
+            builder.endObject();
+            return builder;
         }
 
         @Override
@@ -369,22 +430,46 @@ public class ProjectStateRegistry extends AbstractNamedDiffable<Custom> implemen
             if (this == previousState) {
                 return SimpleDiffable.empty();
             }
-            return new EntryDiff(settings.diff(previousState.settings));
+
+            return new EntryDiff(
+                settings.diff(previousState.settings),
+                DiffableUtils.diff(previousState.reservedStateMetadata, reservedStateMetadata, DiffableUtils.getStringKeySerializer())
+            );
         }
 
-        private record EntryDiff(Diff<Settings> settingsDiff) implements Diff<Entry> {
+        private record EntryDiff(
+            Diff<Settings> settingsDiff,
+            DiffableUtils.MapDiff<String, ReservedStateMetadata, ImmutableOpenMap<String, ReservedStateMetadata>> reservedStateMetadata
+        ) implements Diff<Entry> {
+
             public static EntryDiff readFrom(StreamInput in) throws IOException {
-                return new EntryDiff(Settings.readSettingsDiffFromStream(in));
+                Diff<Settings> settingsDiff = Settings.readSettingsDiffFromStream(in);
+
+                DiffableUtils.MapDiff<String, ReservedStateMetadata, ImmutableOpenMap<String, ReservedStateMetadata>> reservedStateMetadata;
+                if (in.getTransportVersion().onOrAfter(TransportVersions.PROJECT_RESERVED_STATE_MOVE_TO_REGISTRY)) {
+                    reservedStateMetadata = DiffableUtils.readImmutableOpenMapDiff(
+                        in,
+                        DiffableUtils.getStringKeySerializer(),
+                        RESERVED_DIFF_VALUE_READER
+                    );
+                } else {
+                    reservedStateMetadata = DiffableUtils.emptyDiff();
+                }
+
+                return new EntryDiff(settingsDiff, reservedStateMetadata);
             }
 
             @Override
             public Entry apply(Entry part) {
-                return part.withSettings(settingsDiff.apply(part.settings));
+                return new Entry(settingsDiff.apply(part.settings), reservedStateMetadata.apply(part.reservedStateMetadata));
             }
 
             @Override
             public void writeTo(StreamOutput out) throws IOException {
                 out.writeWriteable(settingsDiff);
+                if (out.getTransportVersion().onOrAfter(TransportVersions.PROJECT_RESERVED_STATE_MOVE_TO_REGISTRY)) {
+                    reservedStateMetadata.writeTo(out);
+                }
             }
         }
     }

+ 12 - 3
server/src/main/java/org/elasticsearch/reservedstate/service/FileSettingsService.java

@@ -32,6 +32,7 @@ import org.elasticsearch.common.io.stream.StreamOutput;
 import org.elasticsearch.common.io.stream.Writeable;
 import org.elasticsearch.common.settings.Setting;
 import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.core.FixForMultiProject;
 import org.elasticsearch.env.Environment;
 import org.elasticsearch.health.HealthIndicatorDetails;
 import org.elasticsearch.health.HealthIndicatorImpact;
@@ -125,11 +126,19 @@ public class FileSettingsService extends MasterNodeFileWatchingService implement
      * <p>
      * If there's no file based settings file in this cluster, we'll remove all state reservations for
      * file based settings from the cluster state.
+     *
      * @param clusterState the cluster state before snapshot restore
-     * @param mdBuilder the current metadata builder for the new cluster state
-     * @param projectId the project associated with the restore
+     * @param builder      the current ClusterState builder for the new cluster state
+     * @param mdBuilder    the current metadata builder for the new cluster state
+     * @param projectId    the project associated with the restore
      */
-    public void handleSnapshotRestore(ClusterState clusterState, Metadata.Builder mdBuilder, ProjectId projectId) {
+    @FixForMultiProject(description = "Simplify parameters (ES-12796)")
+    public void handleSnapshotRestore(
+        ClusterState clusterState,
+        ClusterState.Builder builder,
+        Metadata.Builder mdBuilder,
+        ProjectId projectId
+    ) {
         assert clusterState.nodes().isLocalNodeElectedMaster();
 
         ReservedStateMetadata fileSettingsMetadata = clusterState.metadata().reservedStateMetadata().get(NAMESPACE);

+ 2 - 1
server/src/main/java/org/elasticsearch/reservedstate/service/ReservedClusterStateService.java

@@ -19,6 +19,7 @@ import org.elasticsearch.cluster.metadata.ProjectId;
 import org.elasticsearch.cluster.metadata.ProjectMetadata;
 import org.elasticsearch.cluster.metadata.ReservedStateErrorMetadata;
 import org.elasticsearch.cluster.metadata.ReservedStateMetadata;
+import org.elasticsearch.cluster.project.ProjectStateRegistry;
 import org.elasticsearch.cluster.routing.RerouteService;
 import org.elasticsearch.cluster.service.ClusterService;
 import org.elasticsearch.common.Priority;
@@ -451,7 +452,7 @@ public class ReservedClusterStateService {
         ProjectMetadata projectMetadata = getPotentiallyNewProject(state, projectId);
         state = ClusterState.builder(state).putProjectMetadata(projectMetadata).build();
 
-        ReservedStateMetadata existingMetadata = projectMetadata.reservedStateMetadata().get(namespace);
+        ReservedStateMetadata existingMetadata = ProjectStateRegistry.get(state).reservedStateMetadata(projectId).get(namespace);
 
         // We check if we should exit early on the state version from clusterService. The ReservedStateUpdateTask
         // will check again with the most current state version if this continues.

+ 6 - 3
server/src/main/java/org/elasticsearch/reservedstate/service/ReservedProjectStateUpdateTask.java

@@ -65,7 +65,7 @@ public class ReservedProjectStateUpdateTask extends ReservedStateUpdateTask<Rese
         ProjectMetadata currentProject = ReservedClusterStateService.getPotentiallyNewProject(currentState, projectId);
         var result = execute(
             ClusterState.builder(currentState).putProjectMetadata(currentProject).build(),
-            currentProject.reservedStateMetadata()
+            ProjectStateRegistry.get(currentState).reservedStateMetadata(projectId)
         );
         if (result == null) {
             return currentState;
@@ -78,8 +78,11 @@ public class ReservedProjectStateUpdateTask extends ReservedStateUpdateTask<Rese
         );
         ProjectMetadata updatedProjectMetadata = updatedClusterState.getMetadata().getProject(projectId);
         return ClusterState.builder(currentState)
-            .putCustom(ProjectStateRegistry.TYPE, ProjectStateRegistry.builder(updatedProjectStateRegistry).build())
-            .putProjectMetadata(ProjectMetadata.builder(updatedProjectMetadata).put(result.v2()))
+            .putCustom(
+                ProjectStateRegistry.TYPE,
+                ProjectStateRegistry.builder(updatedProjectStateRegistry).putReservedStateMetadata(projectId, result.v2()).build()
+            )
+            .putProjectMetadata(updatedProjectMetadata)
             .build();
     }
 }

+ 10 - 5
server/src/main/java/org/elasticsearch/reservedstate/service/ReservedStateErrorTask.java

@@ -16,9 +16,10 @@ import org.elasticsearch.action.ActionResponse;
 import org.elasticsearch.cluster.ClusterState;
 import org.elasticsearch.cluster.ClusterStateTaskListener;
 import org.elasticsearch.cluster.metadata.Metadata;
-import org.elasticsearch.cluster.metadata.ProjectMetadata;
+import org.elasticsearch.cluster.metadata.ProjectId;
 import org.elasticsearch.cluster.metadata.ReservedStateErrorMetadata;
 import org.elasticsearch.cluster.metadata.ReservedStateMetadata;
+import org.elasticsearch.cluster.project.ProjectStateRegistry;
 
 import static org.elasticsearch.cluster.metadata.ReservedStateMetadata.EMPTY_VERSION;
 import static org.elasticsearch.cluster.metadata.ReservedStateMetadata.NO_VERSION;
@@ -63,7 +64,7 @@ public class ReservedStateErrorTask implements ClusterStateTaskListener {
 
     static ReservedStateMetadata getMetadata(ClusterState state, ErrorState errorState) {
         return errorState.projectId()
-            .map(p -> ReservedClusterStateService.getPotentiallyNewProject(state, p).reservedStateMetadata())
+            .map(ProjectStateRegistry.get(state)::reservedStateMetadata)
             .orElseGet(() -> state.metadata().reservedStateMetadata())
             .get(errorState.namespace());
     }
@@ -93,13 +94,17 @@ public class ReservedStateErrorTask implements ClusterStateTaskListener {
         var errorMetadata = new ReservedStateErrorMetadata(errorState.version(), errorState.errorKind(), errorState.errors());
 
         if (errorState.projectId().isPresent()) {
-            ProjectMetadata project = currentState.metadata().getProject(errorState.projectId().get());
+            ProjectStateRegistry projectStateRegistry = ProjectStateRegistry.get(currentState);
 
-            ReservedStateMetadata reservedMetadata = project.reservedStateMetadata().get(errorState.namespace());
+            ProjectId projectId = errorState.projectId().get();
+            ReservedStateMetadata reservedMetadata = projectStateRegistry.reservedStateMetadata(projectId).get(errorState.namespace());
             ReservedStateMetadata.Builder resBuilder = ReservedStateMetadata.builder(errorState.namespace(), reservedMetadata);
             resBuilder.errorMetadata(errorMetadata);
 
-            stateBuilder.putProjectMetadata(ProjectMetadata.builder(project).put(resBuilder.build()));
+            stateBuilder.putCustom(
+                ProjectStateRegistry.TYPE,
+                ProjectStateRegistry.builder(projectStateRegistry).putReservedStateMetadata(projectId, resBuilder.build()).build()
+            );
         } else {
             Metadata.Builder metadataBuilder = Metadata.builder(currentState.metadata());
 

+ 1 - 1
server/src/main/java/org/elasticsearch/snapshots/RestoreService.java

@@ -1573,7 +1573,7 @@ public final class RestoreService implements ClusterStateApplier {
             // Restore global state if needed
             if (request.includeGlobalState()) {
                 applyGlobalStateRestore(currentState, mdBuilder, projectId);
-                fileSettingsService.handleSnapshotRestore(currentState, mdBuilder, projectId);
+                fileSettingsService.handleSnapshotRestore(currentState, builder, mdBuilder, projectId);
             }
 
             if (completed(shards)) {

+ 11 - 8
server/src/test/java/org/elasticsearch/action/admin/indices/template/reservedstate/ReservedComposableIndexTemplateActionTests.java

@@ -27,6 +27,7 @@ import org.elasticsearch.cluster.metadata.ProjectId;
 import org.elasticsearch.cluster.metadata.ProjectMetadata;
 import org.elasticsearch.cluster.metadata.ReservedStateHandlerMetadata;
 import org.elasticsearch.cluster.metadata.ReservedStateMetadata;
+import org.elasticsearch.cluster.project.ProjectStateRegistry;
 import org.elasticsearch.cluster.project.TestProjectResolvers;
 import org.elasticsearch.cluster.service.ClusterService;
 import org.elasticsearch.common.settings.ClusterSettings;
@@ -754,8 +755,9 @@ public class ReservedComposableIndexTemplateActionTests extends ESTestCase {
 
         var updatedState = processJSON(action, prevState, settingsJSON);
 
-        ProjectMetadata withReservedState = ProjectMetadata.builder(updatedState.state().getMetadata().getProject(projectId))
-            .put(
+        ProjectStateRegistry withReservedState = ProjectStateRegistry.builder(updatedState.state())
+            .putReservedStateMetadata(
+                projectId,
                 ReservedStateMetadata.builder("test")
                     .putHandler(new ReservedStateHandlerMetadata(ReservedComposableIndexTemplateAction.NAME, updatedState.keys()))
                     .build()
@@ -810,7 +812,7 @@ public class ReservedComposableIndexTemplateActionTests extends ESTestCase {
                     IllegalArgumentException.class,
                     () -> TransportPutComposableIndexTemplateAction.verifyIfUsingReservedComponentTemplates(
                         request,
-                        withReservedState.reservedStateMetadata().values()
+                        withReservedState.reservedStateMetadata(projectId).values()
                     )
                 ).getMessage().contains("errors: [[component_template:template_1] is reserved by [test]]")
             );
@@ -824,7 +826,7 @@ public class ReservedComposableIndexTemplateActionTests extends ESTestCase {
             // this should just work, no failure
             TransportPutComposableIndexTemplateAction.verifyIfUsingReservedComponentTemplates(
                 request,
-                withReservedState.reservedStateMetadata().values()
+                withReservedState.reservedStateMetadata(projectId).values()
             );
         }
     }
@@ -922,8 +924,9 @@ public class ReservedComposableIndexTemplateActionTests extends ESTestCase {
             allOf(aMapWithSize(2), hasKey(reservedComposableIndexName(conflictingTemplateName)), hasKey(conflictingTemplateName))
         );
 
-        ProjectMetadata withReservedMetadata = ProjectMetadata.builder(updatedState.state().getMetadata().getProject(projectId))
-            .put(
+        ProjectStateRegistry withReservedState = ProjectStateRegistry.builder(updatedState.state())
+            .putReservedStateMetadata(
+                projectId,
                 new ReservedStateMetadata.Builder("file_settings").putHandler(
                     new ReservedStateHandlerMetadata(ReservedComposableIndexTemplateAction.NAME, updatedState.keys())
                 ).build()
@@ -957,7 +960,7 @@ public class ReservedComposableIndexTemplateActionTests extends ESTestCase {
             expectThrows(
                 IllegalArgumentException.class,
                 () -> fakeAction.validateForReservedState(
-                    withReservedMetadata.reservedStateMetadata().values(),
+                    withReservedState.reservedStateMetadata(projectId).values(),
                     ReservedComposableIndexTemplateAction.NAME,
                     modifiedKeys,
                     pr.name()
@@ -973,7 +976,7 @@ public class ReservedComposableIndexTemplateActionTests extends ESTestCase {
         assertThat(modifiedKeysOK, hasSize(1));
 
         fakeAction.validateForReservedState(
-            withReservedMetadata.reservedStateMetadata().values(),
+            withReservedState.reservedStateMetadata(projectId).values(),
             ReservedComposableIndexTemplateAction.NAME,
             modifiedKeysOK,
             prOK.name()

+ 51 - 6
server/src/test/java/org/elasticsearch/cluster/ClusterStateTests.java

@@ -26,6 +26,8 @@ import org.elasticsearch.cluster.metadata.Metadata;
 import org.elasticsearch.cluster.metadata.MetadataTests;
 import org.elasticsearch.cluster.metadata.ProjectId;
 import org.elasticsearch.cluster.metadata.ProjectMetadata;
+import org.elasticsearch.cluster.metadata.ReservedStateHandlerMetadata;
+import org.elasticsearch.cluster.metadata.ReservedStateMetadata;
 import org.elasticsearch.cluster.node.DiscoveryNode;
 import org.elasticsearch.cluster.node.DiscoveryNodeUtils;
 import org.elasticsearch.cluster.node.DiscoveryNodes;
@@ -44,6 +46,9 @@ import org.elasticsearch.common.UUIDs;
 import org.elasticsearch.common.bytes.BytesArray;
 import org.elasticsearch.common.bytes.BytesReference;
 import org.elasticsearch.common.collect.Iterators;
+import org.elasticsearch.common.io.stream.BytesStreamOutput;
+import org.elasticsearch.common.io.stream.NamedWriteableAwareStreamInput;
+import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
 import org.elasticsearch.common.settings.Setting;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.transport.TransportAddress;
@@ -85,8 +90,10 @@ import static java.util.Collections.emptySet;
 import static java.util.Collections.singletonMap;
 import static org.elasticsearch.cluster.metadata.IndexMetadata.SETTING_VERSION_CREATED;
 import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertToXContentEquivalent;
+import static org.hamcrest.Matchers.aMapWithSize;
 import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasKey;
 import static org.hamcrest.Matchers.instanceOf;
 import static org.hamcrest.Matchers.not;
 import static org.hamcrest.Matchers.notNullValue;
@@ -515,8 +522,7 @@ public class ClusterStateTests extends ESTestCase {
                             "event_ingested_range": { "shards": [] }
                           }
                         },
-                        "index-graveyard": { "tombstones": [] },
-                        "reserved_state": {}
+                        "index-graveyard": { "tombstones": [] }
                       },
                       {
                         "id": "3LftaL7hgfXAsF60Gm6jcD",
@@ -573,15 +579,13 @@ public class ClusterStateTests extends ESTestCase {
                             "event_ingested_range": { "shards": [] }
                           }
                         },
-                        "index-graveyard": { "tombstones": [] },
-                        "reserved_state": {}
+                        "index-graveyard": { "tombstones": [] }
                       },
                       {
                         "id": "WHyuJ0uqBYOPgHX9kYUXlZ",
                         "templates": {},
                         "indices": {},
-                        "index-graveyard": { "tombstones": [] },
-                        "reserved_state": {}
+                        "index-graveyard": { "tombstones": [] }
                       }
                     ],
                     "reserved_state": {}
@@ -810,6 +814,17 @@ public class ClusterStateTests extends ESTestCase {
                           "project.setting": "42",
                           "project.setting2": "43"
                         },
+                        "reserved_state": {
+                          "file_settings": {
+                            "handlers": {
+                              "settings": {
+                                "keys": ["project.setting", "project.setting2"]
+                              }
+                            },
+                            "version": 42,
+                            "errors": null
+                          }
+                        },
                         "marked_for_deletion": true
                       }
                     ],
@@ -929,6 +944,15 @@ public class ClusterStateTests extends ESTestCase {
                         projectId1,
                         Settings.builder().put(PROJECT_SETTING.getKey(), 42).put(PROJECT_SETTING2.getKey(), 43).build()
                     )
+                    .putReservedStateMetadata(
+                        projectId1,
+                        ReservedStateMetadata.builder("file_settings")
+                            .putHandler(
+                                new ReservedStateHandlerMetadata("settings", Set.of(PROJECT_SETTING.getKey(), PROJECT_SETTING2.getKey()))
+                            )
+                            .version(42L)
+                            .build()
+                    )
                     .markProjectForDeletion(projectId1)
                     .build()
             )
@@ -2241,4 +2265,25 @@ public class ClusterStateTests extends ESTestCase {
 
         return Math.toIntExact(chunkCount);
     }
+
+    public void testSerialization() throws IOException {
+        ClusterState clusterState = buildClusterState();
+        BytesStreamOutput out = new BytesStreamOutput();
+        clusterState.writeTo(out);
+
+        // check it deserializes ok
+        NamedWriteableRegistry namedWriteableRegistry = new NamedWriteableRegistry(ClusterModule.getNamedWriteables());
+        ClusterState deserialisedClusterState = ClusterState.readFrom(
+            new NamedWriteableAwareStreamInput(out.bytes().streamInput(), namedWriteableRegistry),
+            null
+        );
+
+        // check it matches the original object
+        Metadata deserializedMetadata = deserialisedClusterState.metadata();
+        assertThat(deserializedMetadata.projects(), aMapWithSize(1));
+        assertThat(deserializedMetadata.projects(), hasKey(ProjectId.DEFAULT));
+
+        assertThat(deserializedMetadata.getProject(ProjectId.DEFAULT).templates(), hasKey("template"));
+        assertThat(deserializedMetadata.getProject(ProjectId.DEFAULT).indices(), hasKey("index"));
+    }
 }

+ 0 - 4
server/src/test/java/org/elasticsearch/cluster/metadata/MetadataTests.java

@@ -969,10 +969,6 @@ public class MetadataTests extends ESTestCase {
             .sum();
 
         int reservedStateSize = metadata.reservedStateMetadata().size();
-        if (params.paramAsBoolean("multi-project", false) == false) {
-            // only one project if not multi-project, add its reserved state to the cluster's collection
-            reservedStateSize += metadata.projects().values().iterator().next().reservedStateMetadata().size();
-        }
 
         // 2 chunks for wrapping reserved state + 1 chunk for each item
         chunkCount += 2 + reservedStateSize;

+ 1 - 7
server/src/test/java/org/elasticsearch/cluster/metadata/ProjectMetadataTests.java

@@ -2745,8 +2745,7 @@ public class ProjectMetadataTests extends ESTestCase {
                           }
                         },
                         "data_stream_aliases": {}
-                      },
-                      "reserved_state": {}
+                      }
                     }
                     """,
                 IndexVersion.current(),
@@ -2849,11 +2848,6 @@ public class ProjectMetadataTests extends ESTestCase {
             }
         }
 
-        if (params.paramAsBoolean("multi-project", false)) {
-            // 2 chunks for wrapping reserved state + 1 chunk for each item
-            chunkCount += 2 + project.reservedStateMetadata().size();
-        }
-
         return Math.toIntExact(chunkCount);
     }
 

+ 2 - 5
server/src/test/java/org/elasticsearch/cluster/metadata/ToAndFromJsonMetadataTests.java

@@ -127,13 +127,11 @@ public class ToAndFromJsonMetadataTests extends ESTestCase {
             .put(idx2, false)
             .put(DataStreamTestHelper.newInstance("data-stream1", List.of(idx1.getIndex())))
             .put(DataStreamTestHelper.newInstance("data-stream2", List.of(idx2.getIndex())))
-            .put(reservedStateMetadata)
-            .put(reservedStateMetadata1)
             .build();
 
         XContentBuilder builder = JsonXContent.contentBuilder();
         builder.startObject();
-        ChunkedToXContent.wrapAsToXContent(Metadata.builder().put(project).build())
+        ChunkedToXContent.wrapAsToXContent(Metadata.builder().put(project).put(reservedStateMetadata).put(reservedStateMetadata1).build())
             .toXContent(
                 builder,
                 new ToXContent.MapParams(Map.of("binary", "true", Metadata.CONTEXT_MODE_PARAM, Metadata.CONTEXT_MODE_GATEWAY))
@@ -282,8 +280,7 @@ public class ToAndFromJsonMetadataTests extends ESTestCase {
                     },
                     "index-graveyard" : {
                       "tombstones" : [ ]
-                    },
-                    "reserved_state" : { }
+                    }
                   }
                 ],
                 "reserved_state" : { }

+ 2 - 2
server/src/test/java/org/elasticsearch/reservedstate/service/FileSettingsServiceTests.java

@@ -453,7 +453,7 @@ public class FileSettingsServiceTests extends ESTestCase {
             .build();
 
         Metadata.Builder metadata = Metadata.builder(state.metadata());
-        fileSettingsService.handleSnapshotRestore(state, metadata, ProjectId.DEFAULT);
+        fileSettingsService.handleSnapshotRestore(state, ClusterState.builder(state), metadata, ProjectId.DEFAULT);
 
         assertThat(metadata.build().reservedStateMetadata(), anEmptyMap());
     }
@@ -476,7 +476,7 @@ public class FileSettingsServiceTests extends ESTestCase {
             .build();
 
         Metadata.Builder metadata = Metadata.builder();
-        fileSettingsService.handleSnapshotRestore(state, metadata, ProjectId.DEFAULT);
+        fileSettingsService.handleSnapshotRestore(state, ClusterState.builder(state), metadata, ProjectId.DEFAULT);
 
         assertThat(
             metadata.build().reservedStateMetadata(),

+ 32 - 13
server/src/test/java/org/elasticsearch/reservedstate/service/ReservedClusterStateServiceTests.java

@@ -21,6 +21,7 @@ import org.elasticsearch.cluster.metadata.ProjectMetadata;
 import org.elasticsearch.cluster.metadata.ReservedStateErrorMetadata;
 import org.elasticsearch.cluster.metadata.ReservedStateHandlerMetadata;
 import org.elasticsearch.cluster.metadata.ReservedStateMetadata;
+import org.elasticsearch.cluster.project.ProjectStateRegistry;
 import org.elasticsearch.cluster.routing.RerouteService;
 import org.elasticsearch.cluster.service.ClusterService;
 import org.elasticsearch.cluster.service.MasterServiceTaskQueue;
@@ -183,7 +184,7 @@ public class ReservedClusterStateServiceTests extends ESTestCase {
     }
 
     private static Map<String, ReservedStateMetadata> getMetadata(ClusterState state, Optional<ProjectId> projectId) {
-        return projectId.map(p -> state.metadata().getProject(p).reservedStateMetadata())
+        return projectId.map(p -> ProjectStateRegistry.get(state).reservedStateMetadata(p))
             .orElseGet(() -> state.metadata().reservedStateMetadata());
     }
 
@@ -739,10 +740,16 @@ public class ReservedClusterStateServiceTests extends ESTestCase {
 
         Optional<ProjectId> projectId = randomBoolean() ? Optional.empty() : Optional.of(randomProjectIdOrDefault());
 
-        Metadata metadata = projectId.map(p -> Metadata.builder().put(ProjectMetadata.builder(p).put(operatorMetadata)))
-            .orElseGet(() -> Metadata.builder().put(operatorMetadata))
-            .build();
-        ClusterState state = ClusterState.builder(new ClusterName("test")).metadata(metadata).build();
+        ClusterState.Builder builder = ClusterState.builder(new ClusterName("test"));
+        if (projectId.isPresent()) {
+            builder.putCustom(
+                ProjectStateRegistry.TYPE,
+                ProjectStateRegistry.builder().putReservedStateMetadata(projectId.get(), operatorMetadata).build()
+            );
+        } else {
+            builder.metadata(Metadata.builder().put(operatorMetadata));
+        }
+        ClusterState state = builder.build();
 
         assertFalse(ReservedStateErrorTask.isNewError(operatorMetadata, 2L, ReservedStateVersionCheck.HIGHER_VERSION_ONLY));
         assertFalse(ReservedStateErrorTask.isNewError(operatorMetadata, 1L, ReservedStateVersionCheck.HIGHER_VERSION_ONLY));
@@ -849,10 +856,16 @@ public class ReservedClusterStateServiceTests extends ESTestCase {
             .putHandler(hmOne)
             .build();
 
-        metadata = projectId.map(p -> Metadata.builder().put(ProjectMetadata.builder(p).put(opMetadata)))
-            .orElseGet(() -> Metadata.builder().put(opMetadata))
-            .build();
-        ClusterState newState = ClusterState.builder(new ClusterName("test")).metadata(metadata).build();
+        builder = ClusterState.builder(new ClusterName("test"));
+        if (projectId.isPresent()) {
+            builder.putCustom(
+                ProjectStateRegistry.TYPE,
+                ProjectStateRegistry.builder().putReservedStateMetadata(projectId.get(), opMetadata).build()
+            );
+        } else {
+            builder.metadata(Metadata.builder().put(opMetadata));
+        }
+        ClusterState newState = builder.build();
 
         // We exit on duplicate errors before we update the reserved state error metadata
         assertThat(
@@ -865,10 +878,16 @@ public class ReservedClusterStateServiceTests extends ESTestCase {
         ReservedStateMetadata operatorMetadata = ReservedStateMetadata.builder("test").version(123L).build();
 
         Optional<ProjectId> projectId = randomBoolean() ? Optional.empty() : Optional.of(randomProjectIdOrDefault());
-        Metadata metadata = projectId.map(p -> Metadata.builder().put(ProjectMetadata.builder(p).put(operatorMetadata)))
-            .orElseGet(() -> Metadata.builder().put(operatorMetadata))
-            .build();
-        ClusterState state = ClusterState.builder(new ClusterName("test")).metadata(metadata).build();
+        ClusterState.Builder builder = ClusterState.builder(new ClusterName("test"));
+        if (projectId.isPresent()) {
+            builder.putCustom(
+                ProjectStateRegistry.TYPE,
+                ProjectStateRegistry.builder().putReservedStateMetadata(projectId.get(), operatorMetadata).build()
+            );
+        } else {
+            builder.metadata(Metadata.builder().put(operatorMetadata));
+        }
+        ClusterState state = builder.build();
 
         ReservedStateUpdateTask<?> task = createEmptyTask(
             projectId,