Browse Source

Add RollupV2 cluster metadata behind feature-flag (#64680)

this commit sets the foundation for following rollup-v2-related
commits to master.

The intention of this metadata is to be used by both the upcoming
RollupV2 action that will create new indices along with their associated
cluster metadata. This metadata is to be used by the SearchService when
filtering shards in the can-match phase to decide which of the indices
belonging to an original index should be queried.

Co-authored-by: Mark Tozzi <mark.tozzi@gmail.com>
Tal Levy 5 years ago
parent
commit
0a1a28d3c3

+ 10 - 0
server/src/main/java/org/elasticsearch/cluster/ClusterModule.java

@@ -35,6 +35,7 @@ import org.elasticsearch.cluster.metadata.MetadataIndexTemplateService;
 import org.elasticsearch.cluster.metadata.MetadataMappingService;
 import org.elasticsearch.cluster.metadata.MetadataUpdateSettingsService;
 import org.elasticsearch.cluster.metadata.RepositoriesMetadata;
+import org.elasticsearch.cluster.metadata.RollupMetadata;
 import org.elasticsearch.cluster.routing.DelayedAllocationService;
 import org.elasticsearch.cluster.routing.allocation.AllocationService;
 import org.elasticsearch.cluster.routing.allocation.ExistingShardsAllocator;
@@ -75,6 +76,7 @@ import org.elasticsearch.ingest.IngestMetadata;
 import org.elasticsearch.persistent.PersistentTasksCustomMetadata;
 import org.elasticsearch.persistent.PersistentTasksNodeService;
 import org.elasticsearch.plugins.ClusterPlugin;
+import org.elasticsearch.rollup.RollupV2;
 import org.elasticsearch.script.ScriptMetadata;
 import org.elasticsearch.snapshots.SnapshotsInfoService;
 import org.elasticsearch.tasks.Task;
@@ -140,6 +142,10 @@ public class ClusterModule extends AbstractModule {
         registerMetadataCustom(entries, ComposableIndexTemplateMetadata.TYPE, ComposableIndexTemplateMetadata::new,
             ComposableIndexTemplateMetadata::readDiffFrom);
         registerMetadataCustom(entries, DataStreamMetadata.TYPE, DataStreamMetadata::new, DataStreamMetadata::readDiffFrom);
+
+        if (RollupV2.ROLLUPV2_FEATURE_FLAG_REGISTERED != null && RollupV2.ROLLUPV2_FEATURE_FLAG_REGISTERED) {
+            registerMetadataCustom(entries, RollupMetadata.TYPE, RollupMetadata::new, RollupMetadata::readDiffFrom);
+        }
         // Task Status (not Diffable)
         entries.add(new Entry(Task.Status.class, PersistentTasksNodeService.Status.NAME, PersistentTasksNodeService.Status::new));
         return entries;
@@ -164,6 +170,10 @@ public class ClusterModule extends AbstractModule {
             ComposableIndexTemplateMetadata::fromXContent));
         entries.add(new NamedXContentRegistry.Entry(Metadata.Custom.class, new ParseField(DataStreamMetadata.TYPE),
             DataStreamMetadata::fromXContent));
+        if (RollupV2.ROLLUPV2_FEATURE_FLAG_REGISTERED != null && RollupV2.ROLLUPV2_FEATURE_FLAG_REGISTERED) {
+            entries.add(new NamedXContentRegistry.Entry(Metadata.Custom.class, new ParseField(RollupMetadata.TYPE),
+                RollupMetadata::fromXContent));
+        }
         return entries;
     }
 

+ 182 - 0
server/src/main/java/org/elasticsearch/cluster/metadata/RollupGroup.java

