diff --git a/server/src/main/java/org/elasticsearch/TransportVersions.java b/server/src/main/java/org/elasticsearch/TransportVersions.java index 416abedf69809..dbf9a9e01bef7 100644 --- a/server/src/main/java/org/elasticsearch/TransportVersions.java +++ b/server/src/main/java/org/elasticsearch/TransportVersions.java @@ -193,6 +193,7 @@ static TransportVersion def(int id) { public static final TransportVersion INTRODUCE_LIFECYCLE_TEMPLATE = def(9_033_0_00); public static final TransportVersion INDEXING_STATS_INCLUDES_RECENT_WRITE_LOAD = def(9_034_0_00); public static final TransportVersion ESQL_AGGREGATE_METRIC_DOUBLE_LITERAL = def(9_035_0_00); + public static final TransportVersion SOURCE_PRIMARY_TERM_IN_START_SHARD = def(9_036_0_00); /* * STOP! READ THIS FIRST! No, really, diff --git a/server/src/main/java/org/elasticsearch/cluster/action/shard/ShardStateAction.java b/server/src/main/java/org/elasticsearch/cluster/action/shard/ShardStateAction.java index 3fa4685db8d4a..5a1d2a12db830 100644 --- a/server/src/main/java/org/elasticsearch/cluster/action/shard/ShardStateAction.java +++ b/server/src/main/java/org/elasticsearch/cluster/action/shard/ShardStateAction.java @@ -25,6 +25,8 @@ import org.elasticsearch.cluster.NotMasterException; import org.elasticsearch.cluster.coordination.FailedToCommitClusterStateException; import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.cluster.metadata.IndexReshardingMetadata; +import org.elasticsearch.cluster.metadata.IndexReshardingState; import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.cluster.metadata.ProjectId; import org.elasticsearch.cluster.metadata.ProjectMetadata; @@ -41,6 +43,7 @@ import org.elasticsearch.common.Strings; 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.util.concurrent.EsExecutors; import org.elasticsearch.core.Nullable; import org.elasticsearch.core.TimeValue; @@ -580,6 +583,31 @@ public void shardStarted( ); } + public void shardSplit( + final ShardRouting shardRouting, + final long primaryTerm, + final String message, + final ShardLongFieldRange timestampRange, + final ShardLongFieldRange eventIngestedRange, + final long sourcePrimaryTerm, + final ActionListener listener + ) { + ClusterState currentState = clusterService.state(); + remoteShardStateUpdateDeduplicator.executeOnce( + new StartedShardEntry( + shardRouting.shardId(), + shardRouting.allocationId().getId(), + primaryTerm, + message, + timestampRange, + eventIngestedRange, + new ShardSplit(sourcePrimaryTerm) + ), + listener, + (req, l) -> sendShardAction(SHARD_STARTED_ACTION_NAME, currentState, req, l) + ); + } + // TODO: Make this a TransportMasterNodeAction and remove duplication of master failover retrying from upstream code private static class ShardStartedTransportHandler implements TransportRequestHandler { private final MasterServiceTaskQueue taskQueue; @@ -691,6 +719,12 @@ public ClusterState execute(BatchExecutionContext batchE matched ); tasksToBeApplied.add(taskContext); + } else if (invalidShardSplit(startedShardEntry, projectId, initialState)) { + logger.debug("{} failing shard started task because split validation failed", startedShardEntry.shardId); + // TODO: Currently invalid shard split triggers if the primary term changes, the source primary term changes or + // is >= the target primary term or if the source is relocating. In the second and third scenario this will be + // swallow currently. In the split process we will need to handle this. + taskContext.success(() -> task.onFailure(new IllegalStateException("Cannot start"))); } else { logger.debug( "{} starting shard {} (shard started task: [{}])", @@ -789,6 +823,31 @@ public ClusterState execute(BatchExecutionContext batchE return maybeUpdatedState; } + private static boolean invalidShardSplit(StartedShardEntry startedShardEntry, ProjectId projectId, ClusterState clusterState) { + ShardSplit shardSplit = startedShardEntry.shardSplit; + if (shardSplit == null) { + return false; + } + IndexRoutingTable routingTable = clusterState.routingTable(projectId).index(startedShardEntry.shardId.getIndex()); + final IndexMetadata indexMetadata = clusterState.metadata().getProject(projectId).index(startedShardEntry.shardId.getIndex()); + assert indexMetadata != null; + IndexReshardingMetadata reshardingMetadata = indexMetadata.getReshardingMetadata(); + assert reshardingMetadata != null; + IndexReshardingState.Split split = reshardingMetadata.getSplit(); + int sourceShardId = startedShardEntry.shardId.getId() % split.shardCountBefore(); + long currentSourcePrimaryTerm = indexMetadata.primaryTerm(sourceShardId); + long primaryTermDiff = startedShardEntry.primaryTerm - currentSourcePrimaryTerm; + // The source primary term must not have changed, the target primary term must at least be equal to or greater and the source + // cannot be relocating. + if (startedShardEntry.shardSplit.sourcePrimaryTerm() != currentSourcePrimaryTerm + || primaryTermDiff < 0 + || routingTable.shard(sourceShardId).primaryShard().relocating()) { + return true; + } else { + return false; + } + } + private static boolean assertStartedIndicesHaveCompleteTimestampRanges(ClusterState clusterState) { for (ProjectId projectId : clusterState.metadata().projects().keySet()) { for (Map.Entry cursor : clusterState.routingTable(projectId).getIndicesRouting().entrySet()) { @@ -827,6 +886,18 @@ public void clusterStatePublished(ClusterState newClusterState) { } } + record ShardSplit(long sourcePrimaryTerm) implements Writeable { + + ShardSplit(StreamInput in) throws IOException { + this(in.readVLong()); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeVLong(sourcePrimaryTerm); + } + } + public static class StartedShardEntry extends TransportRequest { final ShardId shardId; final String allocationId; @@ -834,6 +905,7 @@ public static class StartedShardEntry extends TransportRequest { final String message; final ShardLongFieldRange timestampRange; final ShardLongFieldRange eventIngestedRange; + final ShardSplit shardSplit; StartedShardEntry(StreamInput in) throws IOException { super(in); @@ -847,6 +919,11 @@ public static class StartedShardEntry extends TransportRequest { } else { this.eventIngestedRange = ShardLongFieldRange.UNKNOWN; } + if (in.getTransportVersion().onOrAfter(TransportVersions.SOURCE_PRIMARY_TERM_IN_START_SHARD)) { + this.shardSplit = in.readOptionalWriteable(ShardSplit::new); + } else { + this.shardSplit = null; + } } public StartedShardEntry( @@ -856,6 +933,18 @@ public StartedShardEntry( final String message, final ShardLongFieldRange timestampRange, final ShardLongFieldRange eventIngestedRange + ) { + this(shardId, allocationId, primaryTerm, message, timestampRange, eventIngestedRange, null); + } + + public StartedShardEntry( + final ShardId shardId, + final String allocationId, + final long primaryTerm, + final String message, + final ShardLongFieldRange timestampRange, + final ShardLongFieldRange eventIngestedRange, + @Nullable final ShardSplit shardSplit ) { this.shardId = shardId; this.allocationId = allocationId; @@ -863,6 +952,7 @@ public StartedShardEntry( this.message = message; this.timestampRange = timestampRange; this.eventIngestedRange = eventIngestedRange; + this.shardSplit = shardSplit; } @Override @@ -876,6 +966,9 @@ public void writeTo(StreamOutput out) throws IOException { if (out.getTransportVersion().onOrAfter(TransportVersions.V_8_15_0)) { eventIngestedRange.writeTo(out); } + if (out.getTransportVersion().onOrAfter(TransportVersions.SOURCE_PRIMARY_TERM_IN_START_SHARD)) { + out.writeOptionalWriteable(shardSplit); + } } @Override @@ -891,20 +984,20 @@ public String toString() { @Override public boolean equals(Object o) { - if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; StartedShardEntry that = (StartedShardEntry) o; return primaryTerm == that.primaryTerm - && shardId.equals(that.shardId) - && allocationId.equals(that.allocationId) - && message.equals(that.message) - && timestampRange.equals(that.timestampRange) - && eventIngestedRange.equals(that.eventIngestedRange); + && Objects.equals(shardId, that.shardId) + && Objects.equals(allocationId, that.allocationId) + && Objects.equals(message, that.message) + && Objects.equals(timestampRange, that.timestampRange) + && Objects.equals(eventIngestedRange, that.eventIngestedRange) + && Objects.equals(shardSplit, that.shardSplit); } @Override public int hashCode() { - return Objects.hash(shardId, allocationId, primaryTerm, message, timestampRange, eventIngestedRange); + return Objects.hash(shardId, allocationId, primaryTerm, message, timestampRange, eventIngestedRange, shardSplit); } } @@ -946,7 +1039,5 @@ public NoLongerPrimaryShardException(ShardId shardId, String msg) { public NoLongerPrimaryShardException(StreamInput in) throws IOException { super(in); } - } - } diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetadata.java index 4138a6d4cebe5..1b50384ad4585 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetadata.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetadata.java @@ -893,8 +893,18 @@ public IndexMetadata withInSyncAllocationIds(int shardId, Set inSyncSet) * @return updated instance with incremented primary term */ public IndexMetadata withIncrementedPrimaryTerm(int shardId) { + return withSetPrimaryTerm(shardId, this.primaryTerms[shardId] + 1); + } + + /** + * Creates a copy of this instance that has the primary term for the given shard id set to the value provided. + * @param shardId shard id to set primary term for + * @param primaryTerm primary term to set + * @return updated instance with set primary term + */ + public IndexMetadata withSetPrimaryTerm(int shardId, long primaryTerm) { final long[] incremented = this.primaryTerms.clone(); - incremented[shardId]++; + incremented[shardId] = primaryTerm; return new IndexMetadata( this.index, this.version, diff --git a/server/src/main/java/org/elasticsearch/cluster/routing/ExpectedShardSizeEstimator.java b/server/src/main/java/org/elasticsearch/cluster/routing/ExpectedShardSizeEstimator.java index 4fd8cc113fcf8..e3e47723b30f4 100644 --- a/server/src/main/java/org/elasticsearch/cluster/routing/ExpectedShardSizeEstimator.java +++ b/server/src/main/java/org/elasticsearch/cluster/routing/ExpectedShardSizeEstimator.java @@ -58,6 +58,10 @@ public static boolean shouldReserveSpaceForInitializingShard(ShardRouting shard, // shrink/split/clone operation is going to clone existing locally placed shards using file system hard links // so no additional space is going to be used until future merges case LOCAL_SHARDS -> false; + + // Split currently does not require space locally as it maps to existing store. Future implementations might require the space + // locally. + case SPLIT -> false; }; } diff --git a/server/src/main/java/org/elasticsearch/cluster/routing/RecoverySource.java b/server/src/main/java/org/elasticsearch/cluster/routing/RecoverySource.java index a6d46ce1ff7b8..490fe7346cca2 100644 --- a/server/src/main/java/org/elasticsearch/cluster/routing/RecoverySource.java +++ b/server/src/main/java/org/elasticsearch/cluster/routing/RecoverySource.java @@ -31,6 +31,7 @@ * - {@link PeerRecoverySource} recovery from a primary on another node * - {@link SnapshotRecoverySource} recovery from a snapshot * - {@link LocalShardsRecoverySource} recovery from other shards of another index on the same node + * - {@link SplitRecoverySource} recovery that is split from a source shard */ public abstract class RecoverySource implements Writeable, ToXContentObject { @@ -57,6 +58,7 @@ public static RecoverySource readFrom(StreamInput in) throws IOException { case PEER -> PeerRecoverySource.INSTANCE; case SNAPSHOT -> new SnapshotRecoverySource(in); case LOCAL_SHARDS -> LocalShardsRecoverySource.INSTANCE; + case SPLIT -> SplitRecoverySource.INSTANCE; }; } @@ -78,7 +80,8 @@ public enum Type { EXISTING_STORE, PEER, SNAPSHOT, - LOCAL_SHARDS + LOCAL_SHARDS, + SPLIT } public abstract Type getType(); @@ -319,4 +322,36 @@ public boolean expectEmptyRetentionLeases() { return false; } } + + /** + * split recovery from a source primary shard + */ + public static class SplitRecoverySource extends RecoverySource { + + public static final SplitRecoverySource INSTANCE = new SplitRecoverySource(); + + private SplitRecoverySource() {} + + @Override + public Type getType() { + return Type.SPLIT; + } + + @Override + public String toString() { + return "split recovery"; + } + + @Override + protected void writeAdditionalFields(StreamOutput out) throws IOException { + super.writeAdditionalFields(out); + } + + @Override + public void addAdditionalFields(XContentBuilder builder, Params params) throws IOException { + super.addAdditionalFields(builder, params); + } + + // TODO: Expect empty retention leases? + } } diff --git a/server/src/main/java/org/elasticsearch/cluster/routing/allocation/IndexMetadataUpdater.java b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/IndexMetadataUpdater.java index 8f3915ce586f2..589027a78b90d 100644 --- a/server/src/main/java/org/elasticsearch/cluster/routing/allocation/IndexMetadataUpdater.java +++ b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/IndexMetadataUpdater.java @@ -12,9 +12,11 @@ import org.apache.logging.log4j.Logger; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.cluster.metadata.IndexReshardingMetadata; import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.cluster.metadata.ProjectMetadata; import org.elasticsearch.cluster.routing.GlobalRoutingTable; +import org.elasticsearch.cluster.routing.IndexRoutingTable; import org.elasticsearch.cluster.routing.IndexShardRoutingTable; import org.elasticsearch.cluster.routing.RecoverySource; import org.elasticsearch.cluster.routing.RoutingChangesObserver; @@ -127,15 +129,15 @@ public Metadata applyChanges(Metadata oldMetadata, GlobalRoutingTable newRouting for (Map.Entry shardEntry : indexChanges) { ShardId shardId = shardEntry.getKey(); Updates updates = shardEntry.getValue(); - updatedIndexMetadata = updateInSyncAllocations( - newRoutingTable.routingTable(projectMetadata.id()), - oldIndexMetadata, - updatedIndexMetadata, - shardId, - updates - ); + RoutingTable routingTable = newRoutingTable.routingTable(projectMetadata.id()); + updatedIndexMetadata = updateInSyncAllocations(routingTable, oldIndexMetadata, updatedIndexMetadata, shardId, updates); + IndexRoutingTable indexRoutingTable = routingTable.index(shardEntry.getKey().getIndex()); + RecoverySource recoverySource = indexRoutingTable.shard(shardEntry.getKey().id()).primaryShard().recoverySource(); + boolean split = recoverySource != null && recoverySource.getType() == RecoverySource.Type.SPLIT; updatedIndexMetadata = updates.increaseTerm - ? updatedIndexMetadata.withIncrementedPrimaryTerm(shardId.id()) + ? split + ? updatedIndexMetadata.withSetPrimaryTerm(shardId.id(), splitPrimaryTerm(updatedIndexMetadata, shardId)) + : updatedIndexMetadata.withIncrementedPrimaryTerm(shardId.id()) : updatedIndexMetadata; } if (updatedIndexMetadata != oldIndexMetadata) { @@ -147,6 +149,18 @@ public Metadata applyChanges(Metadata oldMetadata, GlobalRoutingTable newRouting return updatedMetadata.build(); } + private static long splitPrimaryTerm(IndexMetadata updatedIndexMetadata, ShardId shardId) { + IndexReshardingMetadata reshardingMetadata = updatedIndexMetadata.getReshardingMetadata(); + assert reshardingMetadata != null; + + // We take the max of the source and target primary terms. This guarantees that the target primary term stays + // greater than or equal to the source. + return Math.max( + updatedIndexMetadata.primaryTerm(shardId.getId() % reshardingMetadata.shardCountBefore()), + updatedIndexMetadata.primaryTerm(shardId.id()) + 1 + ); + } + /** * Updates in-sync allocations with routing changes that were made to the routing table. */ diff --git a/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java b/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java index 657a3976c46f7..7d74f64c9c920 100644 --- a/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java +++ b/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java @@ -3339,7 +3339,12 @@ public void startRecovery( // } assert recoveryState.getRecoverySource().equals(shardRouting.recoverySource()); switch (recoveryState.getRecoverySource().getType()) { - case EMPTY_STORE, EXISTING_STORE -> executeRecovery("from store", recoveryState, recoveryListener, this::recoverFromStore); + case EMPTY_STORE, EXISTING_STORE, SPLIT -> executeRecovery( + "from store", + recoveryState, + recoveryListener, + this::recoverFromStore + ); case PEER -> { try { markAsRecovering("from " + recoveryState.getSourceNode(), recoveryState); diff --git a/server/src/main/java/org/elasticsearch/index/shard/StoreRecovery.java b/server/src/main/java/org/elasticsearch/index/shard/StoreRecovery.java index 89d9a780728fb..7cd8608d792b1 100644 --- a/server/src/main/java/org/elasticsearch/index/shard/StoreRecovery.java +++ b/server/src/main/java/org/elasticsearch/index/shard/StoreRecovery.java @@ -89,8 +89,9 @@ public final class StoreRecovery { void recoverFromStore(final IndexShard indexShard, ActionListener listener) { if (canRecover(indexShard)) { RecoverySource.Type recoveryType = indexShard.recoveryState().getRecoverySource().getType(); - assert recoveryType == RecoverySource.Type.EMPTY_STORE || recoveryType == RecoverySource.Type.EXISTING_STORE - : "expected store recovery type but was: " + recoveryType; + assert recoveryType == RecoverySource.Type.EMPTY_STORE + || recoveryType == RecoverySource.Type.EXISTING_STORE + || recoveryType == RecoverySource.Type.SPLIT : "expected store recovery type but was: " + recoveryType; logger.debug("starting recovery from store ..."); final var recoveryListener = recoveryListener(indexShard, listener); try { diff --git a/server/src/main/java/org/elasticsearch/indices/cluster/IndicesClusterStateService.java b/server/src/main/java/org/elasticsearch/indices/cluster/IndicesClusterStateService.java index 3c84d7be8c6b4..9d3697cd8ed6d 100644 --- a/server/src/main/java/org/elasticsearch/indices/cluster/IndicesClusterStateService.java +++ b/server/src/main/java/org/elasticsearch/indices/cluster/IndicesClusterStateService.java @@ -26,10 +26,12 @@ import org.elasticsearch.cluster.ClusterStateApplier; import org.elasticsearch.cluster.action.shard.ShardStateAction; import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.cluster.metadata.IndexReshardingMetadata; import org.elasticsearch.cluster.metadata.ProjectMetadata; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.cluster.node.DiscoveryNodes; import org.elasticsearch.cluster.routing.IndexShardRoutingTable; +import org.elasticsearch.cluster.routing.RecoverySource; import org.elasticsearch.cluster.routing.RecoverySource.Type; import org.elasticsearch.cluster.routing.RoutingNode; import org.elasticsearch.cluster.routing.RoutingTable; @@ -705,6 +707,7 @@ private void createShard(ShardRouting shardRouting, ClusterState state) { createShardWhenLockAvailable( shardRouting, state, + project, sourceNode, primaryTerm, 0, @@ -758,6 +761,7 @@ private PendingShardCreation createOrRefreshPendingShardCreation(ShardId shardId private void createShardWhenLockAvailable( ShardRouting shardRouting, ClusterState originalState, + ProjectMetadata project, DiscoveryNode sourceNode, long primaryTerm, int iteration, @@ -766,11 +770,22 @@ private void createShardWhenLockAvailable( ActionListener listener ) { try { + long sourcePrimaryTerm; + if (shardRouting.recoverySource().getType() == Type.SPLIT) { + IndexMetadata indexMetadata = project.index(shardRouting.index()); + IndexReshardingMetadata reshardingMetadata = indexMetadata.getReshardingMetadata(); + assert reshardingMetadata != null; + int preSplitSize = reshardingMetadata.shardCountBefore(); + int sourceShardId = shardRouting.id() % preSplitSize; + sourcePrimaryTerm = indexMetadata.primaryTerm(sourceShardId); + } else { + sourcePrimaryTerm = -1; + } logger.debug("{} creating shard with primary term [{}], iteration [{}]", shardRouting.shardId(), primaryTerm, iteration); indicesService.createShard( shardRouting, recoveryTargetService, - new RecoveryListener(shardRouting, primaryTerm), + new RecoveryListener(shardRouting, primaryTerm, sourcePrimaryTerm), repositoriesService, failedShardHandler, this::updateGlobalCheckpointForShard, @@ -865,6 +880,7 @@ private void createShardWhenLockAvailable( createShardWhenLockAvailable( shardRouting, originalState, + project, sourceNode, primaryTerm, iteration + 1, @@ -996,10 +1012,12 @@ private class RecoveryListener implements PeerRecoveryTargetService.RecoveryList * Primary term with which the shard was created */ private final long primaryTerm; + private final long sourcePrimaryTerm; - private RecoveryListener(final ShardRouting shardRouting, final long primaryTerm) { + private RecoveryListener(final ShardRouting shardRouting, final long primaryTerm, final long sourcePrimaryTerm) { this.shardRouting = shardRouting; this.primaryTerm = primaryTerm; + this.sourcePrimaryTerm = sourcePrimaryTerm; } @Override @@ -1008,14 +1026,34 @@ public void onRecoveryDone( ShardLongFieldRange timestampMillisFieldRange, ShardLongFieldRange eventIngestedMillisFieldRange ) { - shardStateAction.shardStarted( - shardRouting, - primaryTerm, - "after " + state.getRecoverySource(), - timestampMillisFieldRange, - eventIngestedMillisFieldRange, - ActionListener.noop() - ); + if (state.getRecoverySource() instanceof RecoverySource.SplitRecoverySource) { + shardStateAction.shardSplit( + shardRouting, + primaryTerm, + "after " + state.getRecoverySource(), + timestampMillisFieldRange, + eventIngestedMillisFieldRange, + sourcePrimaryTerm, + new ActionListener<>() { + @Override + public void onResponse(Void unused) {} + + @Override + public void onFailure(Exception e) { + onRecoveryFailure(new RecoveryFailedException(state, "failed to start after split", e), true); + } + } + ); + } else { + shardStateAction.shardStarted( + shardRouting, + primaryTerm, + "after " + state.getRecoverySource(), + timestampMillisFieldRange, + eventIngestedMillisFieldRange, + ActionListener.noop() + ); + } } @Override diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/RecoverySourceTests.java b/server/src/test/java/org/elasticsearch/cluster/routing/RecoverySourceTests.java index 1856773000a04..e876732bec612 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/RecoverySourceTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/RecoverySourceTests.java @@ -34,10 +34,11 @@ public void testRecoverySourceTypeOrder() { assertEquals(RecoverySource.Type.PEER.ordinal(), 2); assertEquals(RecoverySource.Type.SNAPSHOT.ordinal(), 3); assertEquals(RecoverySource.Type.LOCAL_SHARDS.ordinal(), 4); + assertEquals(RecoverySource.Type.SPLIT.ordinal(), 5); // check exhaustiveness for (RecoverySource.Type type : RecoverySource.Type.values()) { assertThat(type.ordinal(), greaterThanOrEqualTo(0)); - assertThat(type.ordinal(), lessThanOrEqualTo(4)); + assertThat(type.ordinal(), lessThanOrEqualTo(5)); } } }