@@ -0,0 +1,182 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.cluster.metadata;
+
+import org.elasticsearch.cluster.AbstractDiffable;
+import org.elasticsearch.cluster.Diff;
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.time.WriteableZoneId;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
+import org.elasticsearch.common.xcontent.ToXContentObject;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramInterval;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Object representing information about rollup-v2 indices and their respective original-indexes. These objects
+ * also include information about their capabilities, like which date-intervals and date-timezones they are configured
+ * with. Used by {@link RollupMetadata}.
+ *
+ * The information in this class will be used to decide which index within the <code>group</code> will be chosen
+ * for a specific aggregation. For example, if there are two indices with different intervals (`1h`, `1d`) and
+ * a date-histogram aggregation request is sent for daily intervals, then the index with the associated `1d` interval
+ * will be chosen.
+ */
+public class RollupGroup extends AbstractDiffable<RollupGroup> implements ToXContentObject {
+    private static final ParseField GROUP_FIELD = new ParseField("group");
+    private static final ParseField DATE_INTERVAL_FIELD = new ParseField("interval");
+    private static final ParseField DATE_TIMEZONE_FIELD = new ParseField("timezone");
+
+    // the list of indices part of this rollup group
+    private List<String> group;
+    // a map from index-name to the date interval used in the associated index
+    private Map<String, DateHistogramInterval> dateInterval;
+    // a map from index-name to timezone used in the associated index
+    private Map<String, WriteableZoneId> dateTimezone;
+
+    @SuppressWarnings("unchecked")
+    public static final ConstructingObjectParser<RollupGroup, Void> PARSER =
+        new ConstructingObjectParser<>("rollup_group", false,
+            a -> new RollupGroup((List<String>) a[0], (Map<String, DateHistogramInterval>) a[1], (Map<String, WriteableZoneId>) a[2]));
+
+    static {
+        PARSER.declareStringArray(ConstructingObjectParser.constructorArg(), GROUP_FIELD);
+        PARSER.declareObject(ConstructingObjectParser.constructorArg(), (p, c) -> {
+            Map<String, DateHistogramInterval> intervalMap = new HashMap<>();
+
+            while (p.nextToken() != XContentParser.Token.END_OBJECT) {
+                String name = p.currentName();
+                p.nextToken();
+                String expression = p.text();
+                intervalMap.put(name, new DateHistogramInterval(expression));
+            }
+            return intervalMap;
+        }, DATE_INTERVAL_FIELD);
+        PARSER.declareObject(ConstructingObjectParser.constructorArg(), (p, c) -> {
+            Map<String, WriteableZoneId> zoneMap = new HashMap<>();
+            while (p.nextToken() != XContentParser.Token.END_OBJECT) {
+                String name = p.currentName();
+                p.nextToken();
+                String timezone = p.text();
+                zoneMap.put(name, WriteableZoneId.of(timezone));
+            }
+            return zoneMap;
+        }, DATE_TIMEZONE_FIELD);
+    }
+
+    public RollupGroup(List<String> group, Map<String, DateHistogramInterval> dateInterval, Map<String, WriteableZoneId> dateTimezone) {
+        this.group = group;
+        this.dateInterval = dateInterval;
+        this.dateTimezone = dateTimezone;
+    }
+
+    public RollupGroup(StreamInput in) throws IOException {
+        this.group = in.readStringList();
+        this.dateInterval = in.readMap(StreamInput::readString, DateHistogramInterval::new);
+        this.dateTimezone = in.readMap(StreamInput::readString, WriteableZoneId::new);
+    }
+
+
+    public static RollupGroup fromXContent(XContentParser parser) throws IOException {
+        return PARSER.parse(parser, null);
+    }
+
+    public void add(String name, DateHistogramInterval interval, WriteableZoneId timezone) {
+        group.add(name);
+        dateInterval.put(name, interval);
+        dateTimezone.put(name, timezone);
+    }
+
+    public void remove(String name) {
+        group.remove(name);
+        dateInterval.remove(name);
+        dateTimezone.remove(name);
+    }
+
+    public boolean contains(String name) {
+        return group.contains(name);
+    }
+
+    public DateHistogramInterval getDateInterval(String name) {
+        return dateInterval.get(name);
+    }
+
+    public WriteableZoneId getDateTimezone(String name) {
+        return dateTimezone.get(name);
+    }
+
+    public List<String> getIndices() {
+        return group;
+    }
+
+    static Diff<RollupGroup> readDiffFrom(StreamInput in) throws IOException {
+        return AbstractDiffable.readDiffFrom(RollupGroup::new, in);
+    }
+
+    public static RollupGroup parse(XContentParser parser) {
+        return PARSER.apply(parser, null);
+    }
+
+    @Override
+    public String toString() {
+        return Strings.toString(this);
+    }
+
+    @Override
+    public void writeTo(StreamOutput out) throws IOException {
+        out.writeStringCollection(group);
+        out.writeMap(dateInterval, StreamOutput::writeString, (stream, val) -> val.writeTo(stream));
+        out.writeMap(dateTimezone, StreamOutput::writeString, (stream, val) -> val.writeTo(stream));
+    }
+
+    @Override
+    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+        return builder
+            .startObject()
+            .field(GROUP_FIELD.getPreferredName(), group)
+            .field(DATE_INTERVAL_FIELD.getPreferredName(), dateInterval)
+            .field(DATE_TIMEZONE_FIELD.getPreferredName(), dateTimezone)
+            .endObject();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        RollupGroup that = (RollupGroup) o;
+        return group.equals(that.group) &&
+            dateInterval.equals(that.dateInterval) &&
+            dateTimezone.equals(that.dateTimezone);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(group, dateInterval, dateTimezone);
+    }
+}

+ 192 - 0
server/src/main/java/org/elasticsearch/cluster/metadata/RollupMetadata.java

@@ -0,0 +1,192 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.cluster.metadata;
+
+import org.elasticsearch.Version;
+import org.elasticsearch.cluster.Diff;
+import org.elasticsearch.cluster.DiffableUtils;
+import org.elasticsearch.cluster.NamedDiff;
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentParser;
+
+import java.io.IOException;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Custom {@link Metadata} implementation for storing a map of {@link RollupGroup}s and their names.
+ */
+public class RollupMetadata implements Metadata.Custom {
+    public static final String TYPE = "rollup";
+    public static final String SOURCE_INDEX_NAME_META_FIELD = "source_index";
+    private static final ParseField ROLLUP = new ParseField("rollup");
+
+    @SuppressWarnings("unchecked")
+    private static final ConstructingObjectParser<RollupMetadata, Void> PARSER = new ConstructingObjectParser<>(TYPE, false,
+        a -> new RollupMetadata((Map<String, RollupGroup>) a[0]));
+
+    static {
+        PARSER.declareObject(ConstructingObjectParser.constructorArg(), (p, c) -> {
+            Map<String, RollupGroup> rollupGroups = new HashMap<>();
+            while (p.nextToken() != XContentParser.Token.END_OBJECT) {
+                String name = p.currentName();
+                rollupGroups.put(name, RollupGroup.parse(p));
+            }
+            return rollupGroups;
+        }, ROLLUP);
+    }
+
+    private final Map<String, RollupGroup> rollupIndices;
+
+    public RollupMetadata(Map<String, RollupGroup> rollupIndices) {
+        this.rollupIndices = rollupIndices;
+    }
+
+    public RollupMetadata(StreamInput in) throws IOException {
+        this.rollupIndices = in.readMap(StreamInput::readString, RollupGroup::new);
+    }
+
+    public Map<String, RollupGroup> rollupGroups() {
+        return this.rollupIndices;
+    }
+
+    public boolean contains(String index) {
+        return this.rollupIndices.containsKey(index);
+    }
+
+    @Override
+    public Diff<Metadata.Custom> diff(Metadata.Custom before) {
+        return new RollupMetadata.RollupMetadataDiff((RollupMetadata) before, this);
+    }
+
+    public static NamedDiff<Metadata.Custom> readDiffFrom(StreamInput in) throws IOException {
+        return new RollupMetadataDiff(in);
+    }
+
+    @Override
+    public EnumSet<Metadata.XContentContext> context() {
+        return Metadata.ALL_CONTEXTS;
+    }
+
+    @Override
+    public String getWriteableName() {
+        return TYPE;
+    }
+
+    @Override
+    public Version getMinimalSupportedVersion() {
+        return Version.V_8_0_0;
+    }
+
+    @Override
+    public void writeTo(StreamOutput out) throws IOException {
+        out.writeMap(this.rollupIndices, StreamOutput::writeString, (stream, val) -> val.writeTo(stream));
+    }
+
+    public static RollupMetadata fromXContent(XContentParser parser) throws IOException {
+        return PARSER.parse(parser, null);
+    }
+
+    @Override
+    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+        builder.startObject(ROLLUP.getPreferredName());
+        for (Map.Entry<String, RollupGroup> rollup : rollupIndices.entrySet()) {
+            builder.field(rollup.getKey(), rollup.getValue());
+        }
+        builder.endObject();
+        return builder;
+    }
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(this.rollupIndices);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (obj.getClass() != getClass()) {
+            return false;
+        }
+        RollupMetadata other = (RollupMetadata) obj;
+        return Objects.equals(this.rollupIndices, other.rollupIndices);
+    }
+
+    @Override
+    public String toString() {
+        return Strings.toString(this);
+    }
+
+    public static class Builder {
+
+        private final Map<String, RollupGroup> rollupIndices = new HashMap<>();
+
+        public Builder putRollupGroup(String name, RollupGroup group) {
+            rollupIndices.put(name,  group);
+            return this;
+        }
+
+        public RollupMetadata build() {
+            return new RollupMetadata(rollupIndices);
+        }
+    }
+
+    static class RollupMetadataDiff implements NamedDiff<Metadata.Custom> {
+
+        final Diff<Map<String, RollupGroup>> rollupIndicesDiff;
+
+        RollupMetadataDiff(RollupMetadata before, RollupMetadata after) {
+            this.rollupIndicesDiff = DiffableUtils.diff(before.rollupIndices, after.rollupIndices, DiffableUtils.getStringKeySerializer());
+        }
+
+        RollupMetadataDiff(StreamInput in) throws IOException {
+            this.rollupIndicesDiff = DiffableUtils.readJdkMapDiff(in, DiffableUtils.getStringKeySerializer(),
+                RollupGroup::new, RollupGroup::readDiffFrom);
+        }
+
+        @Override
+        public Metadata.Custom apply(Metadata.Custom part) {
+            return new RollupMetadata(rollupIndicesDiff.apply(((RollupMetadata) part).rollupIndices));
+        }
+
+        @Override
+        public void writeTo(StreamOutput out) throws IOException {
+            rollupIndicesDiff.writeTo(out);
+        }
+
+        @Override
+        public String getWriteableName() {
+            return TYPE;
+        }
+    }
+}

+ 83 - 0
server/src/main/java/org/elasticsearch/common/time/WriteableZoneId.java

@@ -0,0 +1,83 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.common.time;
+
+
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.io.stream.Writeable;
+import org.elasticsearch.common.xcontent.ToXContentFragment;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+
+import java.io.IOException;
+import java.time.ZoneId;
+import java.util.Objects;
+
+/**
+ * Simple wrapper around {@link ZoneId} so that it can be written to XContent
+ */
+public class WriteableZoneId implements Writeable, ToXContentFragment {
+
+    private final ZoneId zoneId;
+
+    public WriteableZoneId(ZoneId zoneId) {
+        this.zoneId = zoneId;
+    }
+
+    public WriteableZoneId(StreamInput in) throws IOException {
+        zoneId = ZoneId.of(in.readString());
+    }
+
+    public static WriteableZoneId of(String input) {
+        return new WriteableZoneId(ZoneId.of(input));
+    }
+
+    public ZoneId zoneId() {
+        return zoneId;
+    }
+
+    @Override
+    public void writeTo(StreamOutput out) throws IOException {
+        out.writeString(zoneId.getId());
+    }
+
+    @Override
+    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+        return builder.value(zoneId.getId());
+    }
+
+    @Override
+    public String toString() {
+        return zoneId.toString();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        WriteableZoneId that = (WriteableZoneId) o;
+        return Objects.equals(zoneId, that.zoneId);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(zoneId);
+    }
+}

+ 44 - 0
server/src/main/java/org/elasticsearch/rollup/RollupV2.java

@@ -0,0 +1,44 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.rollup;
+
+import org.elasticsearch.Build;
+
+public class RollupV2 {
+    public static final Boolean ROLLUPV2_FEATURE_FLAG_REGISTERED;
+
+    static {
+        final String property = System.getProperty("es.rollupv2_feature_flag_registered");
+        if (Build.CURRENT.isSnapshot() && property != null) {
+            throw new IllegalArgumentException("es.rollupv2_feature_flag_registered is only supported in non-snapshot builds");
+        }
+        if ("true".equals(property)) {
+            ROLLUPV2_FEATURE_FLAG_REGISTERED = true;
+        } else if ("false".equals(property)) {
+            ROLLUPV2_FEATURE_FLAG_REGISTERED = false;
+        } else if (property == null) {
+            ROLLUPV2_FEATURE_FLAG_REGISTERED = null;
+        } else {
+            throw new IllegalArgumentException(
+                "expected es.rollupv2_feature_flag_registered to be unset or [true|false] but was [" + property + "]"
+            );
+        }
+    }
+}

+ 65 - 0
server/src/test/java/org/elasticsearch/cluster/metadata/RollupGroupTests.java

@@ -0,0 +1,65 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.elasticsearch.cluster.metadata;
+
+import org.elasticsearch.common.io.stream.Writeable;
+import org.elasticsearch.common.time.WriteableZoneId;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramInterval;
+import org.elasticsearch.test.AbstractSerializingTestCase;
+
+import java.io.IOException;
+import java.time.ZoneOffset;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class RollupGroupTests extends AbstractSerializingTestCase<RollupGroup> {
+
+    @Override
+    protected RollupGroup doParseInstance(XContentParser parser) throws IOException {
+        return RollupGroup.fromXContent(parser);
+    }
+
+    @Override
+    protected Writeable.Reader<RollupGroup> instanceReader() {
+        return RollupGroup::new;
+    }
+
+    @Override
+    protected RollupGroup createTestInstance() {
+        return randomInstance();
+    }
+
+    static RollupGroup randomInstance() {
+        List<String> group = new ArrayList<>();
+        for (int i = 0; i < randomIntBetween(1, 5); i++) {
+            group.add(randomAlphaOfLength(5 + i));
+        }
+        Map<String, DateHistogramInterval> dateInterval = new HashMap<>();
+        Map<String, WriteableZoneId> dateTimezone = new HashMap<>();
+        for (String index : group) {
+            DateHistogramInterval interval = randomFrom(DateHistogramInterval.MINUTE, DateHistogramInterval.HOUR);
+            dateInterval.put(index, interval);
+            dateTimezone.put(index, WriteableZoneId.of(randomFrom(ZoneOffset.getAvailableZoneIds())));
+        }
+        return new RollupGroup(group, dateInterval, dateTimezone);
+    }
+}

+ 62 - 0
server/src/test/java/org/elasticsearch/cluster/metadata/RollupMetadataTests.java

@@ -0,0 +1,62 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.cluster.metadata;
+
+import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
+import org.elasticsearch.test.AbstractNamedWriteableTestCase;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Tests for testing {@link RollupMetadata}, the Rollup V2 cluster metadata
+ */
+public class RollupMetadataTests extends AbstractNamedWriteableTestCase<RollupMetadata> {
+
+    @Override
+    protected RollupMetadata createTestInstance() {
+        if (randomBoolean()) {
+            return new RollupMetadata(Collections.emptyMap());
+        }
+        Map<String, RollupGroup> rollupGroups = new HashMap<>();
+        for (int i = 0; i < randomIntBetween(1, 5); i++) {
+            rollupGroups.put(randomAlphaOfLength(5), RollupGroupTests.randomInstance());
+        }
+        return new RollupMetadata(rollupGroups);
+    }
+
+    @Override
+    protected RollupMetadata mutateInstance(RollupMetadata instance) throws IOException {
+        return randomValueOtherThan(instance, this::createTestInstance);
+    }
+
+    @Override
+    protected NamedWriteableRegistry getNamedWriteableRegistry() {
+        return new NamedWriteableRegistry(Collections.singletonList(new NamedWriteableRegistry.Entry(RollupMetadata.class,
+            RollupMetadata.TYPE, RollupMetadata::new)));
+    }
+
+    @Override
+    protected Class<RollupMetadata> categoryClass() {
+        return RollupMetadata.class;
+    }
+}